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.
1151 lines
30 KiB
1151 lines
30 KiB
#include "DiabloUI/diabloui.h" |
|
|
|
#include <algorithm> |
|
#include <string> |
|
|
|
#include "DiabloUI/art_draw.h" |
|
#include "DiabloUI/button.h" |
|
#include "DiabloUI/dialogs.h" |
|
#include "DiabloUI/scrollbar.h" |
|
#include "controls/controller.h" |
|
#include "controls/input.h" |
|
#include "controls/menu_controls.h" |
|
#include "controls/plrctrls.h" |
|
#include "discord/discord.h" |
|
#include "engine/assets.hpp" |
|
#include "engine/cel_sprite.hpp" |
|
#include "engine/dx.h" |
|
#include "engine/load_pcx.hpp" |
|
#include "engine/render/cl2_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" |
|
#include "utils/str_cat.hpp" |
|
#include "utils/stubs.h" |
|
#include "utils/utf8.hpp" |
|
|
|
#ifdef __SWITCH__ |
|
// for virtual keyboard on Switch |
|
#include "platform/switch/keyboard.h" |
|
#endif |
|
#ifdef __vita__ |
|
// for virtual keyboard on Vita |
|
#include "platform/vita/keyboard.h" |
|
#endif |
|
#ifdef __3DS__ |
|
// for virtual keyboard on 3DS |
|
#include "platform/ctr/keyboard.h" |
|
#endif |
|
|
|
namespace devilution { |
|
|
|
std::optional<OwnedCelSpriteSheetWithFrameHeight> ArtLogo; |
|
|
|
std::array<std::optional<OwnedCelSpriteSheetWithFrameHeight>, 3> ArtFocus; |
|
|
|
std::optional<OwnedCelSpriteWithFrameHeight> ArtBackgroundWidescreen; |
|
std::optional<OwnedCelSpriteSheetWithFrameHeight> ArtBackground; |
|
Art ArtCursor; |
|
|
|
bool textInputActive = true; |
|
std::size_t SelectedItem = 0; |
|
|
|
namespace { |
|
|
|
std::optional<OwnedCelSpriteSheetWithFrameHeight> ArtHero; |
|
std::array<uint8_t, enum_size<HeroClass>::value + 1> ArtHeroPortraitOrder; |
|
std::array<std::optional<OwnedCelSpriteWithFrameHeight>, enum_size<HeroClass>::value + 1> ArtHeroOverrides; |
|
|
|
std::size_t SelectedItemMax; |
|
std::size_t ListViewportSize = 1; |
|
std::size_t listOffset = 0; |
|
|
|
void (*gfnListFocus)(int value); |
|
void (*gfnListSelect)(int value); |
|
void (*gfnListEsc)(); |
|
void (*gfnFullscreen)(); |
|
bool (*gfnListYesNo)(); |
|
std::vector<UiItemBase *> gUiItems; |
|
UiList *gUiList = nullptr; |
|
bool UiItemsWraps; |
|
char *UiTextInput; |
|
int UiTextInputLen; |
|
bool allowEmptyTextInput = false; |
|
|
|
uint32_t fadeTc; |
|
int fadeValue = 0; |
|
|
|
struct ScrollBarState { |
|
bool upArrowPressed; |
|
bool downArrowPressed; |
|
|
|
ScrollBarState() |
|
{ |
|
upArrowPressed = false; |
|
downArrowPressed = false; |
|
} |
|
} scrollBarState; |
|
|
|
void AdjustListOffset(std::size_t itemIndex) |
|
{ |
|
if (itemIndex >= listOffset + ListViewportSize) |
|
listOffset = itemIndex - (ListViewportSize - 1); |
|
if (itemIndex < listOffset) |
|
listOffset = itemIndex; |
|
} |
|
|
|
} // namespace |
|
|
|
void UiInitList(void (*fnFocus)(int value), void (*fnSelect)(int value), void (*fnEsc)(), const std::vector<std::unique_ptr<UiItemBase>> &items, bool itemsWraps, void (*fnFullscreen)(), bool (*fnYesNo)(), size_t selectedItem /*= 0*/) |
|
{ |
|
SelectedItem = selectedItem; |
|
SelectedItemMax = 0; |
|
ListViewportSize = 0; |
|
gfnListFocus = fnFocus; |
|
gfnListSelect = fnSelect; |
|
gfnListEsc = fnEsc; |
|
gfnFullscreen = fnFullscreen; |
|
gfnListYesNo = fnYesNo; |
|
gUiItems.clear(); |
|
for (const auto &item : items) |
|
gUiItems.push_back(item.get()); |
|
UiItemsWraps = itemsWraps; |
|
listOffset = 0; |
|
if (fnFocus != nullptr) |
|
fnFocus(selectedItem); |
|
|
|
#ifndef __SWITCH__ |
|
SDL_StopTextInput(); // input is enabled by default |
|
#endif |
|
textInputActive = false; |
|
UiScrollbar *uiScrollbar = nullptr; |
|
for (const auto &item : items) { |
|
if (item->IsType(UiType::Edit)) { |
|
auto *pItemUIEdit = static_cast<UiEdit *>(item.get()); |
|
SDL_SetTextInputRect(&item->m_rect); |
|
textInputActive = true; |
|
allowEmptyTextInput = pItemUIEdit->m_allowEmpty; |
|
#ifdef __SWITCH__ |
|
switch_start_text_input(pItemUIEdit->m_hint, pItemUIEdit->m_value, pItemUIEdit->m_max_length); |
|
#elif defined(__vita__) |
|
vita_start_text_input(pItemUIEdit->m_hint, pItemUIEdit->m_value, pItemUIEdit->m_max_length); |
|
#elif defined(__3DS__) |
|
ctr_vkbdInput(pItemUIEdit->m_hint, pItemUIEdit->m_value, pItemUIEdit->m_value, pItemUIEdit->m_max_length); |
|
#else |
|
SDL_StartTextInput(); |
|
#endif |
|
UiTextInput = pItemUIEdit->m_value; |
|
UiTextInputLen = pItemUIEdit->m_max_length; |
|
} else if (item->IsType(UiType::List)) { |
|
auto *uiList = static_cast<UiList *>(item.get()); |
|
SelectedItemMax = std::max(uiList->m_vecItems.size() - 1, static_cast<size_t>(0)); |
|
ListViewportSize = uiList->viewportSize; |
|
gUiList = uiList; |
|
if (selectedItem <= SelectedItemMax && HasAnyOf(uiList->GetItem(selectedItem)->uiFlags, UiFlags::NeedsNextElement)) |
|
AdjustListOffset(selectedItem + 1); |
|
} else if (item->IsType(UiType::Scrollbar)) { |
|
uiScrollbar = static_cast<UiScrollbar *>(item.get()); |
|
} |
|
} |
|
|
|
AdjustListOffset(selectedItem); |
|
|
|
if (uiScrollbar != nullptr) { |
|
if (ListViewportSize >= static_cast<std::size_t>(SelectedItemMax + 1)) { |
|
uiScrollbar->Hide(); |
|
} else { |
|
uiScrollbar->Show(); |
|
} |
|
} |
|
} |
|
|
|
void UiInitList_clear() |
|
{ |
|
SelectedItem = 0; |
|
SelectedItemMax = 0; |
|
ListViewportSize = 1; |
|
gfnListFocus = nullptr; |
|
gfnListSelect = nullptr; |
|
gfnListEsc = nullptr; |
|
gfnFullscreen = nullptr; |
|
gfnListYesNo = nullptr; |
|
gUiList = nullptr; |
|
gUiItems.clear(); |
|
UiItemsWraps = false; |
|
} |
|
|
|
void UiPlayMoveSound() |
|
{ |
|
effects_play_sound(IS_TITLEMOV); |
|
} |
|
|
|
void UiPlaySelectSound() |
|
{ |
|
effects_play_sound(IS_TITLSLCT); |
|
} |
|
|
|
namespace { |
|
|
|
void UiFocus(std::size_t itemIndex, bool checkUp, bool ignoreItemsWraps = false) |
|
{ |
|
if (SelectedItem == itemIndex) |
|
return; |
|
|
|
AdjustListOffset(itemIndex); |
|
|
|
const auto *pItem = gUiList->GetItem(itemIndex); |
|
while (HasAnyOf(pItem->uiFlags, UiFlags::ElementHidden | UiFlags::ElementDisabled)) { |
|
if (checkUp) { |
|
if (itemIndex > 0) |
|
itemIndex -= 1; |
|
else if (UiItemsWraps && !ignoreItemsWraps) |
|
itemIndex = SelectedItemMax; |
|
else |
|
checkUp = false; |
|
} else { |
|
if (itemIndex < SelectedItemMax) |
|
itemIndex += 1; |
|
else if (UiItemsWraps && !ignoreItemsWraps) |
|
itemIndex = 0; |
|
else |
|
checkUp = true; |
|
} |
|
pItem = gUiList->GetItem(itemIndex); |
|
} |
|
|
|
if (HasAnyOf(pItem->uiFlags, UiFlags::NeedsNextElement)) |
|
AdjustListOffset(itemIndex + 1); |
|
AdjustListOffset(itemIndex); |
|
|
|
SelectedItem = itemIndex; |
|
|
|
UiPlayMoveSound(); |
|
|
|
if (gfnListFocus != nullptr) |
|
gfnListFocus(itemIndex); |
|
} |
|
|
|
void UiFocusUp() |
|
{ |
|
if (SelectedItem > 0) |
|
UiFocus(SelectedItem - 1, true); |
|
else if (UiItemsWraps) |
|
UiFocus(SelectedItemMax, true); |
|
} |
|
|
|
void UiFocusDown() |
|
{ |
|
if (SelectedItem < SelectedItemMax) |
|
UiFocus(SelectedItem + 1, false); |
|
else if (UiItemsWraps) |
|
UiFocus(0, false); |
|
} |
|
|
|
// UiFocusPageUp/Down mimics the slightly weird behaviour of actual Diablo. |
|
|
|
void UiFocusPageUp() |
|
{ |
|
if (listOffset == 0) { |
|
UiFocus(0, true, true); |
|
} else { |
|
const std::size_t relpos = SelectedItem - listOffset; |
|
std::size_t prevPageStart = SelectedItem - relpos; |
|
if (prevPageStart >= ListViewportSize) |
|
prevPageStart -= ListViewportSize; |
|
else |
|
prevPageStart = 0; |
|
AdjustListOffset(prevPageStart); |
|
UiFocus(listOffset + relpos, true, true); |
|
} |
|
} |
|
|
|
void UiFocusPageDown() |
|
{ |
|
if (listOffset + ListViewportSize > static_cast<std::size_t>(SelectedItemMax)) { |
|
UiFocus(SelectedItemMax, false, true); |
|
} else { |
|
const std::size_t relpos = SelectedItem - listOffset; |
|
std::size_t nextPageEnd = SelectedItem + (ListViewportSize - relpos - 1); |
|
if (nextPageEnd + ListViewportSize <= static_cast<std::size_t>(SelectedItemMax)) |
|
nextPageEnd += ListViewportSize; |
|
else |
|
nextPageEnd = SelectedItemMax; |
|
AdjustListOffset(nextPageEnd); |
|
UiFocus(listOffset + relpos, false, true); |
|
} |
|
} |
|
|
|
void SelheroCatToName(const char *inBuf, char *outBuf, int cnt) |
|
{ |
|
size_t outLen = strlen(outBuf); |
|
char *dest = outBuf + outLen; |
|
size_t destCount = cnt - outLen; |
|
CopyUtf8(dest, inBuf, destCount); |
|
} |
|
|
|
bool HandleMenuAction(MenuAction menuAction) |
|
{ |
|
switch (menuAction) { |
|
case MenuAction_SELECT: |
|
UiFocusNavigationSelect(); |
|
return true; |
|
case MenuAction_UP: |
|
UiFocusUp(); |
|
return true; |
|
case MenuAction_DOWN: |
|
UiFocusDown(); |
|
return true; |
|
case MenuAction_PAGE_UP: |
|
UiFocusPageUp(); |
|
return true; |
|
case MenuAction_PAGE_DOWN: |
|
UiFocusPageDown(); |
|
return true; |
|
case MenuAction_DELETE: |
|
UiFocusNavigationYesNo(); |
|
return true; |
|
case MenuAction_BACK: |
|
if (gfnListEsc == nullptr) |
|
return false; |
|
UiFocusNavigationEsc(); |
|
return true; |
|
default: |
|
return false; |
|
} |
|
} |
|
|
|
} // namespace |
|
|
|
void UiFocusNavigation(SDL_Event *event) |
|
{ |
|
switch (event->type) { |
|
case SDL_KEYUP: |
|
case SDL_MOUSEBUTTONUP: |
|
case SDL_MOUSEMOTION: |
|
#ifndef USE_SDL1 |
|
case SDL_MOUSEWHEEL: |
|
#endif |
|
case SDL_JOYBUTTONUP: |
|
case SDL_JOYAXISMOTION: |
|
case SDL_JOYBALLMOTION: |
|
case SDL_JOYHATMOTION: |
|
#ifndef USE_SDL1 |
|
case SDL_FINGERUP: |
|
case SDL_FINGERMOTION: |
|
case SDL_CONTROLLERBUTTONUP: |
|
case SDL_CONTROLLERAXISMOTION: |
|
case SDL_WINDOWEVENT: |
|
#endif |
|
case SDL_SYSWMEVENT: |
|
mainmenu_restart_repintro(); |
|
break; |
|
} |
|
|
|
if (HandleMenuAction(GetMenuAction(*event))) |
|
return; |
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0) |
|
if (event->type == SDL_MOUSEWHEEL) { |
|
if (event->wheel.y > 0) { |
|
UiFocusUp(); |
|
} else if (event->wheel.y < 0) { |
|
UiFocusDown(); |
|
} |
|
return; |
|
} |
|
#else |
|
if (event->type == SDL_MOUSEBUTTONDOWN) { |
|
switch (event->button.button) { |
|
case SDL_BUTTON_WHEELUP: |
|
UiFocusUp(); |
|
return; |
|
case SDL_BUTTON_WHEELDOWN: |
|
UiFocusDown(); |
|
return; |
|
} |
|
} |
|
#endif |
|
|
|
if (textInputActive) { |
|
switch (event->type) { |
|
case SDL_KEYDOWN: { |
|
switch (event->key.keysym.sym) { |
|
#ifndef USE_SDL1 |
|
case SDLK_v: |
|
if ((SDL_GetModState() & KMOD_CTRL) != 0) { |
|
char *clipboard = SDL_GetClipboardText(); |
|
if (clipboard == nullptr) { |
|
Log("{}", SDL_GetError()); |
|
} else { |
|
SelheroCatToName(clipboard, UiTextInput, UiTextInputLen); |
|
} |
|
} |
|
return; |
|
#endif |
|
case SDLK_BACKSPACE: |
|
case SDLK_LEFT: { |
|
UiTextInput[FindLastUtf8Symbols(UiTextInput)] = '\0'; |
|
return; |
|
} |
|
default: |
|
break; |
|
} |
|
#ifdef USE_SDL1 |
|
if ((event->key.keysym.mod & KMOD_CTRL) == 0) { |
|
std::string utf8; |
|
AppendUtf8(event->key.keysym.unicode, utf8); |
|
SelheroCatToName(utf8.c_str(), UiTextInput, UiTextInputLen); |
|
} |
|
#endif |
|
break; |
|
} |
|
#ifndef USE_SDL1 |
|
case SDL_TEXTINPUT: |
|
if (textInputActive) { |
|
#ifdef __vita__ |
|
CopyUtf8(UiTextInput, event->text.text, UiTextInputLen); |
|
#else |
|
SelheroCatToName(event->text.text, UiTextInput, UiTextInputLen); |
|
#endif |
|
} |
|
return; |
|
#endif |
|
default: |
|
break; |
|
} |
|
} |
|
|
|
if (event->type == SDL_MOUSEBUTTONDOWN || event->type == SDL_MOUSEBUTTONUP) { |
|
if (UiItemMouseEvents(event, gUiItems)) |
|
return; |
|
} |
|
} |
|
|
|
void UiHandleEvents(SDL_Event *event) |
|
{ |
|
if (event->type == SDL_MOUSEMOTION) { |
|
#ifdef USE_SDL1 |
|
OutputToLogical(&event->motion.x, &event->motion.y); |
|
#endif |
|
MousePosition = { event->motion.x, event->motion.y }; |
|
return; |
|
} |
|
|
|
if (event->type == SDL_KEYDOWN && event->key.keysym.sym == SDLK_RETURN) { |
|
const Uint8 *state = SDLC_GetKeyState(); |
|
if (state[SDLC_KEYSTATE_LALT] != 0 || state[SDLC_KEYSTATE_RALT] != 0) { |
|
sgOptions.Graphics.fullscreen.SetValue(!IsFullScreen()); |
|
SaveOptions(); |
|
if (gfnFullscreen != nullptr) |
|
gfnFullscreen(); |
|
return; |
|
} |
|
} |
|
|
|
if (event->type == SDL_QUIT) |
|
diablo_quit(0); |
|
|
|
#ifndef USE_SDL1 |
|
HandleControllerAddedOrRemovedEvent(*event); |
|
|
|
if (event->type == SDL_WINDOWEVENT) { |
|
if (event->window.event == SDL_WINDOWEVENT_SHOWN) { |
|
gbActive = true; |
|
} else if (event->window.event == SDL_WINDOWEVENT_HIDDEN) { |
|
gbActive = false; |
|
} else if (event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { |
|
ReinitializeHardwareCursor(); |
|
} else if (event->window.event == SDL_WINDOWEVENT_FOCUS_LOST) { |
|
music_mute(); |
|
} else if (event->window.event == SDL_WINDOWEVENT_FOCUS_GAINED) { |
|
diablo_focus_unpause(); |
|
} |
|
} |
|
#else |
|
if (event->type == SDL_ACTIVEEVENT && (event->active.state & SDL_APPINPUTFOCUS) != 0) { |
|
if (event->active.gain == 0) |
|
music_mute(); |
|
else |
|
diablo_focus_unpause(); |
|
} |
|
#endif |
|
} |
|
|
|
void UiFocusNavigationSelect() |
|
{ |
|
UiPlaySelectSound(); |
|
if (textInputActive) { |
|
if (!allowEmptyTextInput && strlen(UiTextInput) == 0) { |
|
return; |
|
} |
|
#ifndef __SWITCH__ |
|
SDL_StopTextInput(); |
|
#endif |
|
UiTextInput = nullptr; |
|
UiTextInputLen = 0; |
|
} |
|
if (gfnListSelect != nullptr) |
|
gfnListSelect(SelectedItem); |
|
} |
|
|
|
void UiFocusNavigationEsc() |
|
{ |
|
UiPlaySelectSound(); |
|
if (textInputActive) { |
|
#ifndef __SWITCH__ |
|
SDL_StopTextInput(); |
|
#endif |
|
UiTextInput = nullptr; |
|
UiTextInputLen = 0; |
|
} |
|
if (gfnListEsc != nullptr) |
|
gfnListEsc(); |
|
} |
|
|
|
void UiFocusNavigationYesNo() |
|
{ |
|
if (gfnListYesNo == nullptr) |
|
return; |
|
|
|
if (gfnListYesNo()) |
|
UiPlaySelectSound(); |
|
} |
|
|
|
namespace { |
|
|
|
bool IsInsideRect(const SDL_Event &event, const SDL_Rect &rect) |
|
{ |
|
const SDL_Point point = { event.button.x, event.button.y }; |
|
return SDL_PointInRect(&point, &rect) == SDL_TRUE; |
|
} |
|
|
|
void LoadHeros() |
|
{ |
|
constexpr unsigned PortraitHeight = 76; |
|
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) { |
|
ArtHeroPortraitOrder[static_cast<std::size_t>(HeroClass::Monk)] = 3; |
|
ArtHeroPortraitOrder[static_cast<std::size_t>(HeroClass::Bard)] = 4; |
|
ArtHeroPortraitOrder[enum_size<HeroClass>::value] = 5; |
|
} |
|
if (numPortraits >= 7) { |
|
ArtHeroPortraitOrder[static_cast<std::size_t>(HeroClass::Barbarian)] = 6; |
|
} |
|
|
|
for (size_t i = 0; i <= enum_size<HeroClass>::value; ++i) { |
|
char portraitPath[18]; |
|
*BufCopy(portraitPath, "ui_art\\hero", i, ".pcx") = '\0'; |
|
|
|
SDL_RWops *handle = OpenAsset(portraitPath); |
|
if (handle == nullptr) { |
|
// Portrait overrides are optional, ignore the error and continue. |
|
SDL_ClearError(); |
|
continue; |
|
} |
|
ArtHeroOverrides[i] = PcxToCl2(handle); |
|
} |
|
} |
|
|
|
void LoadUiGFX() |
|
{ |
|
if (gbIsHellfire) { |
|
ArtLogo = LoadPcxSpriteSheetAsCl2("ui_art\\hf_logo2.pcx", /*numFrames=*/16, /*transparentColor=*/0); |
|
} else { |
|
ArtLogo = LoadPcxSpriteSheetAsCl2("ui_art\\smlogo.pcx", /*numFrames=*/15, /*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); |
|
|
|
#if SDL_VERSION_ATLEAST(2, 0, 0) |
|
// Set the palette because `ArtCursor` may be used as the hardware cursor. |
|
if (ArtCursor.surface != nullptr) { |
|
SDL_SetSurfacePalette(ArtCursor.surface.get(), Palette.get()); |
|
} |
|
#endif |
|
|
|
LoadHeros(); |
|
} |
|
|
|
} // namespace |
|
|
|
CelFrameWithHeight UiGetHeroDialogSprite(size_t heroClassIndex) |
|
{ |
|
return ArtHeroOverrides[heroClassIndex] |
|
? ArtHeroOverrides[heroClassIndex]->sprite() |
|
: ArtHero->sprite(ArtHeroPortraitOrder[heroClassIndex]); |
|
} |
|
|
|
void UnloadUiGFX() |
|
{ |
|
ArtHero = std::nullopt; |
|
for (std::optional<devilution::OwnedCelSpriteWithFrameHeight> &override : ArtHeroOverrides) |
|
override = std::nullopt; |
|
ArtCursor.Unload(); |
|
for (auto &art : ArtFocus) |
|
art = std::nullopt; |
|
ArtLogo = std::nullopt; |
|
} |
|
|
|
void UiInitialize() |
|
{ |
|
LoadUiGFX(); |
|
|
|
if (ArtCursor.surface != nullptr) { |
|
if (SDL_ShowCursor(SDL_DISABLE) <= -1) { |
|
ErrSdl(); |
|
} |
|
} |
|
} |
|
|
|
void UiDestroy() |
|
{ |
|
UnloadFonts(); |
|
UnloadUiGFX(); |
|
} |
|
|
|
bool UiValidPlayerName(string_view name) |
|
{ |
|
if (name.empty()) |
|
return false; |
|
|
|
// Currently only allow saving PlayerNameLength bytes as a player name, so if the name is too long we'd have to truncate it. |
|
// That said the input buffer is only 16 bytes long... |
|
if (name.size() > PlayerNameLength) |
|
return false; |
|
|
|
if (name.find_first_of(",<>%&\\\"?*#/: ") != name.npos) |
|
return false; |
|
|
|
// Only basic latin alphabet is supported for multiplayer characters to avoid rendering issues for players who do |
|
// not have fonts.mpq installed |
|
if (!std::all_of(name.begin(), name.end(), IsBasicLatin)) |
|
return false; |
|
|
|
string_view bannedNames[] = { |
|
"gvdl", |
|
"dvou", |
|
"tiju", |
|
"cjudi", |
|
"bttipmf", |
|
"ojhhfs", |
|
"cmj{{bse", |
|
"benjo", |
|
}; |
|
|
|
std::string buffer { name }; |
|
for (char &character : buffer) |
|
character++; |
|
|
|
string_view tempName { buffer }; |
|
for (string_view bannedName : bannedNames) { |
|
if (tempName.find(bannedName) != tempName.npos) |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
Sint16 GetCenterOffset(Sint16 w, Sint16 bw) |
|
{ |
|
if (bw == 0) { |
|
bw = gnScreenWidth; |
|
} |
|
|
|
return (bw - w) / 2; |
|
} |
|
|
|
bool UiLoadBlackBackground() |
|
{ |
|
LoadBackgroundArt(gbIsHellfire ? "ui_art\\black_hellfire.pcx" : "ui_art\\black_diablo.pcx"); |
|
|
|
if (!ArtBackground) |
|
return false; |
|
|
|
// We only needed the black background for the palette, can now deallocate it. |
|
ArtBackground = std::nullopt; |
|
|
|
return true; |
|
} |
|
|
|
void LoadBackgroundArt(const char *pszFile, int frames) |
|
{ |
|
ArtBackground = std::nullopt; |
|
SDL_Color pPal[256]; |
|
ArtBackground = LoadPcxSpriteSheetAsCl2(pszFile, static_cast<uint16_t>(frames), /*transparentColor=*/std::nullopt, pPal); |
|
if (!ArtBackground) |
|
return; |
|
|
|
LoadPalInMem(pPal); |
|
ApplyGamma(logical_palette, orig_palette, 256); |
|
|
|
fadeTc = 0; |
|
fadeValue = 0; |
|
|
|
if (IsHardwareCursorEnabled() && ArtCursor.surface != nullptr && ControlDevice == ControlTypes::KeyboardAndMouse && GetCurrentCursorInfo().type() != CursorType::UserInterface) { |
|
SetHardwareCursor(CursorInfo::UserInterfaceCursor()); |
|
} |
|
|
|
BlackPalette(); |
|
|
|
SDL_FillRect(DiabloUiSurface(), nullptr, 0x000000); |
|
if (DiabloUiSurface() == PalSurface) |
|
BltFast(nullptr, nullptr); |
|
RenderPresent(); |
|
} |
|
|
|
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<UiImageCl2>(ArtBackgroundWidescreen->sprite(), rect, UiFlags::AlignCenter)); |
|
} |
|
if (ArtBackground) { |
|
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<UiImageAnimatedCl2>( |
|
ArtLogo->sheet(), MakeSdlRect(0, GetUIRectangle().position.y, 0, 0), UiFlags::AlignCenter)); |
|
} |
|
|
|
void UiFadeIn() |
|
{ |
|
if (fadeValue < 256) { |
|
if (fadeValue == 0 && fadeTc == 0) |
|
fadeTc = SDL_GetTicks(); |
|
const int prevFadeValue = fadeValue; |
|
fadeValue = static_cast<int>((SDL_GetTicks() - fadeTc) / 2.083); // 32 frames @ 60hz |
|
if (fadeValue > 256) { |
|
fadeValue = 256; |
|
fadeTc = 0; |
|
} |
|
if (fadeValue != prevFadeValue) |
|
SetFadeLevel(fadeValue); |
|
} |
|
|
|
if (DiabloUiSurface() == PalSurface) |
|
BltFast(nullptr, nullptr); |
|
RenderPresent(); |
|
} |
|
|
|
void DrawCel(CelSpriteWithFrameHeight sprite, Point p) |
|
{ |
|
const Surface &out = Surface(DiabloUiSurface()); |
|
Cl2Draw(out, { p.x, static_cast<int>(p.y + sprite.frameHeight) }, sprite.sprite, 0); |
|
} |
|
|
|
void DrawAnimatedCel(CelSpriteWithFrameHeight sprite, Point p) |
|
{ |
|
const Surface &out = Surface(DiabloUiSurface()); |
|
const int frame = GetAnimationFrame(LoadLE32(sprite.sprite.Data())); |
|
Cl2Draw(out, { p.x, static_cast<int>(p.y + sprite.frameHeight) }, sprite.sprite, frame); |
|
} |
|
|
|
void DrawSelector(const SDL_Rect &rect) |
|
{ |
|
int size = FOCUS_SMALL; |
|
if (rect.h >= 42) |
|
size = FOCUS_BIG; |
|
else if (rect.h >= 30) |
|
size = FOCUS_MED; |
|
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.frameHeight)) / 2; |
|
|
|
const Surface &out = Surface(DiabloUiSurface()); |
|
RenderCl2Sprite(out, sprite, { rect.x, y }); |
|
RenderCl2Sprite(out, sprite, { rect.x + rect.w - sprite.width(), y }); |
|
} |
|
|
|
void UiClearScreen() |
|
{ |
|
if (!ArtBackground || gnScreenWidth > ArtBackground->sprite(0).width() || gnScreenHeight > ArtBackground->frameHeight) |
|
SDL_FillRect(DiabloUiSurface(), nullptr, 0x000000); |
|
} |
|
|
|
void UiPollAndRender(std::function<bool(SDL_Event &)> eventHandler) |
|
{ |
|
SDL_Event event; |
|
while (PollEvent(&event) != 0) { |
|
if (eventHandler && eventHandler(event)) |
|
continue; |
|
UiFocusNavigation(&event); |
|
UiHandleEvents(&event); |
|
} |
|
HandleMenuAction(GetMenuHeldUpDownAction()); |
|
UiRenderItems(gUiItems); |
|
DrawMouse(); |
|
UiFadeIn(); |
|
|
|
// Must happen after the very first UiFadeIn, which sets the cursor. |
|
if (IsHardwareCursor()) |
|
SetHardwareCursorVisible(ControlDevice == ControlTypes::KeyboardAndMouse); |
|
|
|
#ifdef __3DS__ |
|
// Keyboard blocks until input is finished |
|
// so defer until after render and fade-in |
|
ctr_vkbdFlush(); |
|
#endif |
|
|
|
discord_manager::UpdateMenu(); |
|
} |
|
|
|
namespace { |
|
|
|
void Render(const UiText *uiText) |
|
{ |
|
const Surface &out = Surface(DiabloUiSurface()); |
|
DrawString(out, uiText->GetText(), MakeRectangle(uiText->m_rect), uiText->GetFlags() | UiFlags::FontSizeDialog); |
|
} |
|
|
|
void Render(const UiArtText *uiArtText) |
|
{ |
|
const Surface &out = Surface(DiabloUiSurface()); |
|
DrawString(out, uiArtText->GetText(), MakeRectangle(uiArtText->m_rect), uiArtText->GetFlags(), uiArtText->GetSpacing(), uiArtText->GetLineHeight()); |
|
} |
|
|
|
void Render(const UiImageCl2 *uiImage) |
|
{ |
|
CelFrameWithHeight sprite = uiImage->sprite(); |
|
int x = uiImage->m_rect.x; |
|
if (uiImage->isCentered()) { |
|
x += GetCenterOffset(sprite.width(), uiImage->m_rect.w); |
|
} |
|
RenderCl2Sprite(Surface(DiabloUiSurface()), sprite, { x, uiImage->m_rect.y }); |
|
} |
|
|
|
void Render(const UiImageAnimatedCl2 *uiImage) |
|
{ |
|
CelFrameWithHeight sprite = uiImage->sprite(GetAnimationFrame(uiImage->numFrames())); |
|
int x = uiImage->m_rect.x; |
|
if (uiImage->isCentered()) { |
|
x += GetCenterOffset(sprite.width(), uiImage->m_rect.w); |
|
} |
|
RenderCl2Sprite(Surface(DiabloUiSurface()), sprite, { x, uiImage->m_rect.y }); |
|
} |
|
|
|
void Render(const UiArtTextButton *uiButton) |
|
{ |
|
const Surface &out = Surface(DiabloUiSurface()); |
|
DrawString(out, uiButton->GetText(), MakeRectangle(uiButton->m_rect), uiButton->GetFlags()); |
|
} |
|
|
|
void Render(const UiList *uiList) |
|
{ |
|
const Surface &out = Surface(DiabloUiSurface()); |
|
|
|
for (std::size_t i = listOffset; i < uiList->m_vecItems.size() && (i - listOffset) < ListViewportSize; ++i) { |
|
SDL_Rect rect = uiList->itemRect(i - listOffset); |
|
const UiListItem *item = uiList->GetItem(i); |
|
if (i == SelectedItem) |
|
DrawSelector(rect); |
|
|
|
Rectangle rectangle = MakeRectangle(rect); |
|
if (item->args.empty()) |
|
DrawString(out, item->m_text, rectangle, uiList->GetFlags() | item->uiFlags, uiList->GetSpacing()); |
|
else |
|
DrawStringWithColors(out, item->m_text, item->args, rectangle, uiList->GetFlags() | item->uiFlags, uiList->GetSpacing()); |
|
} |
|
} |
|
|
|
void Render(const UiScrollbar *uiSb) |
|
{ |
|
const Surface out = Surface(DiabloUiSurface()); |
|
|
|
// Bar background (tiled): |
|
{ |
|
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) { |
|
RenderCl2Sprite(backgroundOut, uiSb->m_bg, { 0, y }); |
|
y += uiSb->m_bg.height(); |
|
} |
|
} |
|
|
|
// Arrows: |
|
{ |
|
const SDL_Rect rect = UpArrowRect(uiSb); |
|
const auto frame = static_cast<uint16_t>(scrollBarState.upArrowPressed ? ScrollBarArrowFrame_UP_ACTIVE : ScrollBarArrowFrame_UP); |
|
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); |
|
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); |
|
RenderCl2Sprite(out, uiSb->m_thumb, { rect.x, rect.y }); |
|
} |
|
} |
|
|
|
void Render(const UiEdit *uiEdit) |
|
{ |
|
DrawSelector(uiEdit->m_rect); |
|
|
|
// To simulate padding we inset the region used to draw text in an edit control |
|
Rectangle rect = MakeRectangle(uiEdit->m_rect).inset({ 43, 1 }); |
|
|
|
const Surface &out = Surface(DiabloUiSurface()); |
|
DrawString(out, uiEdit->m_value, rect, uiEdit->GetFlags() | UiFlags::TextCursor); |
|
} |
|
|
|
void RenderItem(UiItemBase *item) |
|
{ |
|
if (item->IsHidden()) |
|
return; |
|
switch (item->GetType()) { |
|
case UiType::Text: |
|
Render(static_cast<UiText *>(item)); |
|
break; |
|
case UiType::ArtText: |
|
Render(static_cast<UiArtText *>(item)); |
|
break; |
|
case UiType::ImageCl2: |
|
Render(static_cast<UiImageCl2 *>(item)); |
|
break; |
|
case UiType::ImageAnimatedCl2: |
|
Render(static_cast<UiImageAnimatedCl2 *>(item)); |
|
break; |
|
case UiType::ArtTextButton: |
|
Render(static_cast<UiArtTextButton *>(item)); |
|
break; |
|
case UiType::Button: |
|
RenderButton(static_cast<UiButton *>(item)); |
|
break; |
|
case UiType::List: |
|
Render(static_cast<UiList *>(item)); |
|
break; |
|
case UiType::Scrollbar: |
|
Render(static_cast<UiScrollbar *>(item)); |
|
break; |
|
case UiType::Edit: |
|
Render(static_cast<UiEdit *>(item)); |
|
break; |
|
} |
|
} |
|
|
|
bool HandleMouseEventArtTextButton(const SDL_Event &event, const UiArtTextButton *uiButton) |
|
{ |
|
if (event.type != SDL_MOUSEBUTTONUP || event.button.button != SDL_BUTTON_LEFT) { |
|
return false; |
|
} |
|
|
|
uiButton->Activate(); |
|
return true; |
|
} |
|
|
|
#ifdef USE_SDL1 |
|
Uint32 dbClickTimer; |
|
#endif |
|
|
|
bool HandleMouseEventList(const SDL_Event &event, UiList *uiList) |
|
{ |
|
if (event.button.button != SDL_BUTTON_LEFT) |
|
return false; |
|
|
|
if (event.type != SDL_MOUSEBUTTONUP && event.type != SDL_MOUSEBUTTONDOWN) |
|
return false; |
|
|
|
std::size_t index = uiList->indexAt(event.button.y); |
|
if (event.type == SDL_MOUSEBUTTONDOWN) { |
|
uiList->Press(index); |
|
return true; |
|
} |
|
|
|
if (event.type == SDL_MOUSEBUTTONUP && !uiList->IsPressed(index)) |
|
return false; |
|
|
|
index += listOffset; |
|
|
|
if (gfnListFocus != nullptr && SelectedItem != index) { |
|
UiFocus(index, true, false); |
|
#ifdef USE_SDL1 |
|
dbClickTimer = SDL_GetTicks(); |
|
} else if (gfnListFocus == NULL || dbClickTimer + 500 >= SDL_GetTicks()) { |
|
#else |
|
} else if (gfnListFocus == nullptr || event.button.clicks >= 2) { |
|
#endif |
|
if (HasAnyOf(uiList->GetItem(index)->uiFlags, UiFlags::ElementHidden | UiFlags::ElementDisabled)) |
|
return false; |
|
SelectedItem = index; |
|
UiFocusNavigationSelect(); |
|
#ifdef USE_SDL1 |
|
} else { |
|
dbClickTimer = SDL_GetTicks(); |
|
#endif |
|
} |
|
|
|
return true; |
|
} |
|
|
|
bool HandleMouseEventScrollBar(const SDL_Event &event, const UiScrollbar *uiSb) |
|
{ |
|
if (event.button.button != SDL_BUTTON_LEFT) |
|
return false; |
|
if (event.type == SDL_MOUSEBUTTONUP) { |
|
if (scrollBarState.upArrowPressed && IsInsideRect(event, UpArrowRect(uiSb))) { |
|
UiFocusUp(); |
|
return true; |
|
} |
|
if (scrollBarState.downArrowPressed && IsInsideRect(event, DownArrowRect(uiSb))) { |
|
UiFocusDown(); |
|
return true; |
|
} |
|
} else if (event.type == SDL_MOUSEBUTTONDOWN) { |
|
if (IsInsideRect(event, BarRect(uiSb))) { |
|
// Scroll up or down based on thumb position. |
|
const SDL_Rect thumbRect = ThumbRect(uiSb, SelectedItem, SelectedItemMax + 1); |
|
if (event.button.y < thumbRect.y) { |
|
UiFocusPageUp(); |
|
} else if (event.button.y > thumbRect.y + thumbRect.h) { |
|
UiFocusPageDown(); |
|
} |
|
return true; |
|
} |
|
if (IsInsideRect(event, UpArrowRect(uiSb))) { |
|
scrollBarState.upArrowPressed = true; |
|
return true; |
|
} |
|
if (IsInsideRect(event, DownArrowRect(uiSb))) { |
|
scrollBarState.downArrowPressed = true; |
|
return true; |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
bool HandleMouseEvent(const SDL_Event &event, UiItemBase *item) |
|
{ |
|
if (item->IsNotInteractive() || !IsInsideRect(event, item->m_rect)) |
|
return false; |
|
switch (item->GetType()) { |
|
case UiType::ArtTextButton: |
|
return HandleMouseEventArtTextButton(event, static_cast<UiArtTextButton *>(item)); |
|
case UiType::Button: |
|
return HandleMouseEventButton(event, static_cast<UiButton *>(item)); |
|
case UiType::List: |
|
return HandleMouseEventList(event, static_cast<UiList *>(item)); |
|
case UiType::Scrollbar: |
|
return HandleMouseEventScrollBar(event, static_cast<UiScrollbar *>(item)); |
|
default: |
|
return false; |
|
} |
|
} |
|
|
|
} // namespace |
|
|
|
void LoadPalInMem(const SDL_Color *pPal) |
|
{ |
|
for (int i = 0; i < 256; i++) { |
|
orig_palette[i] = pPal[i]; |
|
} |
|
} |
|
|
|
void UiRenderItems(const std::vector<UiItemBase *> &items) |
|
{ |
|
for (const auto &item : items) |
|
RenderItem(item); |
|
} |
|
|
|
void UiRenderItems(const std::vector<std::unique_ptr<UiItemBase>> &items) |
|
{ |
|
for (const auto &item : items) |
|
RenderItem(item.get()); |
|
} |
|
|
|
bool UiItemMouseEvents(SDL_Event *event, const std::vector<UiItemBase *> &items) |
|
{ |
|
if (items.empty()) { |
|
return false; |
|
} |
|
|
|
// In SDL2 mouse events already use logical coordinates. |
|
#ifdef USE_SDL1 |
|
OutputToLogical(&event->button.x, &event->button.y); |
|
#endif |
|
|
|
bool handled = false; |
|
for (const auto &item : items) { |
|
if (HandleMouseEvent(*event, item)) { |
|
handled = true; |
|
break; |
|
} |
|
} |
|
|
|
if (event->type == SDL_MOUSEBUTTONUP && event->button.button == SDL_BUTTON_LEFT) { |
|
scrollBarState.downArrowPressed = scrollBarState.upArrowPressed = false; |
|
for (const auto &item : items) { |
|
if (item->IsType(UiType::Button)) { |
|
HandleGlobalMouseUpButton(static_cast<UiButton *>(item)); |
|
} else if (item->IsType(UiType::List)) { |
|
static_cast<UiList *>(item)->Release(); |
|
} |
|
} |
|
} |
|
|
|
return handled; |
|
} |
|
|
|
bool UiItemMouseEvents(SDL_Event *event, const std::vector<std::unique_ptr<UiItemBase>> &items) |
|
{ |
|
if (items.empty()) { |
|
return false; |
|
} |
|
|
|
// In SDL2 mouse events already use logical coordinates. |
|
#ifdef USE_SDL1 |
|
OutputToLogical(&event->button.x, &event->button.y); |
|
#endif |
|
|
|
bool handled = false; |
|
for (const auto &item : items) { |
|
if (HandleMouseEvent(*event, item.get())) { |
|
handled = true; |
|
break; |
|
} |
|
} |
|
|
|
if (event->type == SDL_MOUSEBUTTONUP && event->button.button == SDL_BUTTON_LEFT) { |
|
scrollBarState.downArrowPressed = scrollBarState.upArrowPressed = false; |
|
for (const auto &item : items) { |
|
if (item->IsType(UiType::Button)) { |
|
HandleGlobalMouseUpButton(static_cast<UiButton *>(item.get())); |
|
} else if (item->IsType(UiType::List)) { |
|
static_cast<UiList *>(item.get())->Release(); |
|
} |
|
} |
|
} |
|
|
|
return handled; |
|
} |
|
|
|
void DrawMouse() |
|
{ |
|
if (ControlDevice != ControlTypes::KeyboardAndMouse || IsHardwareCursor()) |
|
return; |
|
|
|
DrawArt(MousePosition, &ArtCursor); |
|
} |
|
} // namespace devilution
|
|
|