Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions code/framework/src/external/imgui/widgets/world_text.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ namespace Framework::External::ImGUI::Widgets {
}

// Draw multi-line ('\n') UTF-8 text centered on screenPos (pixels). color is an ImU32 with its
// own alpha; `alpha` multiplies on top (distance fade).
inline void DrawWorldText(ImDrawList *drawList, ImVec2 screenPos, const char *text, ImU32 color, float fontSize, WorldTextStyle style, float alpha = 1.0f) {
// own alpha; `alpha` multiplies on top (distance fade). Only the presets: LayoutWorldText and
// DrawWorldTextLaidOut (world_text_layout.h) draw a label's full look.
[[deprecated("Use LayoutWorldText and DrawWorldTextLaidOut from world_text_layout.h")]] inline void DrawWorldText(ImDrawList *drawList, ImVec2 screenPos, const char *text, ImU32 color, float fontSize, WorldTextStyle style, float alpha = 1.0f) {
if (!drawList || !text || !text[0] || alpha <= 0.0f) {
return;
}
Expand Down
386 changes: 386 additions & 0 deletions code/framework/src/external/imgui/widgets/world_text_layout.h

Large diffs are not rendered by default.

165 changes: 165 additions & 0 deletions code/framework/src/external/imgui/widgets/world_text_markup.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* MafiaHub OSS license
* Copyright (c) 2021-2026, MafiaHub. All rights reserved.
*
* This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework.
* See LICENSE file in the source repository for information regarding licensing.
*/

#pragma once

#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace Framework::External::ImGUI::Widgets {
// A stretch of text sharing one inline style. color is 0xAARRGGBB; unset follows the label's own.
struct WorldTextRun {
std::string text;
std::optional<uint32_t> color;
bool bold = false;
bool italic = false;
};

namespace Detail {
inline int HexDigit(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
}
if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
}
return -1;
}

// "#RRGGBB" or "#RRGGBBAA" (CSS order) to 0xAARRGGBB.
inline std::optional<uint32_t> ParseHexColor(std::string_view tag) {
if ((tag.size() != 7 && tag.size() != 9) || tag[0] != '#') {
return std::nullopt;
}
uint32_t value = 0;
for (size_t i = 1; i < tag.size(); ++i) {
const int digit = HexDigit(tag[i]);
if (digit < 0) {
return std::nullopt;
}
value = (value << 4) | static_cast<uint32_t>(digit);
}
if (tag.size() == 7) {
return 0xFF000000u | value;
}
return (value >> 8) | ((value & 0xFFu) << 24);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} // namespace Detail

// Reads one code point at `i` and advances past it; malformed input yields U+FFFD one byte on.
inline uint32_t DecodeUtf8(std::string_view text, size_t &i) {
const auto byte = [&](size_t at) {
return static_cast<uint8_t>(text[at]);
};
const uint8_t lead = byte(i);
size_t length = 0;
uint32_t cp = 0;
if (lead < 0x80) {
++i;
return lead;
}
if ((lead & 0xE0) == 0xC0) {
length = 2;
cp = lead & 0x1F;
}
else if ((lead & 0xF0) == 0xE0) {
length = 3;
cp = lead & 0x0F;
}
else if ((lead & 0xF8) == 0xF0) {
length = 4;
cp = lead & 0x07;
}
if (length == 0 || i + length > text.size()) {
++i;
return 0xFFFD;
}
for (size_t k = 1; k < length; ++k) {
if ((byte(i + k) & 0xC0) != 0x80) {
++i;
return 0xFFFD;
}
cp = (cp << 6) | (byte(i + k) & 0x3F);
}
static constexpr uint32_t kMinimum[] {0, 0, 0x80, 0x800, 0x10000};
i += length;
if (cp < kMinimum[length] || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) {
return 0xFFFD;
}
return cp;
}

// Tags: {#RRGGBB} / {#RRGGBBAA} set the colour, {#} restores the label's, {b}{/b} and {i}{/i}
// toggle bold and italic, {{ is a literal brace. Anything else in braces stays as text.
inline std::vector<WorldTextRun> ParseWorldTextMarkup(std::string_view text) {
std::vector<WorldTextRun> runs;
WorldTextRun current;
const auto flush = [&]() {
if (!current.text.empty()) {
runs.push_back(current);
current.text.clear();
}
};

for (size_t i = 0; i < text.size(); ++i) {
const char c = text[i];
if (c != '{') {
current.text.push_back(c);
continue;
}
if (i + 1 < text.size() && text[i + 1] == '{') {
current.text.push_back('{');
++i;
continue;
}
const size_t close = text.find('}', i + 1);
if (close == std::string_view::npos) {
current.text.push_back(c);
continue;
}

const std::string_view tag = text.substr(i + 1, close - i - 1);
std::optional<uint32_t> color;
bool known = true;
if (tag == "#") {
flush();
current.color.reset();
}
else if ((color = Detail::ParseHexColor(tag))) {
flush();
current.color = color;
}
else if (tag == "b" || tag == "/b") {
flush();
current.bold = tag == "b";
}
else if (tag == "i" || tag == "/i") {
flush();
current.italic = tag == "i";
}
else {
known = false;
}

if (known) {
i = close;
}
else {
current.text.push_back(c);
}
}
flush();
return runs;
}
} // namespace Framework::External::ImGUI::Widgets
55 changes: 55 additions & 0 deletions code/framework/src/integrations/client/ui/text_label_imgui.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* MafiaHub OSS license
* Copyright (c) 2026, MafiaHub. All rights reserved.
*
* This file comes from MafiaHub, hosted at https://github.com/MafiaHub/Framework.
* See LICENSE file in the source repository for information regarding licensing.
*/

