Browse Source

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%.
pull/5084/head
Gleb Mazovetskiy 4 years ago
parent
commit
0cd035ef61
  1. 1
      Source/CMakeLists.txt
  2. 5
      Source/DiabloUI/art_draw.cpp
  3. 2
      Source/DiabloUI/art_draw.h
  4. 16
      Source/DiabloUI/button.cpp
  5. 4
      Source/DiabloUI/button.h
  6. 12
      Source/DiabloUI/credits.cpp
  7. 133
      Source/DiabloUI/diabloui.cpp
  8. 11
      Source/DiabloUI/diabloui.h
  9. 22
      Source/DiabloUI/dialogs.cpp
  10. 2
      Source/DiabloUI/mainmenu.cpp
  11. 24
      Source/DiabloUI/progress.cpp
  12. 12
      Source/DiabloUI/scrollbar.cpp
  13. 20
      Source/DiabloUI/scrollbar.h
  14. 2
      Source/DiabloUI/selgame.cpp
  15. 10
      Source/DiabloUI/selhero.cpp
  16. 10
      Source/DiabloUI/selstart.cpp
  17. 2
      Source/DiabloUI/settingsmenu.cpp
  18. 14
      Source/DiabloUI/title.cpp
  19. 128
      Source/DiabloUI/ui_item.h
  20. 55
      Source/engine/cel_sprite.hpp
  21. 87
      Source/engine/load_pcx.cpp
  22. 17
      Source/engine/load_pcx.hpp
  23. 16
      Source/engine/render/cl2_render.hpp
  24. 19
      Source/engine/render/text_render.cpp
  25. 14
      Source/interfac.cpp
  26. 266
      Source/utils/pcx_to_cl2.cpp
  27. 21
      Source/utils/pcx_to_cl2.hpp

1
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

5
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;

2
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

16
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<OwnedPcxSpriteSheet> ButtonSprites;
std::optional<OwnedCelSpriteSheetWithFrameHeight> 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()) {

4
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);

12
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");
}

