You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

735 lines
18 KiB

/**
* @file interfac.cpp
*
* Implementation of load screens.
*/
#include <cstdint>
#include <optional>
#include <string>
#ifdef USE_SDL3
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_keyboard.h>
#include <SDL3/SDL_rect.h>
#include <SDL3/SDL_surface.h>
#include <SDL3/SDL_timer.h>
#include <SDL3/SDL_version.h>
#else
#include <SDL.h>
#endif
#include <expected.hpp>
#include "control.h"
#include "controls/input.h"
#include "engine/clx_sprite.hpp"
#include "engine/dx.h"
#include "engine/events.hpp"
#include "engine/load_cel.hpp"
#include "engine/load_clx.hpp"
#include "engine/palette.h"
#include "engine/render/clx_render.hpp"
#include "engine/render/primitive_render.hpp"
#include "game_mode.hpp"
#include "headless_mode.hpp"
#include "hwcursor.hpp"
#include "loadsave.h"
#include "multi.h"
#include "pfile.h"
#include "plrmsg.h"
#include "utils/log.hpp"
#include "utils/sdl_geometry.h"
#include "utils/sdl_thread.h"
#ifndef USE_SDL1
#include "controls/touch/renderers.h"
#endif
#ifdef __DJGPP__
#define LOAD_ON_MAIN_THREAD
#endif
namespace devilution {
namespace {
constexpr uint32_t MaxProgress = 534;
constexpr uint32_t ProgressStepSize = 23;
OptionalOwnedClxSpriteList sgpBackCel;
bool IsProgress;
uint32_t sgdwProgress;
int progress_id;
/** The color used for the progress bar as an index into the palette. */
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 } };
OptionalOwnedClxSpriteList ArtCutsceneWidescreen;
#ifdef USE_SDL3
SdlEventType CustomEventType = SDL_EVENT_USER;
#else
SdlEventType CustomEventType = SDL_USEREVENT;
#endif
Cutscenes GetCutSceneFromLevelType(dungeon_type type)
{
switch (type) {
case DTYPE_TOWN:
return CutTown;
case DTYPE_CATHEDRAL:
return CutLevel1;
case DTYPE_CATACOMBS:
return CutLevel2;
case DTYPE_CAVES:
return CutLevel3;
case DTYPE_HELL:
return CutLevel4;
case DTYPE_NEST:
return CutLevel6;
case DTYPE_CRYPT:
return CutLevel5;
default:
return CutLevel1;
}
}
Cutscenes PickCutscene(interface_mode uMsg)
{
switch (uMsg) {
case WM_DIABLOADGAME:
case WM_DIABNEWGAME:
return CutStart;
case WM_DIABRETOWN:
return CutTown;
case WM_DIABNEXTLVL:
case WM_DIABPREVLVL:
case WM_DIABTOWNWARP:
case WM_DIABTWARPUP: {
const int lvl = MyPlayer->plrlevel;
if (lvl == 1 && uMsg == WM_DIABNEXTLVL)
return CutTown;
if (lvl == 16 && uMsg == WM_DIABNEXTLVL)
return CutGate;
return GetCutSceneFromLevelType(GetLevelType(lvl));
5 years ago
}
case WM_DIABWARPLVL:
return CutPortal;
case WM_DIABSETLVL:
case WM_DIABRTNLVL:
if (setlvlnum == SL_BONECHAMB)
return CutLevel2;
if (setlvlnum == SL_VILEBETRAYER)
return CutPortalRed;
if (IsArenaLevel(setlvlnum)) {
if (uMsg == WM_DIABSETLVL)
return GetCutSceneFromLevelType(setlvltype);
return CutTown;
}
return CutLevel1;
default:
app_fatal("Unknown progress mode");
}
}
void LoadCutsceneBackground(interface_mode uMsg)
{
const char *celPath;
const char *palPath;
switch (PickCutscene(uMsg)) {
case CutStart:
ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutstartw.clx");
celPath = "gendata\\cutstart";
palPath = "gendata\\cutstart.pal";
progress_id = 1;
7 years ago
break;
case CutTown:
ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutttw.clx");
celPath = "gendata\\cuttt";
palPath = "gendata\\cuttt.pal";
progress_id = 1;
7 years ago
break;
case CutLevel1:
ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutl1dw.clx");
celPath = "gendata\\cutl1d";
palPath = "gendata\\cutl1d.pal";
progress_id = 0;
7 years ago
break;
case CutLevel2:
ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cut2w.clx");
celPath = "gendata\\cut2";
palPath = "gendata\\cut2.pal";
progress_id = 2;
7 years ago
break;
case CutLevel3:
ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cut3w.clx");
celPath = "gendata\\cut3";
palPath = "gendata\\cut3.pal";
7 years ago
progress_id = 1;
break;
case CutLevel4:
ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cut4w.clx");
celPath = "gendata\\cut4";
palPath = "gendata\\cut4.pal";
7 years ago
progress_id = 1;
break;
case CutLevel5:
ArtCutsceneWidescreen = LoadOptionalClx("nlevels\\cutl5w.clx");
celPath = "nlevels\\cutl5";
palPath = "nlevels\\cutl5.pal";
7 years ago
progress_id = 1;
break;
case CutLevel6:
ArtCutsceneWidescreen = LoadOptionalClx("nlevels\\cutl6w.clx");
celPath = "nlevels\\cutl6";
palPath = "nlevels\\cutl6.pal";
progress_id = 1;
7 years ago
break;
case CutPortal:
ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutportlw.clx");
celPath = "gendata\\cutportl";
palPath = "gendata\\cutportl.pal";
7 years ago
progress_id = 1;
break;
case CutPortalRed:
ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutportrw.clx");
celPath = "gendata\\cutportr";
palPath = "gendata\\cutportr.pal";
progress_id = 1;
break;
case CutGate:
ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutgatew.clx");
celPath = "gendata\\cutgate";
palPath = "gendata\\cutgate.pal";
progress_id = 1;
7 years ago
break;
}
7 years ago
assert(!sgpBackCel);
sgpBackCel = LoadCel(celPath, 640);
LoadPalette(palPath);
UpdateSystemPalette(logical_palette);
7 years ago
sgdwProgress = 0;
}
void FreeCutsceneBackground()
{
sgpBackCel = std::nullopt;
ArtCutsceneWidescreen = std::nullopt;
}
void DrawCutsceneBackground()
{
const Rectangle &uiRectangle = GetUIRectangle();
const Surface &out = GlobalBackBuffer();
#ifdef USE_SDL3
SDL_FillSurfaceRect(out.surface, nullptr, 0);
#else
SDL_FillRect(out.surface, nullptr, 0x000000);
#endif
if (ArtCutsceneWidescreen) {
const ClxSprite sprite = (*ArtCutsceneWidescreen)[0];
RenderClxSprite(out, sprite, { uiRectangle.position.x - (sprite.width() - uiRectangle.size.width) / 2, uiRectangle.position.y });
}
ClxDraw(out, { uiRectangle.position.x, 480 - 1 + uiRectangle.position.y }, (*sgpBackCel)[0]);
}
void DrawCutsceneForeground()
{
const Rectangle &uiRectangle = GetUIRectangle();
const Surface &out = GlobalBackBuffer();
constexpr int ProgressHeight = 22;
SDL_Rect rect = MakeSdlRect(
out.region.x + BarPos[progress_id][0] + uiRectangle.position.x,
out.region.y + BarPos[progress_id][1] + uiRectangle.position.y,
sgdwProgress,
ProgressHeight);
#ifdef USE_SDL3
SDL_FillSurfaceRect(out.surface, &rect, BarColor[progress_id]);
#else
SDL_FillRect(out.surface, &rect, BarColor[progress_id]);
#endif
if (DiabloUiSurface() == PalSurface)
BltFast(&rect, &rect);
RenderPresent();
}
struct {
uint32_t loadStartedAt;
EventHandler prevHandler;
bool skipRendering;
bool done;
uint32_t drawnProgress;
std::array<SDL_Color, 256> palette;
} ProgressEventHandlerState;
void InitRendering()
{
// Blit the background once and then free it.
DrawCutsceneBackground();
if (RenderDirectlyToOutputSurface && PalSurface != nullptr) {
// Render into all the backbuffers if there are multiple.
const void *initialPixels = PalSurface->pixels;
if (DiabloUiSurface() == PalSurface)
BltFast(nullptr, nullptr);
RenderPresent();
while (PalSurface->pixels != initialPixels) {
DrawCutsceneBackground();
if (DiabloUiSurface() == PalSurface)
BltFast(nullptr, nullptr);
RenderPresent();
}
}
FreeCutsceneBackground();
// The loading thread sets `logical_palette`, so we make sure to use
// our own palette for the fade-in.
PaletteFadeIn(8, ProgressEventHandlerState.palette);
}
void CheckShouldSkipRendering()
{
if (!ProgressEventHandlerState.skipRendering) return;
const bool shouldSkip = ProgressEventHandlerState.loadStartedAt + *GetOptions().Gameplay.skipLoadingScreenThresholdMs > SDL_GetTicks();
if (shouldSkip) return;
ProgressEventHandlerState.skipRendering = false;
if (!HeadlessMode) InitRendering();
}
bool HandleProgressBarUpdate()
{
CheckShouldSkipRendering();
SDL_Event event;
// We use the real `PollEvent` here instead of `FetchMessage`
// to process real events rather than the recorded ones in demo mode.
while (PollEvent(&event)) {
CheckShouldSkipRendering();
if (event.type !=
#ifdef USE_SDL3
SDL_EVENT_QUIT
#else
SDL_QUIT
#endif
) {
HandleMessage(event, SDL_GetModState());
}
if (ProgressEventHandlerState.done) return false;
}
return true;
}
void DoLoad(interface_mode uMsg)
{
IncProgress();
sound_init();
IncProgress();
Player &myPlayer = *MyPlayer;
tl::expected<void, std::string> loadResult;
switch (uMsg) {
case WM_DIABLOADGAME:
IncProgress(2);
loadResult = LoadGame(true);
if (loadResult.has_value()) IncProgress(2);
break;
case WM_DIABNEWGAME:
myPlayer.pOriginalCathedral = !gbIsHellfire;
IncProgress();
FreeGameMem();
IncProgress();
pfile_remove_temp_files();
IncProgress();
loadResult = LoadGameLevel(true, ENTRY_MAIN);
if (loadResult.has_value()) IncProgress();
break;
case WM_DIABNEXTLVL:
IncProgress();
if (!gbIsMultiplayer) {
pfile_save_level();
} else {
DeltaSaveLevel();
}
IncProgress();
FreeGameMem();
setlevel = false;
currlevel = myPlayer.plrlevel;
leveltype = GetLevelType(currlevel);
IncProgress();
loadResult = LoadGameLevel(false, ENTRY_MAIN);
if (loadResult.has_value()) IncProgress();
break;
case WM_DIABPREVLVL:
IncProgress();
if (!gbIsMultiplayer) {
pfile_save_level();
} else {
DeltaSaveLevel();
}
IncProgress();
FreeGameMem();
currlevel--;
leveltype = GetLevelType(currlevel);
assert(myPlayer.isOnActiveLevel());
IncProgress();
loadResult = LoadGameLevel(false, ENTRY_PREV);
if (loadResult.has_value()) IncProgress();
break;
case WM_DIABSETLVL:
// Note: ReturnLevel, ReturnLevelType and ReturnLvlPosition is only set to ensure vanilla compatibility
ReturnLevel = GetMapReturnLevel();
ReturnLevelType = GetLevelType(ReturnLevel);
ReturnLvlPosition = GetMapReturnPosition();
IncProgress();
if (!gbIsMultiplayer) {
pfile_save_level();
} else {
DeltaSaveLevel();
}
IncProgress();
setlevel = true;
leveltype = setlvltype;
currlevel = static_cast<uint8_t>(setlvlnum);
FreeGameMem();
IncProgress();
loadResult = LoadGameLevel(false, ENTRY_SETLVL);
if (loadResult.has_value()) IncProgress();
break;
case WM_DIABRTNLVL:
IncProgress();
if (!gbIsMultiplayer) {
pfile_save_level();
} else {
DeltaSaveLevel();
}
IncProgress();
setlevel = false;
FreeGameMem();
IncProgress();
currlevel = GetMapReturnLevel();
leveltype = GetLevelType(currlevel);
loadResult = LoadGameLevel(false, ENTRY_RTNLVL);
if (loadResult.has_value()) IncProgress();
break;
case WM_DIABWARPLVL:
IncProgress();
if (!gbIsMultiplayer) {
pfile_save_level();
} else {
DeltaSaveLevel();
}
IncProgress();
FreeGameMem();
GetPortalLevel();
IncProgress();
loadResult = LoadGameLevel(false, ENTRY_WARPLVL);
if (loadResult.has_value()) IncProgress();
break;
case WM_DIABTOWNWARP:
IncProgress();
if (!gbIsMultiplayer) {
pfile_save_level();
} else {
DeltaSaveLevel();
}
IncProgress();
FreeGameMem();
setlevel = false;
currlevel = myPlayer.plrlevel;
leveltype = GetLevelType(currlevel);
IncProgress();
loadResult = LoadGameLevel(false, ENTRY_TWARPDN);
if (loadResult.has_value()) IncProgress();
break;
case WM_DIABTWARPUP:
IncProgress();
if (!gbIsMultiplayer) {
pfile_save_level();
} else {
DeltaSaveLevel();
}
IncProgress();
FreeGameMem();
currlevel = myPlayer.plrlevel;
leveltype = GetLevelType(currlevel);
IncProgress();
loadResult = LoadGameLevel(false, ENTRY_TWARPUP);
if (loadResult.has_value()) IncProgress();
break;
case WM_DIABRETOWN:
IncProgress();
if (!gbIsMultiplayer) {
pfile_save_level();
} else {
DeltaSaveLevel();
}
IncProgress();
FreeGameMem();
setlevel = false;
currlevel = myPlayer.plrlevel;
leveltype = GetLevelType(currlevel);
IncProgress();
loadResult = LoadGameLevel(false, ENTRY_MAIN);
if (loadResult.has_value()) IncProgress();
break;
default:
loadResult = tl::make_unexpected<std::string>("Unknown progress mode");
break;
}
if (!loadResult.has_value()) {
SDL_Event event;
CustomEventToSdlEvent(event, WM_ERROR);
event.user.data1 = new std::string(std::move(loadResult).error());
if (
#ifdef USE_SDL3
!SDL_PushEvent(&event)
#else
SDL_PushEvent(&event) < 0
#endif
) {
LogError("Failed to send WM_ERROR {}", SDL_GetError());
SDL_ClearError();
}
#ifdef LOAD_ON_MAIN_THREAD
HandleProgressBarUpdate();
#endif
return;
}
SDL_Event event;
CustomEventToSdlEvent(event, WM_DONE);
if (
#ifdef USE_SDL3
!SDL_PushEvent(&event)
#else
SDL_PushEvent(&event) < 0
#endif
) {
LogError("Failed to send WM_DONE {}", SDL_GetError());
SDL_ClearError();
}
#ifdef LOAD_ON_MAIN_THREAD
HandleProgressBarUpdate();
#endif
}
void ProgressEventHandler(const SDL_Event &event, uint16_t modState)
{
DisableInputEventHandler(event, modState);
if (!IsCustomEvent(event.type)) return;
const interface_mode customEvent = GetCustomEvent(event);
switch (customEvent) {
case WM_PROGRESS:
if (!HeadlessMode && ProgressEventHandlerState.drawnProgress != sgdwProgress && !ProgressEventHandlerState.skipRendering) {
DrawCutsceneForeground();
ProgressEventHandlerState.drawnProgress = sgdwProgress;
}
break;
case WM_ERROR:
app_fatal(*static_cast<std::string *>(event.user.data1));
break;
case WM_DONE: {
if (!ProgressEventHandlerState.skipRendering) {
NewCursor(CURSOR_HAND);
if (!HeadlessMode) {
assert(ghMainWnd);
// The loading thread sets `logical_palette`, so we make sure to use
// our own palette for drawing the foreground.
UpdateSystemPalette(ProgressEventHandlerState.palette);
// Ensure that all back buffers have the full progress bar.
const void *initialPixels = PalSurface->pixels;
do {
DrawCutsceneForeground();
if (DiabloUiSurface() == PalSurface)
BltFast(nullptr, nullptr);
RenderPresent();
} while (PalSurface->pixels != initialPixels);
// The loading thread sets `logical_palette`, so we make sure to use
// our own palette for the fade-out.
PaletteFadeOut(8, ProgressEventHandlerState.palette);
// Once the fade-out is done, restore the system palette.
UpdateSystemPalette(logical_palette);
}
}
[[maybe_unused]] EventHandler prevHandler = SetEventHandler(ProgressEventHandlerState.prevHandler);
assert(prevHandler == ProgressEventHandler);
ProgressEventHandlerState.prevHandler = nullptr;
IsProgress = false;
const Player &myPlayer = *MyPlayer;
NetSendCmdLocParam2(true, CMD_PLAYER_JOINLEVEL, myPlayer.position.tile, myPlayer.plrlevel, myPlayer.plrIsOnSetLevel ? 1 : 0);
DelayPlrMessages(SDL_GetTicks() - ProgressEventHandlerState.loadStartedAt);
if (gbSomebodyWonGameKludge && myPlayer.isOnLevel(16)) {
PrepDoEnding();
}
gbSomebodyWonGameKludge = false;
ProgressEventHandlerState.done = true;
#if !defined(USE_SDL1) && !defined(__vita__)
if (renderer != nullptr) {
InitVirtualGamepadTextures(*renderer);
}
#endif
} break;
default:
app_fatal("Unknown progress mode");
break;
}
#ifdef LOAD_ON_MAIN_THREAD
HandleProgressBarUpdate();
#endif
}
} // namespace
void RegisterCustomEvents()
{
#ifndef USE_SDL1
CustomEventType = SDL_RegisterEvents(1);
#endif
}
bool IsCustomEvent(SdlEventType eventType)
{
return eventType == CustomEventType;
}
interface_mode GetCustomEvent(const SDL_Event &event)
{
return static_cast<interface_mode>(event.user.code);
}
void CustomEventToSdlEvent(SDL_Event &event, interface_mode eventType)
{
event.type = CustomEventType;
event.user.code = static_cast<int>(eventType);
}
void interface_msg_pump()
{
SDL_Event event;
uint16_t modState;
while (FetchMessage(&event, &modState)) {
if (event.type !=
#ifdef USE_SDL3
SDL_EVENT_QUIT
#else
SDL_QUIT
#endif
) {
HandleMessage(event, modState);
}
}
}
void IncProgress(uint32_t steps)
{
if (!IsProgress)
return;
const uint32_t prevProgress = sgdwProgress;
sgdwProgress += ProgressStepSize * steps;
if (sgdwProgress > MaxProgress)
sgdwProgress = MaxProgress;
if (!HeadlessMode && sgdwProgress != prevProgress) {
SDL_Event event;
CustomEventToSdlEvent(event, WM_PROGRESS);
if (
#ifdef USE_SDL3
!SDL_PushEvent(&event)
#else
SDL_PushEvent(&event) < 0
#endif
) {
LogError("Failed to send WM_PROGRESS {}", SDL_GetError());
SDL_ClearError();
}
#ifdef LOAD_ON_MAIN_THREAD
HandleProgressBarUpdate();
#endif
}
}
void CompleteProgress()
{
if (HeadlessMode)
return;
if (!IsProgress)
return;
if (sgdwProgress < MaxProgress) {
IncProgress((MaxProgress - sgdwProgress) / ProgressStepSize);
}
}
void ShowProgress(interface_mode uMsg)
{
IsProgress = true;
gbSomebodyWonGameKludge = false;
ProgressEventHandlerState.loadStartedAt = SDL_GetTicks();
ProgressEventHandlerState.prevHandler = SetEventHandler(ProgressEventHandler);
ProgressEventHandlerState.skipRendering = true;
ProgressEventHandlerState.done = false;
ProgressEventHandlerState.drawnProgress = 0;
#ifndef USE_SDL1
DeactivateVirtualGamepad();
FreeVirtualGamepadTextures();
#endif
if (!HeadlessMode) {
assert(ghMainWnd);
interface_msg_pump();
ClearScreenBuffer();
scrollrt_draw_game_screen();
if (IsHardwareCursor())
SetHardwareCursorVisible(false);
BlackPalette();
// Always load the background (even if we end up skipping rendering it).
// This is because the MPQ archive can only be read by a single thread at a time.
LoadCutsceneBackground(uMsg);
// Save the palette at this point because the loading process may replace it.
ProgressEventHandlerState.palette = logical_palette;
}
// Begin loading
#ifdef LOAD_ON_MAIN_THREAD
const uint32_t start = SDL_GetTicks();
DoLoad(uMsg);
LogVerbose("Loading finished in {}ms", SDL_GetTicks() - start);
#else
static interface_mode loadTarget;
loadTarget = uMsg;
SdlThread loadThread = SdlThread([]() {
const uint32_t start = SDL_GetTicks();
DoLoad(loadTarget);
LogVerbose("Load thread finished in {}ms", SDL_GetTicks() - start);
});
while (HandleProgressBarUpdate()) { }
loadThread.join();
#endif
}
} // namespace devilution