#pragma once

#include <external/imgui/widgets/world_text_layout.h>
#include <external/imgui/widgets/world_text_markup.h>
#include <networking/replication/entities/text_label_entity.h>

#include <algorithm>
#include <vector>

namespace Framework::Integrations::Client::UI::TextLabels {
// A replicated TextLabelEntity in the terms of the world text widgets. The game resolves where the
// label is and whether it can be seen; this is only what it looks like.

static_assert(static_cast<int>(::Framework::External::ImGUI::Widgets::WorldTextAlign::Right) == static_cast<int>(::Framework::Networking::Replication::Entities::TextLabelAlign::Right));
static_assert(static_cast<int>(::Framework::External::ImGUI::Widgets::WorldTextAnchor::Top) == static_cast<int>(::Framework::Networking::Replication::Entities::TextLabelAnchor::Top));
static_assert(static_cast<int>(::Framework::External::ImGUI::Widgets::WorldTextAnchor::Bottom) == static_cast<int>(::Framework::Networking::Replication::Entities::TextLabelAnchor::Bottom));
static_assert(::Framework::Networking::Replication::Entities::kTextLabelMaxOutlineWidth <= ::Framework::External::ImGUI::Widgets::kWorldTextMaxOutline, "the widget would cut a label's outline short");
static_assert(::Framework::Networking::Replication::Entities::kTextLabelMaxShadowBlur <= ::Framework::External::ImGUI::Widgets::kWorldTextMaxBlur, "the widget would cut a label's blur short");

inline ::Framework::External::ImGUI::Widgets::WorldTextLook Look(const ::Framework::Networking::Replication::Entities::TextLabelEntity &label) {
::Framework::External::ImGUI::Widgets::WorldTextLook look;
look.color = label.color;
look.outlineWidth = std::clamp(label.outlineWidth, 0.0f, ::Framework::Networking::Replication::Entities::kTextLabelMaxOutlineWidth);
look.outlineColor = label.outlineColor;
look.shadowX = label.shadowOffsetX;
look.shadowY = label.shadowOffsetY;
look.shadowBlur = std::clamp(label.shadowBlur, 0.0f, ::Framework::Networking::Replication::Entities::kTextLabelMaxShadowBlur);
look.shadowColor = label.shadowColor;
look.backgroundColor = label.backgroundColor;
look.backgroundPadding = label.backgroundPadding;
look.backgroundRadius = label.backgroundRadius;
look.anchor = static_cast<::Framework::External::ImGUI::Widgets::WorldTextAnchor>(label.anchor);
return look;
}

inline ::Framework::External::ImGUI::Widgets::WorldTextAlign Align(const ::Framework::Networking::Replication::Entities::TextLabelEntity &label) {
return static_cast<::Framework::External::ImGUI::Widgets::WorldTextAlign>(label.align);
}

// Markup is parsed only when the label asks for it, so plain text never loses a brace.
inline std::vector<::Framework::External::ImGUI::Widgets::WorldTextRun> Runs(const ::Framework::Networking::Replication::Entities::TextLabelEntity &label) {
if (label.markup) {
return ::Framework::External::ImGUI::Widgets::ParseWorldTextMarkup(label.text);
}
return {::Framework::External::ImGUI::Widgets::WorldTextRun {label.text}};
}
} // namespace Framework::Integrations::Client::UI::TextLabels
11 changes: 11 additions & 0 deletions code/framework/src/integrations/server/instance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#include "core_modules.h"

#include "integrations/shared/rpc/emit_script_event.h"
#include "networking/replication/entities/text_label_entity.h"
#include "networking/replication/network_entity.h"
#include "networking/replication/replication_manager.h"
#include "networking/rpc/chat_message.h"
Expand All @@ -40,6 +41,7 @@

#include "scripting/builtins/events.h"
#include "scripting/builtins/player.h"
#include "scripting/builtins/text_label.h"
#include "scripting/engine_scope.h"
#include "scripting/node_engine.h"
#include "scripting/resource/resource_manager.h"
Expand Down Expand Up @@ -238,6 +240,9 @@ namespace Framework::Integrations::Server {
if (replication) {
replication->SetAutoSerializeInterval(static_cast<MafiaNet::Time>(Utils::Time::SecondsToMs(_opts.worldConfig.tickInterval)));
replication->ConfigureGrid(_opts.worldConfig.streamCellSize, _opts.worldConfig.streamWorldMin, _opts.worldConfig.streamWorldMax);
// A text label streams exactly as far as it draws. Held to its own range, labels are
// filed apart and never widen what every other viewer queries.
replication->SetInterestUsesEntityRange(Framework::Networking::Replication::Entities::TextLabelEntity::kTypeName, true);
// Replication owns connection teardown: when a peer drops, it notifies the game (avatar
// still resolvable) just before destroying and broadcasting the destruction of the avatar.
replication->SetOnClientDisconnect([this](MafiaNet::PeerGuid guid) {
Expand Down Expand Up @@ -1426,6 +1431,12 @@ namespace Framework::Integrations::Server {
_voiceServer.SyncAvatars(*replication);
}

// Before the interest rebuild in the packet pump, so attached labels stream from where
// their targets are now.
if (replication) {
Framework::Scripting::Builtins::TextLabel::FollowTargets(*replication);
}

if (_networkingEngine) {
FW_PROFILE_SCOPE_N("Server::Networking");
_networkingEngine->Update();
Expand Down
Loading
Loading