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
3 changes: 3 additions & 0 deletions include/runtime/roblox_text_font_resolver.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,18 @@

#include <cstdint>
#include <string>
#include <vector>

namespace mocktail {
namespace runtime {

// Android's RbxKeyboard resolves the Roblox Font enum through
// assets/android/fonts/font-mappings.json. The selected file lives below
// assets/content/fonts while legacy SourceSansPro faces live in assets/fonts.
// Emoji come from the color fonts Roblox ships next to the mapped faces.
struct RobloxTextFontSelection {
std::string primary_file;
std::vector<std::string> emoji_files;
float size_scale = 0.795F;
bool exact_mapping = false;
};
Expand Down
20 changes: 20 additions & 0 deletions src/runtime/roblox_text_font_resolver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>

namespace mocktail {
namespace runtime {
Expand Down Expand Up @@ -101,6 +102,24 @@ std::string LegacyFontName(int32_t font) {
return "SourceSansPro-Regular.ttf";
}

// TwemojiMozilla covers Unicode emoji; RobloxEmoji holds the private-use
// Roblox and Robux symbols.
constexpr const char* kEmojiFontNames[] = {"TwemojiMozilla.ttf",
"RobloxEmoji.ttf"};

std::vector<std::string> EmojiFontFiles(
const std::filesystem::path& asset_content_root) {
std::vector<std::string> files;
for (const char* name : kEmojiFontNames) {
const std::filesystem::path candidate =
asset_content_root / "fonts" / name;
if (IsRegularFile(candidate)) {
files.push_back(candidate.lexically_normal().string());
}
}
return files;
}

} // namespace

RobloxTextFontSelection ResolveRobloxTextFont(
Expand All @@ -111,6 +130,7 @@ RobloxTextFontSelection ResolveRobloxTextFont(
}

const std::filesystem::path content(asset_content_root);
selection.emoji_files = EmojiFontFiles(content);
const std::optional<FontMapping> mapped = MappingFor(font, content);
if (mapped.has_value()) {
const std::filesystem::path candidate = content / "fonts" / mapped->file;
Expand Down
27 changes: 26 additions & 1 deletion src/runtime/roblox_text_surface_overlay.cc
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ namespace {

constexpr std::size_t kMaximumRasterTextBytes = 4096;
constexpr std::uint64_t kMaximumOverlayPixels = 16ULL * 1024ULL * 1024ULL;
constexpr std::size_t kMaximumFallbackFonts = 6;
constexpr std::size_t kMaximumFallbackFonts = 8;

std::mutex g_active_overlay_mutex;
RobloxTextSurfaceOverlay* g_active_overlay = nullptr;
Expand Down Expand Up @@ -121,10 +121,35 @@ std::vector<std::string> ResolveFontFiles(
} else if (!selection.primary_file.empty()) {
files.push_back(selection.primary_file);
}
files.insert(files.end(), selection.emoji_files.begin(),
selection.emoji_files.end());
if (!FcInit()) {
return files;
}

// The sorted sans list rarely reaches an emoji font before the cap, so ask
// for the system emoji font by name ahead of it.
FcPattern* emoji = FcNameParse(reinterpret_cast<const FcChar8*>("emoji"));
if (emoji != nullptr) {
FcConfigSubstitute(nullptr, emoji, FcMatchPattern);
FcDefaultSubstitute(emoji);
FcResult emoji_result = FcResultNoMatch;
FcPattern* matched = FcFontMatch(nullptr, emoji, &emoji_result);
FcPatternDestroy(emoji);
FcChar8* file = nullptr;
if (matched != nullptr &&
FcPatternGetString(matched, FC_FILE, 0, &file) == FcResultMatch &&
file != nullptr && file[0] != '\0') {
const std::string candidate(reinterpret_cast<const char*>(file));
if (std::find(files.begin(), files.end(), candidate) == files.end()) {
files.push_back(candidate);
}
}
if (matched != nullptr) {
FcPatternDestroy(matched);
}
}

FcPattern* pattern = FcNameParse(reinterpret_cast<const FcChar8*>("sans"));
if (pattern == nullptr) {
return files;
Expand Down
17 changes: 17 additions & 0 deletions tests/roblox_text_font_resolver_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,23 @@ TEST(RobloxTextFontResolverTest, RejectsMappingPathTraversal) {
(assets.root() / "assets/fonts/SourceSansPro-Regular.ttf").string());
}

TEST(RobloxTextFontResolverTest, AddsBundledEmojiFontsAsFallbacks) {
TemporaryFontAssets assets;
ASSERT_FALSE(assets.root().empty());
ASSERT_TRUE(assets.CreateFile("assets/fonts/SourceSansPro-Regular.ttf"));
ASSERT_TRUE(assets.CreateFile("assets/content/fonts/RobloxEmoji.ttf"));
ASSERT_TRUE(assets.CreateFile("assets/content/fonts/TwemojiMozilla.ttf"));

const RobloxTextFontSelection selection =
ResolveRobloxTextFont(3, assets.content().string());

ASSERT_EQ(selection.emoji_files.size(), 2U);
EXPECT_EQ(selection.emoji_files[0],
(assets.content() / "fonts/TwemojiMozilla.ttf").string());
EXPECT_EQ(selection.emoji_files[1],
(assets.content() / "fonts/RobloxEmoji.ttf").string());
}

} // namespace
} // namespace runtime
} // namespace mocktail