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.
 
 
 
 
 
 

319 lines
8.7 KiB

#include "control_chat.hpp"
#include "control.hpp"
#include "control_panel.hpp"
#include "control/control_chat_commands.hpp"
#include "engine/backbuffer_state.hpp"
#include "engine/render/clx_render.hpp"
#include "options.h"
#include "panels/console.hpp"
#include "panels/mainpanel.hpp"
#include "quick_messages.hpp"
#include "utils/display.h"
#include "utils/sdl_compat.h"
#include "utils/str_cat.hpp"
namespace devilution {
std::optional<TextInputState> ChatInputState;
char TalkMessage[MAX_SEND_STR_LEN];
bool TalkButtonsDown[3];
int sgbPlrTalkTbl;
bool WhisperList[MAX_PLRS];
OptionalOwnedClxSpriteList talkButtons;
namespace {
char TalkSave[8][MAX_SEND_STR_LEN];
uint8_t TalkSaveIndex;
uint8_t NextTalkSave;
TextInputCursorState ChatCursor;
int MuteButtons = 3;
int MuteButtonPadding = 2;
Rectangle MuteButtonRect { { 172, 69 }, { 61, 16 } };
void ResetChatMessage()
{
if (CheckChatCommand(TalkMessage))
return;
uint32_t pmask = 0;
for (size_t i = 0; i < Players.size(); i++) {
if (WhisperList[i])
pmask |= 1 << i;
}
NetSendCmdString(pmask, TalkMessage);
}
void ControlPressEnter()
{
if (TalkMessage[0] != 0) {
ResetChatMessage();
uint8_t i = 0;
for (; i < 8; i++) {
if (strcmp(TalkSave[i], TalkMessage) == 0)
break;
}
if (i >= 8) {
strcpy(TalkSave[NextTalkSave], TalkMessage);
NextTalkSave++;
NextTalkSave &= 7;
} else {
uint8_t talkSave = NextTalkSave - 1;
talkSave &= 7;
if (i != talkSave) {
strcpy(TalkSave[i], TalkSave[talkSave]);
*BufCopy(TalkSave[talkSave], ChatInputState->value()) = '\0';
}
}
TalkMessage[0] = '\0';
TalkSaveIndex = NextTalkSave;
}
ResetChat();
}
void ControlUpDown(int v)
{
for (int i = 0; i < 8; i++) {
TalkSaveIndex = (v + TalkSaveIndex) & 7;
if (TalkSave[TalkSaveIndex][0] != 0) {
ChatInputState->assign(TalkSave[TalkSaveIndex]);
return;
}
}
}
} // namespace
void DrawChatBox(const Surface &out)
{
if (!ChatFlag)
return;
const Point mainPanelPosition = GetMainPanel().position;
DrawPanelBox(out, MakeSdlRect(175, sgbPlrTalkTbl + 20, 294, 5), mainPanelPosition + Displacement { 175, 4 });
int off = 0;
for (int i = 293; i > 283; off++, i--) {
DrawPanelBox(out, MakeSdlRect((off / 2) + 175, sgbPlrTalkTbl + off + 25, i, 1), mainPanelPosition + Displacement { (off / 2) + 175, off + 9 });
}
DrawPanelBox(out, MakeSdlRect(185, sgbPlrTalkTbl + 35, 274, 30), mainPanelPosition + Displacement { 185, 19 });
DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + 65, 284, 5), mainPanelPosition + Displacement { 180, 49 });
for (int i = 0; i < 10; i++) {
DrawPanelBox(out, MakeSdlRect(180, sgbPlrTalkTbl + i + 70, i + 284, 1), mainPanelPosition + Displacement { 180, i + 54 });
}
DrawPanelBox(out, MakeSdlRect(170, sgbPlrTalkTbl + 80, 310, 55), mainPanelPosition + Displacement { 170, 64 });
int x = mainPanelPosition.x + 200;
const int y = mainPanelPosition.y + 10;
const uint32_t len = DrawString(out, TalkMessage, { { x, y }, { 250, 39 } },
{
.flags = UiFlags::ColorWhite | UiFlags::PentaCursor,
.lineHeight = 13,
.cursorPosition = static_cast<int>(ChatCursor.position),
.highlightRange = { static_cast<int>(ChatCursor.selection.begin), static_cast<int>(ChatCursor.selection.end) },
});
ChatInputState->truncate(len);
x += 46;
int talkBtn = 0;
for (size_t i = 0; i < Players.size(); i++) {
Player &player = Players[i];
if (&player == MyPlayer)
continue;
const UiFlags color = player.friendlyMode ? UiFlags::ColorWhitegold : UiFlags::ColorRed;
const Point talkPanPosition = mainPanelPosition + Displacement { 172, 84 + 18 * talkBtn };
if (WhisperList[i]) {
// the normal (unpressed) voice button is pre-rendered on the panel, only need to draw over it when the button is held
if (TalkButtonsDown[talkBtn]) {
const unsigned spriteIndex = talkBtn == 0 ? 2 : 3; // the first button sprite includes a tip from the devils wing so is different to the rest.
ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]);
// Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting
// the first button be treated the same as the other two further down the panel.
RenderClxSprite(out, (*TalkButton)[2], talkPanPosition + Displacement { 4, -15 });
}
} else {
unsigned spriteIndex = talkBtn == 0 ? 0 : 1; // the first button sprite includes a tip from the devils wing so is different to the rest.
if (TalkButtonsDown[talkBtn])
spriteIndex += 4; // held button sprites are at index 4 and 5 (with and without wingtip respectively)
ClxDraw(out, talkPanPosition, (*talkButtons)[spriteIndex]);
// Draw the translated string over the top of the default (english) button. This graphic is inset to avoid overlapping the wingtip, letting
// the first button be treated the same as the other two further down the panel.
RenderClxSprite(out, (*TalkButton)[TalkButtonsDown[talkBtn] ? 1 : 0], talkPanPosition + Displacement { 4, -15 });
}
if (player.plractive) {
DrawString(out, player._pName, { { x, y + 60 + talkBtn * 18 }, { 204, 0 } }, { .flags = color });
}
talkBtn++;
}
}
bool CheckMuteButton()
{
if (!ChatFlag)
return false;
Rectangle buttons = MuteButtonRect;
SetPanelObjectPosition(UiPanels::Main, buttons);
buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding);
if (!buttons.contains(MousePosition))
return false;
for (bool &talkButtonDown : TalkButtonsDown) {
talkButtonDown = false;
}
const Point mainPanelPosition = GetMainPanel().position;
TalkButtonsDown[(MousePosition.y - (69 + mainPanelPosition.y)) / 18] = true;
return true;
}
void CheckMuteButtonUp()
{
if (!ChatFlag)
return;
for (bool &talkButtonDown : TalkButtonsDown)
talkButtonDown = false;
Rectangle buttons = MuteButtonRect;
SetPanelObjectPosition(UiPanels::Main, buttons);
buttons.size.height = (MuteButtons * buttons.size.height) + ((MuteButtons - 1) * MuteButtonPadding);
if (!buttons.contains(MousePosition))
return;
int off = (MousePosition.y - buttons.position.y) / (MuteButtonRect.size.height + MuteButtonPadding);
size_t playerId = 0;
for (; playerId < Players.size() && off != -1; ++playerId) {
if (playerId != MyPlayerId)
off--;
}
if (playerId > 0 && playerId <= Players.size())
WhisperList[playerId - 1] = !WhisperList[playerId - 1];
}
void TypeChatMessage()
{
if (!IsChatAvailable())
return;
ChatFlag = true;
TalkMessage[0] = '\0';
ChatInputState.emplace(TextInputState::Options {
.value = TalkMessage,
.cursor = &ChatCursor,
.maxLength = sizeof(TalkMessage) - 1 });
for (bool &talkButtonDown : TalkButtonsDown) {
talkButtonDown = false;
}
sgbPlrTalkTbl = GetMainPanel().size.height + PanelPaddingHeight;
RedrawEverything();
TalkSaveIndex = NextTalkSave;
SDL_Rect rect = MakeSdlRect(GetMainPanel().position.x + 200, GetMainPanel().position.y + 22, 0, 27);
SDL_SetTextInputArea(ghMainWnd, &rect, /*cursor=*/0);
SDLC_StartTextInput(ghMainWnd);
}
void ResetChat()
{
ChatFlag = false;
SDLC_StopTextInput(ghMainWnd);
ChatCursor = {};
ChatInputState = std::nullopt;
sgbPlrTalkTbl = 0;
RedrawEverything();
}
bool IsChatActive()
{
if (!IsChatAvailable())
return false;
if (!ChatFlag)
return false;
return true;
}
bool CheckKeypress(SDL_Keycode vkey)
{
if (!IsChatAvailable())
return false;
if (!ChatFlag)
return false;
switch (vkey) {
case SDLK_ESCAPE:
ResetChat();
return true;
case SDLK_RETURN:
case SDLK_KP_ENTER:
ControlPressEnter();
return true;
case SDLK_DOWN:
ControlUpDown(1);
return true;
case SDLK_UP:
ControlUpDown(-1);
return true;
default:
return vkey >= SDLK_SPACE && vkey <= SDLK_Z;
}
}
void DiabloHotkeyMsg(uint32_t dwMsg)
{
assert(dwMsg < QuickMessages.size());
#ifdef _DEBUG
constexpr std::string_view LuaPrefix = "/lua ";
for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) {
if (!msg.starts_with(LuaPrefix)) continue;
InitConsole();
RunInConsole(std::string_view(msg).substr(LuaPrefix.size()));
}
#endif
if (!IsChatAvailable()) {
return;
}
for (const std::string &msg : GetOptions().Chat.szHotKeyMsgs[dwMsg]) {
#ifdef _DEBUG
if (msg.starts_with(LuaPrefix)) continue;
#endif
char charMsg[MAX_SEND_STR_LEN];
CopyUtf8(charMsg, msg, sizeof(charMsg));
NetSendCmdString(0xFFFFFF, charMsg);
}
}
bool IsChatAvailable()
{
return gbIsMultiplayer;
}
bool HandleTalkTextInputEvent(const SDL_Event &event)
{
return HandleInputEvent(event, ChatInputState);
}
} // namespace devilution