Change logic that splits chat messages with new lines to support fonticons properly

This commit is contained in:
Jan 2021-09-19 15:49:12 +02:00
parent 624daa858e
commit b36df5130b
7 changed files with 392 additions and 13 deletions

View File

@ -2,6 +2,10 @@
namespace Components
{
Game::dvar_t** Chat::cg_chatHeight = reinterpret_cast<Game::dvar_t**>(0x7ED398);
Dvar::Var Chat::cg_chatWidth;
Game::dvar_t** Chat::cg_chatTime = reinterpret_cast<Game::dvar_t**>(0x9F5DE8);
bool Chat::SendChat;
const char* Chat::EvaluateSay(char* text, Game::gentity_t* player)
@ -74,11 +78,128 @@ namespace Components
}
}
void Chat::CheckChatLineEnd(const char*& inputBuffer, char*& lineBuffer, float& len, const int chatHeight, const float chatWidth, char*& lastSpacePos, char*& lastFontIconPos, const int lastColor)
{
if (len > chatWidth)
{
if (lastSpacePos && lastSpacePos > lastFontIconPos)
{
inputBuffer += lastSpacePos - lineBuffer + 1;
lineBuffer = lastSpacePos;
}
else if (lastFontIconPos)
{
inputBuffer += lastFontIconPos - lineBuffer;
lineBuffer = lastFontIconPos;
}
*lineBuffer = 0;
len = 0.0f;
Game::cgsArray[0].teamChatMsgTimes[Game::cgsArray[0].teamChatPos % chatHeight] = Game::cgArray[0].time;
Game::cgsArray[0].teamChatPos++;
lineBuffer = Game::cgsArray[0].teamChatMsgs[Game::cgsArray[0].teamChatPos % chatHeight];
lineBuffer[0] = '^';
lineBuffer[1] = CharForColorIndex(lastColor);
lineBuffer += 2;
lastSpacePos = nullptr;
lastFontIconPos = nullptr;
}
}
void Chat::CG_AddToTeamChat(const char* text)
{
// Text can only be 150 characters maximum. This is bigger than the teamChatMsgs buffers with 160 characters
// Therefore it is not needed to check for buffer lengths
const auto chatHeight = (*cg_chatHeight)->current.integer;
const auto chatWidth = static_cast<float>(cg_chatWidth.get<int>());
const auto chatTime = (*cg_chatTime)->current.integer;
if (chatHeight < 0 || static_cast<unsigned>(chatHeight) > std::extent_v<decltype(Game::cgs_t::teamChatMsgs)> || chatWidth <= 0 || chatTime <= 0)
{
Game::cgsArray[0].teamLastChatPos = 0;
Game::cgsArray[0].teamChatPos = 0;
return;
}
TextRenderer::FontIconInfo fontIconInfo{};
auto len = 0.0f;
auto lastColor = static_cast<int>(TEXT_COLOR_DEFAULT);
char* lastSpace = nullptr;
char* lastFontIcon = nullptr;
char* p = Game::cgsArray[0].teamChatMsgs[Game::cgsArray[0].teamChatPos % chatHeight];
p[0] = '\0';
while (*text)
{
CheckChatLineEnd(text, p, len, chatHeight, chatWidth, lastSpace, lastFontIcon, lastColor);
const char* fontIconEndPos = &text[1];
if(text[0] == ':' && TextRenderer::IsFontIcon(fontIconEndPos, fontIconInfo))
{
// The game calculates width on a per character base. Since the width of a font icon is calculated based on the height of the font
// which is roughly double as much as the average width of a character without an additional multiplier the calculated len of the font icon
// would be less than it most likely would be rendered. Therefore apply a guessed 2.0f multiplier at this location which makes
// the calculated width of a font icon roughly comparable to the width of an average character of the font.
const auto normalizedFontIconWidth = TextRenderer::GetNormalizedFontIconWidth(fontIconInfo);
const auto fontIconWidth = normalizedFontIconWidth * FONT_ICON_CHAT_WIDTH_CALCULATION_MULTIPLIER;
len += fontIconWidth;
lastFontIcon = p;
for(; text < fontIconEndPos; text++)
{
p[0] = text[0];
p++;
}
CheckChatLineEnd(text, p, len, chatHeight, chatWidth, lastSpace, lastFontIcon, lastColor);
}
else if (text[0] == '^' && text[1] != 0 && text[1] >= TextRenderer::COLOR_FIRST_CHAR && text[1] <= TextRenderer::COLOR_LAST_CHAR)
{
p[0] = '^';
p[1] = text[1];
lastColor = ColorIndexForChar(text[1]);
p += 2;
text += 2;
}
else
{
if (text[0] == ' ')
lastSpace = p;
*p++ = *text++;
len += 1.0f;
}
}
*p = 0;
Game::cgsArray[0].teamChatMsgTimes[Game::cgsArray[0].teamChatPos % chatHeight] = Game::cgArray[0].time;
if (Game::cgsArray[0].teamChatPos++ + 1 - Game::cgsArray[0].teamLastChatPos > chatHeight)
Game::cgsArray[0].teamLastChatPos = Game::cgsArray[0].teamChatPos + 1 - chatHeight;
}
__declspec(naked) void Chat::CG_AddToTeamChat_Stub()
{
__asm
{
pushad
push ecx
call CG_AddToTeamChat
add esp, 4h
popad
ret
}
}
Chat::Chat()
{
cg_chatWidth = Dvar::Register<int>("cg_chatWidth", 52, 1, INT_MAX, Game::DVAR_FLAG_SAVED, "The normalized maximum width of a chat message");
// Intercept chat sending
Utils::Hook(0x4D000B, PreSayStub, HOOK_CALL).install()->quick();
Utils::Hook(0x4D00D4, PostSayStub, HOOK_CALL).install()->quick();
Utils::Hook(0x4D0110, PostSayStub, HOOK_CALL).install()->quick();
// Change logic that does word splitting with new lines for chat messages to support fonticons
Utils::Hook(0x592E10, CG_AddToTeamChat_Stub, HOOK_JUMP).install()->quick();
}
}

