/** * @file interfac.cpp * * Implementation of load screens. */ #include #include #include #include #include #include "control.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 namespace devilution { namespace { #if defined(__APPLE__) && defined(USE_SDL1) // On Tiger PPC, SDL_PushEvent from a background thread appears to do nothing. #define SDL_PUSH_EVENT_BG_THREAD_WORKS 0 #else #define SDL_PUSH_EVENT_BG_THREAD_WORKS 1 #endif #if !SDL_PUSH_EVENT_BG_THREAD_WORKS // This workaround is not completely thread-safe but the worst // that can happen is we miss some WM_PROGRESS events, // which is not a problem. struct { std::atomic type; std::string error; } NextCustomEvent; #endif 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; SdlEventType CustomEventsBegin = SDL_USEREVENT; constexpr uint16_t NumCustomEvents = WM_LAST - WM_FIRST + 1; 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: { int lvl = MyPlayer->plrlevel; if (lvl == 1 && uMsg == WM_DIABNEXTLVL) return CutTown; if (lvl == 16 && uMsg == WM_DIABNEXTLVL) return CutGate; return GetCutSceneFromLevelType(GetLevelType(lvl)); } 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; break; case CutTown: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutttw.clx"); celPath = "gendata\\cuttt"; palPath = "gendata\\cuttt.pal"; progress_id = 1; break; case CutLevel1: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutl1dw.clx"); celPath = "gendata\\cutl1d"; palPath = "gendata\\cutl1d.pal"; progress_id = 0; break; case CutLevel2: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cut2w.clx"); celPath = "gendata\\cut2"; palPath = "gendata\\cut2.pal"; progress_id = 2; break; case CutLevel3: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cut3w.clx"); celPath = "gendata\\cut3"; palPath = "gendata\\cut3.pal"; progress_id = 1; break; case CutLevel4: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cut4w.clx"); celPath = "gendata\\cut4"; palPath = "gendata\\cut4.pal"; progress_id = 1; break; case CutLevel5: ArtCutsceneWidescreen = LoadOptionalClx("nlevels\\cutl5w.clx"); celPath = "nlevels\\cutl5"; palPath = "nlevels\\cutl5.pal"; progress_id = 1; break; case CutLevel6: ArtCutsceneWidescreen = LoadOptionalClx("nlevels\\cutl6w.clx"); celPath = "nlevels\\cutl6"; palPath = "nlevels\\cutl6.pal"; progress_id = 1; break; case CutPortal: ArtCutsceneWidescreen = LoadOptionalClx("gendata\\cutportlw.clx"); celPath = "gendata\\cutportl"; palPath = "gendata\\cutportl.pal"; 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; break; } assert(!sgpBackCel); sgpBackCel = LoadCel(celPath, 640); LoadPalette(palPath); sgdwProgress = 0; } void FreeCutsceneBackground() { sgpBackCel = std::nullopt; ArtCutsceneWidescreen = std::nullopt; } void DrawCutsceneBackground() { const Rectangle &uiRectangle = GetUIRectangle(); const Surface &out = GlobalBackBuffer(); SDL_FillRect(out.surface, nullptr, 0x000000); 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); SDL_FillRect(out.surface, &rect, BarColor[progress_id]); if (DiabloUiSurface() == PalSurface) BltFast(&rect, &rect); RenderPresent(); } void DoLoad(interface_mode uMsg) { IncProgress(); sound_init(); IncProgress(); Player &myPlayer = *MyPlayer; tl::expected 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(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("Unknown progress mode"); break; } if (!loadResult.has_value()) { #if SDL_PUSH_EVENT_BG_THREAD_WORKS SDL_Event event; event.type = CustomEventToSdlEvent(WM_ERROR); event.user.data1 = new std::string(std::move(loadResult).error()); if (SDL_PushEvent(&event) < 0) { LogError("Failed to send WM_ERROR {}", SDL_GetError()); SDL_ClearError(); } #else NextCustomEvent.error = std::move(loadResult).error(); NextCustomEvent.type = static_cast(WM_ERROR); #endif return; } #if SDL_PUSH_EVENT_BG_THREAD_WORKS SDL_Event event; event.type = CustomEventToSdlEvent(WM_DONE); if (SDL_PushEvent(&event) < 0) { LogError("Failed to send WM_DONE {}", SDL_GetError()); SDL_ClearError(); } #else NextCustomEvent.type = static_cast(WM_DONE); #endif } struct { uint32_t loadStartedAt; EventHandler prevHandler; bool skipRendering; bool done; uint32_t drawnProgress; std::array 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 `orig_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(); } void ProgressEventHandler(const SDL_Event &event, uint16_t modState) { DisableInputEventHandler(event, modState); if (!IsCustomEvent(event.type)) return; const interface_mode customEvent = GetCustomEvent(event.type); switch (customEvent) { case WM_PROGRESS: if (!HeadlessMode && ProgressEventHandlerState.drawnProgress != sgdwProgress && !ProgressEventHandlerState.skipRendering) { DrawCutsceneForeground(); ProgressEventHandlerState.drawnProgress = sgdwProgress; } break; case WM_ERROR: app_fatal(*static_cast(event.user.data1)); break; case WM_DONE: { if (!ProgressEventHandlerState.skipRendering) { NewCursor(CURSOR_HAND); if (!HeadlessMode) { assert(ghMainWnd); if (RenderDirectlyToOutputSurface && PalSurface != nullptr) { // The loading thread sets `orig_palette`, so we make sure to use // our own palette for drawing the foreground. ApplyGamma(logical_palette, ProgressEventHandlerState.palette, 256); // 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 `orig_palette`, so we make sure to use // our own palette for the fade-out. PaletteFadeOut(8, ProgressEventHandlerState.palette); } } [[maybe_unused]] EventHandler prevHandler = SetEventHandler(ProgressEventHandlerState.prevHandler); assert(prevHandler == ProgressEventHandler); ProgressEventHandlerState.prevHandler = nullptr; IsProgress = false; 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; } } } // namespace void RegisterCustomEvents() { #ifndef USE_SDL1 CustomEventsBegin = SDL_RegisterEvents(NumCustomEvents); #endif } bool IsCustomEvent(SdlEventType eventType) { return eventType >= CustomEventsBegin && eventType < CustomEventsBegin + NumCustomEvents; } interface_mode GetCustomEvent(SdlEventType eventType) { return static_cast(eventType - CustomEventsBegin); } SdlEventType CustomEventToSdlEvent(interface_mode eventType) { return CustomEventsBegin + eventType; } void interface_msg_pump() { SDL_Event event; uint16_t modState; while (FetchMessage(&event, &modState)) { if (event.type != SDL_QUIT) { 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) { #if SDL_PUSH_EVENT_BG_THREAD_WORKS SDL_Event event; event.type = CustomEventToSdlEvent(WM_PROGRESS); if (SDL_PushEvent(&event) < 0) { LogError("Failed to send WM_PROGRESS {}", SDL_GetError()); SDL_ClearError(); } #else NextCustomEvent.type = static_cast(WM_PROGRESS); #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; #if !SDL_PUSH_EVENT_BG_THREAD_WORKS NextCustomEvent.type = -1; #endif #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 = orig_palette; } // Begin loading 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); }); const auto processEvent = [&](const SDL_Event &event) { CheckShouldSkipRendering(); if (event.type != SDL_QUIT) { HandleMessage(event, SDL_GetModState()); } if (ProgressEventHandlerState.done) { loadThread.join(); return false; } return true; }; while (true) { CheckShouldSkipRendering(); SDL_Event event; // We use the real `SDL_PollEvent` here instead of `FetchEvent` // to process real events rather than the recorded ones in demo mode. while (SDL_PollEvent(&event)) { if (!processEvent(event)) return; } #if !SDL_PUSH_EVENT_BG_THREAD_WORKS if (const int customEventType = NextCustomEvent.type.exchange(-1); customEventType != -1) { event.type = CustomEventToSdlEvent(static_cast(customEventType)); if (static_cast(customEventType) == static_cast(WM_ERROR)) { event.user.data1 = &NextCustomEvent.error; } if (!processEvent(event)) return; } #endif } } } // namespace devilution