diff --git a/Phobos.vcxproj b/Phobos.vcxproj index a9e600eb42..449832bae2 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -43,6 +43,8 @@ + + @@ -191,7 +193,6 @@ - @@ -226,6 +227,8 @@ + + diff --git a/docs/User-Interface.md b/docs/User-Interface.md index 5807427f78..960679e711 100644 --- a/docs/User-Interface.md +++ b/docs/User-Interface.md @@ -370,17 +370,23 @@ ShowTimer.Priority=0 ; integer ### Task subtitles display in the middle of the screen -![Message Display In Center](_static/images/messagedisplayincenter.png) +![Message Display In Center](_static/images/messagedisplayincenter.gif) +*Taking a campaign in [Mental Omega](https://www.mentalomega.com) as an example to display messages in center* - Now you can set `MessageApplyHoverState` to true,to make the upper left messages not disappear while mouse hovering over the top of display area. -- You can also let task subtitles (created by trigger 11) to display directly in the middle area of the screen instead of the upper left corner, with a semi transparent background, by setting `MessageDisplayInCenter` to true. - - If you also set `MessageApplyHoverState` to true, when the mouse hovers over the subtitle area (simply judged as a rectangle), its opacity will increase and it will not disappear during this period. +- You can also let task subtitles (created by trigger 11) to display directly in the middle area of the screen instead of the upper left corner, with a semi transparent background, by setting `MessageDisplayInCenter` to true. In this case, all messages within this game can be saved, even after being s/l. + - If you also set `MessageApplyHoverState` to true, when the mouse hovers over the subtitle area (simply judged as a rectangle), its opacity will increase and it will not disappear during this period. If the area is expanded, disabling this option will not prevent mouse clicking behavior from being restricted to this area. + - `MessageDisplayInCenter.LabelsCount` controls the maximum number of subtitle labels that can automatically pop up at a same time in the middle area of the screen. At least 1. + - `MessageDisplayInCenter.RecordsCount` controls the maximum number of historical messages displayed when this middle area is expanded (not the maximum number that can be stored). At least 4. + - The label can be toggled by ["Toggle Message Label" hotkey](#toggle-message-label) in "Interface" category. In `RA2MD.INI`: ```ini [Phobos] -MessageApplyHoverState=false ; boolean -MessageDisplayInCenter=false ; boolean +MessageApplyHoverState=false ; boolean +MessageDisplayInCenter=false ; boolean +MessageDisplayInCenter.LabelsCount=6 ; integer +MessageDisplayInCenter.RecordsCount=12 ; integer ``` ### Type select for buildings @@ -472,6 +478,11 @@ For this command to work in multiplayer - you need to use a version of [YRpp spa - These vanilla CSF entries will be used: `TXT_SAVING_GAME`, `TXT_GAME_WAS_SAVED` and `TXT_ERROR_SAVING_GAME`. - The save should be looks like `Allied Mission 25: Esther's Money - QuickSaved`. +### `[ ]` Toggle Message Label + +- Switches on/off [Task subtitles' label in the middle of the screen](#task-subtitles-display-in-the-middle-of-the-screen). +- For localization add `TXT_TOGGLE_MESSAGE` and `TXT_TOGGLE_MESSAGE_DESC` into your `.csf` file. + ## Loading screen - PCX files can now be used as loadscreen images. diff --git a/docs/_static/images/messagedisplayincenter.gif b/docs/_static/images/messagedisplayincenter.gif new file mode 100644 index 0000000000..00377c1f7f Binary files /dev/null and b/docs/_static/images/messagedisplayincenter.gif differ diff --git a/docs/_static/images/messagedisplayincenter.png b/docs/_static/images/messagedisplayincenter.png deleted file mode 100644 index 940840cdc4..0000000000 Binary files a/docs/_static/images/messagedisplayincenter.png and /dev/null differ diff --git a/src/Commands/Commands.cpp b/src/Commands/Commands.cpp index 8d9d2611a1..0e5ad329af 100644 --- a/src/Commands/Commands.cpp +++ b/src/Commands/Commands.cpp @@ -1,4 +1,3 @@ -#include #include "Commands.h" #include "ObjectInfo.h" @@ -12,7 +11,13 @@ #include "SaveVariablesToFile.h" #include "ToggleSWSidebar.h" #include "FireTacticalSW.h" +#include "ToggleMessageList.h" + +#include + +#include #include +#include DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6) { @@ -22,6 +27,7 @@ DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6) MakeCommand(); MakeCommand(); MakeCommand(); + MakeCommand(); MakeCommand(); if (Phobos::Config::SuperWeaponSidebarCommands) @@ -54,3 +60,38 @@ DEFINE_HOOK(0x533066, CommandClassCallback_Register, 0x6) return 0; } + +static void MouseWheelDownCommand() +{ + if (MessageColumnClass::Instance.IsHovering()) + MessageColumnClass::Instance.ScrollDown(); +} + +static void MouseWheelUpCommand() +{ + if (MessageColumnClass::Instance.IsHovering()) + MessageColumnClass::Instance.ScrollUp(); +} + +DEFINE_HOOK(0x777998, Game_WndProc_ScrollMouseWheel, 0x6) +{ + GET(const WPARAM, WParam, ECX); + + if (WParam & 0x80000000u) + MouseWheelDownCommand(); + else + MouseWheelUpCommand(); + + return 0; +} + +static inline bool CheckSkipScrollSidebar() +{ + return MessageColumnClass::Instance.IsHovering(); +} + +DEFINE_HOOK(0x533F50, Game_ScrollSidebar_Skip, 0x5) +{ + enum { SkipScrollSidebar = 0x533FC3 }; + return CheckSkipScrollSidebar() ? SkipScrollSidebar : 0; +} diff --git a/src/Commands/ToggleMessageList.cpp b/src/Commands/ToggleMessageList.cpp new file mode 100644 index 0000000000..c167529b44 --- /dev/null +++ b/src/Commands/ToggleMessageList.cpp @@ -0,0 +1,29 @@ +#include "ToggleMessageList.h" + +#include +#include + +const char* ToggleMessageListCommandClass::GetName() const +{ + return "Toggle Message Label"; +} + +const wchar_t* ToggleMessageListCommandClass::GetUIName() const +{ + return GeneralUtils::LoadStringUnlessMissing("TXT_TOGGLE_MESSAGE", L"Toggle Message Label"); +} + +const wchar_t* ToggleMessageListCommandClass::GetUICategory() const +{ + return CATEGORY_INTERFACE; +} + +const wchar_t* ToggleMessageListCommandClass::GetUIDescription() const +{ + return GeneralUtils::LoadStringUnlessMissing("TXT_TOGGLE_MESSAGE_DESC", L"Toggle message label in the middle of the screen."); +} + +void ToggleMessageListCommandClass::Execute(WWKey eInput) const +{ + MessageColumnClass::Instance.Toggle(); +} diff --git a/src/Commands/ToggleMessageList.h b/src/Commands/ToggleMessageList.h new file mode 100644 index 0000000000..f1ddf24d6d --- /dev/null +++ b/src/Commands/ToggleMessageList.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Commands.h" + +// Display damage strings +class ToggleMessageListCommandClass : public CommandClass +{ +public: + virtual const char* GetName() const override; + virtual const wchar_t* GetUIName() const override; + virtual const wchar_t* GetUICategory() const override; + virtual const wchar_t* GetUIDescription() const override; + virtual void Execute(WWKey eInput) const override; +}; diff --git a/src/Ext/Scenario/Body.cpp b/src/Ext/Scenario/Body.cpp index e2e4b055fa..3ca0dba57d 100644 --- a/src/Ext/Scenario/Body.cpp +++ b/src/Ext/Scenario/Body.cpp @@ -163,7 +163,7 @@ void ScenarioExt::ExtData::Serialize(T& Stm) .Process(this->TransportReloaders) .Process(this->SWSidebar_Enable) .Process(this->SWSidebar_Indices) -// .Process(this->NewMessageList); // Should not S/L + .Process(this->RecordMessages) ; } diff --git a/src/Ext/Scenario/Body.h b/src/Ext/Scenario/Body.h index ce09cd2a3e..335f74d0f6 100644 --- a/src/Ext/Scenario/Body.h +++ b/src/Ext/Scenario/Body.h @@ -40,7 +40,7 @@ class ScenarioExt bool SWSidebar_Enable; std::vector SWSidebar_Indices; - std::unique_ptr NewMessageList; + std::vector RecordMessages; ExtData(ScenarioClass* OwnerObject) : Extension(OwnerObject) , ShowBriefing { false } @@ -51,7 +51,7 @@ class ScenarioExt , TransportReloaders {} , SWSidebar_Enable { true } , SWSidebar_Indices {} - , NewMessageList {} + , RecordMessages {} { } void SetVariableToByID(bool bIsGlobal, int nIndex, char bState); diff --git a/src/Ext/Side/Body.cpp b/src/Ext/Side/Body.cpp index 9d59ab3cdb..e59f347000 100644 --- a/src/Ext/Side/Body.cpp +++ b/src/Ext/Side/Body.cpp @@ -10,6 +10,14 @@ void SideExt::ExtData::Initialize() this->ArrayIndex = SideClass::FindIndex(pID); this->Sidebar_GDIPositions = this->ArrayIndex == 0; // true = Allied + + // Init MessageTextColor like Ares + if (!_strcmpi(pID, "Nod")) //Soviets + this->MessageTextColor = 11; + else if (!_strcmpi(pID, "ThirdSide")) //Yuri + this->MessageTextColor = 25; + else //Allies or any other country + this->MessageTextColor = 21; }; void SideExt::ExtData::LoadFromINIFile(CCINIClass* pINI) @@ -40,6 +48,7 @@ void SideExt::ExtData::LoadFromINIFile(CCINIClass* pINI) this->ToolTip_Background_Opacity.Read(exINI, pSection, "ToolTip.Background.Opacity"); this->ToolTip_Background_BlurSize.Read(exINI, pSection, "ToolTip.Background.BlurSize"); this->BriefingTheme = pINI->ReadTheme(pSection, "BriefingTheme", this->BriefingTheme); + this->MessageTextColor.Read(exINI, pSection, "MessageTextColor"); this->SuperWeaponSidebar_OnPCX.Read(pINI, pSection, "SuperWeaponSidebar.OnPCX"); this->SuperWeaponSidebar_OffPCX.Read(pINI, pSection, "SuperWeaponSidebar.OffPCX"); this->SuperWeaponSidebar_TopPCX.Read(pINI, pSection, "SuperWeaponSidebar.TopPCX"); @@ -74,6 +83,7 @@ void SideExt::ExtData::Serialize(T& Stm) .Process(this->IngameScore_WinTheme) .Process(this->IngameScore_LoseTheme) .Process(this->BriefingTheme) + .Process(this->MessageTextColor) .Process(this->SuperWeaponSidebar_OnPCX) .Process(this->SuperWeaponSidebar_OffPCX) .Process(this->SuperWeaponSidebar_TopPCX) diff --git a/src/Ext/Side/Body.h b/src/Ext/Side/Body.h index e0f3362ef0..bc0d72a748 100644 --- a/src/Ext/Side/Body.h +++ b/src/Ext/Side/Body.h @@ -36,6 +36,7 @@ class SideExt Nullable ToolTip_Background_Opacity; Nullable ToolTip_Background_BlurSize; Valueable BriefingTheme; + ValueableIdx MessageTextColor; PhobosPCXFile SuperWeaponSidebar_OnPCX; PhobosPCXFile SuperWeaponSidebar_OffPCX; PhobosPCXFile SuperWeaponSidebar_TopPCX; @@ -63,6 +64,7 @@ class SideExt , ToolTip_Background_Opacity { } , ToolTip_Background_BlurSize { } , BriefingTheme { -1 } + , MessageTextColor { -1 } , SuperWeaponSidebar_OnPCX {} , SuperWeaponSidebar_OffPCX {} , SuperWeaponSidebar_TopPCX {} diff --git a/src/Ext/Sidebar/Hooks.cpp b/src/Ext/Sidebar/Hooks.cpp index 6814ddb203..3d5546a7b0 100644 --- a/src/Ext/Sidebar/Hooks.cpp +++ b/src/Ext/Sidebar/Hooks.cpp @@ -4,7 +4,9 @@ #include #include #include + #include +#include DEFINE_HOOK(0x6A593E, SidebarClass_InitForHouse_AdditionalFiles, 0x5) { @@ -99,29 +101,29 @@ DEFINE_HOOK(0x72FCB5, InitSideRectangles_CenterBackground, 0x5) return 0; } -#pragma region SWSidebarButtonsRelated +#pragma region NewButtonsRelated -DEFINE_HOOK(0x692419, DisplayClass_ProcessClickCoords_SWSidebar, 0x7) +DEFINE_HOOK(0x692419, DisplayClass_ProcessClickCoords_SkipOnNewButtons, 0x7) { enum { DoNothing = 0x6925FC }; - if (SWSidebarClass::IsEnabled() && SWSidebarClass::Instance.CurrentColumn) - return DoNothing; - - const auto toggleButton = SWSidebarClass::Instance.ToggleButton; - - return toggleButton && toggleButton->IsHovering ? DoNothing : 0; + return (SWSidebarClass::IsEnabled() && SWSidebarClass::Instance.CurrentColumn + || SWSidebarClass::Instance.ToggleButton && SWSidebarClass::Instance.ToggleButton->IsHovering + || MessageColumnClass::Instance.IsBlocked()) + ? DoNothing : 0; } -DEFINE_HOOK(0x6A5082, SidebarClass_InitClear_InitializeSWSidebar, 0x5) +DEFINE_HOOK(0x6A5082, SidebarClass_InitClear_InitializeNewButtons, 0x5) { SWSidebarClass::Instance.InitClear(); + MessageColumnClass::Instance.InitClear(); return 0; } -DEFINE_HOOK(0x6A5839, SidebarClass_InitIO_InitializeSWSidebar, 0x5) +DEFINE_HOOK(0x6A5839, SidebarClass_InitIO_InitializeNewButtons, 0x5) { SWSidebarClass::Instance.InitIO(); + MessageColumnClass::Instance.InitIO(); return 0; } diff --git a/src/Misc/Hooks.Message.cpp b/src/Misc/Hooks.Message.cpp deleted file mode 100644 index b46f355917..0000000000 --- a/src/Misc/Hooks.Message.cpp +++ /dev/null @@ -1,169 +0,0 @@ -#include -#include - -#include - -namespace MessageTemp -{ - bool OnOldMessages = false; - bool OnNewMessages = false; - bool NewMessageList = false; -} - -static inline bool MouseIsOverOldMessageLists() -{ - const auto pMousePosition = &WWMouseClass::Instance->XY1; - const auto pMessages = &MessageListClass::Instance; - - if (TextLabelClass* pText = pMessages->MessageList) - { - const int textHeight = pMessages->Height; - int height = pMessages->MessagePos.Y; - - for ( ; pText; pText = static_cast(pText->GetNext())) - height += textHeight; - - if (pMousePosition->Y < (height + 2)) - return true; - } - - return false; -} - -static inline bool MouseIsOverNewMessageLists() -{ - const auto pMousePosition = &WWMouseClass::Instance->XY1; - - if (const auto pMessages = ScenarioExt::Global()->NewMessageList.get()) - { - if (TextLabelClass* pText = pMessages->MessageList) - { - if (pMousePosition->Y >= pMessages->MessagePos.Y - && pMousePosition->X >= pMessages->MessagePos.X - && pMousePosition->X <= pMessages->MessagePos.X + pMessages->Width) - { - const int textHeight = pMessages->Height; - int height = pMessages->MessagePos.Y; - - for ( ; pText; pText = static_cast(pText->GetNext())) - height += textHeight; - - if (pMousePosition->Y < (height + 2)) - return true; - } - } - } - - return false; -} - -DEFINE_HOOK(0x69300B, ScrollClass_MouseUpdate_NewMessageListCheck, 0x6) -{ - const bool check = Phobos::Config::MessageApplyHoverState; - MessageTemp::OnOldMessages = check && MouseIsOverOldMessageLists(); - MessageTemp::OnNewMessages = check && Phobos::Config::MessageDisplayInCenter && MouseIsOverNewMessageLists(); - - return 0; -} - -DEFINE_HOOK(0x4F4583, GScreenClass_NewMessageListDraw, 0x6) -{ - MessageTemp::NewMessageList = true; - - if (const auto pList = ScenarioExt::Global()->NewMessageList.get()) - pList->Draw(); - - MessageTemp::NewMessageList = false; - - return 0; -} - -DEFINE_HOOK(0x55DDA0, MainLoop_FrameStep_NewMessageListManage, 0x5) -{ - enum { SkipGameCode = 0x55DDAA }; - - if (!MessageTemp::OnOldMessages) - MessageListClass::Instance.Manage(); - - if (!MessageTemp::OnNewMessages) - { - if (const auto pList = ScenarioExt::Global()->NewMessageList.get()) - pList->Manage(); - } - - return SkipGameCode; -} - -DEFINE_HOOK(0x6DE11D, TActionClass_Execute_AddMessageInCenter, 0x5) -{ - enum { SkipGameCode = 0x6DE122 }; - - if (const auto pList = ScenarioExt::Global()->NewMessageList.get()) - R->ECX(pList); - else // !Phobos::Config::MessageDisplayInCenter - R->ECX(&MessageListClass::Instance); - - return SkipGameCode; -} - -DEFINE_HOOK(0x4A8BCE, DisplayClass_Set_View_Dimensions, 0x5) -{ - if (Phobos::Config::MessageDisplayInCenter) - { - const auto& pScenarioExt = ScenarioExt::Global(); - - if (!pScenarioExt->NewMessageList) // Load game - pScenarioExt->NewMessageList = std::make_unique(); - - const auto& rect = DSurface::ViewBounds; - const auto sideWidth = rect.Width / 6; - const auto width = rect.Width - (sideWidth * 2); - const auto pList = pScenarioExt->NewMessageList.get(); - - // Except for X and Y, they are all original values - pList->Init((rect.X + sideWidth), (rect.Height - rect.Height / 8 - 120), 6, 98, 18, -1, -1, 0, 20, 98, width); - pList->SetWidth(width); - } - - return 0; -} - -DEFINE_HOOK(0x684AD3, UnknownClass_sub_684620_InitMessageList, 0x5) -{ - if (Phobos::Config::MessageDisplayInCenter) - { - const auto& pScenarioExt = ScenarioExt::Global(); - - if (!pScenarioExt->NewMessageList) // Start game - pScenarioExt->NewMessageList = std::make_unique(); - - const auto& rect = DSurface::ViewBounds; - const auto sideWidth = rect.Width / 6; - const auto width = rect.Width - (sideWidth * 2); - const auto pList = pScenarioExt->NewMessageList.get(); - - // Except for X and Y, they are all original values - pList->Init((rect.X + sideWidth), (rect.Height - rect.Height / 8 - 120), 6, 98, 18, -1, -1, 0, 20, 98, width); - } - - return 0; -} - -DEFINE_HOOK(0x623A9F, DSurface_sub_623880_DrawBitFontStrings, 0x5) -{ - if (!MessageTemp::NewMessageList) - return 0; - - enum { SkipGameCode = 0x623AAB }; - - GET(RectangleStruct* const, pRect, EAX); - GET(DSurface* const, pSurface, ECX); - GET(const int, height, EBP); - - pRect->Height = height; - auto black = ColorStruct { 0, 0, 0 }; - const auto trans = (MessageTemp::OnNewMessages || ScenarioClass::Instance->UserInputLocked) ? 70 : 40; - pSurface->FillRectTrans(pRect, &black, trans); - - return SkipGameCode; -} diff --git a/src/Misc/MessageColumn.cpp b/src/Misc/MessageColumn.cpp new file mode 100644 index 0000000000..f47049ddda --- /dev/null +++ b/src/Misc/MessageColumn.cpp @@ -0,0 +1,1085 @@ +#include "MessageColumn.h" + +#include +#include +#include +#include + +#include + +MessageColumnClass MessageColumnClass::Instance; + +// -------------------------------------------------- + +MessageToggleClass::MessageToggleClass(int x, int y, int width, int height) + : GadgetClass(x, y, width, height, GadgetFlag::LeftPress | GadgetFlag::LeftRelease, false) +{ + this->Disabled = true; +} + +bool MessageToggleClass::Draw(bool forced) +{ + return false; +} + +void MessageToggleClass::OnMouseEnter() +{ + this->Hovering = true; + MessageColumnClass::Instance.MouseEnter(true); +} + +void MessageToggleClass::OnMouseLeave() +{ + this->Hovering = false; + this->Clicking = false; + MessageColumnClass::Instance.MouseLeave(true); +} + +bool MessageToggleClass::Action(GadgetFlag flags, DWORD* pKey, KeyModifier modifier) +{ + if (!this->Clicking) + { + if ((flags & GadgetFlag::LeftPress) && this->Hovering) + { + this->Clicking = true; + + if (MessageColumnClass::Instance.IsExpanded()) + MessageColumnClass::Instance.PackUp(true); + else + MessageColumnClass::Instance.Expand(); + } + } + else if (flags & GadgetFlag::LeftRelease) + { + this->Clicking = false; + } + + this->GadgetClass::Action(flags, pKey, KeyModifier::None); + return true; +} + +void MessageToggleClass::DrawShape() const +{ + if (this->Disabled) + return; + + RectangleStruct drawRect { this->X, this->Y, this->Width, this->Height }; + ColorStruct color = MessageColumnClass::Instance.GetColor(); + MessageColumnClass::Instance.IncreaseBrightness(color); + const int opacity = this->Hovering && !this->Clicking + ? MessageColumnClass::MediumOpacity : MessageColumnClass::LowOpacity; + + if (MessageColumnClass::Instance.IsExpanded()) + { + DSurface::Composite->FillRectTrans(&drawRect, &color, opacity); + + color = ColorStruct { 255, 0, 0 }; + + if (this->Hovering && !this->Clicking) + MessageColumnClass::Instance.IncreaseBrightness(color); + + const int drawColor = Drawing::RGB_To_Int(color); + constexpr int offset = 4; + constexpr int iconSide = MessageToggleClass::ButtonSide - (offset * 2); + drawRect.X += offset; + drawRect.Y += offset; + drawRect.Width = iconSide; + drawRect.Height = iconSide; + + DSurface::Composite->FillRect(&drawRect, drawColor); + } + else + { + DSurface::Composite->FillRectTrans(&drawRect, &color, opacity); + + color = MessageColumnClass::Instance.GetColor(); + + if (this->Hovering && !this->Clicking) + MessageColumnClass::Instance.IncreaseBrightness(color); + + const int drawColor = Drawing::RGB_To_Int(color); + constexpr int interval = 2; + constexpr int drawCount = 3; + constexpr int offset = (MessageToggleClass::ButtonSide - interval * (drawCount * 2 - 1)) / 2; + drawRect.X += offset; + drawRect.Y += offset; + drawRect.Width = MessageToggleClass::ButtonSide - (2 * offset); + drawRect.Height = interval; + + DSurface::Composite->FillRect(&drawRect, drawColor); + + drawRect.Y += (interval * 2); + + DSurface::Composite->FillRect(&drawRect, drawColor); + + drawRect.Y += (interval * 2); + + DSurface::Composite->FillRect(&drawRect, drawColor); + } +} + +// -------------------------------------------------- + +MessageButtonClass::MessageButtonClass(int id, int x, int y, int width, int height) + : MessageToggleClass(x, y, width, height) + , ID(id) +{ + this->Disabled = true; + this->Flags |= GadgetFlag::LeftHeld; +} + +bool MessageButtonClass::Action(GadgetFlag flags, DWORD* pKey, KeyModifier modifier) +{ + if (!this->Clicking) + { + if ((flags & GadgetFlag::LeftPress) && this->Hovering) + { + this->CheckTime = MessageColumnClass::GetSystemTime() + MessageButtonClass::HoldInitialDelay; + this->Clicking = true; + + if (this->ID) + MessageColumnClass::Instance.ScrollDown(); + else + MessageColumnClass::Instance.ScrollUp(); + } + } + else if (flags & GadgetFlag::LeftRelease) + { + this->Clicking = false; + } + else if (flags & GadgetFlag::LeftHeld) + { + const int timeExpired = MessageColumnClass::GetSystemTime() - this->CheckTime; + + if (timeExpired > 0) + { + this->CheckTime += MessageButtonClass::HoldTriggerDelay; + + if (this->ID) + MessageColumnClass::Instance.ScrollDown(); + else + MessageColumnClass::Instance.ScrollUp(); + } + } + + this->GadgetClass::Action(flags, pKey, KeyModifier::None); + return true; +} + +void MessageButtonClass::DrawShape() const +{ + if (this->Disabled) + return; + + constexpr int intervalX = 5; + constexpr int intervalY = 1; + RectangleStruct drawRect { this->X, this->Y, this->Width, this->Height }; + ColorStruct color = MessageColumnClass::Instance.GetColor(); + MessageColumnClass::Instance.IncreaseBrightness(color, 3); + const bool can = this->ID ? MessageColumnClass::Instance.CanScrollDown() : MessageColumnClass::Instance.CanScrollUp(); + const bool highLight = can && this->Hovering; + const int opacity = highLight ? MessageColumnClass::MediumOpacity : MessageColumnClass::LowOpacity; + + DSurface::Composite->FillRectTrans(&drawRect, &color, opacity); + + color = can ? MessageColumnClass::Instance.GetColor() : ColorStruct { 0, 0, 0 }; + + if (can && this->Clicking) + MessageColumnClass::Instance.IncreaseBrightness(color, 2); + else if (highLight) + MessageColumnClass::Instance.IncreaseBrightness(color); + + const int drawColor = Drawing::RGB_To_Int(color); + drawRect.X += intervalX; + drawRect.Y += intervalY; + drawRect.Width -= (intervalX * 2); + drawRect.Height = MessageToggleClass::ButtonIconWidth; + + DSurface::Composite->FillRect(&drawRect, drawColor); +} + +// -------------------------------------------------- + +MessageScrollClass::MessageScrollClass(int id, int x, int y, int width, int height) + : GadgetClass(x, y, width, height, GadgetFlag::LeftPress | GadgetFlag::LeftHeld | GadgetFlag::LeftRelease, true) + , ID(id) +{ + this->Disabled = true; +} + +bool MessageScrollClass::Draw(bool forced) +{ + return false; +} + +void MessageScrollClass::OnMouseEnter() +{ + this->Hovering = true; + MessageColumnClass::Instance.MouseEnter(); +} + +void MessageScrollClass::OnMouseLeave() +{ + this->Hovering = false; + MessageColumnClass::Instance.MouseLeave(); +} + +bool MessageScrollClass::Clicked(DWORD* pKey, GadgetFlag flags, int x, int y, KeyModifier modifier) +{ + if (!MessageColumnClass::IsStickyButton(this)) + { + if (this->ID) // Scroll_Board + { + if (!(flags & GadgetFlag::LeftPress) || !MessageColumnClass::Instance.IsExpanded() || !this->Hovering) + return false; + + this->LastY = y; + this->LastScroll = MessageColumnClass::Instance.GetScrollIndex(); + } + else // Scroll_Bar + { + if (!(flags & GadgetFlag::LeftPress) || !this->Hovering) + return false; + + int maxScroll = 0; + int thumbHeight = this->Height; + int thumbPosY = this->Y; + + if (MessageColumnClass::Instance.GetThumbDimension(&maxScroll, &thumbHeight, &thumbPosY)) + { + if (y >= thumbPosY && y <= thumbPosY + thumbHeight) + { + this->LastY = y; + this->LastScroll = MessageColumnClass::Instance.GetScrollIndex(); + } + else + { + const int newScroll = maxScroll * (y - this->Y - thumbHeight / 2) / (this->Height - thumbHeight); + MessageColumnClass::Instance.SetScroll(newScroll); + this->LastY = y; + this->LastScroll = newScroll; + } + } + } + } + else if (flags & GadgetFlag::LeftHeld) + { + if (this->ID) // Scroll_Board + { + const int indexOffset = (this->LastY - y) / MessageToggleClass::ButtonSide; + MessageColumnClass::Instance.SetScroll(this->LastScroll + indexOffset); + } + else // Scroll_Bar + { + int maxScroll = 0; + int thumbHeight = this->Height; + + if (MessageColumnClass::Instance.GetThumbDimension(&maxScroll, &thumbHeight)) + { + const int indexOffset = maxScroll * (y - this->LastY) / (this->Height - thumbHeight); + MessageColumnClass::Instance.SetScroll(this->LastScroll + indexOffset); + } + } + } + + this->GadgetClass::Action(flags, pKey, modifier); + return true; +} + +void MessageScrollClass::DrawShape() const +{ + if (this->ID) // Scroll_Board + { + if ((MessageColumnClass::Instance.IsHovering() && Phobos::Config::MessageApplyHoverState) + || MessageColumnClass::Instance.IsExpanded() + || ScenarioClass::Instance->UserInputLocked) + { + RectangleStruct drawRect { this->X, this->Y, this->Width, this->Height }; + auto color = MessageColumnClass::Instance.GetColor(); + MessageColumnClass::Instance.DecreaseBrightness(color, 3); + + DSurface::Composite->FillRectTrans(&drawRect, &color, MessageColumnClass::LowOpacity); + } + } + else // Scroll_Bar + { + if (!this->Disabled) + { + constexpr int offset = 1; + RectangleStruct drawRect { this->X + offset, this->Y, this->Width - (offset * 2), this->Height }; + ColorStruct color = MessageColumnClass::Instance.GetColor(); + MessageColumnClass::Instance.DecreaseBrightness(color); + + int thumbHeight = this->Height; + int thumbPos = 0; + MessageColumnClass::Instance.GetThumbDimension(nullptr, &thumbHeight, &thumbPos); + const int thumbY = WWMouseClass::Instance->XY1.Y - this->Y; + const bool onThumb = thumbY >= thumbPos && thumbY <= (thumbPos + thumbHeight); + const int opacity = this->Hovering && !onThumb + ? MessageColumnClass::HighOpacity : MessageColumnClass::MediumOpacity; + + DSurface::Composite->FillRectTrans(&drawRect, &color, opacity); + + color = MessageColumnClass::Instance.GetColor(); + + if (MessageColumnClass::IsStickyButton(this)) + MessageColumnClass::Instance.IncreaseBrightness(color, 2); + else if (this->Hovering && onThumb) + MessageColumnClass::Instance.IncreaseBrightness(color); + + const int drawColor = Drawing::RGB_To_Int(color); + drawRect.Y += thumbPos; + drawRect.Height = thumbHeight; + + DSurface::Composite->FillRect(&drawRect, drawColor); + } + } +} + +// -------------------------------------------------- + +MessageLabelClass::MessageLabelClass(int x, int y, size_t id, int deleteTime, bool animate, int drawDelay) + : GadgetClass(x, y, 1, 1, GadgetFlag(0), false) + , ID(id) + , DeleteTime(deleteTime) + , Animate(animate) + , DrawDelay(drawDelay) +{} + +bool MessageLabelClass::Draw(bool forced) +{ + if (this->DrawDelay > MessageColumnClass::GetSystemTime()) + return false; + + if (!GadgetClass::Draw(forced)) + return false; + + if (!ColorScheme::Array.Count) + return false; + + const auto pBit = BitFont::Instance; + const int surfaceWidth = DSurface::Temp->GetWidth(); + RectangleStruct rect { this->X, this->Y, MessageColumnClass::Instance.GetWidth(), pBit->field_1C }; + const int remainWidth = surfaceWidth - rect.X; + + if (rect.Width > remainWidth) + { + rect.Width = remainWidth; + + if (rect.Width <= 0 || rect.Height <= 0) + return false; + } + + const wchar_t* text = this->GetText(); + size_t textLen = wcslen(text); + + if (this->Animate) + { +#pragma warning(suppress: 4996) + const auto time = Imports::TimeGetTime()(); + const size_t animPos = this->AnimPos; + this->AnimPos = animPos ? (animPos + ((time - this->AnimTiming) >> 4u)) : 1u; + + if (this->AnimPos != animPos) + this->AnimTiming = time; + + if (this->AnimPos <= textLen) + VocClass::PlayGlobal(RulesClass::Instance->MessageCharTyped, 0x2000, 1.0f); + } + + reinterpret_cast(0x623880) + (DSurface::Temp, &rect, text, textLen, pBit, MessageColumnClass::Instance.GetTextColor(), &this->DrawPos, this->IsFocused(), false, true, this->AnimPos); + + return true; +} + +// -------------------------------------------------- + +MessageColumnClass::~MessageColumnClass() +{ + this->Initialize(); +} + +void MessageColumnClass::InitClear() +{ + this->Initialize(); + + if (this->Button_Toggle) + { + GScreenClass::Instance.RemoveButton(this->Button_Toggle); + GameDelete(this->Button_Toggle); + this->Button_Toggle = nullptr; + } + + if (this->Button_Up) + { + GScreenClass::Instance.RemoveButton(this->Button_Up); + GameDelete(this->Button_Up); + this->Button_Up = nullptr; + } + + if (this->Button_Down) + { + GScreenClass::Instance.RemoveButton(this->Button_Down); + GameDelete(this->Button_Down); + this->Button_Down = nullptr; + } + + if (this->Scroll_Bar) + { + GScreenClass::Instance.RemoveButton(this->Scroll_Bar); + GameDelete(this->Scroll_Bar); + this->Scroll_Bar = nullptr; + } + + if (this->Scroll_Board) + { + GScreenClass::Instance.RemoveButton(this->Scroll_Board); + GameDelete(this->Scroll_Board); + this->Scroll_Board = nullptr; + } +} + +void MessageColumnClass::InitIO() +{ + if (Unsorted::ArmageddonMode || !Phobos::Config::MessageDisplayInCenter) + return; + + const auto& rect = DSurface::ViewBounds; + const int sideWidth = rect.Width / 6; + int width = rect.Width - (sideWidth * 2); + int posX = rect.X + sideWidth; + int posY = rect.Height - rect.Height / 8; + const int maxLines = posY / MessageToggleClass::ButtonSide - 1; + constexpr int maxChars = 112; + constexpr int minRecord = 4; + constexpr int minCount = 1; + const int maxRecord = Math::clamp(Phobos::Config::MessageDisplayInCenter_RecordsCount, minRecord, maxLines); + const int maxCount = Math::clamp(Phobos::Config::MessageDisplayInCenter_LabelsCount, minCount, maxLines); + + this->Initialize(posX, posY, maxCount, maxRecord, maxChars, width); + + posX -= 1; + width += 2; + + // Button_Toggle + { + const int locX = posX + width - MessageToggleClass::ButtonSide; + const int locY = posY - MessageToggleClass::ButtonSide; + const auto pButton = GameCreate(locX, locY, MessageToggleClass::ButtonSide, MessageToggleClass::ButtonSide); + pButton->Zap(); + GScreenClass::Instance.AddButton(pButton); + this->Button_Toggle = pButton; + } + + // Button_Up + { + const int locX = rect.Width * 5 / 12; + const int locY = posY - (MessageToggleClass::ButtonSide * this->MaxRecord) - 1 - MessageToggleClass::ButtonHeight; + const auto pButton = GameCreate(0, locX, locY, sideWidth, MessageToggleClass::ButtonHeight); + pButton->Zap(); + GScreenClass::Instance.AddButton(pButton); + this->Button_Up = pButton; + } + + // Button_Down + { + const int locX = rect.Width * 5 / 12; + const int locY = posY; + const auto pButton = GameCreate(1, locX, locY, sideWidth, MessageToggleClass::ButtonHeight); + pButton->Zap(); + GScreenClass::Instance.AddButton(pButton); + this->Button_Down = pButton; + } + + // Scroll_Bar + { + constexpr int buttonInterval = 3; + const int locX = posX + width - ((MessageToggleClass::ButtonSide - MessageToggleClass::ButtonHeight) / 2 + MessageToggleClass::ButtonHeight); + const int locY = posY - (MessageToggleClass::ButtonSide * this->MaxRecord) + (buttonInterval - 1); + const int barHeight = posY - (MessageToggleClass::ButtonSide + buttonInterval) - locY; + const auto pButton = GameCreate(0, locX, locY, MessageToggleClass::ButtonHeight, barHeight); + pButton->Zap(); + GScreenClass::Instance.AddButton(pButton); + this->Scroll_Bar = pButton; + } + + // Scroll_Board + { + const auto pButton = GameCreate(1, posX, posY, width, 1); + pButton->Zap(); + GScreenClass::Instance.AddButton(pButton); + this->Scroll_Board = pButton; + } + + const int color = SideExt::ExtMap.Find(SideClass::Array.Items[ScenarioClass::Instance->PlayerSideIndex])->MessageTextColor; + + // 0x72A4C5 + if (const auto pScheme = ColorScheme::Array.Items[(color < 0 || color >= ColorScheme::Array.Count) ? 0 : color]) + reinterpret_cast(0x517440)(&pScheme->BaseColor, &this->Color); + + this->Update(); +} + +void MessageColumnClass::Initialize(int x, int y, int maxCount, int maxRecord, int maxChars, int width) +{ + this->LabelList = nullptr; + this->LabelsPos = Point2D { x, y }; + this->MaxCount = maxCount; + this->MaxRecord = maxRecord; + this->MaxChars = maxChars; + this->Height = MessageToggleClass::ButtonSide; + this->Width = width - MessageColumnClass::TextReservedSpace; + this->Color = ColorStruct { 0, 0, 0 }; + this->PackUp(true); + this->Hovering = false; + this->Drawing = false; + this->Blocked = false; +} + +MessageLabelClass* MessageColumnClass::AddMessage(const wchar_t* name, const wchar_t* message, int timeout, bool silent, int delay) +{ + if (!message) + return nullptr; + + const auto pBit = BitFont::Instance; + + if (!pBit) + return nullptr; + + std::wstring buffer; + + if (name) + buffer = std::wstring(name) + L":"; + + int prefixWidth = 0; + pBit->GetTextDimension(buffer.c_str(), &prefixWidth, nullptr, 0); + const int availableWidth = this->Width - prefixWidth - MessageColumnClass::TextReservedSpace; + + if (availableWidth <= 0) + return nullptr; + + const int messageLen = static_cast(wcslen(message)); + // As vanilla + const int charsToCopy = reinterpret_cast(0x433F50)(pBit, message, availableWidth, 111, 1); + + if (charsToCopy < 0) + return nullptr; + + buffer.append(message, charsToCopy); + + if (this->MaxCount > 0 && (this->GetLabelCount() + 1) > this->MaxCount) + { + if (auto pLabel = this->LabelList) + this->RemoveTextLabel(pLabel); + else + return nullptr; + } + + const size_t newID = ScenarioExt::Global()->RecordMessages.size(); + + if (!MessageColumnClass::AddRecordString(buffer)) + return nullptr; + + const int currentTime = MessageColumnClass::GetSystemTime(); + + if (!silent) + VocClass::PlayGlobal(RulesClass::Instance->IncomingMessage, 0x2000, 1.0f); + + const auto pLabel = new MessageLabelClass + ( + this->LabelsPos.X, + this->LabelsPos.Y, + newID, + (timeout == -1) ? 0 : (timeout + currentTime), + !silent, + delay + currentTime + ); + + if (this->LabelList) + pLabel->AddTail(*this->LabelList); + else + this->LabelList = pLabel; + + this->Update(); + + if (charsToCopy < messageLen) + { + const wchar_t* remainingText = &message[charsToCopy]; + + while (*remainingText && *remainingText < 0x20) + ++remainingText; + + if (*remainingText) + { + int nextDelay = delay; + + if (!silent) + nextDelay += (charsToCopy * 2 - 1); + + this->AddMessage(name, remainingText, timeout, silent, nextDelay); + } + } + + return pLabel; +} + +void MessageColumnClass::MouseEnter(bool block) +{ + this->Hovering = true; + + if (block) + this->Blocked = true; + + if (const auto pButton = this->Button_Toggle) + pButton->Disabled = false; + + MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false); +} + +void MessageColumnClass::MouseLeave(bool block) +{ + this->Hovering = false; + + if (block) + this->Blocked = false; + + if (!this->IsExpanded()) + { + if (const auto pButton = this->Button_Toggle) + pButton->Disabled = true; + } + + MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false); +} + +bool MessageColumnClass::CanScrollUp() +{ + return this->IsExpanded() && this->GetScrollIndex() > 0; +} + +bool MessageColumnClass::CanScrollDown() +{ + return this->IsExpanded() && this->GetScrollIndex() < this->GetMaxScroll(); +} + +void MessageColumnClass::ScrollUp() +{ + if (this->CanScrollUp()) + --this->ScrollIndex; +} + +void MessageColumnClass::ScrollDown() +{ + if (this->CanScrollDown()) + ++this->ScrollIndex; +} + +void MessageColumnClass::SetScroll(int index) +{ + this->ScrollIndex = Math::clamp(index, 0, this->GetMaxScroll()); +} + +void MessageColumnClass::Expand() +{ + this->Expanded = true; + this->ScrollIndex = this->GetMaxScroll(); + + if (const auto pButton = this->Button_Toggle) + pButton->Disabled = false; + + if (const auto pButton = this->Button_Up) + pButton->Disabled = false; + + if (const auto pButton = this->Button_Down) + pButton->Disabled = false; + + if (const auto pButton = this->Scroll_Bar) + pButton->Disabled = false; + + this->Refresh(); +} + +void MessageColumnClass::PackUp(bool clear) +{ + if (const auto pButton = this->Scroll_Bar) + { + if (MessageColumnClass::IsStickyButton(pButton)) + return; + } + + if (const auto pButton = this->Scroll_Board) + { + if (MessageColumnClass::IsStickyButton(pButton)) + return; + } + + this->Expanded = false; + this->ScrollIndex = this->GetMaxScroll(); + + if (const auto pButton = this->Button_Up) + pButton->Disabled = true; + + if (const auto pButton = this->Button_Down) + pButton->Disabled = true; + + if (const auto pButton = this->Scroll_Bar) + pButton->Disabled = true; + + if (!clear) + { + if (auto pLabel = this->GetLastLabel()) + { + const int currentTime = MessageColumnClass::GetSystemTime(); + + for ( ; pLabel; pLabel = static_cast(pLabel->GetPrev())) + { + if (pLabel->DrawDelay > currentTime) + pLabel->DrawDelay = currentTime; + + if (pLabel->Animate) + { + pLabel->Animate = false; + pLabel->AnimPos = 0; + } + } + + this->Refresh(); + + return; + } + } + + this->CleanUp(); +} + +void MessageColumnClass::CleanUp() +{ + for (auto pLabel = this->LabelList; pLabel; pLabel = this->LabelList) + { + this->LabelList = static_cast(pLabel->Remove()); + delete pLabel; + } + + if (const auto pButton = this->Scroll_Board) + { + pButton->Y = this->LabelsPos.Y - 1; + pButton->Height = 1; + pButton->Disabled = true; + } + + if (const auto pButton = this->Button_Toggle) + pButton->Disabled = true; +} + +void MessageColumnClass::Refresh() +{ + if (const auto pButton = this->Scroll_Board) + { + const int count = this->IsExpanded() ? this->MaxRecord : this->GetLabelCount(); + const int height = this->Height * count; + pButton->Height = height + 1; + pButton->Y = this->LabelsPos.Y - pButton->Height; + pButton->Disabled = height == 0; + } + + if (!this->IsHovering() && !this->IsExpanded()) + { + if (const auto pButton = this->Button_Toggle) + pButton->Disabled = true; + } +} + +void MessageColumnClass::Update() +{ + int y = this->LabelsPos.Y; + + for (auto pLabel = this->GetLastLabel(); pLabel; pLabel = static_cast(pLabel->GetPrev())) + { + y -= this->Height; + pLabel->Y = y; + } + + this->Refresh(); +} + +void MessageColumnClass::Toggle() +{ + if (this->IsExpanded()) + this->PackUp(); + else + this->Expand(); +} + +void MessageColumnClass::Manage() +{ + const int currentTime = MessageColumnClass::GetSystemTime(); + bool changed = false; + + for (auto pLabel = this->LabelList; pLabel; ) + { + if (pLabel->DeleteTime && currentTime > pLabel->DeleteTime) + { + const auto pNextLabel = static_cast(pLabel->GetNext()); + this->RemoveTextLabel(pLabel); + pLabel = pNextLabel; + changed = true; + + continue; + } + + pLabel = static_cast(pLabel->GetNext()); + } + + if (changed) + this->Update(); +} + +void MessageColumnClass::DrawAll() +{ + if (const auto pButton = this->Scroll_Board) + pButton->DrawShape(); + + if (const auto pButton = this->Scroll_Bar) + pButton->DrawShape(); + + if (const auto pButton = this->Button_Toggle) + pButton->DrawShape(); + + if (const auto pButton = this->Button_Up) + pButton->DrawShape(); + + if (const auto pButton = this->Button_Down) + pButton->DrawShape(); + + if (this->IsExpanded()) + { + const auto& messages = ScenarioExt::Global()->RecordMessages; + + if (messages.empty()) + return; + + int startY = this->LabelsPos.Y; + const int maxIndex = static_cast(messages.size()) - 1; + const int startIndex = Math::min(this->GetScrollIndex() + (this->MaxRecord - 1), maxIndex); + const int endIndex = Math::max(0, startIndex - this->MaxRecord + 1); + const int color = Drawing::RGB_To_Int(this->GetColor()); + constexpr TextPrintType print = TextPrintType::UseGradPal | TextPrintType::FullShadow | TextPrintType::Point6Grad; + + for (int i = startIndex; i >= endIndex; --i) + { + startY -= this->Height; + Point2D textLocation { this->LabelsPos.X, startY }; + RectangleStruct drawRect { 0, 0, textLocation.X + this->Width, textLocation.Y + this->Height }; + DSurface::Composite->DrawTextA(messages[i].c_str(), &drawRect, &textLocation, color, 0, print); + } + } + else if (const auto pLabel = this->LabelList) + { + this->Drawing = true; + pLabel->DrawAll(true); + this->Drawing = false; + } +} + +inline int MessageColumnClass::GetSystemTime() +{ + const auto& sysTimer = Make_Global(0x887338); + int currentTime = sysTimer.TimeLeft; + + if (sysTimer.StartTime != -1) + currentTime += SystemTimer::GetTime() - sysTimer.StartTime; + + return currentTime; +} + +inline bool MessageColumnClass::IsStickyButton(const GadgetClass* pButton) +{ + return pButton == Make_Global(0x8B3E88); +} + +inline void MessageColumnClass::IncreaseBrightness(ColorStruct& color, int level) +{ + color.R = static_cast(255 - ((255 - color.R) >> level)); + color.G = static_cast(255 - ((255 - color.G) >> level)); + color.B = static_cast(255 - ((255 - color.B) >> level)); +} + +inline void MessageColumnClass::DecreaseBrightness(ColorStruct& color, int level) +{ + color.R = static_cast(color.R >> level); + color.G = static_cast(color.G >> level); + color.B = static_cast(color.B >> level); +} + +inline bool MessageColumnClass::GetThumbDimension(int* pMax, int* pHeight, int* pPosY) const +{ + const int totalRecords = static_cast(ScenarioExt::Global()->RecordMessages.size()); + const int maxScroll = totalRecords - this->MaxRecord; + + if (maxScroll <= 0) + return false; + + if (pMax) + *pMax = maxScroll; + + if (!pHeight) + return false; + + const int thumbHeight = Math::max((*pHeight * this->MaxRecord / totalRecords), MessageToggleClass::ButtonSide); + + if (pPosY) + *pPosY += (*pHeight - thumbHeight) * this->GetScrollIndex() / maxScroll; + + *pHeight = thumbHeight; + + return true; +} + +inline bool MessageColumnClass::AddRecordString(const std::wstring& message, size_t copySize) +{ + if (message.empty()) + return false; + + ScenarioExt::Global()->RecordMessages.push_back(message.substr(0, Math::min(copySize, message.size()))); + return true; +} + +inline void MessageColumnClass::RemoveTextLabel(MessageLabelClass* pLabel) +{ + this->LabelList = static_cast(pLabel->Remove()); + delete pLabel; +} + +inline int MessageColumnClass::GetLabelCount() const +{ + int num = 0; + + for (auto pLabel = this->LabelList; pLabel; pLabel = static_cast(pLabel->GetNext())) + ++num; + + return num; +} + +inline MessageLabelClass* MessageColumnClass::GetLastLabel() const +{ + auto pLabel = this->LabelList; + + if (!pLabel) + return nullptr; + + auto pNextLabel = static_cast(pLabel->GetNext()); + + for ( ; pNextLabel; pNextLabel = static_cast(pLabel->GetNext())) + pLabel = pNextLabel; + + return pLabel; +} + +int MessageColumnClass::GetMaxScroll() const +{ + return Math::max(0, static_cast(ScenarioExt::Global()->RecordMessages.size()) - this->MaxRecord); +} + +// -------------------------------------------------- + +DEFINE_HOOK(0x4AAC92, TacticalGadgetClass_Action_ResetMessageColumnStatus, 0x5) +{ + GET_STACK(const GadgetFlag, flags, STACK_OFFSET(0x30, 0x4)); + + if ((flags & (GadgetFlag::LeftPress | GadgetFlag::RightPress)) + && MessageColumnClass::Instance.IsExpanded() + && !MessageColumnClass::Instance.IsHovering()) + { + MessageColumnClass::Instance.PackUp(); + } + + return 0; +} + +namespace MessageTemp +{ + bool OnOldMessages = false; +} + +static inline bool MouseOverMessageLists() +{ + const auto& position = WWMouseClass::Instance->XY1; + const auto& messages = MessageListClass::Instance; + + if (TextLabelClass* pText = messages.MessageList) + { + const int textHeight = messages.Height; + int height = messages.MessagePos.Y; + + for ( ; pText; pText = static_cast(pText->GetNext())) + height += textHeight; + + if (position.Y < (height + 2)) + return true; + } + + return false; +} + +DEFINE_HOOK(0x4F43BE, GScreenClass_GetInputAndUpdate_CheckHoverState, 0x7) +{ + if (Phobos::Config::MessageApplyHoverState) + MessageTemp::OnOldMessages = MouseOverMessageLists(); + + return 0; +} + +DEFINE_HOOK(0x4F4583, GScreenClass_NewMessageListDraw, 0x6) +{ + MessageColumnClass::Instance.DrawAll(); + + return 0; +} + +DEFINE_HOOK(0x55DDA0, MainLoop_FrameStep_NewMessageListManage, 0x5) +{ + enum { SkipGameCode = 0x55DDAA }; + + if (!MessageTemp::OnOldMessages) + MessageListClass::Instance.Manage(); + + if (!MessageColumnClass::Instance.IsExpanded() && (!MessageColumnClass::Instance.IsHovering() || !Phobos::Config::MessageApplyHoverState)) + MessageColumnClass::Instance.Manage(); + + return SkipGameCode; +} + +void __fastcall AddTActionMessage(MessageListClass* pThis, void* _, const wchar_t* name, int id, const wchar_t* message, int color, TextPrintType style, int timeout, bool silent) +{ + if (Phobos::Config::MessageDisplayInCenter) + MessageColumnClass::Instance.AddMessage(name, message, timeout, silent); + else + pThis->AddMessage(name, id, message, color, style, timeout, silent); +} +DEFINE_FUNCTION_JUMP(CALL, 0x6DE122, AddTActionMessage) + +DEFINE_HOOK(0x623A9F, DSurface_sub_623880_DrawBitFontStrings, 0x5) +{ + if (!MessageColumnClass::Instance.IsDrawing()) + return 0; + + enum { SkipGameCode = 0x623AAB }; + + GET(RectangleStruct* const, pRect, EAX); + GET(DSurface* const, pSurface, ECX); + GET(const int, height, EBP); + + pRect->Height = height; + + if ((!MessageColumnClass::Instance.IsHovering() || !Phobos::Config::MessageApplyHoverState) + && !MessageColumnClass::Instance.IsExpanded() + && !ScenarioClass::Instance->UserInputLocked) + { + auto color = MessageColumnClass::Instance.GetColor(); + MessageColumnClass::Instance.DecreaseBrightness(color, 3); + pSurface->FillRectTrans(pRect, &color, MessageColumnClass::LowOpacity); + } + + return SkipGameCode; +} diff --git a/src/Misc/MessageColumn.h b/src/Misc/MessageColumn.h new file mode 100644 index 0000000000..e67eae8783 --- /dev/null +++ b/src/Misc/MessageColumn.h @@ -0,0 +1,192 @@ +#pragma once + +#include + +#include + +#include + +// -------------------------------------------------- + +class MessageToggleClass : public GadgetClass +{ +public: + static constexpr int ButtonSide = 18; + static constexpr int ButtonIconWidth = 4; + static constexpr int ButtonHeight = ButtonIconWidth + 2; + + MessageToggleClass() = default; + MessageToggleClass(int x, int y, int width, int height); + + ~MessageToggleClass() = default; + + virtual bool Draw(bool forced) override; + virtual void OnMouseEnter() override; + virtual void OnMouseLeave() override; + virtual bool Action(GadgetFlag flags, DWORD* pKey, KeyModifier modifier) override; + + void DrawShape() const; + + bool Hovering { false }; + bool Clicking { false }; +}; + +// -------------------------------------------------- + +class MessageButtonClass : public MessageToggleClass +{ +public: + static constexpr int HoldInitialDelay = 30; + static constexpr int HoldTriggerDelay = 5; + + MessageButtonClass() = default; + MessageButtonClass(int id, int x, int y, int width, int height); + + ~MessageButtonClass() = default; + + virtual bool Action(GadgetFlag flags, DWORD* pKey, KeyModifier modifier) override; + + void DrawShape() const; + + int ID { 0 }; + int CheckTime { 0 }; +}; + +// -------------------------------------------------- + +class MessageScrollClass : public GadgetClass +{ +public: + MessageScrollClass() = default; + MessageScrollClass(int id, int x, int y, int width, int height); + + ~MessageScrollClass() = default; + + virtual bool Draw(bool forced) override; + virtual void OnMouseEnter() override; + virtual void OnMouseLeave() override; + virtual bool Clicked(DWORD* pKey, GadgetFlag flags, int x, int y, KeyModifier modifier) override; + + void DrawShape() const; + + bool Hovering { false }; + int ID { 0 }; + int LastY { 0 }; + int LastScroll { 0 }; +}; + +// -------------------------------------------------- + +class MessageLabelClass : public GadgetClass +{ +public: + MessageLabelClass() = default; + MessageLabelClass(int x, int y, size_t id, int deleteTime, bool animate, int drawDelay); + + ~MessageLabelClass() = default; + + virtual bool Draw(bool bForced) override; + + inline const wchar_t* GetText() const { return ScenarioExt::Global()->RecordMessages[this->ID].c_str(); } + + size_t ID { 0 }; + int DeleteTime { 0 }; + bool Animate { false }; + size_t AnimPos { 0 }; + size_t AnimTiming { 0 }; + size_t DrawPos { 0 }; + int DrawDelay { 0 }; +}; + +// -------------------------------------------------- + +class MessageColumnClass +{ +public: + static MessageColumnClass Instance; + static constexpr int TextReservedSpace = 8; + static constexpr int HighOpacity = 90; + static constexpr int MediumOpacity = 60; + static constexpr int LowOpacity = 30; + +public: + MessageColumnClass() = default; + ~MessageColumnClass(); + + void InitClear(); + void InitIO(); + +private: + void Initialize(int x = 0, int y = 0, int maxCount = 0, int maxRecord = 0, int maxChars = 0, int width = 640); + +public: + MessageLabelClass* AddMessage(const wchar_t* name, const wchar_t* message, int timeout, bool silent, int delay = 0); + + void MouseEnter(bool block = false); + void MouseLeave(bool block = false); + bool CanScrollUp(); + bool CanScrollDown(); + void ScrollUp(); + void ScrollDown(); + void SetScroll(int index = 0); + void Expand(); + void PackUp(bool clear = false); + +private: + void CleanUp(); + void Refresh(); + void Update(); + +public: + void Toggle(); + void Manage(); + void DrawAll(); + + inline int GetWidth() const { return this->Width; } + inline size_t GetTextColor() const { return (this->Color.B << 16) | (this->Color.G << 8) | this->Color.R; } + inline ColorStruct GetColor() const { return this->Color; } + inline int GetScrollIndex() const { return this->ScrollIndex; } + inline bool IsHovering() const { return this->Hovering; } + inline bool IsExpanded() const { return this->Expanded; } + inline bool IsDrawing() const { return this->Drawing; } + inline bool IsBlocked() const { return (this->Expanded || this->Blocked) && this->Hovering; } + + static inline int GetSystemTime(); + static inline bool IsStickyButton(const GadgetClass* pButton); + static inline void IncreaseBrightness(ColorStruct& color, int level = 1); + static inline void DecreaseBrightness(ColorStruct& color, int level = 1); + + inline bool GetThumbDimension(int* pMax, int* pHeight, int* pPosY = nullptr) const; + +private: + static inline bool AddRecordString(const std::wstring& message, size_t copySize = std::wstring::npos); + + inline void RemoveTextLabel(MessageLabelClass* pLabel); + inline int GetLabelCount() const; + inline MessageLabelClass* GetLastLabel() const; + inline int GetMaxScroll() const; + + MessageLabelClass* LabelList { nullptr }; + Point2D LabelsPos { Point2D::Empty }; + + int MaxCount { 0 }; + int MaxRecord { 0 }; + int MaxChars { 0 }; + int Height { 0 }; + int Width { 0 }; + ColorStruct Color { ColorStruct { 0, 0, 0 } }; + + MessageToggleClass* Button_Toggle { nullptr }; + MessageButtonClass* Button_Up { nullptr }; + MessageButtonClass* Button_Down { nullptr }; + MessageScrollClass* Scroll_Bar { nullptr }; + MessageScrollClass* Scroll_Board { nullptr }; + + int ScrollIndex { 0 }; + bool Hovering { false }; + bool Expanded { false }; + bool Drawing { false }; + bool Blocked { false }; +}; + +// -------------------------------------------------- diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp index 32f6a0b953..26e9bf10dd 100644 --- a/src/Phobos.INI.cpp +++ b/src/Phobos.INI.cpp @@ -55,6 +55,8 @@ bool Phobos::Config::EnableSelectBox = false; bool Phobos::Config::DigitalDisplay_Enable = false; bool Phobos::Config::MessageApplyHoverState = false; bool Phobos::Config::MessageDisplayInCenter = false; +int Phobos::Config::MessageDisplayInCenter_LabelsCount = 6; +int Phobos::Config::MessageDisplayInCenter_RecordsCount = 12; bool Phobos::Config::RealTimeTimers = false; bool Phobos::Config::RealTimeTimers_Adaptive = false; int Phobos::Config::CampaignDefaultGameSpeed = 2; @@ -86,6 +88,8 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5) Phobos::Config::ShowPlacementPreview = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowPlacementPreview", true); Phobos::Config::MessageApplyHoverState = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "MessageApplyHoverState", false); Phobos::Config::MessageDisplayInCenter = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "MessageDisplayInCenter", false); + Phobos::Config::MessageDisplayInCenter_LabelsCount = CCINIClass::INI_RA2MD.ReadInteger(phobosSection, "MessageDisplayInCenter.LabelsCount", 6); + Phobos::Config::MessageDisplayInCenter_RecordsCount = CCINIClass::INI_RA2MD.ReadInteger(phobosSection, "MessageDisplayInCenter.RecordsCount", 12); Phobos::Config::RealTimeTimers = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "RealTimeTimers", false); Phobos::Config::RealTimeTimers_Adaptive = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "RealTimeTimers.Adaptive", false); Phobos::Config::EnableSelectBox = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "EnableSelectBox", false); diff --git a/src/Phobos.h b/src/Phobos.h index e30ca6b066..d0580ff99b 100644 --- a/src/Phobos.h +++ b/src/Phobos.h @@ -90,6 +90,8 @@ class Phobos static bool DigitalDisplay_Enable; static bool MessageApplyHoverState; static bool MessageDisplayInCenter; + static int MessageDisplayInCenter_LabelsCount; + static int MessageDisplayInCenter_RecordsCount; static bool RealTimeTimers; static bool RealTimeTimers_Adaptive; static int CampaignDefaultGameSpeed; diff --git a/src/Utilities/SavegameDef.h b/src/Utilities/SavegameDef.h index 717b718406..5d9c419eff 100644 --- a/src/Utilities/SavegameDef.h +++ b/src/Utilities/SavegameDef.h @@ -296,13 +296,46 @@ namespace Savegame return true; } } + return false; } bool WriteToStream(PhobosStreamWriter& Stm, const std::string& Value) const { - Stm.Save(Value.size()); - Stm.Write(reinterpret_cast(Value.c_str()), Value.size()); + size_t size = Value.size(); + Stm.Save(size); + Stm.Write(reinterpret_cast(Value.c_str()), size); + + return true; + } + }; + + template <> + struct Savegame::PhobosStreamObject + { + bool ReadFromStream(PhobosStreamReader& Stm, std::wstring& Value, bool RegisterForChange) const + { + size_t size = 0; + + if (Stm.Load(size)) + { + std::vector buffer(size); + + if (!size || Stm.Read(reinterpret_cast(buffer.data()), size * sizeof(wchar_t))) + { + Value.assign(buffer.begin(), buffer.end()); + return true; + } + } + + return false; + } + + bool WriteToStream(PhobosStreamWriter& Stm, const std::wstring& Value) const + { + size_t size = Value.size(); + Stm.Save(size); + Stm.Write(reinterpret_cast(Value.c_str()), size * sizeof(wchar_t)); return true; }