View File

@ -4,15 +4,24 @@ namespace Components
{
class Chat : public Component
{
static constexpr auto FONT_ICON_CHAT_WIDTH_CALCULATION_MULTIPLIER = 2.0f;
public:
Chat();
private:
static Game::dvar_t** cg_chatHeight;
static Dvar::Var cg_chatWidth;
static Game::dvar_t** cg_chatTime;
static bool SendChat;
static const char* EvaluateSay(char* text, Game::gentity_t* player);
static void PreSayStub();
static void PostSayStub();
static void CheckChatLineEnd(const char*& inputBuffer, char*& lineBuffer, float& len, int chatHeight, float chatWidth, char*& lastSpacePos, char*& lastFontIconPos, int lastColor);
static void CG_AddToTeamChat(const char* text);
static void CG_AddToTeamChat_Stub();
};
}

View File

@ -636,6 +636,21 @@ namespace Components
return true;
}
float TextRenderer::GetNormalizedFontIconWidth(const FontIconInfo& fontIcon)
{
const auto* colorMap = GetFontIconColorMap(fontIcon.material);
if (colorMap == nullptr)
return 0;
const auto sizeMultiplier = fontIcon.big ? 1.5f : 1.0f;
auto colWidth = static_cast<float>(colorMap->width);
auto colHeight = static_cast<float>(colorMap->height);
if (fontIcon.material->info.textureAtlasColumnCount > 1)
colWidth /= static_cast<float>(fontIcon.material->info.textureAtlasColumnCount);
if (fontIcon.material->info.textureAtlasRowCount > 1)
colHeight /= static_cast<float>(fontIcon.material->info.textureAtlasRowCount);
return (colWidth / colHeight) * sizeMultiplier;
}
float TextRenderer::GetFontIconWidth(const FontIconInfo& fontIcon, const Game::Font_s* font, const float xScale)
{
const auto* colorMap = GetFontIconColorMap(fontIcon.material);

View File

@ -49,14 +49,6 @@ namespace Components
Game::Material* material;
};
struct FontIconInfo
{
Game::Material* material;
bool flipHorizontal;
bool flipVertical;
bool big;
};
struct HsvColor
{
unsigned char h;
@ -99,8 +91,6 @@ namespace Components
float maxMaterialNameWidth;
};
static constexpr char COLOR_FIRST_CHAR = '0';
static constexpr char COLOR_LAST_CHAR = CharForColorIndex(TEXT_COLOR_COUNT - 1);
static constexpr unsigned MY_ALTCOLOR_TWO = 0x0DCE6FFE6;
static constexpr unsigned COLOR_MAP_HASH = 0xA0AB1041;
static constexpr auto FONT_ICON_AUTOCOMPLETE_BOX_PADDING = 6.0f;
@ -153,6 +143,17 @@ namespace Components
static Game::dvar_t** con_inputBoxColor;
public:
static constexpr char COLOR_FIRST_CHAR = '0';
static constexpr char COLOR_LAST_CHAR = CharForColorIndex(TEXT_COLOR_COUNT - 1);
struct FontIconInfo
{
Game::Material* material;
bool flipHorizontal;
bool flipVertical;
bool big;
};
static void DrawText2D(const char* text, float x, float y, Game::Font_s* font, float xScale, float yScale, float sinAngle, float cosAngle, Game::GfxColor color, int maxLength, int renderFlags, int cursorPos, char cursorLetter, float padding, Game::GfxColor glowForcedColor, int fxBirthTime, int fxLetterTime, int fxDecayStartTime, int fxDecayDuration, Game::Material* fxMaterial, Game::Material* fxMaterialGlow);
static int R_TextWidth_Hk(const char* text, int maxChars, Game::Font_s* font);
static unsigned int ColorIndex(char index);
@ -163,6 +164,10 @@ namespace Components
static void StripAllTextIcons(const char* in, char* out, size_t max);
static std::string StripAllTextIcons(const std::string& in);
static bool IsFontIcon(const char*& text, FontIconInfo& fontIcon);
static float GetNormalizedFontIconWidth(const FontIconInfo& fontIcon);
static float GetFontIconWidth(const FontIconInfo& fontIcon, const Game::Font_s* font, float xScale);
TextRenderer();
private:
@ -193,8 +198,6 @@ namespace Components
static void GetUnpackedColorByNameStub();
static Game::GfxImage* GetFontIconColorMap(const Game::Material* fontIconMaterial);
static bool IsFontIcon(const char*& text, FontIconInfo& fontIcon);
static float GetFontIconWidth(const FontIconInfo& fontIcon, const Game::Font_s* font, float xScale);
static float DrawFontIcon(const FontIconInfo& fontIcon, float x, float y, float sinAngle, float cosAngle, const Game::Font_s* font, float xScale, float yScale, unsigned color);
static float GetMonospaceWidth(Game::Font_s* font, int rendererFlags);

