-
Notifications
You must be signed in to change notification settings - Fork 27
Color PlayerList names from nametags #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,30 +1,308 @@ | ||
| #include "pch.h" | ||
| #include "TabList.h" | ||
| #include "client/Latite.h" | ||
| #include "client/event/events/RenderNameTagEvent.h" | ||
| #include "client/event/events/TickEvent.h" | ||
| #include "client/misc/NameTagCache.h" | ||
| #include "../../../../render/asset/Assets.h" | ||
| #include "mc/common/world/actor/player/Player.h" | ||
| #include "util/Logger.h" | ||
| #include <algorithm> | ||
| #include <cctype> | ||
| #include <unordered_set> | ||
| #include <vector> | ||
|
|
||
| namespace { | ||
| bool readFormatCode(std::string const& text, size_t index, char& code, size_t& codeSize) { | ||
| if (index + 1 < text.size() && static_cast<unsigned char>(text[index]) == 0xA7) { | ||
| code = text[index + 1]; | ||
| codeSize = 2; | ||
| return true; | ||
| } | ||
| if (index + 2 < text.size() | ||
| && static_cast<unsigned char>(text[index]) == 0xC2 | ||
| && static_cast<unsigned char>(text[index + 1]) == 0xA7) { | ||
| code = text[index + 2]; | ||
| codeSize = 3; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| bool isColorCode(char code) { | ||
| return std::isxdigit(static_cast<unsigned char>(code)); | ||
| } | ||
|
|
||
| d2d::Color minecraftColor(char code, d2d::Color const& fallback) { | ||
| switch (static_cast<char>(std::tolower(static_cast<unsigned char>(code)))) { | ||
| case '0': return d2d::Color::RGB(0, 0, 0); | ||
| case '1': return d2d::Color::RGB(0, 0, 170); | ||
| case '2': return d2d::Color::RGB(0, 170, 0); | ||
| case '3': return d2d::Color::RGB(0, 170, 170); | ||
| case '4': return d2d::Color::RGB(170, 0, 0); | ||
| case '5': return d2d::Color::RGB(170, 0, 170); | ||
| case '6': return d2d::Color::RGB(255, 170, 0); | ||
| case '7': return d2d::Color::RGB(170, 170, 170); | ||
| case '8': return d2d::Color::RGB(85, 85, 85); | ||
| case '9': return d2d::Color::RGB(85, 85, 255); | ||
| case 'a': return d2d::Color::RGB(85, 255, 85); | ||
| case 'b': return d2d::Color::RGB(85, 255, 255); | ||
| case 'c': return d2d::Color::RGB(255, 85, 85); | ||
| case 'd': return d2d::Color::RGB(255, 85, 255); | ||
| case 'e': return d2d::Color::RGB(255, 255, 85); | ||
| case 'f': return d2d::Color::RGB(255, 255, 255); | ||
| case 'r': return fallback; | ||
| default: return fallback; | ||
| } | ||
| } | ||
|
|
||
| std::string stripFormatCodes(std::string const& text) { | ||
| std::string stripped; | ||
| stripped.reserve(text.size()); | ||
| for (size_t i = 0; i < text.size();) { | ||
| char code = 0; | ||
| size_t codeSize = 0; | ||
| if (readFormatCode(text, i, code, codeSize)) { | ||
| i += codeSize; | ||
| continue; | ||
| } | ||
| stripped += text[i++]; | ||
| } | ||
| return stripped; | ||
| } | ||
|
|
||
| bool hasFormatCode(std::string const& text) { | ||
| for (size_t i = 0; i < text.size();) { | ||
| char code = 0; | ||
| size_t codeSize = 0; | ||
| if (readFormatCode(text, i, code, codeSize)) return true; | ||
| i++; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| int colorCodeIndex(char code) { | ||
| if (code >= '0' && code <= '9') return code - '0'; | ||
| if (code >= 'a' && code <= 'f') return code - 'a' + 10; | ||
| if (code >= 'A' && code <= 'F') return code - 'A' + 10; | ||
| return 16; | ||
| } | ||
|
|
||
| int firstColorSortIndex(std::string const& text) { | ||
| for (size_t i = 0; i < text.size();) { | ||
| char code = 0; | ||
| size_t codeSize = 0; | ||
| if (readFormatCode(text, i, code, codeSize)) { | ||
| if (isColorCode(code)) return colorCodeIndex(code); | ||
| i += codeSize; | ||
| continue; | ||
| } | ||
| i++; | ||
| } | ||
| return 16; | ||
| } | ||
|
|
||
| std::string tabRowName(SDK::PlayerListEntry& entry, std::unordered_map<std::string, std::string> const& coloredNameCache) { | ||
| if (auto it = coloredNameCache.find(entry.name); it != coloredNameCache.end()) return it->second; | ||
| return entry.name; | ||
| } | ||
|
|
||
| std::vector<SDK::PlayerListEntry*> sortedPlayerListRows(SDK::Level* level, | ||
| std::unordered_map<std::string, std::string> const& coloredNameCache) { | ||
| std::vector<SDK::PlayerListEntry*> rows; | ||
| if (!level || !level->getPlayerList()) return rows; | ||
|
|
||
| rows.reserve(level->getPlayerList()->size()); | ||
| for (auto& ent : *level->getPlayerList()) { | ||
| rows.push_back(&ent.second); | ||
| } | ||
|
|
||
| std::stable_sort(rows.begin(), rows.end(), [&](auto* a, auto* b) { | ||
| std::string aName = tabRowName(*a, coloredNameCache); | ||
| std::string bName = tabRowName(*b, coloredNameCache); | ||
| const int aColor = firstColorSortIndex(aName); | ||
| const int bColor = firstColorSortIndex(bName); | ||
| if (aColor != bColor) return aColor < bColor; | ||
| return stripFormatCodes(aName) < stripFormatCodes(bName); | ||
| }); | ||
|
|
||
| return rows; | ||
| } | ||
|
|
||
| std::string colorizedPlayerName(std::string const& playerName, std::string const& nameTag) { | ||
| const size_t namePos = stripFormatCodes(nameTag).find(playerName); | ||
| if (namePos == std::string::npos) return playerName; | ||
| const size_t nameEnd = namePos + playerName.size(); | ||
|
|
||
| std::string activeColor; | ||
| std::string formattedName; | ||
| size_t visiblePos = 0; | ||
| for (size_t i = 0; i < nameTag.size();) { | ||
| char code = 0; | ||
| size_t codeSize = 0; | ||
| if (readFormatCode(nameTag, i, code, codeSize)) { | ||
| if (isColorCode(code) || code == 'r' || code == 'R') { | ||
| activeColor.assign(nameTag, i, codeSize); | ||
| } | ||
| if (visiblePos >= namePos && visiblePos < nameEnd) { | ||
| formattedName.append(nameTag, i, codeSize); | ||
| } | ||
| i += codeSize; | ||
| continue; | ||
| } | ||
| if (visiblePos >= nameEnd) break; | ||
| if (visiblePos >= namePos) { | ||
| if (formattedName.empty()) formattedName += activeColor; | ||
| formattedName += nameTag[i]; | ||
| } | ||
| visiblePos++; | ||
| i++; | ||
| } | ||
| return formattedName.empty() ? playerName : formattedName; | ||
| } | ||
|
|
||
| void drawFormattedText(D2DUtil& dc, d2d::Rect const& rc, std::string const& text, d2d::Color const& fallbackColor, | ||
| Renderer::FontSelection font, float textSize) { | ||
| float x = rc.left; | ||
| d2d::Color color = fallbackColor; | ||
| std::string segment; | ||
| auto flush = [&]() { | ||
| if (segment.empty()) return; | ||
| std::wstring wide = util::StrToWStr(segment); | ||
| dc.drawText({ x, rc.top, rc.right, rc.bottom }, wide, color, font, textSize, | ||
| DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_PARAGRAPH_ALIGNMENT_NEAR, false); | ||
| x += dc.getTextSize(wide, font, textSize, true, false).x; | ||
| segment.clear(); | ||
| }; | ||
|
|
||
| for (size_t i = 0; i < text.size();) { | ||
| char code = 0; | ||
| size_t codeSize = 0; | ||
| if (readFormatCode(text, i, code, codeSize)) { | ||
| flush(); | ||
| color = minecraftColor(code, fallbackColor).asAlpha(fallbackColor.a); | ||
| i += codeSize; | ||
| continue; | ||
| } | ||
| segment += text[i++]; | ||
| } | ||
| flush(); | ||
| } | ||
|
|
||
| ColorValue colorOrDefault(ValueType const& value, ColorValue const& fallback) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the rationale behind integrating this function? |
||
| if (!std::holds_alternative<ColorValue>(value)) return fallback; | ||
| return std::get<ColorValue>(value); | ||
| } | ||
|
|
||
| float floatOrDefault(ValueType const& value, float fallback) { | ||
| if (!std::holds_alternative<FloatValue>(value)) return fallback; | ||
| return std::get<FloatValue>(value).value; | ||
| } | ||
| } | ||
|
|
||
| TabList::TabList() : Module("PlayerList", LocalizeString::get("client.module.tabList.name"), | ||
| LocalizeString::get("client.module.tabList.desc"), HUD, VK_TAB) { | ||
| addSetting("textColor", LocalizeString::get("client.module.tabList.textColor.name"), | ||
| LocalizeString::get("client.module.tabList.textColor.desc"), textCol); | ||
| addSetting("bgColor", LocalizeString::get("client.module.tabList.bgColor.name"), | ||
| LocalizeString::get("client.module.tabList.bgColor.desc"), bgCol); | ||
| addSliderSetting("textSize", LocalizeString::get("client.textmodule.props.textSize.name"), L"", textSizeS, | ||
| FloatValue(2.f), FloatValue(100.f), FloatValue(2.f)); | ||
| listen<RenderOverlayEvent>(static_cast<EventListenerFunc>(&TabList::onRenderOverlay)); | ||
| listen<RenderNameTagEvent>(static_cast<EventListenerFunc>(&TabList::onRenderNameTag), true); | ||
| listen<TickEvent>(static_cast<EventListenerFunc>(&TabList::onTick), true); | ||
|
Comment on lines
+212
to
+213
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do these listeners run with callWhenInactive = true? If the cache must be maintained globally, that seems like service-level state rather than module event work, otherwise these events should just follow the normal module listener path and only run while enabled. |
||
| } | ||
|
|
||
| void TabList::onRenderNameTag(Event& evG) { | ||
| auto& ev = static_cast<RenderNameTagEvent&>(evG); | ||
| auto* tag = ev.getNametag(); | ||
| if (!tag || !hasFormatCode(*tag)) return; | ||
|
|
||
| auto* client = SDK::ClientInstance::get(); | ||
| if (!client || !client->minecraft) return; | ||
|
|
||
| auto* level = client->minecraft->getLevel(); | ||
| if (!level || !level->getPlayerList()) return; | ||
|
|
||
| for (auto& ent : *level->getPlayerList()) { | ||
| std::string rowName = colorizedPlayerName(ent.second.name, *tag); | ||
| if (hasFormatCode(rowName)) { | ||
| coloredNameCache[ent.second.name] = rowName; | ||
| } | ||
| } | ||
|
ukiyo-dev marked this conversation as resolved.
Comment on lines
+227
to
+232
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This fallback matches every rendered nametag string against every player-list name, so substring collisions can color the wrong row (Alex matching Alexis, names appearing in prefixes/suffixes, etc.). Since RenderNameTagEvent only exposes text and not the actor, this the best source for player identity. I'd be nice if you could compare against actor/runtime IDs (maybe even xuids?) to make this more consistent, if they're unique that way. Therefore, I'd avoid using this path for cache population unless the event can carry such kind of IDs. |
||
| } | ||
|
|
||
| void TabList::onTick(Event& evG) { | ||
| auto& ev = static_cast<TickEvent&>(evG); | ||
| auto* level = ev.getLevel(); | ||
| if (!level || !level->getPlayerList()) { | ||
| coloredNameCache.clear(); | ||
| return; | ||
| } | ||
|
|
||
| std::unordered_set<std::string> activePlayers; | ||
| activePlayers.reserve(level->getPlayerList()->size()); | ||
| for (auto& ent : *level->getPlayerList()) { | ||
| activePlayers.insert(ent.second.name); | ||
| } | ||
|
|
||
| std::unordered_set<uint64_t> activeRuntimeIds; | ||
| for (auto* actor : level->getRuntimeActorList()) { | ||
| if (!actor || !actor->isPlayer()) continue; | ||
|
|
||
| auto runtimeId = actor->getRuntimeID(); | ||
| activeRuntimeIds.insert(runtimeId); | ||
|
|
||
| auto* player = static_cast<SDK::Player*>(actor); | ||
| if (!activePlayers.contains(player->playerName)) continue; | ||
|
|
||
| auto nameTag = NameTagCache::getNetworkNameTag(runtimeId); | ||
| if (!nameTag) continue; | ||
|
|
||
| std::string rowName = colorizedPlayerName(player->playerName, *nameTag); | ||
| if (hasFormatCode(rowName)) { | ||
| coloredNameCache[player->playerName] = rowName; | ||
| } | ||
|
Comment on lines
+259
to
+265
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This leaves stale formatted names in coloredNameCache. If a player's network nametag disappears, becomes empty, or changes back to an unformatted name, this code just skips the update and keeps rendering the previous color until the player leaves. We should erase/update the per-player cache when getNetworkNameTag is missing or colorizedPlayerName has no format codes. |
||
| } | ||
| NameTagCache::retainNetworkNameTags(activeRuntimeIds); | ||
|
|
||
| for (auto it = coloredNameCache.begin(); it != coloredNameCache.end();) { | ||
| if (!activePlayers.contains(it->first)) { | ||
| it = coloredNameCache.erase(it); | ||
| continue; | ||
| } | ||
| ++it; | ||
| } | ||
| } | ||
|
|
||
| void TabList::afterLoadConfig() { | ||
| if (!std::holds_alternative<ColorValue>(textCol)) { | ||
| Logger::Warn("TabList: textColor setting was invalid, restoring default"); | ||
| textCol = ColorValue(1.f, 1.f, 1.f, 1.f); | ||
| } | ||
| if (!std::holds_alternative<ColorValue>(bgCol)) { | ||
| Logger::Warn("TabList: bgColor setting was invalid, restoring default"); | ||
| bgCol = ColorValue(0.f, 0.f, 0.f, 0.5f); | ||
| } | ||
| if (!std::holds_alternative<FloatValue>(textSizeS)) { | ||
| Logger::Warn("TabList: textSize setting was invalid, restoring default"); | ||
| textSizeS = FloatValue(20.f); | ||
| } | ||
| } | ||
|
|
||
| void TabList::onRenderOverlay(Event& evG) { | ||
| auto& ev = static_cast<RenderOverlayEvent&>(evG); | ||
| (void)evG; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just remove the variable if you're going to cast it to void? If you want to silence compiler warnings add a comment that does it. |
||
|
|
||
| auto plr = SDK::ClientInstance::get()->getLocalPlayer(); | ||
| if (!plr) return; | ||
|
|
||
| D2DUtil dc; | ||
| auto lvl = SDK::ClientInstance::get()->minecraft->getLevel(); | ||
| auto name = lvl->getLevelName(); | ||
| if (!lvl || !lvl->getPlayerList()) return; | ||
|
|
||
| size_t size = lvl->getPlayerList()->size(); | ||
|
|
||
| float textP = 20.f; | ||
| float textP = floatOrDefault(textSizeS, 20.f); | ||
|
|
||
| std::wstring txt; | ||
| if (SDK::RakNetConnector::get() && SDK::RakNetConnector::get()->featuredServer.size() > 0) { | ||
|
|
@@ -35,17 +313,20 @@ void TabList::onRenderOverlay(Event& evG) { | |
| } | ||
|
|
||
| constexpr auto font = Renderer::FontSelection::PrimaryRegular; | ||
| const ColorValue textColor = colorOrDefault(textCol, ColorValue(1.f, 1.f, 1.f, 1.f)); | ||
| const ColorValue backgroundColor = colorOrDefault(bgCol, ColorValue(0.f, 0.f, 0.f, 0.5f)); | ||
| float sectionHeight = textP * 1.3f; | ||
|
|
||
| float logoSize = sectionHeight; | ||
| float logoPad = 4.f; | ||
|
|
||
| float longestText = dc.getTextSize(txt, font, textP).x; | ||
| for (auto& ent : *lvl->getPlayerList()) { | ||
| auto w = dc.getTextSize(util::StrToWStr(ent.second.name), font, textP).x + 3.f; | ||
| auto const& name = util::StrToWStr(ent.second.name); | ||
| auto sortedRows = sortedPlayerListRows(lvl, coloredNameCache); | ||
| for (auto* row : sortedRows) { | ||
| std::string rowName = tabRowName(*row, coloredNameCache); | ||
| auto w = dc.getTextSize(util::StrToWStr(stripFormatCodes(rowName)), font, textP).x + 3.f; | ||
| for (auto& user : Latite::get().getLatiteUsers()) { | ||
| if (user == ent.second.name) { | ||
| if (user == row->name) { | ||
| w += logoPad + logoSize; | ||
| } | ||
| } | ||
|
|
@@ -76,17 +357,17 @@ void TabList::onRenderOverlay(Event& evG) { | |
| dc.ctx->SetTransform(mat * D2D1::Matrix3x2F::Translation(ss.x / 2.f - calcWidth / 2.f, 20.f)); | ||
|
|
||
|
|
||
| dc.fillRectangle({ 0.f, 0.f, calcWidth, calcHeight + oY }, std::get<ColorValue>(this->bgCol).getMainColor()); | ||
| dc.drawRectangle({ 0.f, 0.f, calcWidth, calcHeight + oY }, d2d::Color(std::get<ColorValue>(this->bgCol).getMainColor()).asAlpha(1.f), 2.f); | ||
| dc.fillRectangle({ 0.f, 0.f, calcWidth, calcHeight + oY }, backgroundColor.getMainColor()); | ||
| dc.drawRectangle({ 0.f, 0.f, calcWidth, calcHeight + oY }, d2d::Color(backgroundColor.getMainColor()).asAlpha(1.f), 2.f); | ||
|
|
||
| dc.drawText({ 0.f, 0.f, calcWidth, oY }, txt, std::get<ColorValue>(this->textCol).getMainColor(), font, textP, DWRITE_TEXT_ALIGNMENT_CENTER); | ||
| dc.drawText({ 0.f, 0.f, calcWidth, oY }, txt, textColor.getMainColor(), font, textP, DWRITE_TEXT_ALIGNMENT_CENTER); | ||
|
|
||
|
|
||
| for (auto& ent : *lvl->getPlayerList()) { | ||
| auto const& name = util::StrToWStr(ent.second.name); | ||
| for (auto* row : sortedRows) { | ||
| std::string rowName = tabRowName(*row, coloredNameCache); | ||
| d2d::Rect rc = { x, y, x + longestText, y + sectionHeight }; | ||
| for (auto& user : Latite::get().getLatiteUsers()) { | ||
| if (user == ent.second.name) { | ||
| if (user == row->name) { | ||
| rc.left += logoSize + logoPad; | ||
| d2d::Rect logoRc = { x, y, x + logoSize, y + logoSize }; | ||
| dc.ctx->DrawBitmap(Latite::getAssets().logoWhite.getBitmap(), logoRc); | ||
|
|
@@ -96,7 +377,7 @@ void TabList::onRenderOverlay(Event& evG) { | |
| // render | ||
| //dc.drawRectangle(rc, d2d::Colors::BLACK, 0.5f); | ||
|
|
||
| dc.drawText(rc, name, std::get<ColorValue>(textCol).getMainColor(), font, textP, DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_PARAGRAPH_ALIGNMENT_NEAR, false); | ||
| drawFormattedText(dc, rc, rowName, textColor.getMainColor(), font, textP); | ||
|
|
||
| idx++; | ||
| if (idx < maxPerTab) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.