133
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<OwnedPcxSpriteSheet> ArtLogo;
std::optional<OwnedCelSpriteSheetWithFrameHeight> ArtLogo;
std::array<std::optional<OwnedPcxSpriteSheet>, 3> ArtFocus;
std::array<std::optional<OwnedCelSpriteSheetWithFrameHeight>, 3> ArtFocus;
std::optional<OwnedPcxSprite> ArtBackgroundWidescreen;
std::optional<OwnedPcxSpriteSheet> ArtBackground;
std::optional<OwnedCelSpriteWithFrameHeight> ArtBackgroundWidescreen;
std::optional<OwnedCelSpriteSheetWithFrameHeight> ArtBackground;
Art ArtCursor;
bool textInputActive = true;
@ -58,9 +57,9 @@ std::size_t SelectedItem = 0;
namespace {
std::optional<OwnedPcxSpriteSheet> ArtHero;
std::optional<OwnedCelSpriteSheetWithFrameHeight> ArtHero;
std::array<uint8_t, enum_size<HeroClass>::value + 1> ArtHeroPortraitOrder;
std::array<std::optional<OwnedPcxSprite>, enum_size<HeroClass>::value + 1> ArtHeroOverrides;
std::array<std::optional<OwnedCelSpriteWithFrameHeight>, enum_size<HeroClass>::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<OwnedPcxSprite> 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<int>(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<devilution::OwnedPcxSprite> &override : ArtHeroOverrides)
for (std::optional<devilution::OwnedCelSpriteWithFrameHeight> &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<uint16_t>(frames), /*transparentColor=*/std::nullopt, pPal);
ArtBackground = LoadPcxSpriteSheetAsCl2(pszFile, static_cast<uint16_t>(frames), /*transparentColor=*/std::nullopt, pPal);
if (!ArtBackground)
return;
@ -712,17 +710,17 @@ void UiAddBackground(std::vector<std::unique_ptr<UiItemBase>> *vecDialog)
{
const SDL_Rect rect = MakeSdlRect(0, GetUIRectangle().position.y, 0, 0);
if (ArtBackgroundWidescreen) {
vecDialog->push_back(std::make_unique<UiImagePcx>(PcxSprite { *ArtBackgroundWidescreen }, rect, UiFlags::AlignCenter));
vecDialog->push_back(std::make_unique<UiImageCl2>(ArtBackgroundWidescreen->sprite(), rect, UiFlags::AlignCenter));
}
if (ArtBackground) {
vecDialog->push_back(std::make_unique<UiImagePcx>(PcxSpriteSheet { *ArtBackground }.sprite(0), rect, UiFlags::AlignCenter));
vecDialog->push_back(std::make_unique<UiImageCl2>(ArtBackground->sprite(0), rect, UiFlags::AlignCenter));
}
}
void UiAddLogo(std::vector<std::unique_ptr<UiItemBase>> *vecDialog)
{
vecDialog->push_back(std::make_unique<UiImageAnimatedPcx>(
PcxSpriteSheet { *ArtLogo }, MakeSdlRect(0, GetUIRectangle().position.y, 0, 0), UiFlags::AlignCenter));
vecDialog->push_back(std::make_unique<UiImageAnimatedCl2>(
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<int>(sprite.height())) / 2;
const int y = rect.y + (rect.h - static_cast<int>(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<uint16_t>(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<uint16_t>(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<UiArtText *>(item));
break;
case UiType::Image:
Render(static_cast<UiImage *>(item));
break;
case UiType::ImageCel:
Render(static_cast<UiImageCel *>(item));
break;
case UiType::ImagePcx:
Render(static_cast<UiImagePcx *>(item));
case UiType::ImageCl2:
Render(static_cast<UiImageCl2 *>(item));
break;
case UiType::ImageAnimatedPcx:
Render(static_cast<UiImageAnimatedPcx *>(item));
case UiType::ImageAnimatedCl2:
Render(static_cast<UiImageAnimatedCl2 *>(item));
break;
case UiType::ArtTextButton:
Render(static_cast<UiArtTextButton *>(item));

11
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<OwnedPcxSpriteSheet> ArtLogo;
extern std::array<std::optional<OwnedPcxSpriteSheet>, 3> ArtFocus;
extern std::optional<OwnedPcxSprite> ArtBackgroundWidescreen;
extern std::optional<OwnedPcxSpriteSheet> ArtBackground;
extern std::optional<OwnedCelSpriteSheetWithFrameHeight> ArtLogo;
extern std::array<std::optional<OwnedCelSpriteSheetWithFrameHeight>, 3> ArtFocus;
extern std::optional<OwnedCelSpriteWithFrameHeight> ArtBackgroundWidescreen;
extern std::optional<OwnedCelSpriteSheetWithFrameHeight> ArtBackground;
extern Art ArtCursor;
extern bool (*gfnHeroInfo)(bool (*fninfofunc)(_uiheroinfo *));
@ -108,7 +107,7 @@ void UiPollAndRender(std::function<bool(SDL_Event &)> eventHandler = nullptr);
void UiRenderItems(const std::vector<UiItemBase *> &items);
void UiRenderItems(const std::vector<std::unique_ptr<UiItemBase>> &items);
void UiInitList_clear();
PcxSprite UiGetHeroDialogSprite(size_t heroClassIndex);
CelFrameWithHeight UiGetHeroDialogSprite(size_t heroClassIndex);
void mainmenu_restart_repintro();
} // namespace devilution

22
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<OwnedPcxSprite> dialogPcx;
std::optional<OwnedCelSpriteWithFrameHeight> dialogPcx;
std::string wrappedText;
bool dialogEnd;
@ -34,22 +34,22 @@ void DialogActionOK()
std::vector<std::unique_ptr<UiItemBase>> vecNULL;
std::vector<std::unique_ptr<UiItemBase>> vecOkDialog;
std::optional<PcxSprite> LoadDialogSprite(bool hasCaption, bool isError)
std::optional<CelFrameWithHeight> 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<PcxSprite> dialogSprite = LoadDialogSprite(!caption.empty(), error);
std::optional<CelFrameWithHeight> 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<UiImagePcx>(*dialogSprite, rect1));
vecOkDialog.push_back(std::make_unique<UiImageCl2>(*dialogSprite, rect1));
SDL_Rect rect2 = MakeSdlRect(uiPosition.x + 200, uiPosition.y + 211, textWidth, 80);
vecOkDialog.push_back(std::make_unique<UiText>(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<UiButton>(_("OK"), &DialogActionOK, rect3));
} else {
SDL_Rect rect1 = MakeSdlRect(uiPosition.x + 127, uiPosition.y + 100, dialogSprite->width(), dialogSprite->height());
vecOkDialog.push_back(std::make_unique<UiImagePcx>(*dialogSprite, rect1));
vecOkDialog.push_back(std::make_unique<UiImageCl2>(*dialogSprite, rect1));
SDL_Rect rect2 = MakeSdlRect(uiPosition.x + 147, uiPosition.y + 110, textWidth, 20);
vecOkDialog.push_back(std::make_unique<UiText>(caption, rect2, UiFlags::AlignCenter | UiFlags::ColorDialogYellow));

2
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");

24
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<OwnedPcxSprite> ArtPopupSm;
std::optional<OwnedPcxSprite> ArtProgBG;
std::optional<OwnedPcxSprite> ProgFil;
std::optional<OwnedCelSpriteWithFrameHeight> ArtPopupSm;
std::optional<OwnedCelSpriteWithFrameHeight> ArtProgBG;
std::optional<OwnedCelSpriteWithFrameHeight> ProgFil;
std::vector<std::unique_ptr<UiItemBase>> 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 });
}

12
Source/DiabloUI/scrollbar.cpp

@ -4,15 +4,15 @@
namespace devilution {
std::optional<OwnedPcxSprite> ArtScrollBarBackground;
std::optional<OwnedPcxSprite> ArtScrollBarThumb;
std::optional<OwnedPcxSpriteSheet> ArtScrollBarArrow;
std::optional<OwnedCelSpriteWithFrameHeight> ArtScrollBarBackground;
std::optional<OwnedCelSpriteWithFrameHeight> ArtScrollBarThumb;
std::optional<OwnedCelSpriteSheetWithFrameHeight> 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()

20
Source/DiabloUI/scrollbar.h

@ -3,13 +3,13 @@
#include <cstdint>
#include "DiabloUI/ui_item.h"
#include "engine/pcx_sprite.hpp"
#include "engine/cel_sprite.hpp"
namespace devilution {
extern std::optional<OwnedPcxSprite> ArtScrollBarBackground;
extern std::optional<OwnedPcxSprite> ArtScrollBarThumb;
extern std::optional<OwnedPcxSpriteSheet> ArtScrollBarArrow;
extern std::optional<OwnedCelSpriteWithFrameHeight> ArtScrollBarBackground;
extern std::optional<OwnedCelSpriteWithFrameHeight> ArtScrollBarThumb;
extern std::optional<OwnedCelSpriteSheetWithFrameHeight> 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<Uint16>(sb->m_arrow.frameHeight());
Tmp.h = static_cast<Uint16>(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<Sint16>(sb->m_rect.y + sb->m_rect.h - sb->m_arrow.frameHeight());
Tmp.y = static_cast<Sint16>(sb->m_rect.y + sb->m_rect.h - sb->m_arrow.frameHeight);
Tmp.w = SCROLLBAR_ARROW_WIDTH,
Tmp.h = static_cast<Uint16>(sb->m_arrow.frameHeight());
Tmp.h = static_cast<Uint16>(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<Sint16>(sb->m_rect.y + sb->m_arrow.frameHeight());
Tmp.y = static_cast<Sint16>(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<Sint16>(sb->m_rect.x + THUMB_OFFSET_X);
Tmp.y = static_cast<Sint16>(sb->m_rect.y + sb->m_arrow.frameHeight() + thumb_y);
Tmp.y = static_cast<Sint16>(sb->m_rect.y + sb->m_arrow.frameHeight + thumb_y);
Tmp.w = static_cast<Uint16>(sb->m_rect.w - THUMB_OFFSET_X);
Tmp.h = static_cast<Uint16>(sb->m_thumb.height());

2
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<UiScrollbar>(PcxSprite { *ArtScrollBarBackground }, PcxSprite { *ArtScrollBarThumb }, PcxSpriteSheet { *ArtScrollBarArrow }, rectScrollbar));
vecSelGameDialog.push_back(std::make_unique<UiScrollbar>(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<UiArtText>(_(ConnectionNames[provider]).data(), rect1, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiSilver, 3));

10
Source/DiabloUI/selhero.cpp

@ -45,7 +45,7 @@ std::vector<std::unique_ptr<UiItemBase>> vecSelHeroDialog;
std::vector<std::unique_ptr<UiListItem>> vecSelHeroDlgItems;
std::vector<std::unique_ptr<UiItemBase>> 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<UiImagePcx>(
PcxSpriteSheet { *ArtBackground }.sprite(0),
std::make_unique<UiImageCl2>(
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<UiArtText>(&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<UiImagePcx>(UiGetHeroDialogSprite(0), rect2, UiFlags::None);
auto heroImg = std::make_unique<UiImageCl2>(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<UiList>(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<UiScrollbar>(PcxSprite { *ArtScrollBarBackground }, PcxSprite { *ArtScrollBarThumb }, PcxSpriteSheet { *ArtScrollBarArrow }, rect2));
vecSelDlgItems.push_back(std::make_unique<UiScrollbar>(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<UiArtTextButton>(_("OK"), &UiFocusNavigationSelect, rect3, UiFlags::AlignCenter | UiFlags::FontSize30 | UiFlags::ColorUiGold));

10
Source/DiabloUI/selstart.cpp

@ -15,8 +15,6 @@ bool endMenu;
std::vector<std::unique_ptr<UiListItem>> vecDialogItems;
std::vector<std::unique_ptr<UiItemBase>> vecDialog;
Art artLogo;
void ItemSelected(int value)
{
auto option = static_cast<StartUpGameMode>(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<UiImage>(&artLogo, rect, UiFlags::AlignCenter, /*bAnimated=*/true));
vecDialogItems.push_back(std::make_unique<UiListItem>(_("Enter Hellfire"), static_cast<int>(StartUpGameMode::Hellfire)));
vecDialogItems.push_back(std::make_unique<UiListItem>(_("Switch to Diablo"), static_cast<int>(StartUpGameMode::Diablo)));
vecDialog.push_back(std::make_unique<UiList>(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();

2
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<UiArtText>(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<UiScrollbar>(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<UiScrollbar>(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<UiArtText>(optionDescription, MakeSdlRect(rectDescription), UiFlags::FontSize12 | UiFlags::ColorUiSilverDark | UiFlags::AlignCenter, 1, IsSmallFontTall() ? 22 : 18));
size_t itemToSelect = 1;

14
Source/DiabloUI/title.cpp

@ -11,7 +11,7 @@
namespace devilution {
namespace {
std::optional<OwnedPcxSpriteSheet> DiabloTitleLogo;
std::optional<OwnedCelSpriteSheetWithFrameHeight> DiabloTitleLogo;
std::vector<std::unique_ptr<UiItemBase>> 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<UiImagePcx>(PcxSprite { *ArtBackgroundWidescreen }, rect, UiFlags::AlignCenter));
vecTitleScreen.push_back(std::make_unique<UiImageAnimatedPcx>(PcxSpriteSheet { *ArtBackground }, rect, UiFlags::AlignCenter));
vecTitleScreen.push_back(std::make_unique<UiImageCl2>(ArtBackgroundWidescreen->sprite(), rect, UiFlags::AlignCenter));
vecTitleScreen.push_back(std::make_unique<UiImageAnimatedCl2>(ArtBackground->sheet(), rect, UiFlags::AlignCenter));
} else {
UiAddBackground(&vecTitleScreen);
vecTitleScreen.push_back(std::make_unique<UiImageAnimatedPcx>(
PcxSpriteSheet { *DiabloTitleLogo }, MakeSdlRect(0, uiPosition.y + 182, 0, 0), UiFlags::AlignCenter));
vecTitleScreen.push_back(std::make_unique<UiImageAnimatedCl2>(
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<UiArtText>(_("Copyright © 1996-2001 Blizzard Entertainment").data(), rect, UiFlags::AlignCenter | UiFlags::FontSize24 | UiFlags::ColorUiSilver));

128
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;
};
//=============================================================================

55
Source/engine/cel_sprite.hpp

@ -4,6 +4,7 @@
#include <utility>
#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

87
Source/engine/load_pcx.cpp

@ -4,12 +4,16 @@
#include <memory>
#include <utility>
#ifdef DEBUG_PCX_TO_CL2_SIZE
#include <iostream>
#endif
#include <SDL.h>
#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<OwnedPcxSprite> LoadPcxAsset(const char *path, std::optional<uint8_t> transparentColor, SDL_Color *outPalette)
namespace {
std::optional<OwnedCelSpriteWithFrameHeight> LoadPcxSpriteAsCl2(const char *filename, int numFramesOrFrameHeight, std::optional<uint8_t> 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<OwnedPcxSprite> LoadPcxAsset(SDL_RWops *handle, std::optional<uint8_t> 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<uint8_t[]> 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<OwnedCelSpriteWithFrameHeight> result = PcxToCl2(handle, numFramesOrFrameHeight, transparentColor, outPalette);
if (!result)
return std::nullopt;
return result;
}
SDL_RWclose(handle);
} // namespace
return OwnedPcxSprite { std::move(fileBuffer), static_cast<uint16_t>(width), static_cast<uint16_t>(height), transparentColor };
std::optional<OwnedCelSpriteSheetWithFrameHeight> LoadPcxSpriteSheetAsCl2(const char *filename, int numFramesOrFrameHeight, std::optional<uint8_t> transparentColor, SDL_Color *outPalette)
{
std::optional<OwnedCelSpriteWithFrameHeight> result = LoadPcxSpriteAsCl2(filename, numFramesOrFrameHeight, transparentColor, outPalette);
return OwnedCelSpriteSheetWithFrameHeight { std::move(result->ownedSprite), result->frameHeight };
}
std::optional<OwnedPcxSpriteSheet> LoadPcxSpriteSheetAsset(const char *path, uint16_t numFrames, std::optional<uint8_t> transparentColor, SDL_Color *palette)
std::optional<OwnedCelSpriteWithFrameHeight> LoadPcxAsCl2(const char *filename, std::optional<uint8_t> transparentColor, SDL_Color *outPalette)
{
std::optional<OwnedPcxSprite> 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

17
Source/engine/load_pcx.hpp

@ -4,13 +4,22 @@
#include <SDL.h>
#include "engine/pcx_sprite.hpp"
#include "engine/cel_sprite.hpp"
#include "utils/stdcompat/optional.hpp"
namespace devilution {
std::optional<OwnedPcxSprite> LoadPcxAsset(const char *path, std::optional<uint8_t> transparentColor = std::nullopt, SDL_Color *outPalette = nullptr);
std::optional<OwnedPcxSprite> LoadPcxAsset(SDL_RWops *handle, std::optional<uint8_t> transparentColor = std::nullopt, SDL_Color *outPalette = nullptr);
std::optional<OwnedPcxSpriteSheet> LoadPcxSpriteSheetAsset(const char *path, uint16_t numFrames, std::optional<uint8_t> 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<OwnedCelSpriteSheetWithFrameHeight>
*/
std::optional<OwnedCelSpriteSheetWithFrameHeight> LoadPcxSpriteSheetAsCl2(const char *filename, int numFramesOrFrameHeight, std::optional<uint8_t> transparentColor = std::nullopt, SDL_Color *outPalette = nullptr);
std::optional<OwnedCelSpriteWithFrameHeight> LoadPcxAsCl2(const char *filename, std::optional<uint8_t> transparentColor = std::nullopt, SDL_Color *outPalette = nullptr);
} // namespace devilution

16
Source/engine/render/cl2_render.hpp

@ -34,6 +34,14 @@ void Cl2ApplyTrans(byte *p, const std::array<uint8_t, 256> &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<int>(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<int>(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

19
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<uint32_t, std::optional<OwnedPcxSpriteSheet>> Fonts;
using Font = const OwnedCelSpriteSheetWithFrameHeight;
std::unordered_map<uint32_t, std::optional<OwnedCelSpriteSheetWithFrameHeight>> Fonts;
std::unordered_map<uint32_t, std::array<uint8_t, 256>> FontKerns;
std::array<int, 6> 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<OwnedPcxSpriteSheet> &font = Fonts[fontId];
std::optional<OwnedCelSpriteSheetWithFrameHeight> &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);
}
}

14
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<OwnedPcxSprite> ArtCutsceneWidescreen;
std::optional<OwnedCelSpriteWithFrameHeight> 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);
}

266
Source/utils/pcx_to_cl2.cpp

@ -0,0 +1,266 @@
#include "utils/pcx_to_cl2.hpp"
#include <array>
#include <cstdint>
#include <cstring>
#include <memory>
#include <vector>
#include "appfat.h"
#include "utils/endian.hpp"
#include "utils/pcx.hpp"
#include "utils/stdcompat/cstddef.hpp"
#ifdef DEBUG_PCX_TO_CL2_SIZE
#include <iomanip>
#include <iostream>
#endif
#ifdef USE_SDL1
#include "utils/sdl2_to_1_2_backports.h"
#endif
namespace devilution {
namespace {
void AppendCl2TransparentRun(unsigned width, std::vector<uint8_t> &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<uint8_t> &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<uint8_t> &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<uint8_t> &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<OwnedCelSpriteWithFrameHeight> PcxToCl2(SDL_RWops *handle, int numFramesOrFrameHeight, std::optional<uint8_t> 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<uint8_t[]> 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<uint8_t> cl2Data;
cl2Data.reserve(GetReservationSize(pixelDataSize));
cl2Data.resize(4 * (2 + static_cast<size_t>(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<uint8_t[]>(new uint8_t[static_cast<size_t>(frameHeight) * width]);
const unsigned srcSkip = width % 2;
uint8_t *dataPtr = fileBuffer.get();
for (unsigned frame = 1; frame <= numFrames; ++frame) {
WriteLE32(&cl2Data[4 * static_cast<size_t>(frame)], static_cast<uint32_t>(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<size_t>(j) * width];
for (unsigned x = 0; x < static_cast<unsigned>(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<uint16_t>(cl2Data.size() - frameHeaderPos));
break;
}
}
AppendCl2TransparentRun(transparentRunWidth, cl2Data);
}
WriteLE32(&cl2Data[4 * (1 + static_cast<size_t>(numFrames))], static_cast<uint32_t>(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<byte[]>(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<int>(cl2Data.size()) - static_cast<int>(pixelDataSize)) / ((float)pixelDataSize) * 100 << "%" << std::endl;
#endif
return OwnedCelSpriteWithFrameHeight {
OwnedCelSprite { std::move(out), static_cast<uint16_t>(width) },
static_cast<uint16_t>(frameHeight)
};
}
} // namespace devilution

21
Source/utils/pcx_to_cl2.hpp

@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
#include <SDL.h>
#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<OwnedCelSpriteWithFrameHeight> PcxToCl2(SDL_RWops *handle, int numFramesOrFrameHeight = 1, std::optional<uint8_t> transparentColor = std::nullopt, SDL_Color *outPalette = nullptr);
} // namespace devilution
Loading…
Cancel
Save