View File

@ -474,6 +474,7 @@ namespace Game
clientActive_t* clients = reinterpret_cast<clientActive_t*>(0xB2C698);
cg_s* cgArray = reinterpret_cast<cg_s*>(0x7F0F78);
cgs_t* cgsArray = reinterpret_cast<cgs_t*>(0x7ED3B8);
PlayerKeyState* playerKeys = reinterpret_cast<PlayerKeyState*>(0xA1B7D0);
kbutton_t* playersKb = reinterpret_cast<kbutton_t*>(0xA1A9A8);

View File

@ -974,6 +974,7 @@ namespace Game
extern clientActive_t* clients;
extern cg_s* cgArray;
extern cgs_t* cgsArray;
extern PlayerKeyState* playerKeys;
extern kbutton_t* playersKb;

View File

@ -6380,7 +6380,10 @@ namespace Game
void* nextSnap;
char _pad1[0x673DC];
int frametime; // + 0x6A754
char _pad2[0x960C]; // + 0x6A758
int time;
int oldTime;
int physicalsTime;
char _pad2[0x9600]; // + 0x6A758
float compassMapWorldSize[2]; // + 0x73D64
char _pad3[0x74]; // + 0x73D6C
float selectedLocation[2]; // + 0x73DE0
@ -6391,6 +6394,8 @@ namespace Game
char _pad4[0x89740];
};
static_assert(sizeof(cg_s) == 0xFD540);
static constexpr auto MAX_GAMEPADS = 1;
static constexpr auto GPAD_VALUE_MASK = 0xFFFFFFFu;
@ -6629,6 +6634,230 @@ namespace Game
};
#pragma warning(pop)
enum ShockViewTypes
{
SHELLSHOCK_VIEWTYPE_BLURRED = 0x0,
SHELLSHOCK_VIEWTYPE_FLASHED = 0x1,
SHELLSHOCK_VIEWTYPE_NONE = 0x2,
};
struct shellshock_parms_t
{
struct
{
int blurredFadeTime;
int blurredEffectTime;
int flashWhiteFadeTime;
int flashShotFadeTime;
ShockViewTypes type;
} screenBlend;
struct
{
int fadeTime;
float kickRate;
float kickRadius;
} view;
struct
{
bool affect;
char loop[64];
char loopSilent[64];
char end[64];
char endAbort[64];
int fadeInTime;
int fadeOutTime;
float drylevel;
float wetlevel;
char roomtype[16];
float channelvolume[64];
int modEndDelay;
int loopFadeTime;
int loopEndDelay;
} sound;
struct
{
bool affect;
int fadeTime;
float mouseSensitivity;
float maxPitchSpeed;
float maxYawSpeed;
} lookControl;
struct
{
bool affect;
} movement;
};
struct XAnimParent
{
unsigned short flags;
unsigned short children;
};
struct XAnimEntry
{
unsigned short numAnims;
unsigned short parent;
union
{
XAnimParts* parts;
XAnimParent animParent;
};
};
struct XAnim_s
{
unsigned int size;
const char* debugName;
const char** debugAnimNames;
XAnimEntry entries[1];
};
struct animation_s
{
char name[64];
int initialLerp;
float moveSpeed;
int duration;
int nameHash;
int flags;
int64_t movetype;
int noteType;
};
struct lerpFrame_t
{
float yawAngle;
int yawing;
float pitchAngle;
int pitching;
int animationNumber;
animation_s* animation;
int animationTime;
float oldFramePos[3];
float animSpeedScale;
int oldFrameSnapshotTime;
};
struct clientControllers_t
{
float angles[4][3];
float tag_origin_angles[3];
float tag_origin_offset[3];
};
struct __declspec(align(4)) XAnimTree_s
{
XAnim_s* anims;
int info_usage;
volatile int calcRefCount;
volatile int modifyRefCount;
unsigned __int16 children;
};
enum PlayerDiveState
{
DIVE_NONE = 0x0,
DIVE_FORWARD = 0x1,
DIVE_FORWARDLEFT = 0x2,
DIVE_LEFT = 0x3,
DIVE_BACKLEFT = 0x4,
DIVE_BACK = 0x5,
DIVE_BACKRIGHT = 0x6,
DIVE_RIGHT = 0x7,
DIVE_FORWARDRIGHT = 0x8,
};
struct clientInfo_t
{
int infoValid;
int nextValid;
int clientNum;
char name[16];
team_t team;
team_t oldteam;
int rank;
int prestige;
unsigned int perks[2];
int score;
int location;
int health;
char model[64];
char attachModelNames[6][64];
char attachTagNames[6][64];
unsigned int partBits[6];
lerpFrame_t legs;
lerpFrame_t torso;
float lerpMoveDir;
float lerpLean;
float playerAngles[3];
int legsAnim;
int torsoAnim;
float fTorsoPitch;
float fWaistPitch;
int leftHandGun;
int dobjDirty;
clientControllers_t control;
unsigned int clientConditions[18][2];
XAnimTree_s* pXAnimTree;
int iDObjWeapon;
char weaponModel;
int stanceTransitionTime;
int turnAnimEndTime;
char turnAnimType;
bool hideWeapon;
bool usingKnife;
int dualWielding;
PlayerDiveState diveState;
int riotShieldNext;
unsigned int playerCardIcon;
unsigned int playerCardTitle;
unsigned int playerCardNameplate;
};
struct cgs_t
{
int viewX;
int viewY;
int viewWidth;
int viewHeight;
float viewAspect;
int serverCommandSequence;
int processedSnapshotNum;
int localServer;
char gametype[32];
char szHostName[256];
bool hardcore;
int maxclients;
int privateClients;
char mapname[64];
int gameEndTime;
int voteTime;
int voteYes;
int voteNo;
char voteString[256];
XModel* gameModels[512];
FxEffectDef* smokeGrenadeFx;
shellshock_parms_t holdBreathParams;
char teamChatMsgs[8][160];
int teamChatMsgTimes[8];
int teamChatPos;
int teamLastChatPos;
float compassWidth;
float compassHeight;
float compassY;
clientInfo_t corpseinfo[8];
bool entUpdateToggleContextKey;
XAnim_s* helicopterAnims;
};
static_assert(sizeof(cgs_t) == 0x3BA4);
#pragma endregion
#ifndef IDA