diff --git a/assets/opensb/font/twemoji.woff2 b/assets/opensb/font/twemoji.woff2 new file mode 100644 index 0000000..6cc58b6 Binary files /dev/null and b/assets/opensb/font/twemoji.woff2 differ diff --git a/assets/opensb/interface.config.patch b/assets/opensb/interface.config.patch index d798609..47076b2 100644 --- a/assets/opensb/interface.config.patch +++ b/assets/opensb/interface.config.patch @@ -1,9 +1,14 @@ { + "font" : { + "defaultFont" : "hobo", + "fallbackFont" : "unifont", + "emojiFont" : "twemoji" + }, "nametag" : { "showMasterNames" : true, // border is double here as it otherwise fades out near the end // (could just the end to opaque too, but then diagonals are more jaggy) - "textStyle" : { "backDirectives" : "?border=1;222;2224?border=1;222;2224" }, + "textStyle" : { "backDirectives" : "border=1;222;2224?border=1;222;2224" }, "inspectOpacityRate" : 0.15, "movementThreshold" : 0.5, "offset" : [0, 13] @@ -19,7 +24,7 @@ "paneTextStyle" : {}, "textBoxTextStyle" : {}, "itemSlotTextStyle" : { - "backDirectives" : "?border=1;444;4444" + "backDirectives" : "border=1;444;4444" }, "teamBarNameStyle" : {}, "cursorTooltip" : { "textStyle" : {} }, @@ -27,7 +32,7 @@ "debugTextStyle" : { "font" : "iosevka-semibold", "fontSize" : 7, - "backDirectives" : "?border=2;111a;1114" + "backDirectives" : "border=1;333a;3334" }, "debugSpatialClearTime" : 0.0, "debugOffset" : [80, 130], @@ -35,7 +40,7 @@ // Change planet name to support the new internal string formatting. "planetNameFormatString" : "- {} -", "planetTextStyle" : { - "backDirectives" : "?border=6;000;000", + "backDirectives" : "border=6;000;000", "fontSize" : 24 }, diff --git a/assets/opensb/interface/chat/chat.config.patch b/assets/opensb/interface/chat/chat.config.patch index d71b88d..afdc3f8 100644 --- a/assets/opensb/interface/chat/chat.config.patch +++ b/assets/opensb/interface/chat/chat.config.patch @@ -2,7 +2,8 @@ "config" : { "lineHeight" : 1, "padding" : [1, 1], - "textStyle" : { "backDirectives" : "?border=1;111a;1114" }, + "textStyle" : { "backDirectives" : "border=1;111a;1117" }, + "chatFormatString" : "^backdirectives=border=1=333=3337;<^backdirectives=border=1=333=3330?border=1=333=3337,white;{}^reset,backdirectives=border=1=333=3337;>^reset,#eee; {}", "colors" : { "local" : "^white;", "party" : "^blue;", diff --git a/source/core/StarFont.cpp b/source/core/StarFont.cpp index b21b85a..86262f5 100644 --- a/source/core/StarFont.cpp +++ b/source/core/StarFont.cpp @@ -7,7 +7,7 @@ namespace Star { -constexpr int FontLoadFlags = FT_LOAD_FORCE_AUTOHINT; +constexpr int FontLoadFlags = FT_LOAD_NO_SVG | FT_LOAD_COLOR; struct FTContext { FT_Library library; @@ -90,47 +90,70 @@ unsigned Font::width(String::Char c) { } -std::pair Font::render(String::Char c) { +tuple Font::render(String::Char c) { if (!m_fontImpl) throw FontException("Font::render called on uninitialized font."); FT_Face face = m_fontImpl->face; - FT_UInt glyph_index = FT_Get_Char_Index(face, c); - if (FT_Load_Glyph(face, glyph_index, FontLoadFlags) != 0) - return {}; + if (m_loadedPixelSize != m_pixelSize || m_loadedChar != c) { + FT_UInt glyph_index = FT_Get_Char_Index(face, c); + if (FT_Load_Glyph(face, glyph_index, FontLoadFlags) != 0) + return {}; - /* convert to an anti-aliased bitmap */ - if (FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL) != 0) - return {}; + /* convert to an anti-aliased bitmap */ + if (FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL) != 0) + return {}; + } + + m_loadedPixelSize = m_pixelSize; + m_loadedChar = m_loadedChar; FT_GlyphSlot slot = face->glyph; unsigned width = slot->bitmap.width; unsigned height = slot->bitmap.rows; + bool colored = false; - Image image(width + 2, height + 2, PixelFormat::RGBA32); - Vec4B white(255, 255, 255, 0); - image.fill(white); - - for (unsigned y = 0; y != height; ++y) { - uint8_t* p = slot->bitmap.buffer + y * slot->bitmap.pitch; - for (unsigned x = 0; x != width; ++x) { - if (x < width && y < height) { - uint8_t value = *(p + x); - if (m_alphaThreshold) { - if (value >= m_alphaThreshold) { - white[3] = 255; - image.set(x + 1, height - y, white); - } - } else if (value) { - white[3] = value; + Image image(width + 2, height + 2, PixelFormat::BGRA32); + if (slot->bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) { + Vec4B white(255, 255, 255, 0); + image.fill(white); + for (unsigned y = 0; y != height; ++y) { + uint8_t* p = slot->bitmap.buffer + y * slot->bitmap.pitch; + for (unsigned x = 0; x != width; ++x) { + if (x < width && y < height) { + white[3] = m_alphaThreshold + ? (*(p + x) >= m_alphaThreshold ? 255 : 0) + : *(p + x); image.set(x + 1, height - y, white); } } } + } else if (colored = slot->bitmap.pixel_mode == FT_PIXEL_MODE_BGRA) { + unsigned bpp = image.bytesPerPixel(); + uint8_t* data = image.data() + bpp + ((image.width() * (image.height() - 2)) * bpp); // offset by 1 pixel as it's padded + for (size_t y = 0; y != height; ++y) { + memcpy(data - (y * image.width() * bpp), + slot->bitmap.buffer + y * slot->bitmap.pitch, + slot->bitmap.pitch); + } + // unfortunately FreeType pre-multiplied the color channels :( + image.forEachPixel([](unsigned, unsigned, Vec4B& pixel) { + if (pixel[3] != 0 && pixel[3] != 255) { + float a = byteToFloat(pixel[3]); + pixel[0] /= a; + pixel[1] /= a; + pixel[2] /= a; + } + }); + } else { + return {}; } - return { std::move(image), {slot->bitmap_left - 1, (slot->bitmap_top - height) + (m_pixelSize / 4) - 1} }; + return make_tuple( + std::move(image), + Vec2I(slot->bitmap_left - 1, (slot->bitmap_top - height) + (m_pixelSize / 4) - 1), + colored); } bool Font::exists(String::Char c) { diff --git a/source/core/StarFont.hpp b/source/core/StarFont.hpp index dee50f7..6686dbc 100644 --- a/source/core/StarFont.hpp +++ b/source/core/StarFont.hpp @@ -34,13 +34,15 @@ public: // May return empty image on unrenderable character (Normally, this will // render a box, but if there is an internal freetype error this may return // an empty image). - std::pair render(String::Char c); + tuple render(String::Char c); bool exists(String::Char c); private: FontImplPtr m_fontImpl; ByteArrayConstPtr m_fontBuffer; unsigned m_pixelSize; + unsigned m_loadedPixelSize; + String::Char m_loadedChar; uint8_t m_alphaThreshold; HashMap, unsigned> m_widthCache; diff --git a/source/frontend/StarBaseScriptPane.cpp b/source/frontend/StarBaseScriptPane.cpp index 6db092d..7facc1c 100644 --- a/source/frontend/StarBaseScriptPane.cpp +++ b/source/frontend/StarBaseScriptPane.cpp @@ -90,7 +90,8 @@ void BaseScriptPane::tick(float dt) { bool BaseScriptPane::sendEvent(InputEvent const& event) { // Intercept GuiClose before the canvas child so GuiClose always closes // BaseScriptPanes without having to support it in the script. - if (context()->actions(event).contains(InterfaceAction::GuiClose)) { + if (context()->actions(event).contains(InterfaceAction::GuiClose) + && m_config.getBool("dismissable", true)) { dismiss(); return true; } diff --git a/source/frontend/StarChat.cpp b/source/frontend/StarChat.cpp index d780d23..bbb28c1 100644 --- a/source/frontend/StarChat.cpp +++ b/source/frontend/StarChat.cpp @@ -27,6 +27,7 @@ Chat::Chat(UniverseClientPtr client) : m_client(client) { m_chatVisTime = config.get("visTime").toFloat(); m_fadeRate = config.get("fadeRate").toDouble(); m_chatHistoryLimit = config.get("chatHistoryLimit").toInt(); + m_chatFormatString = config.getString("chatFormatString"); m_portraitTextOffset = jsonToVec2I(config.get("portraitTextOffset")); m_portraitImageOffset = jsonToVec2I(config.get("portraitImageOffset")); @@ -180,7 +181,7 @@ void Chat::addMessages(List const& messages, bool showPane) guiContext.setTextStyle(m_chatTextStyle); StringList lines; if (message.fromNick != "" && message.portrait == "") - lines = guiContext.wrapInterfaceText(strf("<{}> {}", message.fromNick, message.text), wrapWidth); + lines = guiContext.wrapInterfaceText(strf(m_chatFormatString.utf8Ptr(), message.fromNick, message.text), wrapWidth); else lines = guiContext.wrapInterfaceText(message.text, wrapWidth); @@ -203,7 +204,8 @@ void Chat::addMessages(List const& messages, bool showPane) show(); } - m_receivedMessages.resize(std::min((unsigned)m_receivedMessages.size(), m_chatHistoryLimit)); + if ((unsigned)m_receivedMessages.size() > m_chatHistoryLimit) + m_receivedMessages.resize(m_chatHistoryLimit); } void Chat::addHistory(String const& chat) { @@ -216,6 +218,13 @@ void Chat::addHistory(String const& chat) { m_client->playerStorage()->setMetadata("chatHistory", JsonArray::from(m_chatHistory)); } +void Chat::clear(size_t count) { + if (count > m_receivedMessages.size()) + m_receivedMessages.clear(); + else + m_receivedMessages.erase(m_receivedMessages.begin(), m_receivedMessages.begin() + count); +} + void Chat::renderImpl() { Pane::renderImpl(); if (m_textBox->hasFocus()) diff --git a/source/frontend/StarChat.hpp b/source/frontend/StarChat.hpp index 80cf683..df2d63c 100644 --- a/source/frontend/StarChat.hpp +++ b/source/frontend/StarChat.hpp @@ -30,6 +30,7 @@ public: void addLine(String const& text, bool showPane = true); void addMessages(List const& messages, bool showPane = true); void addHistory(String const& chat); + void clear(size_t count = std::numeric_limits::max()); String currentChat() const; bool setCurrentChat(String const& chat, bool moveCursor = false); @@ -69,6 +70,7 @@ private: TextStyle m_chatTextStyle; unsigned m_chatHistoryLimit; int m_historyOffset; + String m_chatFormatString; CanvasWidgetPtr m_chatLog; Vec2I m_chatLogPadding; diff --git a/source/frontend/StarInterfaceLuaBindings.cpp b/source/frontend/StarInterfaceLuaBindings.cpp index 9e0bb2e..9523036 100644 --- a/source/frontend/StarInterfaceLuaBindings.cpp +++ b/source/frontend/StarInterfaceLuaBindings.cpp @@ -13,6 +13,11 @@ namespace Star { LuaCallbacks LuaBindings::makeInterfaceCallbacks(MainInterface* mainInterface) { LuaCallbacks callbacks; + callbacks.registerCallbackWithSignature( + "hudVisible", bind(mem_fn(&MainInterface::hudVisible), mainInterface)); + callbacks.registerCallbackWithSignature( + "setHudVisible", bind(mem_fn(&MainInterface::setHudVisible), mainInterface, _1)); + callbacks.registerCallback("bindCanvas", [mainInterface](String const& canvasName, Maybe ignoreInterfaceScale) -> Maybe { if (auto canvas = mainInterface->fetchCanvas(canvasName, ignoreInterfaceScale.value(false))) return canvas; @@ -87,6 +92,10 @@ LuaCallbacks LuaBindings::makeChatCallbacks(MainInterface* mainInterface, Univer return chat->setCurrentChat(text, moveCursor.value(false)); }); + callbacks.registerCallback("clear", [chat](Maybe count) { + chat->clear(count.value(std::numeric_limits::max())); + }); + return callbacks; } diff --git a/source/frontend/StarMainInterface.cpp b/source/frontend/StarMainInterface.cpp index 533aa58..5163eeb 100644 --- a/source/frontend/StarMainInterface.cpp +++ b/source/frontend/StarMainInterface.cpp @@ -893,6 +893,14 @@ bool MainInterface::fixedCamera() const { return m_clientCommandProcessor->fixedCameraEnabled(); } +bool MainInterface::hudVisible() const { + return !m_disableHud; +} + +void MainInterface::setHudVisible(bool visible) { + m_disableHud = !visible; +} + void MainInterface::warpToOrbitedWorld(bool deploy) { if (m_client->canBeamDown(deploy)) { if (deploy) diff --git a/source/frontend/StarMainInterface.hpp b/source/frontend/StarMainInterface.hpp index ec79f68..667a2fe 100644 --- a/source/frontend/StarMainInterface.hpp +++ b/source/frontend/StarMainInterface.hpp @@ -100,7 +100,6 @@ public: Vec2F cursorWorldPosition() const; - void toggleDebugDisplay(); bool isDebugDisplayed(); void doChat(String const& chat, bool addToHistory); @@ -112,6 +111,8 @@ public: void queueJoinRequest(pair> request); bool fixedCamera() const; + bool hudVisible() const; + void setHudVisible(bool visible = true); void warpToOrbitedWorld(bool deploy = false); void warpToOwnShip(); @@ -209,7 +210,7 @@ private: QuestIndicatorPainterPtr m_questIndicatorPainter; ChatBubbleManagerPtr m_chatBubbleManager; - bool m_disableHud{false}; + bool m_disableHud = false; String m_lastCommand; diff --git a/source/game/StarPlayerStorage.cpp b/source/game/StarPlayerStorage.cpp index 1946ce2..7857f8d 100644 --- a/source/game/StarPlayerStorage.cpp +++ b/source/game/StarPlayerStorage.cpp @@ -115,7 +115,7 @@ Maybe PlayerStorage::playerUuidByName(String const& name, Maybe exce RecursiveMutexLocker locker(m_mutex); - size_t longest = SIZE_MAX; + size_t longest = std::numeric_limits::max(); for (auto& cache : m_savedPlayersCache) { if (except && *except == cache.first) continue; diff --git a/source/rendering/StarFontTextureGroup.cpp b/source/rendering/StarFontTextureGroup.cpp index d5e6ef9..5f0cdbc 100644 --- a/source/rendering/StarFontTextureGroup.cpp +++ b/source/rendering/StarFontTextureGroup.cpp @@ -14,13 +14,13 @@ void FontTextureGroup::cleanup(int64_t timeout) { void FontTextureGroup::switchFont(String const& font) { if (font.empty()) { - m_font = m_defaultFont; + m_activeFont = m_defaultFont; m_fontName.clear(); } else if (m_fontName != font) { m_fontName = font; auto find = m_fonts.find(font); - m_font = find != m_fonts.end() ? find->second : m_defaultFont; + m_activeFont = find != m_fonts.end() ? find->second : m_defaultFont; } } @@ -28,31 +28,35 @@ String const& FontTextureGroup::activeFont() { return m_fontName; } -void FontTextureGroup::addFont(FontPtr const& font, String const& name, bool isDefault) { +void FontTextureGroup::addFont(FontPtr const& font, String const& name) { m_fonts[name] = font; - if (isDefault) - m_defaultFont = m_font = font; } void FontTextureGroup::clearFonts() { m_fonts.clear(); - m_font = m_defaultFont; + m_activeFont = m_defaultFont; } -void FontTextureGroup::setFallbackFont(String const& fontName) { - if (auto font = m_fonts.ptr(fontName)) - m_fallbackFont = *font; +void FontTextureGroup::setFixedFonts(String const& defaultFontName, String const& fallbackFontName, String const& emojiFontName) { + if (auto defaultFont = m_fonts.ptr(defaultFontName)) + m_defaultFont = m_activeFont = *defaultFont; + if (auto fallbackFont = m_fonts.ptr(fallbackFontName)) + m_fallbackFont = *fallbackFont; + if (auto emojiFont = m_fonts.ptr(emojiFontName)) + m_emojiFont = *emojiFont; } const FontTextureGroup::GlyphTexture& FontTextureGroup::glyphTexture(String::Char c, unsigned size, Directives const* processingDirectives) { - Font* font = (m_font->exists(c) || !m_fallbackFont) ? m_font.get() : m_fallbackFont.get(); + Font* font = getFontForCharacter(c); + if (font == m_emojiFont.get()) + processingDirectives = nullptr; auto res = m_glyphs.insert(GlyphDescriptor{c, size, processingDirectives ? processingDirectives->hash() : 0, font}, GlyphTexture()); - + auto& glyphTexture = res.first->second; if (res.second) { font->setPixelSize(size); - auto pair = font->render(c); - Image& image = pair.first; + auto renderResult = font->render(c); + Image& image = get<0>(renderResult); if (processingDirectives) { try { Directives const& directives = *processingDirectives; @@ -61,7 +65,7 @@ const FontTextureGroup::GlyphTexture& FontTextureGroup::glyphTexture(String::Cha for (auto& entry : directives->entries) processImageOperation(entry.operation, image); - res.first->second.offset = (preSize - Vec2F(image.size())) / 2; + glyphTexture.offset = (preSize - Vec2F(image.size())) / 2; } catch (StarException const&) { image.forEachPixel([](unsigned x, unsigned y, Vec4B& pixel) { @@ -69,15 +73,14 @@ const FontTextureGroup::GlyphTexture& FontTextureGroup::glyphTexture(String::Cha }); } } - else - res.first->second.offset = Vec2F(); - res.first->second.offset += Vec2F(pair.second); - res.first->second.texture = m_textureGroup->create(image); + glyphTexture.colored = get<2>(renderResult); + glyphTexture.offset += Vec2F(get<1>(renderResult)); + glyphTexture.texture = m_textureGroup->create(image); } - res.first->second.time = Time::monotonicMilliseconds(); - return res.first->second; + glyphTexture.time = Time::monotonicMilliseconds(); + return glyphTexture; } TexturePtr FontTextureGroup::glyphTexturePtr(String::Char c, unsigned size) { @@ -89,9 +92,31 @@ TexturePtr FontTextureGroup::glyphTexturePtr(String::Char c, unsigned size, Dire } unsigned FontTextureGroup::glyphWidth(String::Char c, unsigned fontSize) { - Font* font = (m_font->exists(c) || !m_fallbackFont) ? m_font.get() : m_fallbackFont.get(); + Font* font = getFontForCharacter(c); font->setPixelSize(fontSize); return font->width(c); } +Font* FontTextureGroup::getFontForCharacter(String::Char c) { + if (((c >= 0x1F600 && c <= 0x1F64F) || // Emoticons + (c >= 0x1F300 && c <= 0x1F5FF) || // Misc Symbols and Pictographs + (c >= 0x1F680 && c <= 0x1F6FF) || // Transport and Map + (c >= 0x1F1E6 && c <= 0x1F1FF) || // Regional country flags + (c >= 0x2600 && c <= 0x26FF ) || // Misc symbols 9728 - 9983 + (c >= 0x2700 && c <= 0x27BF ) || // Dingbats + (c >= 0xFE00 && c <= 0xFE0F ) || // Variation Selectors + (c >= 0x1F900 && c <= 0x1F9FF) || // Supplemental Symbols and Pictographs + (c >= 0x1F018 && c <= 0x1F270) || // Various asian characters + (c >= 65024 && c <= 65039 ) || // Variation selector + (c >= 9100 && c <= 9300 ) || // Misc items + (c >= 8400 && c <= 8447 ))&& // Combining Diacritical Marks for Symbols + m_emojiFont->exists(c) + ) + return m_emojiFont.get(); + else if (m_activeFont->exists(c) || !m_fallbackFont) + return m_activeFont.get(); + else + return m_fallbackFont.get(); +} + } diff --git a/source/rendering/StarFontTextureGroup.hpp b/source/rendering/StarFontTextureGroup.hpp index 864b44f..b2fa572 100644 --- a/source/rendering/StarFontTextureGroup.hpp +++ b/source/rendering/StarFontTextureGroup.hpp @@ -16,7 +16,8 @@ public: struct GlyphTexture { TexturePtr texture; - int64_t time; + bool colored = false; + int64_t time = 0; Vec2F offset; }; @@ -35,16 +36,18 @@ public: // Switches the current font void switchFont(String const& font); String const& activeFont(); - void addFont(FontPtr const& font, String const& name, bool isDefault = false); + void addFont(FontPtr const& font, String const& name); void clearFonts(); - void setFallbackFont(String const& fontName); - + void setFixedFonts(String const& defaultFontName, String const& fallbackFontName, String const& emojiFontName); private: + Font* getFontForCharacter(String::Char); + CaseInsensitiveStringMap m_fonts; String m_fontName; - FontPtr m_font; + FontPtr m_activeFont; FontPtr m_defaultFont; FontPtr m_fallbackFont; + FontPtr m_emojiFont; TextureGroupPtr m_textureGroup; HashMap m_glyphs; diff --git a/source/rendering/StarTextPainter.cpp b/source/rendering/StarTextPainter.cpp index fa90208..7fc02bc 100644 --- a/source/rendering/StarTextPainter.cpp +++ b/source/rendering/StarTextPainter.cpp @@ -320,14 +320,9 @@ void TextPainter::reloadFonts() { m_fontTextureGroup.clearFonts(); m_fontTextureGroup.cleanup(0); auto assets = Root::singleton().assets(); - String defaultName = "hobo"; - auto defaultFont = loadFont("/hobo.ttf", defaultName); auto loadFontsByExtension = [&](String const& ext) { for (auto& fontPath : assets->scanExtension(ext)) { auto font = assets->font(fontPath); - if (font == defaultFont) - continue; - auto name = AssetPath::filename(fontPath); name = name.substr(0, name.findLast(".")); addFont(loadFont(fontPath, name), name); @@ -335,8 +330,10 @@ void TextPainter::reloadFonts() { }; loadFontsByExtension("ttf"); loadFontsByExtension("woff2"); - m_fontTextureGroup.addFont(defaultFont, defaultName, true); - m_fontTextureGroup.setFallbackFont("unifont"); + m_fontTextureGroup.setFixedFonts( + assets->json("/interface.config:font.defaultFont").toString(), + assets->json("/interface.config:font.fallbackFont").toString(), + assets->json("/interface.config:font.emojiFont").toString()); } void TextPainter::cleanup(int64_t timeout) { @@ -518,13 +515,15 @@ void TextPainter::renderPrimitives() { } void TextPainter::renderGlyph(String::Char c, Vec2F const& screenPos, List& out, unsigned fontSize, - float scale, Vec4B const& color, Directives const* processingDirectives) { + float scale, Vec4B color, Directives const* processingDirectives) { if (!fontSize) return; const FontTextureGroup::GlyphTexture& glyphTexture = m_fontTextureGroup.glyphTexture(c, fontSize, processingDirectives); - Vec2F offset = glyphTexture.offset * scale; - out.emplace_back(std::in_place_type_t(), glyphTexture.texture, Vec2F::round(screenPos + offset), scale, color, 0.0f); + if (glyphTexture.colored) + color[0] = color[1] = color[2] = 255; + out.emplace_back(std::in_place_type_t(), + glyphTexture.texture, Vec2F::round(screenPos + glyphTexture.offset * scale), scale, color, 0.0f); } FontPtr TextPainter::loadFont(String const& fontPath, Maybe fontName) { diff --git a/source/rendering/StarTextPainter.hpp b/source/rendering/StarTextPainter.hpp index bff3a64..05f16f2 100644 --- a/source/rendering/StarTextPainter.hpp +++ b/source/rendering/StarTextPainter.hpp @@ -86,7 +86,7 @@ private: RectF doRenderGlyph(String::Char c, TextPositioning const& position, bool reallyRender); void renderPrimitives(); - void renderGlyph(String::Char c, Vec2F const& screenPos, List& out, unsigned fontSize, float scale, Vec4B const& color, Directives const* processingDirectives = nullptr); + void renderGlyph(String::Char c, Vec2F const& screenPos, List& out, unsigned fontSize, float scale, Vec4B color, Directives const* processingDirectives = nullptr); static FontPtr loadFont(String const& fontPath, Maybe fontName = {}); RendererPtr m_renderer;