From 0cd035ef61d2d1c2254f8319cccc7ddb6a5dc24f Mon Sep 17 00:00:00 2001 From: Gleb Mazovetskiy Date: Tue, 19 Jul 2022 15:22:02 +0100 Subject: [PATCH] Load PCX as CL2 Similar to #5059, which converted CEL to CL2 at load time, we now do the same for PCX. Some size stats: https://gist.github.com/glebm/067bf1ec73f9d14514932cfe237e4e8e Notably, fonts RAM usage is decreased by ~35%. --- Source/CMakeLists.txt | 1 + Source/DiabloUI/art_draw.cpp | 5 - Source/DiabloUI/art_draw.h | 2 - Source/DiabloUI/button.cpp | 16 +- Source/DiabloUI/button.h | 4 +- Source/DiabloUI/credits.cpp | 12 +- Source/DiabloUI/diabloui.cpp | 133 +++++--------- Source/DiabloUI/diabloui.h | 11 +- Source/DiabloUI/dialogs.cpp | 22 +-- Source/DiabloUI/mainmenu.cpp | 2 +- Source/DiabloUI/progress.cpp | 24 +-- Source/DiabloUI/scrollbar.cpp | 12 +- Source/DiabloUI/scrollbar.h | 20 +- Source/DiabloUI/selgame.cpp | 2 +- Source/DiabloUI/selhero.cpp | 10 +- Source/DiabloUI/selstart.cpp | 10 +- Source/DiabloUI/settingsmenu.cpp | 2 +- Source/DiabloUI/title.cpp | 14 +- Source/DiabloUI/ui_item.h | 128 ++----------- Source/engine/cel_sprite.hpp | 55 +++++- Source/engine/load_pcx.cpp | 87 +++------ Source/engine/load_pcx.hpp | 17 +- Source/engine/render/cl2_render.hpp | 16 ++ Source/engine/render/text_render.cpp | 19 +- Source/interfac.cpp | 14 +- Source/utils/pcx_to_cl2.cpp | 266 +++++++++++++++++++++++++++ Source/utils/pcx_to_cl2.hpp | 21 +++ 27 files changed, 554 insertions(+), 371 deletions(-) create mode 100644 Source/utils/pcx_to_cl2.cpp create mode 100644 Source/utils/pcx_to_cl2.hpp diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 345a41043..c8a87b383 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -161,6 +161,7 @@ set(libdevilutionx_SRCS utils/logged_fstream.cpp utils/paths.cpp utils/pcx.cpp + utils/pcx_to_cl2.cpp utils/sdl_bilinear_scale.cpp utils/sdl_thread.cpp utils/str_cat.cpp diff --git a/Source/DiabloUI/art_draw.cpp b/Source/DiabloUI/art_draw.cpp index 77d78fecb..83f140e10 100644 --- a/Source/DiabloUI/art_draw.cpp +++ b/Source/DiabloUI/art_draw.cpp @@ -73,11 +73,6 @@ void DrawArt(const Surface &out, Point position, Art *art, int nFrame, Uint16 sr ErrSdl(); } -void DrawAnimatedArt(Art *art, Point screenPosition) -{ - DrawArt(screenPosition, art, GetAnimationFrame(art->frames)); -} - int GetAnimationFrame(int frames, int fps) { int frame = (SDL_GetTicks() / fps) % frames; diff --git a/Source/DiabloUI/art_draw.h b/Source/DiabloUI/art_draw.h index 728052f3a..138e97867 100644 --- a/Source/DiabloUI/art_draw.h +++ b/Source/DiabloUI/art_draw.h @@ -11,8 +11,6 @@ void DrawArt(Point screenPosition, Art *art, int nFrame = 0, Uint16 srcW = 0, Ui void DrawArt(const Surface &out, Point position, Art *art, int nFrame = 0, Uint16 srcW = 0, Uint16 srcH = 0); -void DrawAnimatedArt(Art *art, Point screenPosition); - int GetAnimationFrame(int frames, int fps = 60); } // namespace devilution diff --git a/Source/DiabloUI/button.cpp b/Source/DiabloUI/button.cpp index a7800716d..79f7ea20f 100644 --- a/Source/DiabloUI/button.cpp +++ b/Source/DiabloUI/button.cpp @@ -1,9 +1,9 @@ #include "DiabloUI/button.h" #include "DiabloUI/diabloui.h" +#include "engine/cel_sprite.hpp" #include "engine/load_pcx.hpp" -#include "engine/pcx_sprite.hpp" -#include "engine/render/pcx_render.hpp" +#include "engine/render/cl2_render.hpp" #include "engine/render/text_render.hpp" #include "utils/display.h" @@ -11,15 +11,15 @@ namespace devilution { namespace { -std::optional ButtonSprites; +std::optional ButtonSprites; } // namespace void LoadDialogButtonGraphics() { - ButtonSprites = LoadPcxSpriteSheetAsset("ui_art\\dvl_but_sml.pcx", 2); + ButtonSprites = LoadPcxSpriteSheetAsCl2("ui_art\\dvl_but_sml.pcx", 2); if (ButtonSprites == std::nullopt) { - ButtonSprites = LoadPcxSpriteSheetAsset("ui_art\\but_sml.pcx", 15); + ButtonSprites = LoadPcxSpriteSheetAsCl2("ui_art\\but_sml.pcx", 15); } } @@ -28,15 +28,15 @@ void FreeDialogButtonGraphics() ButtonSprites = std::nullopt; } -PcxSprite ButtonSprite(bool pressed) +CelFrameWithHeight ButtonSprite(bool pressed) { - return PcxSpriteSheet { *ButtonSprites }.sprite(pressed ? 1 : 0); + return ButtonSprites->sprite(pressed ? 1 : 0); } void RenderButton(UiButton *button) { const Surface &out = Surface(DiabloUiSurface()).subregion(button->m_rect.x, button->m_rect.y, button->m_rect.w, button->m_rect.h); - RenderPcxSprite(out, ButtonSprite(button->IsPressed()), { 0, 0 }); + RenderCl2Sprite(out, ButtonSprite(button->IsPressed()), { 0, 0 }); Rectangle textRect { { 0, 0 }, { button->m_rect.w, button->m_rect.h } }; if (!button->IsPressed()) { diff --git a/Source/DiabloUI/button.h b/Source/DiabloUI/button.h index a3151a5f7..008554074 100644 --- a/Source/DiabloUI/button.h +++ b/Source/DiabloUI/button.h @@ -1,7 +1,7 @@ #pragma once #include "DiabloUI/ui_item.h" -#include "engine/pcx_sprite.hpp" +#include "engine/cel_sprite.hpp" namespace devilution { @@ -10,7 +10,7 @@ const Uint16 DialogButtonHeight = 28; void LoadDialogButtonGraphics(); void FreeDialogButtonGraphics(); -PcxSprite ButtonSprite(bool pressed); +CelFrameWithHeight ButtonSprite(bool pressed); void RenderButton(UiButton *button); bool HandleMouseEventButton(const SDL_Event &event, UiButton *button); void HandleGlobalMouseUpButton(UiButton *button); diff --git a/Source/DiabloUI/credits.cpp b/Source/DiabloUI/credits.cpp index d906939c8..2e440c8e2 100644 --- a/Source/DiabloUI/credits.cpp +++ b/Source/DiabloUI/credits.cpp @@ -11,7 +11,7 @@ #include "controls/input.h" #include "controls/menu_controls.h" #include "engine/load_pcx.hpp" -#include "engine/render/pcx_render.hpp" +#include "engine/render/cl2_render.hpp" #include "engine/render/text_render.hpp" #include "hwcursor.hpp" #include "utils/display.h" @@ -98,8 +98,8 @@ void CreditsRenderer::Render() SDL_FillRect(DiabloUiSurface(), nullptr, 0x000000); const Point uiPosition = GetUIRectangle().position; if (ArtBackgroundWidescreen) - RenderPcxSprite(Surface(DiabloUiSurface()), PcxSprite { *ArtBackgroundWidescreen }, uiPosition - Displacement { 320, 0 }); - RenderPcxSprite(Surface(DiabloUiSurface()), PcxSpriteSheet { *ArtBackground }.sprite(0), uiPosition); + RenderCl2Sprite(Surface(DiabloUiSurface()), ArtBackgroundWidescreen->sprite(), uiPosition - Displacement { 320, 0 }); + RenderCl2Sprite(Surface(DiabloUiSurface()), ArtBackground->sprite(0), uiPosition); const std::size_t linesBegin = std::max(offsetY / LINE_H, 0); const std::size_t linesEnd = std::min(linesBegin + MAX_VISIBLE_LINES, linesToRender.size()); @@ -170,7 +170,7 @@ bool TextDialog(char const *const *text, std::size_t textLines) bool UiCreditsDialog() { - ArtBackgroundWidescreen = LoadPcxAsset("ui_art\\creditsw.pcx"); + ArtBackgroundWidescreen = LoadPcxAsCl2("ui_art\\creditsw.pcx"); LoadBackgroundArt("ui_art\\credits.pcx"); return TextDialog(CreditLines, CreditLinesSize); @@ -179,10 +179,10 @@ bool UiCreditsDialog() bool UiSupportDialog() { if (gbIsHellfire) { - ArtBackgroundWidescreen = LoadPcxAsset("ui_art\\supportw.pcx"); + ArtBackgroundWidescreen = LoadPcxAsCl2("ui_art\\supportw.pcx"); LoadBackgroundArt("ui_art\\support.pcx"); } else { - ArtBackgroundWidescreen = LoadPcxAsset("ui_art\\creditsw.pcx"); + ArtBackgroundWidescreen = LoadPcxAsCl2("ui_art\\creditsw.pcx"); LoadBackgroundArt("ui_art\\credits.pcx"); } diff --git a/Source/DiabloUI/diabloui.cpp b/Source/DiabloUI/diabloui.cpp index 8a4f6a840..437e7af50 100644 --- a/Source/DiabloUI/diabloui.cpp +++ b/Source/DiabloUI/diabloui.cpp @@ -16,13 +16,12 @@ #include "engine/cel_sprite.hpp" #include "engine/dx.h" #include "engine/load_pcx.hpp" -#include "engine/pcx_sprite.hpp" #include "engine/render/cl2_render.hpp" -#include "engine/render/pcx_render.hpp" #include "hwcursor.hpp" #include "utils/display.h" #include "utils/language.h" #include "utils/log.hpp" +#include "utils/pcx_to_cl2.hpp" #include "utils/sdl_compat.h" #include "utils/sdl_geometry.h" #include "utils/sdl_wrap.h" @@ -45,12 +44,12 @@ namespace devilution { -std::optional ArtLogo; +std::optional ArtLogo; -std::array, 3> ArtFocus; +std::array, 3> ArtFocus; -std::optional ArtBackgroundWidescreen; -std::optional ArtBackground; +std::optional ArtBackgroundWidescreen; +std::optional ArtBackground; Art ArtCursor; bool textInputActive = true; @@ -58,9 +57,9 @@ std::size_t SelectedItem = 0; namespace { -std::optional ArtHero; +std::optional ArtHero; std::array::value + 1> ArtHeroPortraitOrder; -std::array, enum_size::value + 1> ArtHeroOverrides; +std::array, enum_size::value + 1> ArtHeroOverrides; std::size_t SelectedItemMax; std::size_t ListViewportSize = 1; @@ -527,12 +526,11 @@ bool IsInsideRect(const SDL_Event &event, const SDL_Rect &rect) void LoadHeros() { - std::optional portraits = LoadPcxAsset("ui_art\\heros.pcx"); - if (!portraits) - return; constexpr unsigned PortraitHeight = 76; - const uint16_t numPortraits = PcxSprite { *portraits }.height() / PortraitHeight; - ArtHero = OwnedPcxSpriteSheet { std::move(*portraits), numPortraits }; + ArtHero = LoadPcxSpriteSheetAsCl2("ui_art\\heros.pcx", -static_cast(PortraitHeight)); + if (!ArtHero) + return; + const uint16_t numPortraits = ArtHero->sheet().numFrames(); ArtHeroPortraitOrder = { 0, 1, 2, 2, 1, 0, 3 }; if (numPortraits >= 6) { @@ -554,20 +552,20 @@ void LoadHeros() SDL_ClearError(); continue; } - ArtHeroOverrides[i] = LoadPcxAsset(handle); + ArtHeroOverrides[i] = PcxToCl2(handle); } } void LoadUiGFX() { if (gbIsHellfire) { - ArtLogo = LoadPcxSpriteSheetAsset("ui_art\\hf_logo2.pcx", /*numFrames=*/16, /*transparentColor=*/1); + ArtLogo = LoadPcxSpriteSheetAsCl2("ui_art\\hf_logo2.pcx", /*numFrames=*/16, /*transparentColor=*/0); } else { - ArtLogo = LoadPcxSpriteSheetAsset("ui_art\\smlogo.pcx", /*numFrames=*/15, /*transparentColor=*/250); + ArtLogo = LoadPcxSpriteSheetAsCl2("ui_art\\smlogo.pcx", /*numFrames=*/15, /*transparentColor=*/250); } - ArtFocus[FOCUS_SMALL] = LoadPcxSpriteSheetAsset("ui_art\\focus16.pcx", /*numFrames=*/8, /*transparentColor=*/250); - ArtFocus[FOCUS_MED] = LoadPcxSpriteSheetAsset("ui_art\\focus.pcx", /*numFrames=*/8, /*transparentColor=*/250); - ArtFocus[FOCUS_BIG] = LoadPcxSpriteSheetAsset("ui_art\\focus42.pcx", /*numFrames=*/8, /*transparentColor=*/250); + ArtFocus[FOCUS_SMALL] = LoadPcxSpriteSheetAsCl2("ui_art\\focus16.pcx", /*numFrames=*/8, /*transparentColor=*/250); + ArtFocus[FOCUS_MED] = LoadPcxSpriteSheetAsCl2("ui_art\\focus.pcx", /*numFrames=*/8, /*transparentColor=*/250); + ArtFocus[FOCUS_BIG] = LoadPcxSpriteSheetAsCl2("ui_art\\focus42.pcx", /*numFrames=*/8, /*transparentColor=*/250); LoadMaskedArt("ui_art\\cursor.pcx", &ArtCursor, 1, 0); @@ -583,17 +581,17 @@ void LoadUiGFX() } // namespace -PcxSprite UiGetHeroDialogSprite(size_t heroClassIndex) +CelFrameWithHeight UiGetHeroDialogSprite(size_t heroClassIndex) { return ArtHeroOverrides[heroClassIndex] - ? PcxSprite { *ArtHeroOverrides[heroClassIndex] } - : PcxSpriteSheet { *ArtHero }.sprite(ArtHeroPortraitOrder[heroClassIndex]); + ? ArtHeroOverrides[heroClassIndex]->sprite() + : ArtHero->sprite(ArtHeroPortraitOrder[heroClassIndex]); } void UnloadUiGFX() { ArtHero = std::nullopt; - for (std::optional &override : ArtHeroOverrides) + for (std::optional &override : ArtHeroOverrides) override = std::nullopt; ArtCursor.Unload(); for (auto &art : ArtFocus) @@ -686,7 +684,7 @@ void LoadBackgroundArt(const char *pszFile, int frames) { ArtBackground = std::nullopt; SDL_Color pPal[256]; - ArtBackground = LoadPcxSpriteSheetAsset(pszFile, static_cast(frames), /*transparentColor=*/std::nullopt, pPal); + ArtBackground = LoadPcxSpriteSheetAsCl2(pszFile, static_cast(frames), /*transparentColor=*/std::nullopt, pPal); if (!ArtBackground) return; @@ -712,17 +710,17 @@ void UiAddBackground(std::vector> *vecDialog) { const SDL_Rect rect = MakeSdlRect(0, GetUIRectangle().position.y, 0, 0); if (ArtBackgroundWidescreen) { - vecDialog->push_back(std::make_unique(PcxSprite { *ArtBackgroundWidescreen }, rect, UiFlags::AlignCenter)); + vecDialog->push_back(std::make_unique(ArtBackgroundWidescreen->sprite(), rect, UiFlags::AlignCenter)); } if (ArtBackground) { - vecDialog->push_back(std::make_unique(PcxSpriteSheet { *ArtBackground }.sprite(0), rect, UiFlags::AlignCenter)); + vecDialog->push_back(std::make_unique(ArtBackground->sprite(0), rect, UiFlags::AlignCenter)); } } void UiAddLogo(std::vector> *vecDialog) { - vecDialog->push_back(std::make_unique( - PcxSpriteSheet { *ArtLogo }, MakeSdlRect(0, GetUIRectangle().position.y, 0, 0), UiFlags::AlignCenter)); + vecDialog->push_back(std::make_unique( + ArtLogo->sheet(), MakeSdlRect(0, GetUIRectangle().position.y, 0, 0), UiFlags::AlignCenter)); } void UiFadeIn() @@ -765,20 +763,20 @@ void DrawSelector(const SDL_Rect &rect) size = FOCUS_BIG; else if (rect.h >= 30) size = FOCUS_MED; - const PcxSpriteSheet spriteSheet { *ArtFocus[size] }; - const PcxSprite sprite = spriteSheet.sprite(GetAnimationFrame(spriteSheet.numFrames())); + CelSpriteSheetWithFrameHeight sheet = ArtFocus[size]->sheet(); + const CelFrameWithHeight sprite = sheet.sprite(GetAnimationFrame(sheet.numFrames())); // TODO FOCUS_MED appares higher than the box - const int y = rect.y + (rect.h - static_cast(sprite.height())) / 2; + const int y = rect.y + (rect.h - static_cast(sprite.frameHeight)) / 2; const Surface &out = Surface(DiabloUiSurface()); - RenderPcxSprite(out, sprite, { rect.x, y }); - RenderPcxSprite(out, sprite, { rect.x + rect.w - sprite.width(), y }); + RenderCl2Sprite(out, sprite, { rect.x, y }); + RenderCl2Sprite(out, sprite, { rect.x + rect.w - sprite.width(), y }); } void UiClearScreen() { - if (!ArtBackground || gnScreenWidth > PcxSpriteSheet { *ArtBackground }.width() || gnScreenHeight > PcxSpriteSheet { *ArtBackground }.frameHeight()) + if (!ArtBackground || gnScreenWidth > ArtBackground->sprite(0).width() || gnScreenHeight > ArtBackground->frameHeight) SDL_FillRect(DiabloUiSurface(), nullptr, 0x000000); } @@ -823,51 +821,24 @@ void Render(const UiArtText *uiArtText) DrawString(out, uiArtText->GetText(), MakeRectangle(uiArtText->m_rect), uiArtText->GetFlags(), uiArtText->GetSpacing(), uiArtText->GetLineHeight()); } -void Render(const UiImage *uiImage) -{ - int x = uiImage->m_rect.x; - if (uiImage->IsCentered() && uiImage->GetArt() != nullptr) { - x += GetCenterOffset(uiImage->GetArt()->w(), uiImage->m_rect.w); - } - if (uiImage->IsAnimated()) { - DrawAnimatedArt(uiImage->GetArt(), { x, uiImage->m_rect.y }); - } else { - DrawArt({ x, uiImage->m_rect.y }, uiImage->GetArt(), uiImage->GetFrame(), uiImage->m_rect.w); - } -} - -void Render(const UiImageCel *uiImage) +void Render(const UiImageCl2 *uiImage) { - const CelSpriteWithFrameHeight &sprite = uiImage->GetSprite(); + CelFrameWithHeight sprite = uiImage->sprite(); int x = uiImage->m_rect.x; - if (uiImage->IsCentered()) { - x += GetCenterOffset(sprite.sprite.Width(), uiImage->m_rect.w); - } - if (uiImage->IsAnimated()) { - DrawAnimatedCel(sprite, { x, uiImage->m_rect.y }); - } else { - DrawCel(sprite, { x, uiImage->m_rect.y }); - } -} - -void Render(const UiImagePcx *uiImage) -{ - PcxSprite sprite = uiImage->GetSprite(); - int x = uiImage->m_rect.x; - if (uiImage->IsCentered()) { + if (uiImage->isCentered()) { x += GetCenterOffset(sprite.width(), uiImage->m_rect.w); } - RenderPcxSprite(Surface(DiabloUiSurface()), sprite, { x, uiImage->m_rect.y }); + RenderCl2Sprite(Surface(DiabloUiSurface()), sprite, { x, uiImage->m_rect.y }); } -void Render(const UiImageAnimatedPcx *uiImage) +void Render(const UiImageAnimatedCl2 *uiImage) { - PcxSprite sprite = uiImage->GetSprite(GetAnimationFrame(uiImage->NumFrames())); + CelFrameWithHeight sprite = uiImage->sprite(GetAnimationFrame(uiImage->numFrames())); int x = uiImage->m_rect.x; - if (uiImage->IsCentered()) { + if (uiImage->isCentered()) { x += GetCenterOffset(sprite.width(), uiImage->m_rect.w); } - RenderPcxSprite(Surface(DiabloUiSurface()), sprite, { x, uiImage->m_rect.y }); + RenderCl2Sprite(Surface(DiabloUiSurface()), sprite, { x, uiImage->m_rect.y }); } void Render(const UiArtTextButton *uiButton) @@ -900,12 +871,12 @@ void Render(const UiScrollbar *uiSb) // Bar background (tiled): { - const int bgY = uiSb->m_rect.y + uiSb->m_arrow.frameHeight(); + const int bgY = uiSb->m_rect.y + uiSb->m_arrow.frameHeight; const int bgH = DownArrowRect(uiSb).y - bgY; const Surface backgroundOut = out.subregion(uiSb->m_rect.x, bgY, SCROLLBAR_BG_WIDTH, bgH); int y = 0; while (y < bgH) { - RenderPcxSprite(backgroundOut, uiSb->m_bg, { 0, y }); + RenderCl2Sprite(backgroundOut, uiSb->m_bg, { 0, y }); y += uiSb->m_bg.height(); } } @@ -914,18 +885,18 @@ void Render(const UiScrollbar *uiSb) { const SDL_Rect rect = UpArrowRect(uiSb); const auto frame = static_cast(scrollBarState.upArrowPressed ? ScrollBarArrowFrame_UP_ACTIVE : ScrollBarArrowFrame_UP); - RenderPcxSprite(out.subregion(rect.x, 0, SCROLLBAR_ARROW_WIDTH, out.h()), uiSb->m_arrow.sprite(frame), { 0, rect.y }); + RenderCl2Sprite(out.subregion(rect.x, 0, SCROLLBAR_ARROW_WIDTH, out.h()), uiSb->m_arrow.sprite(frame), { 0, rect.y }); } { const SDL_Rect rect = DownArrowRect(uiSb); const auto frame = static_cast(scrollBarState.downArrowPressed ? ScrollBarArrowFrame_DOWN_ACTIVE : ScrollBarArrowFrame_DOWN); - RenderPcxSprite(out.subregion(rect.x, 0, SCROLLBAR_ARROW_WIDTH, out.h()), uiSb->m_arrow.sprite(frame), { 0, rect.y }); + RenderCl2Sprite(out.subregion(rect.x, 0, SCROLLBAR_ARROW_WIDTH, out.h()), uiSb->m_arrow.sprite(frame), { 0, rect.y }); } // Thumb: if (SelectedItemMax > 0) { const SDL_Rect rect = ThumbRect(uiSb, SelectedItem, SelectedItemMax + 1); - RenderPcxSprite(out, uiSb->m_thumb, { rect.x, rect.y }); + RenderCl2Sprite(out, uiSb->m_thumb, { rect.x, rect.y }); } } @@ -951,17 +922,11 @@ void RenderItem(UiItemBase *item) case UiType::ArtText: Render(static_cast(item)); break; - case UiType::Image: - Render(static_cast(item)); - break; - case UiType::ImageCel: - Render(static_cast(item)); - break; - case UiType::ImagePcx: - Render(static_cast(item)); + case UiType::ImageCl2: + Render(static_cast(item)); break; - case UiType::ImageAnimatedPcx: - Render(static_cast(item)); + case UiType::ImageAnimatedCl2: + Render(static_cast(item)); break; case UiType::ArtTextButton: Render(static_cast(item)); diff --git a/Source/DiabloUI/diabloui.h b/Source/DiabloUI/diabloui.h index c68651eb6..27e3b40bc 100644 --- a/Source/DiabloUI/diabloui.h +++ b/Source/DiabloUI/diabloui.h @@ -8,7 +8,6 @@ #include "DiabloUI/art.h" #include "DiabloUI/ui_item.h" #include "engine/cel_sprite.hpp" -#include "engine/pcx_sprite.hpp" #include "player.h" #include "utils/display.h" @@ -62,10 +61,10 @@ struct _uiheroinfo { bool spawned; }; -extern std::optional ArtLogo; -extern std::array, 3> ArtFocus; -extern std::optional ArtBackgroundWidescreen; -extern std::optional ArtBackground; +extern std::optional ArtLogo; +extern std::array, 3> ArtFocus; +extern std::optional ArtBackgroundWidescreen; +extern std::optional ArtBackground; extern Art ArtCursor; extern bool (*gfnHeroInfo)(bool (*fninfofunc)(_uiheroinfo *)); @@ -108,7 +107,7 @@ void UiPollAndRender(std::function eventHandler = nullptr); void UiRenderItems(const std::vector &items); void UiRenderItems(const std::vector> &items); void UiInitList_clear(); -PcxSprite UiGetHeroDialogSprite(size_t heroClassIndex); +CelFrameWithHeight UiGetHeroDialogSprite(size_t heroClassIndex); void mainmenu_restart_repintro(); } // namespace devilution diff --git a/Source/DiabloUI/dialogs.cpp b/Source/DiabloUI/dialogs.cpp index a4c6dbdb8..bf411cbff 100644 --- a/Source/DiabloUI/dialogs.cpp +++ b/Source/DiabloUI/dialogs.cpp @@ -7,10 +7,10 @@ #include "control.h" #include "controls/input.h" #include "controls/menu_controls.h" +#include "engine/cel_sprite.hpp" #include "engine/dx.h" #include "engine/load_pcx.hpp" #include "engine/palette.h" -#include "engine/pcx_sprite.hpp" #include "hwcursor.hpp" #include "utils/display.h" #include "utils/language.h" @@ -21,7 +21,7 @@ namespace devilution { namespace { -std::optional dialogPcx; +std::optional dialogPcx; std::string wrappedText; bool dialogEnd; @@ -34,22 +34,22 @@ void DialogActionOK() std::vector> vecNULL; std::vector> vecOkDialog; -std::optional LoadDialogSprite(bool hasCaption, bool isError) +std::optional LoadDialogSprite(bool hasCaption, bool isError) { constexpr uint8_t TransparentColor = 255; if (!hasCaption) { - dialogPcx = LoadPcxAsset(isError ? "ui_art\\srpopup.pcx" : "ui_art\\spopup.pcx", TransparentColor); + dialogPcx = LoadPcxAsCl2(isError ? "ui_art\\srpopup.pcx" : "ui_art\\spopup.pcx", TransparentColor); } else if (isError) { - dialogPcx = LoadPcxAsset("ui_art\\dvl_lrpopup.pcx"); + dialogPcx = LoadPcxAsCl2("ui_art\\dvl_lrpopup.pcx"); if (dialogPcx == std::nullopt) { - dialogPcx = LoadPcxAsset("ui_art\\lrpopup.pcx", /*transparentColor=*/255); + dialogPcx = LoadPcxAsCl2("ui_art\\lrpopup.pcx", /*transparentColor=*/255); } } else { - dialogPcx = LoadPcxAsset("ui_art\\lpopup.pcx", TransparentColor); + dialogPcx = LoadPcxAsCl2("ui_art\\lpopup.pcx", TransparentColor); } if (!dialogPcx) return std::nullopt; - return PcxSprite { *dialogPcx }; + return dialogPcx->sprite(); } bool Init(string_view caption, string_view text, bool error, bool renderBehind) @@ -62,7 +62,7 @@ bool Init(string_view caption, string_view text, bool error, bool renderBehind) } LoadDialogButtonGraphics(); - std::optional dialogSprite = LoadDialogSprite(!caption.empty(), error); + std::optional dialogSprite = LoadDialogSprite(!caption.empty(), error); if (!dialogSprite) return false; @@ -74,7 +74,7 @@ bool Init(string_view caption, string_view text, bool error, bool renderBehind) const Point uiPosition = GetUIRectangle().position; if (caption.empty()) { SDL_Rect rect1 = MakeSdlRect(uiPosition.x + 180, uiPosition.y + 168, dialogSprite->width(), dialogSprite->height()); - vecOkDialog.push_back(std::make_unique(*dialogSprite, rect1)); + vecOkDialog.push_back(std::make_unique(*dialogSprite, rect1)); SDL_Rect rect2 = MakeSdlRect(uiPosition.x + 200, uiPosition.y + 211, textWidth, 80); vecOkDialog.push_back(std::make_unique(wrappedText, rect2, UiFlags::AlignCenter | UiFlags::ColorDialogWhite)); @@ -82,7 +82,7 @@ bool Init(string_view caption, string_view text, bool error, bool renderBehind) vecOkDialog.push_back(std::make_unique(_("OK"), &DialogActionOK, rect3)); } else { SDL_Rect rect1 = MakeSdlRect(uiPosition.x + 127, uiPosition.y + 100, dialogSprite->width(), dialogSprite->height()); - vecOkDialog.push_back(std::make_unique(*dialogSprite, rect1)); + vecOkDialog.push_back(std::make_unique(*dialogSprite, rect1)); SDL_Rect rect2 = MakeSdlRect(uiPosition.x + 147, uiPosition.y + 110, textWidth, 20); vecOkDialog.push_back(std::make_unique(caption, rect2, UiFlags::AlignCenter | UiFlags::ColorDialogYellow)); diff --git a/Source/DiabloUI/mainmenu.cpp b/Source/DiabloUI/mainmenu.cpp index 5b4905742..94d03dc87 100644 --- a/Source/DiabloUI/mainmenu.cpp +++ b/Source/DiabloUI/mainmenu.cpp @@ -45,7 +45,7 @@ void MainmenuLoad(const char *name) if (!gbIsSpawn || gbIsHellfire) { if (gbIsHellfire) - ArtBackgroundWidescreen = LoadPcxAsset("ui_art\\mainmenuw.pcx"); + ArtBackgroundWidescreen = LoadPcxAsCl2("ui_art\\mainmenuw.pcx"); LoadBackgroundArt("ui_art\\mainmenu.pcx"); } else { LoadBackgroundArt("ui_art\\swmmenu.pcx"); diff --git a/Source/DiabloUI/progress.cpp b/Source/DiabloUI/progress.cpp index 15b1dcd0c..b8dd0854a 100644 --- a/Source/DiabloUI/progress.cpp +++ b/Source/DiabloUI/progress.cpp @@ -6,20 +6,20 @@ #include "control.h" #include "controls/input.h" #include "controls/menu_controls.h" +#include "engine/cel_sprite.hpp" #include "engine/dx.h" #include "engine/load_pcx.hpp" #include "engine/palette.h" -#include "engine/pcx_sprite.hpp" -#include "engine/render/pcx_render.hpp" +#include "engine/render/cl2_render.hpp" #include "hwcursor.hpp" #include "utils/display.h" #include "utils/language.h" namespace devilution { namespace { -std::optional ArtPopupSm; -std::optional ArtProgBG; -std::optional ProgFil; +std::optional ArtPopupSm; +std::optional ArtProgBG; +std::optional ProgFil; std::vector> vecProgress; bool endMenu; @@ -31,14 +31,14 @@ void DialogActionCancel() void ProgressLoadBackground() { UiLoadBlackBackground(); - ArtPopupSm = LoadPcxAsset("ui_art\\spopup.pcx"); - ArtProgBG = LoadPcxAsset("ui_art\\prog_bg.pcx"); + ArtPopupSm = LoadPcxAsCl2("ui_art\\spopup.pcx"); + ArtProgBG = LoadPcxAsCl2("ui_art\\prog_bg.pcx"); } void ProgressLoadForeground() { LoadDialogButtonGraphics(); - ProgFil = LoadPcxAsset("ui_art\\prog_fil.pcx"); + ProgFil = LoadPcxAsCl2("ui_art\\prog_fil.pcx"); const Point uiPosition = GetUIRectangle().position; SDL_Rect rect3 = { (Sint16)(uiPosition.x + 265), (Sint16)(uiPosition.y + 267), DialogButtonWidth, DialogButtonHeight }; @@ -70,8 +70,8 @@ void ProgressRenderBackground() const Surface &out = Surface(DiabloUiSurface()); const Point position = GetPosition(); - RenderPcxSprite(out.subregion(position.x, position.y, 280, 140), PcxSprite { *ArtPopupSm }, { 0, 0 }); - RenderPcxSprite(out.subregion(GetCenterOffset(227), 0, 227, out.h()), PcxSprite { *ArtProgBG }, { 0, position.y + 52 }); + RenderCl2Sprite(out.subregion(position.x, position.y, 280, 140), ArtPopupSm->sprite(), { 0, 0 }); + RenderCl2Sprite(out.subregion(GetCenterOffset(227), 0, 227, out.h()), ArtProgBG->sprite(), { 0, position.y + 52 }); } void ProgressRenderForeground(int progress) @@ -81,10 +81,10 @@ void ProgressRenderForeground(int progress) if (progress != 0) { const int x = GetCenterOffset(227); const int w = 227 * progress / 100; - RenderPcxSprite(out.subregion(x, 0, w, out.h()), PcxSprite { *ProgFil }, { 0, position.y + 52 }); + RenderCl2Sprite(out.subregion(x, 0, w, out.h()), ProgFil->sprite(), { 0, position.y + 52 }); } // Not rendering an actual button, only the top 2 rows of its graphics. - RenderPcxSprite( + RenderCl2Sprite( out.subregion(GetCenterOffset(110), position.y + 99, DialogButtonWidth, 2), ButtonSprite(/*pressed=*/false), { 0, 0 }); } diff --git a/Source/DiabloUI/scrollbar.cpp b/Source/DiabloUI/scrollbar.cpp index 48d09f2d4..e3a6fb69d 100644 --- a/Source/DiabloUI/scrollbar.cpp +++ b/Source/DiabloUI/scrollbar.cpp @@ -4,15 +4,15 @@ namespace devilution { -std::optional ArtScrollBarBackground; -std::optional ArtScrollBarThumb; -std::optional ArtScrollBarArrow; +std::optional ArtScrollBarBackground; +std::optional ArtScrollBarThumb; +std::optional ArtScrollBarArrow; void LoadScrollBar() { - ArtScrollBarBackground = LoadPcxAsset("ui_art\\sb_bg.pcx"); - ArtScrollBarThumb = LoadPcxAsset("ui_art\\sb_thumb.pcx"); - ArtScrollBarArrow = LoadPcxSpriteSheetAsset("ui_art\\sb_arrow.pcx", 4); + ArtScrollBarBackground = LoadPcxAsCl2("ui_art\\sb_bg.pcx"); + ArtScrollBarThumb = LoadPcxAsCl2("ui_art\\sb_thumb.pcx"); + ArtScrollBarArrow = LoadPcxSpriteSheetAsCl2("ui_art\\sb_arrow.pcx", 4); } void UnloadScrollBar() diff --git a/Source/DiabloUI/scrollbar.h b/Source/DiabloUI/scrollbar.h index 30f742448..6250238bc 100644 --- a/Source/DiabloUI/scrollbar.h +++ b/Source/DiabloUI/scrollbar.h @@ -3,13 +3,13 @@ #include #include "DiabloUI/ui_item.h" -#include "engine/pcx_sprite.hpp" +#include "engine/cel_sprite.hpp" namespace devilution { -extern std::optional ArtScrollBarBackground; -extern std::optional ArtScrollBarThumb; -extern std::optional ArtScrollBarArrow; +extern std::optional ArtScrollBarBackground; +extern std::optional ArtScrollBarThumb; +extern std::optional ArtScrollBarArrow; const Uint16 SCROLLBAR_BG_WIDTH = 25; enum ScrollBarArrowFrame : uint8_t { @@ -27,7 +27,7 @@ inline SDL_Rect UpArrowRect(const UiScrollbar *sb) Tmp.x = sb->m_rect.x; Tmp.y = sb->m_rect.y; Tmp.w = SCROLLBAR_ARROW_WIDTH; - Tmp.h = static_cast(sb->m_arrow.frameHeight()); + Tmp.h = static_cast(sb->m_arrow.frameHeight); return Tmp; } @@ -36,23 +36,23 @@ inline SDL_Rect DownArrowRect(const UiScrollbar *sb) { SDL_Rect Tmp; Tmp.x = sb->m_rect.x; - Tmp.y = static_cast(sb->m_rect.y + sb->m_rect.h - sb->m_arrow.frameHeight()); + Tmp.y = static_cast(sb->m_rect.y + sb->m_rect.h - sb->m_arrow.frameHeight); Tmp.w = SCROLLBAR_ARROW_WIDTH, - Tmp.h = static_cast(sb->m_arrow.frameHeight()); + Tmp.h = static_cast(sb->m_arrow.frameHeight); return Tmp; } inline Uint16 BarHeight(const UiScrollbar *sb) { - return sb->m_rect.h - 2 * sb->m_arrow.frameHeight(); + return sb->m_rect.h - 2 * sb->m_arrow.frameHeight; } inline SDL_Rect BarRect(const UiScrollbar *sb) { SDL_Rect Tmp; Tmp.x = sb->m_rect.x; - Tmp.y = static_cast(sb->m_rect.y + sb->m_arrow.frameHeight()); + Tmp.y = static_cast(sb->m_rect.y + sb->m_arrow.frameHeight); Tmp.w = SCROLLBAR_ARROW_WIDTH, Tmp.h = BarHeight(sb); @@ -67,7 +67,7 @@ inline SDL_Rect ThumbRect(const UiScrollbar *sb, std::size_t selected_index, std SDL_Rect Tmp; Tmp.x = static_cast(sb->m_rect.x + THUMB_OFFSET_X); - Tmp.y = static_cast(sb->m_rect.y + sb->m_arrow.frameHeight() + thumb_y); + Tmp.y = static_cast(sb->m_rect.y + sb->m_arrow.frameHeight + thumb_y); Tmp.w = static_cast(sb->m_rect.w - THUMB_OFFSET_X); Tmp.h = static_cast(sb->m_thumb.height()); diff --git a/Source/DiabloUI/selgame.cpp b/Source/DiabloUI/selgame.cpp index c2c25be83..ad5c6b09b 100644 --- a/Source/DiabloUI/selgame.cpp +++ b/Source/DiabloUI/selgame.cpp @@ -126,7 +126,7 @@ void UiInitGameSelectionList(string_view search) const Point uiPosition = GetUIRectangle().position; SDL_Rect rectScrollbar = { (Sint16)(uiPosition.x + 590), (Sint16)(uiPosition.y + 244), 25, 178 }; - vecSelGameDialog.push_back(std::make_unique(PcxSprite { *ArtScrollBarBackground }, PcxSprite { *ArtScrollBarThumb }, PcxSpriteSheet { *ArtScrollBarArrow }, rectScrollbar)); + vecSelGameDialog.push_back(std::make_unique(ArtScrollBarBackground->sprite(), ArtScrollBarThumb->sprite(), ArtScrollBarArrow->sheet(), rectScrollbar)); SDL_Rect rect1 = { (Sint16)(uiPosition.x + 24), (Sint16)(uiPosition.y + 161), 590, 35 }; vecSelGameDialog.push_back(std::make_unique(_(ConnectionNames[provider]).data(), rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); diff --git a/Source/DiabloUI/selhero.cpp b/Source/DiabloUI/selhero.cpp index 0cdc25848..9e167d770 100644 --- a/Source/DiabloUI/selhero.cpp +++ b/Source/DiabloUI/selhero.cpp @@ -45,7 +45,7 @@ std::vector> vecSelHeroDialog; std::vector> vecSelHeroDlgItems; std::vector> vecSelDlgItems; -UiImagePcx *SELHERO_DIALOG_HERO_IMG; +UiImageCl2 *SELHERO_DIALOG_HERO_IMG; void SelheroListFocus(int value); void SelheroListSelect(int value); @@ -236,8 +236,8 @@ void AddSelHeroBackground() { LoadBackgroundArt("ui_art\\selhero.pcx"); vecSelHeroDialog.insert(vecSelHeroDialog.begin(), - std::make_unique( - PcxSpriteSheet { *ArtBackground }.sprite(0), + std::make_unique( + ArtBackground->sprite(0), MakeSdlRect(0, GetUIRectangle().position.y, 0, 0), UiFlags::AlignCenter)); } @@ -450,7 +450,7 @@ void selhero_Init() vecSelHeroDialog.push_back(std::make_unique(&title, rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3)); SDL_Rect rect2 = { (Sint16)(uiPosition.x + 30), (Sint16)(uiPosition.y + 211), 180, 76 }; - auto heroImg = std::make_unique(UiGetHeroDialogSprite(0), rect2, UiFlags::None); + auto heroImg = std::make_unique(UiGetHeroDialogSprite(0), rect2, UiFlags::None); SELHERO_DIALOG_HERO_IMG = heroImg.get(); vecSelHeroDialog.push_back(std::move(heroImg)); @@ -511,7 +511,7 @@ void selhero_List_Init() vecSelDlgItems.push_back(std::make_unique(vecSelHeroDlgItems, 6, uiPosition.x + 265, (uiPosition.y + 256), 320, 26, UiFlags::AlignCenter | UiFlags::FontSize24 | UiFlags::ColorUiGold)); SDL_Rect rect2 = { (Sint16)(uiPosition.x + 585), (Sint16)(uiPosition.y + 244), 25, 178 }; - vecSelDlgItems.push_back(std::make_unique(PcxSprite { *ArtScrollBarBackground }, PcxSprite { *ArtScrollBarThumb }, PcxSpriteSheet { *ArtScrollBarArrow }, rect2)); + vecSelDlgItems.push_back(std::make_unique(ArtScrollBarBackground->sprite(), ArtScrollBarThumb->sprite(), ArtScrollBarArrow->sheet(), rect2)); SDL_Rect rect3 = { (Sint16)(uiPosition.x + 239), (Sint16)(uiPosition.y + 429), 120, 35 }; vecSelDlgItems.push_back(std::make_unique(_("OK"), &UiFocusNavigationSelect, rect3, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold)); diff --git a/Source/DiabloUI/selstart.cpp b/Source/DiabloUI/selstart.cpp index 6ef8848dc..9ad72516d 100644 --- a/Source/DiabloUI/selstart.cpp +++ b/Source/DiabloUI/selstart.cpp @@ -15,8 +15,6 @@ bool endMenu; std::vector> vecDialogItems; std::vector> vecDialog; -Art artLogo; - void ItemSelected(int value) { auto option = static_cast(vecDialogItems[value]->m_value); @@ -34,15 +32,12 @@ void EscPressed() void UiSelStartUpGameOption() { - ArtBackgroundWidescreen = LoadPcxAsset("ui_art\\mainmenuw.pcx"); + ArtBackgroundWidescreen = LoadPcxAsCl2("ui_art\\mainmenuw.pcx"); LoadBackgroundArt("ui_art\\mainmenu.pcx"); - LoadMaskedArt("ui_art\\hf_logo2.pcx", &artLogo, 16); UiAddBackground(&vecDialog); + UiAddLogo(&vecDialog); const Point uiPosition = GetUIRectangle().position; - SDL_Rect rect = MakeSdlRect(0, uiPosition.y, 0, 0); - vecDialog.push_back(std::make_unique(&artLogo, rect, UiFlags::AlignCenter, /*bAnimated=*/true)); - vecDialogItems.push_back(std::make_unique(_("Enter Hellfire"), static_cast(StartUpGameMode::Hellfire))); vecDialogItems.push_back(std::make_unique(_("Switch to Diablo"), static_cast(StartUpGameMode::Diablo))); vecDialog.push_back(std::make_unique(vecDialogItems, vecDialogItems.size(), uiPosition.x + 64, uiPosition.y + 240, 510, 43, UiFlags::AlignCenter | UiFlags::FontSize42 | UiFlags::ColorUiGold, 5)); @@ -56,7 +51,6 @@ void UiSelStartUpGameOption() UiPollAndRender(); } - artLogo.Unload(); ArtBackground = std::nullopt; ArtBackgroundWidescreen = std::nullopt; vecDialogItems.clear(); diff --git a/Source/DiabloUI/settingsmenu.cpp b/Source/DiabloUI/settingsmenu.cpp index 99edc7c12..31bf25be7 100644 --- a/Source/DiabloUI/settingsmenu.cpp +++ b/Source/DiabloUI/settingsmenu.cpp @@ -263,7 +263,7 @@ void UiSettingsMenu() string_view titleText = shownMenu == ShownMenuType::Settings ? _("Settings") : selectedOption->GetName(); vecDialog.push_back(std::make_unique(titleText.data(), MakeSdlRect(uiRectangle.position.x, uiRectangle.position.y + 161, uiRectangle.size.width, 35), UiFlags::FontSize30 | UiFlags::ColorUiSilver | UiFlags::AlignCenter, 8)); - vecDialog.push_back(std::make_unique(PcxSprite { *ArtScrollBarBackground }, PcxSprite { *ArtScrollBarThumb }, PcxSpriteSheet { *ArtScrollBarArrow }, MakeSdlRect(rectList.position.x + rectList.size.width + 5, rectList.position.y, 25, rectList.size.height))); + vecDialog.push_back(std::make_unique(ArtScrollBarBackground->sprite(), ArtScrollBarThumb->sprite(), ArtScrollBarArrow->sheet(), MakeSdlRect(rectList.position.x + rectList.size.width + 5, rectList.position.y, 25, rectList.size.height))); vecDialog.push_back(std::make_unique(optionDescription, MakeSdlRect(rectDescription), UiFlags::FontSize12 | UiFlags::ColorUiSilverDark | UiFlags::AlignCenter, 1, IsSmallFontTall() ? 22 : 18)); size_t itemToSelect = 1; diff --git a/Source/DiabloUI/title.cpp b/Source/DiabloUI/title.cpp index 6df7809d2..863423cf3 100644 --- a/Source/DiabloUI/title.cpp +++ b/Source/DiabloUI/title.cpp @@ -11,7 +11,7 @@ namespace devilution { namespace { -std::optional DiabloTitleLogo; +std::optional DiabloTitleLogo; std::vector> vecTitleScreen; @@ -19,10 +19,10 @@ void TitleLoad() { if (gbIsHellfire) { LoadBackgroundArt("ui_art\\hf_logo1.pcx", 16); - ArtBackgroundWidescreen = LoadPcxAsset("ui_art\\hf_titlew.pcx"); + ArtBackgroundWidescreen = LoadPcxAsCl2("ui_art\\hf_titlew.pcx"); } else { LoadBackgroundArt("ui_art\\title.pcx"); - DiabloTitleLogo = LoadPcxSpriteSheetAsset("ui_art\\logo.pcx", /*numFrames=*/15, /*transparentColor=*/250); + DiabloTitleLogo = LoadPcxSpriteSheetAsCl2("ui_art\\logo.pcx", /*numFrames=*/15, /*transparentColor=*/250); } } @@ -44,13 +44,13 @@ void UiTitleDialog() if (gbIsHellfire) { SDL_Rect rect = MakeSdlRect(0, uiPosition.y, 0, 0); if (ArtBackgroundWidescreen) - vecTitleScreen.push_back(std::make_unique(PcxSprite { *ArtBackgroundWidescreen }, rect, UiFlags::AlignCenter)); - vecTitleScreen.push_back(std::make_unique(PcxSpriteSheet { *ArtBackground }, rect, UiFlags::AlignCenter)); + vecTitleScreen.push_back(std::make_unique(ArtBackgroundWidescreen->sprite(), rect, UiFlags::AlignCenter)); + vecTitleScreen.push_back(std::make_unique(ArtBackground->sheet(), rect, UiFlags::AlignCenter)); } else { UiAddBackground(&vecTitleScreen); - vecTitleScreen.push_back(std::make_unique( - PcxSpriteSheet { *DiabloTitleLogo }, MakeSdlRect(0, uiPosition.y + 182, 0, 0), UiFlags::AlignCenter)); + vecTitleScreen.push_back(std::make_unique( + DiabloTitleLogo->sheet(), MakeSdlRect(0, uiPosition.y + 182, 0, 0), UiFlags::AlignCenter)); SDL_Rect rect = MakeSdlRect(uiPosition.x, uiPosition.y + 410, 640, 26); vecTitleScreen.push_back(std::make_unique(_("Copyright © 1996-2001 Blizzard Entertainment").data(), rect, UiFlags::AlignCenter | UiFlags::FontSize24 | UiFlags::ColorUiSilver)); diff --git a/Source/DiabloUI/ui_item.h b/Source/DiabloUI/ui_item.h index df1093370..2f9ed5137 100644 --- a/Source/DiabloUI/ui_item.h +++ b/Source/DiabloUI/ui_item.h @@ -8,7 +8,6 @@ #include "DiabloUI/art.h" #include "DiabloUI/ui_flags.hpp" #include "engine/cel_sprite.hpp" -#include "engine/pcx_sprite.hpp" #include "engine/render/text_render.hpp" #include "utils/enum_traits.h" #include "utils/stubs.h" @@ -19,10 +18,8 @@ enum class UiType { Text, ArtText, ArtTextButton, - Image, - ImageCel, - ImagePcx, - ImageAnimatedPcx, + ImageCl2, + ImageAnimatedCl2, Button, List, Scrollbar, @@ -92,144 +89,59 @@ private: }; //============================================================================= - -class UiImage : public UiItemBase { -public: - UiImage(Art *art, SDL_Rect rect, UiFlags flags = UiFlags::None, bool animated = false, int frame = 0) - : UiItemBase(UiType::Image, rect, flags) - , art_(art) - , animated_(animated) - , frame_(frame) - { - } - - [[nodiscard]] bool IsCentered() const - { - return HasAnyOf(GetFlags(), UiFlags::AlignCenter); - } - - [[nodiscard]] Art *GetArt() const - { - return art_; - } - - [[nodiscard]] bool IsAnimated() const - { - return animated_; - } - - [[nodiscard]] int GetFrame() const - { - return frame_; - } - - void SetFrame(int frame) - { - frame_ = frame; - } - -private: - Art *art_; - bool animated_; - int frame_; -}; - -//============================================================================= -class UiImageCel : public UiItemBase { -public: - UiImageCel(CelSpriteWithFrameHeight sprite, SDL_Rect rect, UiFlags flags = UiFlags::None, bool animated = false, int frame = 0) - : UiItemBase(UiType::ImageCel, rect, flags) - , sprite_(sprite) - , animated_(animated) - , frame_(frame) - { - } - - [[nodiscard]] bool IsCentered() const - { - return HasAnyOf(GetFlags(), UiFlags::AlignCenter); - } - - [[nodiscard]] CelSpriteWithFrameHeight GetSprite() const - { - return sprite_; - } - - [[nodiscard]] bool IsAnimated() const - { - return animated_; - } - - [[nodiscard]] int GetFrame() const - { - return frame_; - } - - void SetFrame(int frame) - { - frame_ = frame; - } - -private: - CelSpriteWithFrameHeight sprite_; - bool animated_; - int frame_; -}; - -//============================================================================= -class UiImagePcx : public UiItemBase { +class UiImageCl2 : public UiItemBase { public: - UiImagePcx(PcxSprite sprite, SDL_Rect rect, UiFlags flags = UiFlags::None) - : UiItemBase(UiType::ImagePcx, rect, flags) + UiImageCl2(CelFrameWithHeight sprite, SDL_Rect rect, UiFlags flags = UiFlags::None) + : UiItemBase(UiType::ImageCl2, rect, flags) , sprite_(sprite) { } - [[nodiscard]] bool IsCentered() const + [[nodiscard]] bool isCentered() const { return HasAnyOf(GetFlags(), UiFlags::AlignCenter); } - [[nodiscard]] PcxSprite GetSprite() const + [[nodiscard]] CelFrameWithHeight sprite() const { return sprite_; } - void setSprite(PcxSprite sprite) + void setSprite(CelFrameWithHeight sprite) { sprite_ = sprite; } private: - PcxSprite sprite_; + CelFrameWithHeight sprite_; }; //============================================================================= -class UiImageAnimatedPcx : public UiItemBase { +class UiImageAnimatedCl2 : public UiItemBase { public: - UiImageAnimatedPcx(PcxSpriteSheet sheet, SDL_Rect rect, UiFlags flags = UiFlags::None) - : UiItemBase(UiType::ImageAnimatedPcx, rect, flags) + UiImageAnimatedCl2(CelSpriteSheetWithFrameHeight sheet, SDL_Rect rect, UiFlags flags = UiFlags::None) + : UiItemBase(UiType::ImageAnimatedCl2, rect, flags) , sheet_(sheet) { } - [[nodiscard]] bool IsCentered() const + [[nodiscard]] bool isCentered() const { return HasAnyOf(GetFlags(), UiFlags::AlignCenter); } - [[nodiscard]] PcxSprite GetSprite(uint16_t frame) const + [[nodiscard]] CelFrameWithHeight sprite(uint16_t frame) const { return sheet_.sprite(frame); } - [[nodiscard]] uint16_t NumFrames() const + [[nodiscard]] uint16_t numFrames() const { return sheet_.numFrames(); } private: - PcxSpriteSheet sheet_; + CelSpriteSheetWithFrameHeight sheet_; }; //============================================================================= @@ -296,7 +208,7 @@ private: class UiScrollbar : public UiItemBase { public: - UiScrollbar(PcxSprite bg, PcxSprite thumb, PcxSpriteSheet arrow, SDL_Rect rect, UiFlags flags = UiFlags::None) + UiScrollbar(CelFrameWithHeight bg, CelFrameWithHeight thumb, CelSpriteSheetWithFrameHeight arrow, SDL_Rect rect, UiFlags flags = UiFlags::None) : UiItemBase(UiType::Scrollbar, rect, flags) , m_bg(bg) , m_thumb(thumb) @@ -305,9 +217,9 @@ public: } // private: - PcxSprite m_bg; - PcxSprite m_thumb; - PcxSpriteSheet m_arrow; + CelFrameWithHeight m_bg; + CelFrameWithHeight m_thumb; + CelSpriteSheetWithFrameHeight m_arrow; }; //============================================================================= diff --git a/Source/engine/cel_sprite.hpp b/Source/engine/cel_sprite.hpp index 11c74d36f..a62abf723 100644 --- a/Source/engine/cel_sprite.hpp +++ b/Source/engine/cel_sprite.hpp @@ -4,6 +4,7 @@ #include #include "appfat.h" +#include "engine/cel_header.hpp" #include "utils/pointer_value_union.hpp" #include "utils/stdcompat/cstddef.hpp" #include "utils/stdcompat/optional.hpp" @@ -285,9 +286,59 @@ struct CelSpriteWithFrameHeight { unsigned frameHeight; }; +struct CelFrameWithHeight { + CelSprite sprite; + uint16_t frameHeight; + uint16_t frame; + + [[nodiscard]] uint16_t width() const + { + return sprite.Width(frame); + } + [[nodiscard]] uint16_t height() const + { + return frameHeight; + } +}; + struct OwnedCelSpriteWithFrameHeight { - OwnedCelSprite sprite; - unsigned frameHeight; + OwnedCelSprite ownedSprite; + uint16_t frameHeight; + + [[nodiscard]] CelFrameWithHeight sprite() const + { + return { CelSprite { ownedSprite }, frameHeight, 0 }; + } +}; + +struct CelSpriteSheetWithFrameHeight { + CelSprite sheet; + uint16_t frameHeight; + + [[nodiscard]] unsigned numFrames() const + { + return LoadLE32(sheet.Data()); + } + + [[nodiscard]] CelFrameWithHeight sprite(uint16_t frame) const + { + return { CelSprite { sheet }, frameHeight, frame }; + } +}; + +struct OwnedCelSpriteSheetWithFrameHeight { + OwnedCelSprite ownedSprite; + uint16_t frameHeight; + + [[nodiscard]] CelSpriteSheetWithFrameHeight sheet() const + { + return CelSpriteSheetWithFrameHeight { CelSprite { ownedSprite }, frameHeight }; + } + + [[nodiscard]] CelFrameWithHeight sprite(uint16_t frame) const + { + return CelFrameWithHeight { CelSprite { ownedSprite }, frameHeight, frame }; + } }; } // namespace devilution diff --git a/Source/engine/load_pcx.cpp b/Source/engine/load_pcx.cpp index 0eedb38d7..ac6139b32 100644 --- a/Source/engine/load_pcx.cpp +++ b/Source/engine/load_pcx.cpp @@ -4,12 +4,16 @@ #include #include +#ifdef DEBUG_PCX_TO_CL2_SIZE +#include +#endif + #include -#include "appfat.h" #include "engine/assets.hpp" #include "utils/log.hpp" #include "utils/pcx.hpp" +#include "utils/pcx_to_cl2.hpp" #ifdef USE_SDL1 #include "utils/sdl2_to_1_2_backports.h" @@ -17,79 +21,34 @@ namespace devilution { -std::optional LoadPcxAsset(const char *path, std::optional transparentColor, SDL_Color *outPalette) +namespace { +std::optional LoadPcxSpriteAsCl2(const char *filename, int numFramesOrFrameHeight, std::optional transparentColor, SDL_Color *outPalette) { - SDL_RWops *handle = OpenAsset(path); + SDL_RWops *handle = OpenAsset(filename); if (handle == nullptr) { - LogError("Missing file: {}", path); - return std::nullopt; - } - return LoadPcxAsset(handle, transparentColor, outPalette); -} - -std::optional LoadPcxAsset(SDL_RWops *handle, std::optional transparentColor, SDL_Color *outPalette) -{ - int width; - int height; - uint8_t bpp; - if (!LoadPcxMeta(handle, width, height, bpp)) { - SDL_RWclose(handle); - return std::nullopt; - } - assert(bpp == 8); - - ptrdiff_t pixelDataSize = SDL_RWsize(handle); - if (pixelDataSize < 0) { - SDL_RWclose(handle); + LogError("Missing file: {}", filename); return std::nullopt; } - constexpr unsigned NumPaletteColors = 256; - constexpr unsigned PcxPaletteSize = 1 + NumPaletteColors * 3; - pixelDataSize -= PcxHeaderSize; - if (outPalette != nullptr) - pixelDataSize -= PcxPaletteSize; - - std::unique_ptr fileBuffer { new uint8_t[pixelDataSize] }; - if (SDL_RWread(handle, fileBuffer.get(), pixelDataSize, 1) == 0) { - SDL_RWclose(handle); - return std::nullopt; - } - - if (outPalette != nullptr) { - // The file has a 256 color palette that needs to be loaded. - uint8_t paletteData[PcxPaletteSize]; - if (SDL_RWread(handle, paletteData, PcxPaletteSize, 1) == 0) { - SDL_RWclose(handle); - return std::nullopt; - } - const uint8_t *dataPtr = paletteData; - [[maybe_unused]] constexpr unsigned PcxPaletteSeparator = 0x0C; - assert(*dataPtr == PcxPaletteSeparator); // sanity check the delimiter - ++dataPtr; - - SDL_Color *out = outPalette; - for (unsigned i = 0; i < NumPaletteColors; ++i) { - out->r = *dataPtr++; - out->g = *dataPtr++; - out->b = *dataPtr++; -#ifndef USE_SDL1 - out->a = SDL_ALPHA_OPAQUE; +#ifdef DEBUG_PCX_TO_CL2_SIZE + std::cout << filename; #endif - ++out; - } - } + std::optional result = PcxToCl2(handle, numFramesOrFrameHeight, transparentColor, outPalette); + if (!result) + return std::nullopt; + return result; +} - SDL_RWclose(handle); +} // namespace - return OwnedPcxSprite { std::move(fileBuffer), static_cast(width), static_cast(height), transparentColor }; +std::optional LoadPcxSpriteSheetAsCl2(const char *filename, int numFramesOrFrameHeight, std::optional transparentColor, SDL_Color *outPalette) +{ + std::optional result = LoadPcxSpriteAsCl2(filename, numFramesOrFrameHeight, transparentColor, outPalette); + return OwnedCelSpriteSheetWithFrameHeight { std::move(result->ownedSprite), result->frameHeight }; } -std::optional LoadPcxSpriteSheetAsset(const char *path, uint16_t numFrames, std::optional transparentColor, SDL_Color *palette) +std::optional LoadPcxAsCl2(const char *filename, std::optional transparentColor, SDL_Color *outPalette) { - std::optional ownedSprite = LoadPcxAsset(path, transparentColor, palette); - if (ownedSprite == std::nullopt) - return std::nullopt; - return OwnedPcxSpriteSheet { *std::move(ownedSprite), numFrames }; + return LoadPcxSpriteAsCl2(filename, 1, transparentColor, outPalette); } } // namespace devilution diff --git a/Source/engine/load_pcx.hpp b/Source/engine/load_pcx.hpp index 221e0ee53..25e7a58ee 100644 --- a/Source/engine/load_pcx.hpp +++ b/Source/engine/load_pcx.hpp @@ -4,13 +4,22 @@ #include -#include "engine/pcx_sprite.hpp" +#include "engine/cel_sprite.hpp" #include "utils/stdcompat/optional.hpp" namespace devilution { -std::optional LoadPcxAsset(const char *path, std::optional transparentColor = std::nullopt, SDL_Color *outPalette = nullptr); -std::optional LoadPcxAsset(SDL_RWops *handle, std::optional transparentColor = std::nullopt, SDL_Color *outPalette = nullptr); -std::optional LoadPcxSpriteSheetAsset(const char *path, uint16_t numFrames, std::optional transparentColor = std::nullopt, SDL_Color *outPalette = nullptr); +/** + * @brief Loads a PCX file as a CL2 sprite sheet. + * + * @param filename + * @param numFramesOrFrameHeight Pass a positive value with the number of frames, or the frame height as a negative value. + * @param transparentColor + * @param outPalette + * @return std::optional + */ +std::optional LoadPcxSpriteSheetAsCl2(const char *filename, int numFramesOrFrameHeight, std::optional transparentColor = std::nullopt, SDL_Color *outPalette = nullptr); + +std::optional LoadPcxAsCl2(const char *filename, std::optional transparentColor = std::nullopt, SDL_Color *outPalette = nullptr); } // namespace devilution diff --git a/Source/engine/render/cl2_render.hpp b/Source/engine/render/cl2_render.hpp index ffa473b50..9389a32e3 100644 --- a/Source/engine/render/cl2_render.hpp +++ b/Source/engine/render/cl2_render.hpp @@ -34,6 +34,14 @@ void Cl2ApplyTrans(byte *p, const std::array &ttbl, int numFrames) */ void Cl2Draw(const Surface &out, Point position, CelSprite cel, int frame); +/** + * @brief Same as Cl2Draw but position.y is the top of the sprite instead of the bottom. + */ +inline void RenderCl2Sprite(const Surface &out, CelFrameWithHeight cel, Point position) +{ + Cl2Draw(out, { position.x, position.y + static_cast(cel.frameHeight) - 1 }, cel.sprite, cel.frame); +} + /** * @brief Blit a solid colder shape one pixel larger than the given sprite shape, to the given buffer at the given coordinates * @param col Color index from current palette @@ -65,6 +73,14 @@ void Cl2DrawOutlineSkipColorZero(const Surface &out, uint8_t col, Point position */ void Cl2DrawTRN(const Surface &out, Point position, CelSprite cel, int frame, uint8_t *trn); +/** + * @brief Same as Cl2DrawTRN but position.y is the top of the sprite instead of the bottom. + */ +inline void RenderCl2SpriteWithTRN(const Surface &out, CelFrameWithHeight cel, Point position, uint8_t *trn) +{ + Cl2DrawTRN(out, { position.x, position.y + static_cast(cel.frameHeight) - 1 }, cel.sprite, cel.frame, trn); +} + void Cl2DrawBlendedTRN(const Surface &out, Point position, CelSprite cel, int frame, uint8_t *trn); // defined in scrollrt.cpp diff --git a/Source/engine/render/text_render.cpp b/Source/engine/render/text_render.cpp index 8ddc48445..f88eb5d10 100644 --- a/Source/engine/render/text_render.cpp +++ b/Source/engine/render/text_render.cpp @@ -22,7 +22,6 @@ #include "engine/palette.h" #include "engine/point.hpp" #include "engine/render/cl2_render.hpp" -#include "pcx_render.hpp" #include "utils/display.h" #include "utils/language.h" #include "utils/sdl_compat.h" @@ -37,8 +36,8 @@ namespace { constexpr char32_t ZWSP = U'\u200B'; // Zero-width space -using Font = const OwnedPcxSpriteSheet; -std::unordered_map> Fonts; +using Font = const OwnedCelSpriteSheetWithFrameHeight; +std::unordered_map> Fonts; std::unordered_map> FontKerns; std::array FontSizes = { 12, 24, 30, 42, 46, 22 }; @@ -181,7 +180,7 @@ uint32_t GetFontId(GameFontTables size, uint16_t row) return (size << 16) | row; } -const OwnedPcxSpriteSheet *LoadFont(GameFontTables size, text_color color, uint16_t row) +const OwnedCelSpriteSheetWithFrameHeight *LoadFont(GameFontTables size, text_color color, uint16_t row) { if (ColorTranslations[color] != nullptr && !ColorTranslationsData[color]) { ColorTranslationsData[color].emplace(); @@ -197,9 +196,9 @@ const OwnedPcxSpriteSheet *LoadFont(GameFontTables size, text_color color, uint1 char path[32]; GetFontPath(size, row, "pcx", &path[0]); - std::optional &font = Fonts[fontId]; + std::optional &font = Fonts[fontId]; constexpr unsigned NumFrames = 256; - font = LoadPcxSpriteSheetAsset(path, NumFrames, /*transparentColor=*/1); + font = LoadPcxSpriteSheetAsCl2(path, NumFrames, /*transparentColor=*/1); if (!font) { LogError("Error loading font: {}", path); return nullptr; @@ -208,13 +207,13 @@ const OwnedPcxSpriteSheet *LoadFont(GameFontTables size, text_color color, uint1 return &(*font); } -void DrawFont(const Surface &out, Point position, const OwnedPcxSpriteSheet *font, text_color color, int frame) +void DrawFont(const Surface &out, Point position, const OwnedCelSpriteSheetWithFrameHeight *font, text_color color, int frame) { - PcxSprite glyph = PcxSpriteSheet { *font }.sprite(frame); + CelFrameWithHeight glyph = font->sprite(frame); if (ColorTranslationsData[color]) { - RenderPcxSpriteWithColorMap(out, glyph, position, *ColorTranslationsData[color]); + RenderCl2SpriteWithTRN(out, glyph, position, ColorTranslationsData[color]->data()); } else { - RenderPcxSprite(out, glyph, position); + RenderCl2Sprite(out, glyph, position); } } diff --git a/Source/interfac.cpp b/Source/interfac.cpp index 5254fe92b..b68484069 100644 --- a/Source/interfac.cpp +++ b/Source/interfac.cpp @@ -15,9 +15,7 @@ #include "engine/load_cel.hpp" #include "engine/load_pcx.hpp" #include "engine/palette.h" -#include "engine/pcx_sprite.hpp" #include "engine/render/cl2_render.hpp" -#include "engine/render/pcx_render.hpp" #include "hwcursor.hpp" #include "init.h" #include "loadsave.h" @@ -44,7 +42,7 @@ const uint8_t BarColor[3] = { 138, 43, 254 }; /** The screen position of the top left corner of the progress bar. */ const int BarPos[3][2] = { { 53, 37 }, { 53, 421 }, { 53, 37 } }; -std::optional ArtCutsceneWidescreen; +std::optional ArtCutsceneWidescreen; Cutscenes PickCutscene(interface_mode uMsg) { @@ -104,7 +102,7 @@ void LoadCutsceneBackground(interface_mode uMsg) switch (PickCutscene(uMsg)) { case CutStart: - ArtCutsceneWidescreen = LoadPcxAsset("gendata\\cutstartw.pcx"); + ArtCutsceneWidescreen = LoadPcxAsCl2("gendata\\cutstartw.pcx"); celPath = "Gendata\\Cutstart.cel"; palPath = "Gendata\\Cutstart.pal"; progress_id = 1; @@ -145,13 +143,13 @@ void LoadCutsceneBackground(interface_mode uMsg) progress_id = 1; break; case CutPortal: - ArtCutsceneWidescreen = LoadPcxAsset("gendata\\cutportlw.pcx"); + ArtCutsceneWidescreen = LoadPcxAsCl2("gendata\\cutportlw.pcx"); celPath = "Gendata\\Cutportl.cel"; palPath = "Gendata\\Cutportl.pal"; progress_id = 1; break; case CutPortalRed: - ArtCutsceneWidescreen = LoadPcxAsset("gendata\\cutportrw.pcx"); + ArtCutsceneWidescreen = LoadPcxAsCl2("gendata\\cutportrw.pcx"); celPath = "Gendata\\Cutportr.cel"; palPath = "Gendata\\Cutportr.pal"; progress_id = 1; @@ -181,8 +179,8 @@ void DrawCutsceneBackground() const Rectangle &uiRectangle = GetUIRectangle(); const Surface &out = GlobalBackBuffer(); if (ArtCutsceneWidescreen) { - const PcxSprite sprite { *ArtCutsceneWidescreen }; - RenderPcxSprite(out, sprite, { uiRectangle.position.x - (sprite.width() - uiRectangle.size.width) / 2, uiRectangle.position.y }); + const CelFrameWithHeight sprite = ArtCutsceneWidescreen->sprite(); + RenderCl2Sprite(out, sprite, { uiRectangle.position.x - (sprite.width() - uiRectangle.size.width) / 2, uiRectangle.position.y }); } Cl2Draw(out, { uiRectangle.position.x, 480 - 1 + uiRectangle.position.y }, CelSprite { *sgpBackCel }, 0); } diff --git a/Source/utils/pcx_to_cl2.cpp b/Source/utils/pcx_to_cl2.cpp new file mode 100644 index 000000000..f2e6024d7 --- /dev/null +++ b/Source/utils/pcx_to_cl2.cpp @@ -0,0 +1,266 @@ +#include "utils/pcx_to_cl2.hpp" + +#include +#include +#include +#include +#include + +#include "appfat.h" +#include "utils/endian.hpp" +#include "utils/pcx.hpp" +#include "utils/stdcompat/cstddef.hpp" + +#ifdef DEBUG_PCX_TO_CL2_SIZE +#include +#include +#endif + +#ifdef USE_SDL1 +#include "utils/sdl2_to_1_2_backports.h" +#endif + +namespace devilution { + +namespace { + +void AppendCl2TransparentRun(unsigned width, std::vector &out) +{ + while (width >= 0x7F) { + out.push_back(0x7F); + width -= 0x7F; + } + if (width == 0) + return; + out.push_back(width); +} + +void AppendCl2FillRun(uint8_t color, unsigned width, std::vector &out) +{ + while (width >= 0x3F) { + out.push_back(0x80); + out.push_back(color); + width -= 0x3F; + } + if (width == 0) + return; + out.push_back(0xBF - width); + out.push_back(color); +} + +void AppendCl2PixelsRun(const uint8_t *src, unsigned width, std::vector &out) +{ + while (width >= 0x41) { + out.push_back(0xBF); + for (size_t i = 0; i < 0x41; ++i) + out.push_back(src[i]); + width -= 0x41; + src += 0x41; + } + if (width == 0) + return; + out.push_back(256 - width); + for (size_t i = 0; i < width; ++i) + out.push_back(src[i]); +} + +void AppendCl2PixelsOrFillRun(const uint8_t *src, unsigned length, std::vector &out) +{ + const uint8_t *begin = src; + const uint8_t *prevColorBegin = src; + unsigned prevColorRunLength = 1; + uint8_t prevColor = *src++; + while (--length > 0) { + const uint8_t color = *src; + if (prevColor == color) { + ++prevColorRunLength; + } else { + // A tunable parameter that decides at which minimum length we encode a fill run. + // 3 appears to be optimal for most of our data (much better than 2, rarely very slightly worse than 4). + constexpr unsigned MinFillRunLength = 3; + if (prevColorRunLength >= MinFillRunLength) { + AppendCl2PixelsRun(begin, prevColorBegin - begin, out); + AppendCl2FillRun(prevColor, prevColorRunLength, out); + begin = src; + } + prevColorBegin = src; + prevColorRunLength = 1; + prevColor = color; + } + ++src; + } + AppendCl2PixelsRun(begin, prevColorBegin - begin, out); + AppendCl2FillRun(prevColor, prevColorRunLength, out); +} + +size_t GetReservationSize(size_t pcxSize) +{ + // For the most part, CL2 is smaller than PCX, with a few exceptions. + switch (pcxSize) { + case 2352187: // ui_art\hf_logo1.pcx + return 2464867; + case 172347: // ui_art\creditsw.pcx + return 172347; + case 157275: // ui_art\credits.pcx + return 173367; + default: + return pcxSize; + } +} + +} // namespace + +std::optional PcxToCl2(SDL_RWops *handle, int numFramesOrFrameHeight, std::optional transparentColor, SDL_Color *outPalette) +{ + int width; + int height; + uint8_t bpp; + if (!LoadPcxMeta(handle, width, height, bpp)) { + SDL_RWclose(handle); + return std::nullopt; + } + assert(bpp == 8); + + unsigned numFrames; + unsigned frameHeight; + if (numFramesOrFrameHeight > 0) { + numFrames = numFramesOrFrameHeight; + frameHeight = height / numFrames; + } else { + frameHeight = -numFramesOrFrameHeight; + numFrames = height / frameHeight; + } + + ptrdiff_t pixelDataSize = SDL_RWsize(handle); + if (pixelDataSize < 0) { + SDL_RWclose(handle); + return std::nullopt; + } + + pixelDataSize -= PcxHeaderSize; + + std::unique_ptr fileBuffer { new uint8_t[pixelDataSize] }; + if (SDL_RWread(handle, fileBuffer.get(), pixelDataSize, 1) == 0) { + SDL_RWclose(handle); + return std::nullopt; + } + + // CEL header: frame count, frame offset for each frame, file size + std::vector cl2Data; + cl2Data.reserve(GetReservationSize(pixelDataSize)); + cl2Data.resize(4 * (2 + static_cast(numFrames))); + WriteLE32(cl2Data.data(), numFrames); + + // We process the PCX a whole frame at a time because the lines are reversed in CEL. + auto frameBuffer = std::unique_ptr(new uint8_t[static_cast(frameHeight) * width]); + + const unsigned srcSkip = width % 2; + uint8_t *dataPtr = fileBuffer.get(); + for (unsigned frame = 1; frame <= numFrames; ++frame) { + WriteLE32(&cl2Data[4 * static_cast(frame)], static_cast(cl2Data.size())); + + // Frame header: 5 16-bit offsets to 32-pixel height blocks. + const size_t frameHeaderPos = cl2Data.size(); + constexpr size_t FrameHeaderSize = 10; + cl2Data.resize(cl2Data.size() + FrameHeaderSize); + + // Frame header offset (first of five): + WriteLE16(&cl2Data[frameHeaderPos], FrameHeaderSize); + + for (unsigned j = 0; j < frameHeight; ++j) { + uint8_t *buffer = &frameBuffer[static_cast(j) * width]; + for (unsigned x = 0; x < static_cast(width);) { + constexpr uint8_t PcxMaxSinglePixel = 0xBF; + const uint8_t byte = *dataPtr++; + if (byte <= PcxMaxSinglePixel) { + *buffer++ = byte; + ++x; + continue; + } + constexpr uint8_t PcxRunLengthMask = 0x3F; + const uint8_t runLength = (byte & PcxRunLengthMask); + std::memset(buffer, *dataPtr++, runLength); + buffer += runLength; + x += runLength; + } + dataPtr += srcSkip; + } + + unsigned transparentRunWidth = 0; + size_t line = 0; + while (line != frameHeight) { + // Process line: + const uint8_t *src = &frameBuffer[(frameHeight - (line + 1)) * width]; + if (transparentColor) { + unsigned solidRunWidth = 0; + for (const uint8_t *srcEnd = src + width; src != srcEnd; ++src) { + if (*src == *transparentColor) { + if (solidRunWidth != 0) { + AppendCl2PixelsOrFillRun(src - transparentRunWidth - solidRunWidth, solidRunWidth, cl2Data); + solidRunWidth = 0; + } + ++transparentRunWidth; + } else { + AppendCl2TransparentRun(transparentRunWidth, cl2Data); + transparentRunWidth = 0; + ++solidRunWidth; + } + } + if (solidRunWidth != 0) { + AppendCl2PixelsOrFillRun(src - solidRunWidth, solidRunWidth, cl2Data); + } + } else { + AppendCl2PixelsOrFillRun(src, width, cl2Data); + } + + // Frame header offset: + switch (++line) { + case 32: + case 64: + case 96: + case 128: + // Finish any active transparent run to not allow it to go over an offset line boundary. + AppendCl2TransparentRun(transparentRunWidth, cl2Data); + transparentRunWidth = 0; + WriteLE16(&cl2Data[frameHeaderPos + line / 16], static_cast(cl2Data.size() - frameHeaderPos)); + break; + } + } + AppendCl2TransparentRun(transparentRunWidth, cl2Data); + } + WriteLE32(&cl2Data[4 * (1 + static_cast(numFrames))], static_cast(cl2Data.size())); + + if (outPalette != nullptr) { + [[maybe_unused]] constexpr unsigned PcxPaletteSeparator = 0x0C; + assert(*dataPtr == PcxPaletteSeparator); // PCX may not have a palette + ++dataPtr; + + for (unsigned i = 0; i < 256; ++i) { + outPalette->r = *dataPtr++; + outPalette->g = *dataPtr++; + outPalette->b = *dataPtr++; +#ifndef USE_SDL1 + outPalette->a = SDL_ALPHA_OPAQUE; +#endif + ++outPalette; + } + } + + SDL_RWclose(handle); + + // Release buffers before allocating the result array to reduce peak memory use. + frameBuffer = nullptr; + fileBuffer = nullptr; + + auto out = std::unique_ptr(new byte[cl2Data.size()]); + memcpy(&out[0], cl2Data.data(), cl2Data.size()); +#ifdef DEBUG_PCX_TO_CL2_SIZE + std::cout << "\t" << pixelDataSize << "\t" << cl2Data.size() << "\t" << std::setprecision(1) << std::fixed << (static_cast(cl2Data.size()) - static_cast(pixelDataSize)) / ((float)pixelDataSize) * 100 << "%" << std::endl; +#endif + return OwnedCelSpriteWithFrameHeight { + OwnedCelSprite { std::move(out), static_cast(width) }, + static_cast(frameHeight) + }; +} + +} // namespace devilution diff --git a/Source/utils/pcx_to_cl2.hpp b/Source/utils/pcx_to_cl2.hpp new file mode 100644 index 000000000..4e8fff316 --- /dev/null +++ b/Source/utils/pcx_to_cl2.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include + +#include "engine/cel_sprite.hpp" +#include "utils/stdcompat/optional.hpp" + +namespace devilution { + +/** @brief Loads a PCX file as a CL2 sprite. + * + * + * @param handle A non-null SDL_RWops handle. Closed by this function. + * @param numFramesOrFrameHeight Pass a positive value with the number of frames, or the frame height as a negative value. + * @param transparentColorIndex The PCX palette index of the transparent color. + */ +std::optional PcxToCl2(SDL_RWops *handle, int numFramesOrFrameHeight = 1, std::optional transparentColor = std::nullopt, SDL_Color *outPalette = nullptr); + +} // namespace devilution