diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b70df252..8069b426a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ set(Impacto_Src src/inputsystem.cpp src/voicetable.cpp src/animation.cpp + src/externalfont.cpp src/text/text.cpp src/text/typewritereffect.cpp @@ -246,6 +247,7 @@ set(Impacto_Src src/hud/skipicondisplay.cpp src/hud/waiticondisplay.cpp src/hud/tipsnotification.cpp + src/hud/achievementnotification.cpp src/hud/cc/dialoguebox.cpp src/hud/cc/nametagdisplay.cpp @@ -599,6 +601,7 @@ set(Impacto_Header src/hud/skipicondisplay.h src/hud/waiticondisplay.h src/hud/tipsnotification.h + src/hud/achievementnotification.h src/hud/cc/dialoguebox.h src/hud/cc/nametagdisplay.h @@ -1059,9 +1062,25 @@ list(APPEND Impacto_Include_Dirs ${WebP_INCLUDE_DIRS}) if (VCPKG_TOOLCHAIN) find_package(fmt CONFIG REQUIRED) + find_package(Freetype REQUIRED) + find_package(harfbuzz CONFIG REQUIRED) endif () list(APPEND Impacto_Libs fmt::fmt) +if (VCPKG_TOOLCHAIN) + list(APPEND Impacto_Libs + Freetype::Freetype + harfbuzz::harfbuzz + ) +else () + pkg_check_modules(freetype2 REQUIRED IMPORTED_TARGET freetype2) + pkg_check_modules(harfbuzz REQUIRED IMPORTED_TARGET harfbuzz) + list(APPEND Impacto_Libs + PkgConfig::freetype2 + PkgConfig::harfbuzz + ) +endif () + list(APPEND Impacto_Libs pugixml::pugixml atrac9 diff --git a/games/common/NotoSansCJKjp-Regular.otf b/games/common/NotoSansCJKjp-Regular.otf new file mode 100644 index 000000000..f56224957 Binary files /dev/null and b/games/common/NotoSansCJKjp-Regular.otf differ diff --git a/games/common/achievementnotification.png b/games/common/achievementnotification.png new file mode 100644 index 000000000..9db0898fb Binary files /dev/null and b/games/common/achievementnotification.png differ diff --git a/profiles/cc/game.lua b/profiles/cc/game.lua index e6f0e305d..965a1b25a 100644 --- a/profiles/cc/game.lua +++ b/profiles/cc/game.lua @@ -49,6 +49,7 @@ include('cc/tipssystem.lua'); include('cc/vfs.lua'); include('cc/sprites.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('cc/charset.lua'); --include('cc/font.lua'); include('cc/font-lb.lua'); diff --git a/profiles/cclcc/game.lua b/profiles/cclcc/game.lua index ff1aa6630..e27acd63f 100644 --- a/profiles/cclcc/game.lua +++ b/profiles/cclcc/game.lua @@ -53,6 +53,7 @@ include('cclcc/vfs.lua'); include('cclcc/charset.lua'); include('cclcc/gamespecific.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('common/charset.lua'); --include('cclcc/font.lua'); --include('cclcc/font-lb.lua'); @@ -74,4 +75,4 @@ include('cclcc/hud/extramenus.lua'); include('cclcc/hud/tipsnotification.lua'); include('cclcc/hud/systemmenu.lua'); include('cclcc/hud/savemenu.lua'); -include('cclcc/hud/helpmenu.lua'); \ No newline at end of file +include('cclcc/hud/helpmenu.lua'); diff --git a/profiles/chlcc/game.lua b/profiles/chlcc/game.lua index f86ba1267..cfe13622e 100644 --- a/profiles/chlcc/game.lua +++ b/profiles/chlcc/game.lua @@ -45,6 +45,7 @@ include('chlcc/tipssystem.lua'); include('chlcc/vfs.lua'); include('chlcc/sprites.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('chlcc/charset.lua'); include('common/charset.lua'); if root.Language == "Japanese" then @@ -69,4 +70,4 @@ include('chlcc/hud/optionsmenu.lua'); include('chlcc/hud/extramenus.lua'); include('chlcc/hud/trophymenu.lua'); include('chlcc/gamespecific.lua'); -include('chlcc/waveeffects.lua'); \ No newline at end of file +include('chlcc/waveeffects.lua'); diff --git a/profiles/chn/game.lua b/profiles/chn/game.lua index dd3c14845..1482251da 100644 --- a/profiles/chn/game.lua +++ b/profiles/chn/game.lua @@ -44,6 +44,7 @@ include('chn/tipssystem.lua'); include('chn/vfs.lua'); include('chn/sprites.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('chn/charset.lua'); include('chn/font.lua'); --include('chn/font-lb.lua'); @@ -57,4 +58,4 @@ include('chn/hud/backlogmenu.lua'); include('chn/hud/sysmesboxdisplay.lua'); include('chn/hud/selectiondisplay.lua'); include('chn/hud/tipsmenu.lua'); -include('chn/hud/tipsnotification.lua'); \ No newline at end of file +include('chn/hud/tipsnotification.lua'); diff --git a/profiles/common/achievementnotification.lua b/profiles/common/achievementnotification.lua new file mode 100644 index 000000000..7d857aca2 --- /dev/null +++ b/profiles/common/achievementnotification.lua @@ -0,0 +1,16 @@ +root.AchievementNotification = { + Enabled = true, + BackgroundPath = "games/common/achievementnotification.png", + FontPath = "games/common/NotoSansCJKjp-Regular.otf", + DisplayDuration = 5.0, + FadeDuration = 0.5, + IconSize = 64.0, + IconOffset = { X = 20.0, Y = 20.0 }, + TextGap = 20.0, + TextRightPadding = 20.0, + TitleFontSize = 24.0, + DescriptionFontSize = 16.0, + TextLineGap = 6.0, + TextColor = 0xFFFFFF, + OutlineColor = 0x000000, +}; diff --git a/profiles/darling/game.lua b/profiles/darling/game.lua index f0b7dd531..3383bc04a 100644 --- a/profiles/darling/game.lua +++ b/profiles/darling/game.lua @@ -41,6 +41,7 @@ include('darling/tipssystem.lua'); include('darling/vfs.lua'); include('darling/sprites.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('darling/charset.lua'); include('darling/font.lua'); include('darling/dialogue.lua'); @@ -53,4 +54,4 @@ include('darling/hud/backlogmenu.lua'); include('darling/hud/sysmesboxdisplay.lua'); include('darling/hud/selectiondisplay.lua'); include('darling/hud/tipsmenu.lua'); -include('darling/hud/tipsnotification.lua'); \ No newline at end of file +include('darling/hud/tipsnotification.lua'); diff --git a/profiles/dash/game.lua b/profiles/dash/game.lua index 639b1c7a5..6fdfc5a1c 100644 --- a/profiles/dash/game.lua +++ b/profiles/dash/game.lua @@ -40,6 +40,7 @@ include('dash/tipssystem.lua'); include('dash/vfs.lua'); include('dash/sprites.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('dash/charset.lua'); --include('dash/font.lua'); include('dash/font-lb.lua'); @@ -55,4 +56,4 @@ include('dash/scene3d/scene3d.lua'); include('dash/hud/selectiondisplay.lua'); include('dash/hud/tipsmenu.lua'); include('dash/hud/tipsnotification.lua'); -include('dash/gamespecific.lua'); \ No newline at end of file +include('dash/gamespecific.lua'); diff --git a/profiles/mo6tw/game.lua b/profiles/mo6tw/game.lua index cdf8f3b43..57a59b475 100644 --- a/profiles/mo6tw/game.lua +++ b/profiles/mo6tw/game.lua @@ -38,6 +38,7 @@ include('mo6tw/tipssystem.lua'); include('mo6tw/vfs.lua'); include('mo6tw/sprites.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('mo6tw/charset.lua'); include('mo6tw/font.lua'); include('mo6tw/dialogue.lua'); diff --git a/profiles/mo7/game.lua b/profiles/mo7/game.lua index 6de850b0d..4fad30227 100644 --- a/profiles/mo7/game.lua +++ b/profiles/mo7/game.lua @@ -43,6 +43,7 @@ include('mo7/tipssystem.lua'); include('mo7/vfs.lua'); include('mo7/sprites.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('mo7/charset.lua'); include('mo7/font.lua'); include('mo7/dialogue.lua'); @@ -55,4 +56,4 @@ include('mo7/hud/backlogmenu.lua'); include('mo7/hud/sysmesboxdisplay.lua'); include('mo7/hud/selectiondisplay.lua'); include('mo7/hud/tipsmenu.lua'); -include('mo7/hud/tipsnotification.lua'); \ No newline at end of file +include('mo7/hud/tipsnotification.lua'); diff --git a/profiles/mo8/game.lua b/profiles/mo8/game.lua index 9e39af713..b319703c2 100644 --- a/profiles/mo8/game.lua +++ b/profiles/mo8/game.lua @@ -42,6 +42,7 @@ include('mo8/tipssystem.lua'); include('mo8/vfs.lua'); include('mo8/sprites.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('mo8/charset.lua'); include('mo8/font.lua'); --include('mo8/font-lb.lua'); @@ -57,4 +58,4 @@ include('mo8/hud/savemenu.lua'); include('mo8/hud/sysmesboxdisplay.lua'); include('mo8/hud/selectiondisplay.lua'); include('mo8/hud/tipsmenu.lua'); -include('mo8/hud/tipsnotification.lua'); \ No newline at end of file +include('mo8/hud/tipsnotification.lua'); diff --git a/profiles/rne/game.lua b/profiles/rne/game.lua index 935b6f769..06a861387 100644 --- a/profiles/rne/game.lua +++ b/profiles/rne/game.lua @@ -37,6 +37,7 @@ include('rne/tipssystem.lua'); include('rne/vfs.lua'); include('rne/sprites.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('rne/charset.lua'); --include('rne/font.lua'); include('rne/font-lb.lua'); @@ -52,4 +53,4 @@ include('rne/scene3d/scene3d.lua'); include('rne/hud/selectiondisplay.lua'); include('rne/hud/tipsmenu.lua'); include('rne/hud/tipsnotification.lua'); -include('rne/gamespecific.lua'); \ No newline at end of file +include('rne/gamespecific.lua'); diff --git a/profiles/sgps3/game.lua b/profiles/sgps3/game.lua index 587f2c676..0a7dd39bf 100644 --- a/profiles/sgps3/game.lua +++ b/profiles/sgps3/game.lua @@ -36,6 +36,7 @@ include('sgps3/tipssystem.lua'); include('sgps3/vfs.lua'); include('sgps3/sprites.lua'); include('common/animation.lua'); +include('common/achievementnotification.lua'); include('sgps3/charset.lua'); include('sgps3/font.lua'); include('sgps3/dialogue.lua'); diff --git a/src/externalfont.cpp b/src/externalfont.cpp new file mode 100644 index 000000000..f72ae555b --- /dev/null +++ b/src/externalfont.cpp @@ -0,0 +1,223 @@ +#include "font.h" + +#include "io/physicalfilestream.h" +#include "log.h" +#include "renderer/renderer.h" +#include "texture/texture.h" + +#include +#include + +#include +#include FT_FREETYPE_H + +#include +#include +#include + +namespace Impacto { + +struct ExternalFont::Impl { + struct FreeTypeLibraryDeleter { + void operator()(FT_Library library) const { + if (library != nullptr) FT_Done_FreeType(library); + } + }; + + struct FreeTypeFaceDeleter { + void operator()(FT_Face face) const { + if (face != nullptr) FT_Done_Face(face); + } + }; + + struct HarfBuzzFontDeleter { + void operator()(hb_font_t* font) const { + if (font != nullptr) hb_font_destroy(font); + } + }; + + std::unique_ptr, FreeTypeLibraryDeleter> + FreeTypeLibrary; + std::unique_ptr, FreeTypeFaceDeleter> FontFace; + std::unique_ptr HarfBuzzFace; + std::vector FontData; +}; + +ExternalFont::ExternalFont() : FontImpl(new Impl()) {} + +ExternalFont::~ExternalFont() { + Reset(); + delete FontImpl; +} + +bool ExternalFont::Load(std::string const& path, + std::string const& logContext) { + Reset(); + + Io::Stream* stream = nullptr; + IoError err = Io::PhysicalFileStream::Create(path, &stream); + if (err != IoError_OK) { + ImpLog(LogLevel::Error, LogChannel::Profile, "Could not open {:s} {:s}\n", + logContext, path); + return false; + } + + FontImpl->FontData.resize(stream->Meta.Size); + int64_t readSize = + stream->Read(FontImpl->FontData.data(), FontImpl->FontData.size()); + delete stream; + + if (readSize != static_cast(FontImpl->FontData.size())) { + ImpLog(LogLevel::Error, LogChannel::Profile, "Could not read {:s} {:s}\n", + logContext, path); + Reset(); + return false; + } + + FT_Library ftLibrary = nullptr; + if (FT_Init_FreeType(&ftLibrary) != 0) { + ImpLog(LogLevel::Error, LogChannel::Profile, + "Could not initialize FreeType for {:s}\n", logContext); + Reset(); + return false; + } + FontImpl->FreeTypeLibrary.reset(ftLibrary); + + FT_Face face = nullptr; + if (FT_New_Memory_Face( + FontImpl->FreeTypeLibrary.get(), FontImpl->FontData.data(), + static_cast(FontImpl->FontData.size()), 0, &face) != 0) { + ImpLog(LogLevel::Error, LogChannel::Profile, "Could not load {:s} {:s}\n", + logContext, path); + Reset(); + return false; + } + FontImpl->FontFace.reset(face); + + FontImpl->HarfBuzzFace.reset( + hb_ft_font_create_referenced(FontImpl->FontFace.get())); + if (!FontImpl->HarfBuzzFace) { + ImpLog(LogLevel::Error, LogChannel::Profile, + "Could not initialize HarfBuzz for {:s}\n", logContext); + Reset(); + return false; + } + + return true; +} + +void ExternalFont::Reset() { + FontImpl->HarfBuzzFace.reset(); + FontImpl->FontFace.reset(); + FontImpl->FreeTypeLibrary.reset(); + FontImpl->FontData.clear(); +} + +bool ExternalFont::IsLoaded() const { + return FontImpl->HarfBuzzFace != nullptr && FontImpl->FontFace != nullptr; +} + +std::vector ExternalFont::ShapeLine( + std::string_view text, float fontSize, float& width) { + width = 0.0f; + if (text.empty() || !IsLoaded()) return {}; + + FT_Set_Pixel_Sizes(FontImpl->FontFace.get(), 0, + static_cast(std::round(fontSize))); + hb_ft_font_changed(FontImpl->HarfBuzzFace.get()); + + hb_buffer_t* buffer = hb_buffer_create(); + hb_buffer_add_utf8(buffer, text.data(), static_cast(text.size()), 0, + static_cast(text.size())); + hb_buffer_guess_segment_properties(buffer); + hb_shape(FontImpl->HarfBuzzFace.get(), buffer, nullptr, 0); + + unsigned glyphCount = 0; + hb_glyph_info_t* glyphInfo = hb_buffer_get_glyph_infos(buffer, &glyphCount); + hb_glyph_position_t* glyphPos = + hb_buffer_get_glyph_positions(buffer, &glyphCount); + + std::vector glyphs; + glyphs.reserve(glyphCount); + for (unsigned i = 0; i < glyphCount; i++) { + ExternalFontShapedGlyph glyph{ + .GlyphIndex = glyphInfo[i].codepoint, + .Offset = {static_cast(glyphPos[i].x_offset) / 64.0f, + -static_cast(glyphPos[i].y_offset) / 64.0f}, + .Advance = {static_cast(glyphPos[i].x_advance) / 64.0f, + -static_cast(glyphPos[i].y_advance) / 64.0f}, + }; + width += glyph.Advance.x; + glyphs.push_back(glyph); + } + + hb_buffer_destroy(buffer); + return glyphs; +} + +void ExternalFont::RenderShapedLine( + std::span glyphs, float fontSize, + glm::vec2 origin, glm::vec4 tint, + std::vector& outGlyphs) { + if (!IsLoaded()) return; + + FT_Set_Pixel_Sizes(FontImpl->FontFace.get(), 0, + static_cast(std::round(fontSize))); + + glm::vec2 pen = origin; + for (ExternalFontShapedGlyph const& glyph : glyphs) { + if (FT_Load_Glyph(FontImpl->FontFace.get(), glyph.GlyphIndex, + FT_LOAD_DEFAULT) != 0) { + pen += glyph.Advance; + continue; + } + if (FT_Render_Glyph(FontImpl->FontFace.get()->glyph, + FT_RENDER_MODE_NORMAL) != 0) { + pen += glyph.Advance; + continue; + } + + FT_GlyphSlot slot = FontImpl->FontFace.get()->glyph; + FT_Bitmap const& bitmap = slot->bitmap; + if (bitmap.width == 0 || bitmap.rows == 0) { + pen += glyph.Advance; + continue; + } + + Texture texture; + texture.Init(TexFmt_U8, static_cast(bitmap.width), + static_cast(bitmap.rows)); + for (uint32_t y = 0; y < bitmap.rows; ++y) { + const uint8_t* srcRow = + bitmap.pitch >= 0 + ? bitmap.buffer + y * bitmap.pitch + : bitmap.buffer + (bitmap.rows - 1 - y) * -bitmap.pitch; + std::span dstRow = + std::span(texture.Buffer).subspan(y * bitmap.width, bitmap.width); + std::memcpy(dstRow.data(), srcRow, bitmap.width); + } + + SpriteSheet sheet{static_cast(bitmap.width), + static_cast(bitmap.rows)}; + sheet.Texture = texture.Submit(); + + glm::vec2 glyphPos = + pen + glyph.Offset + glm::vec2(slot->bitmap_left, -slot->bitmap_top); + outGlyphs.push_back({Sprite{sheet, 0, 0, static_cast(bitmap.width), + static_cast(bitmap.rows)}, + glyphPos, tint}); + + pen += glyph.Advance; + } +} + +void ExternalFont::FreeGlyphTextures(std::vector& glyphs) { + for (ExternalFontGlyph const& glyph : glyphs) { + if (glyph.GlyphSprite.Sheet.Texture != 0) { + Renderer->FreeTexture(glyph.GlyphSprite.Sheet.Texture); + } + } + glyphs.clear(); +} + +} // namespace Impacto diff --git a/src/font.h b/src/font.h index 5ae114eaa..9f82cea8a 100644 --- a/src/font.h +++ b/src/font.h @@ -1,5 +1,8 @@ #pragma once +#include +#include +#include #include #include @@ -97,4 +100,41 @@ class LBFont : public Font { } }; -} // namespace Impacto \ No newline at end of file +struct ExternalFontShapedGlyph { + uint32_t GlyphIndex; + glm::vec2 Offset; + glm::vec2 Advance; +}; + +struct ExternalFontGlyph { + Sprite GlyphSprite; + glm::vec2 Position; + glm::vec4 Tint; +}; + +class ExternalFont { + public: + ExternalFont(); + ~ExternalFont(); + + ExternalFont(ExternalFont const&) = delete; + ExternalFont& operator=(ExternalFont const&) = delete; + + bool Load(std::string const& path, std::string const& logContext); + void Reset(); + bool IsLoaded() const; + + std::vector ShapeLine(std::string_view text, + float fontSize, float& width); + void RenderShapedLine(std::span glyphs, + float fontSize, glm::vec2 origin, glm::vec4 tint, + std::vector& outGlyphs); + + static void FreeGlyphTextures(std::vector& glyphs); + + private: + struct Impl; + Impl* FontImpl; +}; + +} // namespace Impacto diff --git a/src/game.cpp b/src/game.cpp index 49cad4c77..1f671dacc 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -30,6 +30,7 @@ #include "hud/saveicondisplay.h" #include "hud/loadingdisplay.h" #include "hud/tipsnotification.h" +#include "hud/achievementnotification.h" #include "games/cclcc/systemmenu.h" #include "effects/wave.h" #include "effects/blur.h" @@ -171,6 +172,7 @@ static void Init() { Profile::ExtraMenus::Configure(); DateDisplay::Init(); TipsNotification::Init(); + AchievementNotification::Init(); // Default controls Vm::Interface::UpdatePADcustomType(0); @@ -293,6 +295,7 @@ void UpdateSystem(float dt) { SaveIconDisplay::Update(updateInterval); LoadingDisplay::Update(updateInterval); DateDisplay::Update(updateInterval); + AchievementNotification::Update(updateInterval); if (ScrWork[SW_GAMESTATE] & 5 && !GetFlag(SF_GAMEPAUSE)) { UI::GameSpecific::Update(updateInterval); @@ -654,6 +657,7 @@ void Render() { menu->Render(); } } + AchievementNotification::Render(); } if (+Profile::GameFeatures & +GameFeature::CharacterViewer) { @@ -678,4 +682,4 @@ void Render() { } // namespace Game -} // namespace Impacto \ No newline at end of file +} // namespace Impacto diff --git a/src/hud/achievementnotification.cpp b/src/hud/achievementnotification.cpp new file mode 100644 index 000000000..6994fb27b --- /dev/null +++ b/src/hud/achievementnotification.cpp @@ -0,0 +1,276 @@ +#include "achievementnotification.h" + +#include "../animation.h" +#include "../data/achievementsystem.h" +#include "../font.h" +#include "../io/physicalfilestream.h" +#include "../log.h" +#include "../profile/game.h" +#include "../profile/profile_internal.h" +#include "../renderer/renderer.h" +#include "../texture/texture.h" + +#include +#include +#include +#include + +namespace Impacto { +namespace AchievementNotification { + +static Sprite BackgroundSprite; +static ExternalFont NotificationFont; +static std::vector TextGlyphs; +static Animation FadeAnimation; +static std::string BackgroundPath; +static std::string FontPath; +static float DisplayDuration; +static float FadeDuration; +static float IconSize; +static glm::vec2 IconOffset; +static float TextGap; +static float TextRightPadding; +static float TitleFontSize; +static float DescriptionFontSize; +static float TextLineGap; +static uint32_t TextColor; +static uint32_t OutlineColor; +static float DisplayTimer = 0.0f; +static int CurrentAchievementId = -1; +static bool IsEnabled = true; +static bool IsConfigured = false; +static bool IsShowing = false; +static bool TextConfigured = false; +static std::queue NotificationQueue; + +static void FreeTextGlyphs() { ExternalFont::FreeGlyphTextures(TextGlyphs); } + +static float GetEffectiveScale() { + const float widthScale = + Profile::DesignWidth / static_cast(Profile::ResolutionWidth); + const float heightScale = + Profile::DesignHeight / static_cast(Profile::ResolutionHeight); + return std::min(widthScale, heightScale); +} + +static void LoadConfig() { + Profile::EnsurePushMemberOfType("AchievementNotification", LUA_TTABLE); + + IsEnabled = Profile::TryGetMember("Enabled").value_or(true); + BackgroundPath = Profile::EnsureGetMember("BackgroundPath"); + FontPath = Profile::EnsureGetMember("FontPath"); + DisplayDuration = Profile::EnsureGetMember("DisplayDuration"); + FadeDuration = Profile::EnsureGetMember("FadeDuration"); + IconSize = Profile::EnsureGetMember("IconSize"); + IconOffset = Profile::EnsureGetMember("IconOffset"); + TextGap = Profile::EnsureGetMember("TextGap"); + TextRightPadding = Profile::EnsureGetMember("TextRightPadding"); + TitleFontSize = Profile::EnsureGetMember("TitleFontSize"); + DescriptionFontSize = Profile::EnsureGetMember("DescriptionFontSize"); + TextLineGap = Profile::EnsureGetMember("TextLineGap"); + TextColor = Profile::EnsureGetMember("TextColor"); + OutlineColor = Profile::EnsureGetMember("OutlineColor"); + + Profile::Pop(); +} + +static bool LoadBackground(std::string const& path) { + Io::Stream* stream = nullptr; + IoError err = Io::PhysicalFileStream::Create(path, &stream); + if (err != IoError_OK) { + ImpLog(LogLevel::Error, LogChannel::Profile, + "Could not open achievement notification background {:s}\n", path); + return false; + } + + Texture texture; + if (!texture.Load(stream)) { + ImpLog(LogLevel::Error, LogChannel::TextureLoad, + "Could not load achievement notification background {:s}\n", path); + delete stream; + return false; + } + delete stream; + + SpriteSheet sheet(static_cast(texture.Width), + static_cast(texture.Height)); + sheet.Texture = texture.Submit(); + BackgroundSprite = + Sprite(sheet, 0.0f, 0.0f, sheet.DesignWidth, sheet.DesignHeight); + return true; +} + +static void BuildTextLine(std::string const& text, float fontSize, float left, + float baselineY, float availableWidth, + uint32_t color) { + float width = 0.0f; + std::vector glyphs = + NotificationFont.ShapeLine(text, fontSize, width); + if (glyphs.empty()) return; + + float finalFontSize = fontSize; + if (width > availableWidth && width > 0.0f) { + finalFontSize *= availableWidth / width; + glyphs = NotificationFont.ShapeLine(text, finalFontSize, width); + } + + const glm::vec4 textTint = RgbIntToFloat(color); + const glm::vec4 outlineTint = RgbIntToFloat(OutlineColor); + + NotificationFont.RenderShapedLine(glyphs, finalFontSize, + {left + 1.0f, baselineY + 1.0f}, + outlineTint, TextGlyphs); + NotificationFont.RenderShapedLine(glyphs, finalFontSize, {left, baselineY}, + textTint, TextGlyphs); +} + +static void BuildTextGlyphs(AchievementSystem::Achievement const* achievement, + int achievementId) { + FreeTextGlyphs(); + if (!TextConfigured || !NotificationFont.IsLoaded()) return; + + std::string title = achievement != nullptr ? achievement->Name() : ""; + std::string description = + achievement != nullptr ? achievement->Description() : ""; + if (title.empty() && description.empty()) { + title = "Achievement " + std::to_string(achievementId); + description = "Achievement System not implemented."; + } + + const float scale = GetEffectiveScale(); + const float backgroundWidth = BackgroundSprite.ScaledWidth() * scale; + const float backgroundHeight = BackgroundSprite.ScaledHeight() * scale; + const float iconRight = (IconOffset.x + IconSize) * scale; + const float textLeft = iconRight + TextGap * scale; + const float textRight = backgroundWidth - TextRightPadding * scale; + const float centerY = backgroundHeight / 2.0f; + + const float titleFontSize = TitleFontSize * scale; + const float descriptionFontSize = DescriptionFontSize * scale; + const float textLineGap = TextLineGap * scale; + const float titleBaseline = + centerY - (descriptionFontSize + textLineGap) / 2.0f; + const float descriptionBaseline = + centerY + (titleFontSize + textLineGap) / 2.0f; + const float availableWidth = textRight - textLeft; + + BuildTextLine(title, titleFontSize, textLeft, titleBaseline, availableWidth, + TextColor); + BuildTextLine(description, descriptionFontSize, textLeft, descriptionBaseline, + availableWidth, TextColor); +} + +static void StartNextNotification() { + if (NotificationQueue.empty()) return; + + CurrentAchievementId = static_cast(NotificationQueue.front()); + NotificationQueue.pop(); + + const AchievementSystem::Achievement* achievement = + AchievementSystem::GetAchievement(CurrentAchievementId); + BuildTextGlyphs(achievement, CurrentAchievementId); + + DisplayTimer = DisplayDuration; + IsShowing = true; + FadeAnimation.StartIn(true); +} + +void Init() { + IsEnabled = true; + IsConfigured = false; + DisplayTimer = 0.0f; + CurrentAchievementId = -1; + IsShowing = false; + TextConfigured = false; + NotificationQueue = {}; + FreeTextGlyphs(); + NotificationFont.Reset(); + + LoadConfig(); + if (!IsEnabled) return; + + FadeAnimation.DurationIn = FadeDuration; + FadeAnimation.DurationOut = FadeDuration; + FadeAnimation.LoopMode = AnimationLoopMode::Stop; + + if (!LoadBackground(BackgroundPath)) return; + + if (!FontPath.empty()) { + TextConfigured = + NotificationFont.Load(FontPath, "achievement notification font"); + } else { + ImpLog(LogLevel::Warning, LogChannel::Profile, + "Achievement notification font path is not configured\n"); + } + + IsConfigured = true; +} + +void Update(float dt) { + if (!IsEnabled || !IsConfigured) return; + + FadeAnimation.Update(dt); + + if (IsShowing && FadeAnimation.IsIn()) { + DisplayTimer -= dt; + if (DisplayTimer <= 0.0f) { + IsShowing = false; + FadeAnimation.StartOut(); + } + } + + if (!IsShowing && FadeAnimation.IsOut()) { + StartNextNotification(); + } +} + +void Render() { + if (!IsEnabled || !IsConfigured || FadeAnimation.IsOut()) return; + + glm::vec4 tint(1.0f); + tint.a = glm::smoothstep(0.0f, 1.0f, FadeAnimation.Progress); + + const float scale = GetEffectiveScale(); + const float backgroundWidth = BackgroundSprite.ScaledWidth() * scale; + const float backgroundHeight = BackgroundSprite.ScaledHeight() * scale; + const glm::vec2 pos = {Profile::DesignWidth - backgroundWidth, + Profile::DesignHeight - backgroundHeight}; + + Renderer->DrawSprite(BackgroundSprite, + RectF(pos.x, pos.y, backgroundWidth, backgroundHeight), + tint); + + const AchievementSystem::Achievement* achievement = + AchievementSystem::GetAchievement(CurrentAchievementId); + if (achievement != nullptr && achievement->Icon().ScaledWidth() > 0.0f && + achievement->Icon().ScaledHeight() > 0.0f) { + const Sprite& icon = achievement->Icon(); + const RectF iconDest(pos.x + IconOffset.x * scale, + pos.y + IconOffset.y * scale, IconSize * scale, + IconSize * scale); + Renderer->DrawSprite(icon, iconDest, tint); + } + + if (!TextGlyphs.empty()) { + Renderer->SetBlendMode(RendererBlendMode::Premultiplied); + for (ExternalFontGlyph const& glyph : TextGlyphs) { + glm::vec4 glyphTint = glyph.Tint; + glyphTint.a *= tint.a; + Renderer->DrawSubtitleGlyph(glyph.GlyphSprite, pos + glyph.Position, + glyphTint); + } + Renderer->SetBlendMode(RendererBlendMode::Normal); + } +} + +void Show(int achievementId) { + if (!IsEnabled || !IsConfigured) return; + + NotificationQueue.push(achievementId); + if (!IsShowing && FadeAnimation.IsOut()) { + StartNextNotification(); + } +} + +} // namespace AchievementNotification +} // namespace Impacto diff --git a/src/hud/achievementnotification.h b/src/hud/achievementnotification.h new file mode 100644 index 000000000..7a2d683a6 --- /dev/null +++ b/src/hud/achievementnotification.h @@ -0,0 +1,12 @@ +#pragma once + +namespace Impacto { +namespace AchievementNotification { + +void Init(); +void Update(float dt); +void Render(); +void Show(int achievementId); + +} // namespace AchievementNotification +} // namespace Impacto diff --git a/src/vm/inst_misc.cpp b/src/vm/inst_misc.cpp index c2229ebc2..6fd39c9ef 100644 --- a/src/vm/inst_misc.cpp +++ b/src/vm/inst_misc.cpp @@ -13,6 +13,7 @@ #include "../ui/ui.h" #include "../data/savesystem.h" #include "../audio/audiosystem.h" +#include "../hud/achievementnotification.h" #include "../profile/vm.h" #include "../games/cclcc/systemmenu.h" @@ -53,10 +54,12 @@ VmInstruction(InstSetAchievement) { PopUint8(type); if (type == 1) { PopExpression(arg1); + AchievementNotification::Show(arg1); ImpLogSlow(LogLevel::Warning, LogChannel::VMStub, "STUB instruction Achievement(type: {:d}, arg1: {:d})\n", type, arg1); } else { + AchievementNotification::Show(type); ImpLogSlow(LogLevel::Warning, LogChannel::VMStub, "STUB instruction Achievement(type: {:d})\n", type); } @@ -652,4 +655,4 @@ VmInstruction(InstExitGame) { } // namespace Vm -} // namespace Impacto \ No newline at end of file +} // namespace Impacto diff --git a/vcpkg.json b/vcpkg.json index b154f3227..faac99501 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -41,6 +41,8 @@ }, "libwebp", "fmt", + "freetype", + "harfbuzz", "libass" ], "overrides": [ @@ -53,4 +55,4 @@ "version": "2.32.10" } ] -} \ No newline at end of file +}