From c2e12b8e4f28ea0439900644f254170eb5f2f7b5 Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 2 Sep 2026 12:58:52 +0200 Subject: [PATCH 1/5] Implement Tiberian Sun branching campaign map selection screen with PCX/SHP assets, custom palettes, voiceovers, typewriter briefings and video cutscenes --- CREDITS.md | 1 + Phobos.vcxproj | 2 + docs/User-Interface.md | 82 + docs/Whats-New.md | 1 + src/Ext/Scenario/Hooks.Variables.cpp | 20 +- src/Misc/MapSelectionClass.cpp | 2208 ++++++++++++++++++++++++++ src/Misc/MapSelectionClass.h | 142 ++ src/Utilities/GeneralUtils.cpp | 73 + src/Utilities/GeneralUtils.h | 1 + 9 files changed, 2528 insertions(+), 2 deletions(-) create mode 100644 src/Misc/MapSelectionClass.cpp create mode 100644 src/Misc/MapSelectionClass.h diff --git a/CREDITS.md b/CREDITS.md index f8cb8f4bcf..f1835d9906 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -133,6 +133,7 @@ This page lists all the individual contributions to the project by their author. - Disable `DamageSound` for buildings - Power plant damage factor - **FS-21**: + - Interactive mission selection screen - Dump Object Info enhancements - `Powered.KillSpawns` - `Spawner.LimitRange` diff --git a/Phobos.vcxproj b/Phobos.vcxproj index 67dc28e6ee..9b87035002 100644 --- a/Phobos.vcxproj +++ b/Phobos.vcxproj @@ -253,6 +253,7 @@ + @@ -396,6 +397,7 @@ + diff --git a/docs/User-Interface.md b/docs/User-Interface.md index 7bb7a27133..addbc5e590 100644 --- a/docs/User-Interface.md +++ b/docs/User-Interface.md @@ -654,6 +654,88 @@ In `uimd.ini`: DisableEmptySpawnPositions=false ; boolean ``` +## Mission selection screen + +- In vanilla Yuri's Revenge, branching campaign progression was disabled and skipped. Now you can use the Tiberian Sun interactive mission selection screen between campaign missions with full support for territory hover zones, mission briefings, voiceovers, video cutscenes, overlay animations, target cursors, and soundtrack themes. + - Configuration is read from `mapselmd.ini`. + - `[Sides]` and faction sections map the player's side/house to campaign stages. + - Each stage section defines the map background, videos, animations, target coordinates, sounds, and selectable mission choices. + - `MapVQ` specifies the introductory cutscene movie or video background. + - `Map` specifies an optional static background SHP image. + - `MapPCX` specifies an optional static background PCX image (taking precedence over `Map`). + - `Palette` specifies the palette used for UI rendering and animations (defaults to `mapsel.pal`). + - `OverlayPalette` specifies the palette used for territory overlay rendering (defaults to `msovrly.pal`). + - `Overlays` specifies a comma-separated list of territory overlay SHP files that sweep into the map after the intro video. + - `OverlaysPCX` specifies a comma-separated list of PCX overlay files (taking precedence over `Overlays`). + - `Targets` defines the count and (X,Y) coordinates of animated mission target crosshairs on the map. + - `TargetFlyIn` specifies the target zoom-in SHP animation (defaults to `TARGET1.SHP`). + - `TargetFlyInPCX` specifies a base PCX filename for the target zoom-in animation (declaring `TARGET1.PCX` or `TARGET1 0000.PCX` automatically loads the sequence of frame files `TARGET1 0000.PCX`, `TARGET1 0001.PCX`, etc., taking precedence over `TargetFlyIn`). + - `TargetMarker` specifies the target crosshair marker SHP animation (defaults to `TARGET2.SHP`). + - `TargetMarkerPCX` specifies a base PCX filename for the target crosshair marker animation (declaring `TARGET2.PCX` or `TARGET2 0000.PCX` automatically loads the sequence of frame files `TARGET2 0000.PCX`, `TARGET2 0001.PCX`, etc., taking precedence over `TargetMarker`). + - `Anim` defines stage-specific decorative SHP animations (`,,,[,]`) on the map interface (logos, radar sweeps, spinning globes, beacons). An optional custom palette filename can be provided (defaulting to `Palette`). + - `AnimPCX` defines stage-specific decorative multi-frame PCX animations (`,,,`). Declaring `` (or ``) automatically loads the sequence of frame files (`base 0000.PCX`, `base 0001.PCX`, etc.) and takes precedence over `Anim`. + - `[Anims]` defines global SHP animations shared across all stages (`=,,,[,]`). An optional custom palette filename can be provided (defaulting to `Palette`). + - `[AnimsPCX]` defines global multi-frame PCX animations shared across all stages (`=,,,`). + - `ClickMap` specifies an 8-bit indexed 256-color PCX mask image. Color indices `0` and `255` represent non-clickable background areas (oceans, borders, or transparent zones), while palette color indices `1` through `254` represent clickable territory zones (referenced by `=`). + - `=` binds a palette color index from the `ClickMap` PCX to a mission choice section (e.g. `3=NODB03` and `4=NODA03`). When the player hovers over pixels painted with palette color index `3`, the game highlights and displays the briefing for `[NODB03]`, and launches its scenario upon clicking. Up to 254 unique clickable territory zones per screen are supported. + - `VoiceOver` defines the spoken audio brief played when hovering over a mission (or opening the stage). + - `HoverSound` defines an optional SFX sound when hovering over a mission choice. + - `ClickSound` defines the sound played when confirming a mission selection. + - `OverlaySound` defines the sound played during the territory overlay sweep. + - `TargetSound` defines the sound played during target crosshair zoom-in animation. + - `MouseOnMapSound` / `MouseOffMapSound` defines sounds played when cursor enters or leaves the map window. + - `Theme` specifies an optional soundtrack theme played during map selection. + - `TextColor` specifies the text color (`R,G,B`) for the mission briefing (defaults to the active player's house color). + - `TextRect` specifies an optional custom bounding rectangle (`,,,`) for the mission briefing text, relative to the map window (defaults to `92,322,332,78` matching Tiberian Sun's HUD plate). + - `Description` specifies the CSF string key or text for the mission briefing, displayed in the metallic briefing panel with automatic multi-line word wrapping and typewriter animation. + +In `mapselmd.ini`: +```ini +[Sides] += ; Side + +[SOMESIDE] ; Side += ; Stage + +[SOMESTAGE] ; Stage +Scenario= ; Scenario filename +Description= ; CSF label or text +Map= ; SHP filename +MapPCX= ; PCX filename +Palette=mapsel.pal ; Palette filename +OverlayPalette=msovrly.pal ; Palette filename +MapVQ= ; video filename +Overlays= ; list of SHP filenames +OverlaysPCX= ; list of PCX filenames +Targets= ; integer, point list +TargetFlyIn=TARGET1.SHP ; SHP filename +TargetFlyInPCX= ; PCX filename +TargetMarker=TARGET2.SHP ; SHP filename +TargetMarkerPCX= ; PCX filename +ClickMap= ; PCX filename +Theme= ; Soundtrack theme ID +VoiceOver= ; WAV sound filename +OverlaySound= ; WAV sound filename +TargetSound= ; WAV sound filename +HoverSound= ; WAV sound filename +ClickSound= ; WAV sound filename +EnterRegionSound= ; WAV sound filename +ExitRegionSound= ; WAV sound filename +MouseOnMapSound= ; WAV sound filename +MouseOffMapSound= ; WAV sound filename +TextColor= ; ColorStruct (R,G,B) +TextRect=92,322,332,78 ; Rectangle (x,y,width,height) +Anim= ; Animation SHP (file.shp,x,y,delay[,palette.pal]) +AnimPCX= ; Animation PCX (base.pcx,x,y,delay) += ; Choice Stage + +[Anims] ; Optional global SHP animations += ; Animation SHP (file.shp,x,y,delay[,palette.pal]) + +[AnimsPCX] ; Optional global PCX animations += ; Animation PCX (base.pcx,x,y,delay) +``` + ## Miscellanous ### Skip saving game on starting a new campaign diff --git a/docs/Whats-New.md b/docs/Whats-New.md index 661d60777b..4e514e6cc4 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -426,6 +426,7 @@ HideShakeEffects=false ; boolean #### New: - [Customized transport plane for teams](AI-Scripting-and-Mapping.md#customized-transport-plane-for-teams) (by FlyStar) - [Modify ammo on impact](New-or-Enhanced-Logics.md#modify-ammo-on-impact) (by FS-21) +- [Mission selection screen](User-Interface.md#mission-selection-screen) (by FS-21) - [Customize whether mind-controlled Insignificant technos can be auto-targeted](Fixed-or-Improved-Logics.md#customize-whether-mind-controlled-Insignificant-technos-can-be-auto-targeted) (by Noble_Fish) - [AutoDeath based on player power status and player credits](New-or-Enhanced-Logics.md#kill-object-automatically) (by Flactine) - [Roof production anim](New-or-Enhanced-Logics.md#roof-production-anim) (by Noble_Fish) diff --git a/src/Ext/Scenario/Hooks.Variables.cpp b/src/Ext/Scenario/Hooks.Variables.cpp index 505928f7e9..ef151daded 100644 --- a/src/Ext/Scenario/Hooks.Variables.cpp +++ b/src/Ext/Scenario/Hooks.Variables.cpp @@ -1,4 +1,5 @@ #include "Body.h" +#include DEFINE_HOOK(0x689910, ScenarioClass_SetLocalToByID, 0x5) { @@ -183,8 +184,23 @@ static bool __fastcall MapSelectClass_SetNextScenario_CustomMission(MapSelectCla return false; } - // Return to the original function. - return pThis->SetNextScenario(pItem); + // Try opening our interactive Map Selection Screen if SkipMapSelect is false + if (MapSelectionClass::OpenMapSelectionWindow(pItem)) + { + return true; + } + + // Safe fallback to NextScenario if map selection was cancelled or empty (prevents crashing on dead vanilla code) + const char* nextScenario = (pItem->NextScenario && pItem->NextScenario[0]) ? pItem->NextScenario : pItem->FileName; + if (nextScenario && nextScenario[0]) + { + pItem->Stage = 0; + strncpy(pItem->FileName, nextScenario, 0x104u); + pItem->FileName[259] = '\0'; + return true; + } + + return false; } DEFINE_JUMP(LJMP, 0x685A38, 0x685A63) // Skip the original code diff --git a/src/Misc/MapSelectionClass.cpp b/src/Misc/MapSelectionClass.cpp new file mode 100644 index 0000000000..fe8a512505 --- /dev/null +++ b/src/Misc/MapSelectionClass.cpp @@ -0,0 +1,2208 @@ +#include "MapSelectionClass.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#pragma comment(lib, "winmm.lib") + +using LoadFile_FastCall_t = void* (__fastcall*)(const char* pFileName, bool bLoadAsSHP); + +static inline void* LoadGameFile(const char* pFileName, bool bLoadAsSHP = false) +{ + auto const pFunc = reinterpret_cast(0x5B40B0); + return pFunc(pFileName, bLoadAsSHP); +} + +static void PlayMapSelAudio(const char* pAudioName) +{ + if (!pAudioName || !pAudioName[0]) + return; + + std::string baseName = pAudioName; + // Strip volume percentage if present (e.g. "GSWEEP.AUD,60" -> "GSWEEP.AUD") + size_t commaPos = baseName.find(','); + if (commaPos != std::string::npos) + { + baseName = baseName.substr(0, commaPos); + } + while (!baseName.empty() && (baseName.back() == ' ' || baseName.back() == '\t' || baseName.back() == '\r' || baseName.back() == '\n')) + baseName.pop_back(); + while (!baseName.empty() && (baseName.front() == ' ' || baseName.front() == '\t')) + baseName.erase(baseName.begin()); + + if (baseName.empty()) + return; + + // Strip any legacy extension (.aud, .wav) + std::string rawName = baseName; + size_t dotPos = rawName.find_last_of('.'); + if (dotPos != std::string::npos) + { + std::string ext = rawName.substr(dotPos); + if (_stricmp(ext.c_str(), ".aud") == 0 || _stricmp(ext.c_str(), ".wav") == 0) + { + rawName = rawName.substr(0, dotPos); + } + } + + std::string wavNameLower = rawName + ".wav"; + std::string wavNameUpper = rawName + ".WAV"; + + // Try Windows PlaySound on disk file first (100% reliable, async, non-blocking) + if (GetFileAttributesA(wavNameLower.c_str()) != INVALID_FILE_ATTRIBUTES) + { + Debug::Log("[MapSelection] Playing WAV via PlaySound '%s'\n", wavNameLower.c_str()); + PlaySoundA(wavNameLower.c_str(), NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT); + return; + } + if (GetFileAttributesA(wavNameUpper.c_str()) != INVALID_FILE_ATTRIBUTES) + { + Debug::Log("[MapSelection] Playing WAV via PlaySound '%s'\n", wavNameUpper.c_str()); + PlaySoundA(wavNameUpper.c_str(), NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT); + return; + } + + // Try playing .wav via AudioStream (from MIX files or memory) + AudioStream* pStream = AudioStream::Instance; + if (pStream) + { + CCFileClass testFileLower(wavNameLower.c_str()); + CCFileClass testFileUpper(wavNameUpper.c_str()); + + if (testFileLower.Exists()) + { + Debug::Log("[MapSelection] Playing WAV via AudioStream '%s'\n", wavNameLower.c_str()); + typedef bool(__thiscall* PlayWAV_t)(void* pThis, const char* pFileName, bool bLoop); + auto playWAV = reinterpret_cast(0x407B60); + playWAV(pStream, wavNameLower.c_str(), false); + return; + } + else if (testFileUpper.Exists()) + { + Debug::Log("[MapSelection] Playing WAV via AudioStream '%s'\n", wavNameUpper.c_str()); + typedef bool(__thiscall* PlayWAV_t)(void* pThis, const char* pFileName, bool bLoop); + auto playWAV = reinterpret_cast(0x407B60); + playWAV(pStream, wavNameUpper.c_str(), false); + return; + } + } + + // Fallback to VocClass (Sound.ini sound entry) + int vocIdx = VocClass::FindIndex(rawName.c_str()); + if (vocIdx < 0) + vocIdx = VocClass::FindIndex(baseName.c_str()); + + if (vocIdx >= 0) + { + Debug::Log("[MapSelection] Playing Voc sound '%s' (idx %d)\n", rawName.c_str(), vocIdx); + VocClass::PlayGlobal(vocIdx, 0x2000, 1.0f); + return; + } + + // Fallback to VoxClass (EVA messages in eva.ini) + int voxIdx = VoxClass::FindIndex(rawName.c_str()); + if (voxIdx < 0) + voxIdx = VoxClass::FindIndex(baseName.c_str()); + + if (voxIdx >= 0) + { + Debug::Log("[MapSelection] Playing Vox EVA sound '%s' (idx %d)\n", rawName.c_str(), voxIdx); + VoxClass::PlayIndex(voxIdx, -1, -1); + return; + } + + Debug::Log("[MapSelection] Audio '%s' / '%s.wav' not found anywhere.\n", baseName.c_str(), rawName.c_str()); +} + +static bool PlayMapSelBinkVideo(const char* pVideoName) +{ + if (!pVideoName || !pVideoName[0]) + return false; + + std::string movieName = pVideoName; + size_t dotPos = movieName.find_last_of('.'); + if (dotPos != std::string::npos) + { + std::string ext = movieName.substr(dotPos); + if (_stricmp(ext.c_str(), ".vqa") == 0 || _stricmp(ext.c_str(), ".bik") == 0) + { + movieName = movieName.substr(0, dotPos); + } + } + + std::string bikNameLower = movieName + ".bik"; + std::string bikNameUpper = movieName + ".BIK"; + + CCFileClass testBikLower(bikNameLower.c_str()); + CCFileClass testBikUpper(bikNameUpper.c_str()); + + if (testBikLower.Exists() || testBikUpper.Exists()) + { + Debug::Log("[MapSelection] Playing Bink video '%s'\n", movieName.c_str()); + Game::PlayMovie(movieName.c_str(), -1, -1, -1, -1, -1); + return true; + } + + Debug::Log("[MapSelection] Bink video '%s' not found, skipping video playback.\n", bikNameLower.c_str()); + return false; +} + +struct CaseInsensitiveLess +{ + bool operator()(const std::string& a, const std::string& b) const + { + return _stricmp(a.c_str(), b.c_str()) < 0; + } +}; + +static std::map g_ExtraCSFMap; +static bool g_ExtraCSFLoaded = false; + +static void LoadExtraCSFFile(const char* pFileName) +{ + if (!pFileName || !pFileName[0]) + return; + + std::vector raw; + CCFileClass file(pFileName); + if (file.Exists() && file.Open(FileAccessMode::Read)) + { + int sz = file.GetFileSize(); + if (sz >= 24) + { + raw.resize(sz); + file.ReadBytes(raw.data(), sz); + file.Close(); + } + } + if (raw.size() < 24) + return; + + DWORD signature = *reinterpret_cast(&raw[0]); + if (signature != 0x43534620) // " FSC" + return; + + int numLabels = *reinterpret_cast(&raw[8]); + size_t pos = 24; + + for (int i = 0; i < numLabels && pos + 12 <= raw.size(); ++i) + { + DWORD lblSig = *reinterpret_cast(&raw[pos]); + int numStr = *reinterpret_cast(&raw[pos + 4]); + int nameLen = *reinterpret_cast(&raw[pos + 8]); + pos += 12; + + if (pos + nameLen > raw.size()) + break; + + std::string labelName(reinterpret_cast(&raw[pos]), nameLen); + pos += nameLen; + + for (int s = 0; s < numStr && pos + 8 <= raw.size(); ++s) + { + DWORD strSig = *reinterpret_cast(&raw[pos]); + int strLen = *reinterpret_cast(&raw[pos + 4]); + pos += 8; + + if (pos + strLen * 2 > raw.size()) + break; + + std::wstring value; + value.reserve(strLen); + for (int c = 0; c < strLen; ++c) + { + WORD ch = *reinterpret_cast(&raw[pos + c * 2]); + value.push_back(static_cast(~ch)); + } + pos += strLen * 2; + + if (strSig == 0x53545257) // "WRTS" + { + if (pos + 4 <= raw.size()) + { + int extraLen = *reinterpret_cast(&raw[pos]); + pos += 4 + extraLen; + } + } + + if (s == 0) + { + g_ExtraCSFMap[labelName] = value; + } + } + } + Debug::Log("[MapSelection] Loaded %zu strings from extra CSF '%s'\n", g_ExtraCSFMap.size(), pFileName); +} + +static void EnsureExtraCSFLoaded() +{ + if (g_ExtraCSFLoaded) + return; + g_ExtraCSFLoaded = true; + + LoadExtraCSFFile("stringtable00.csf"); + LoadExtraCSFFile("stringtable.csf"); + LoadExtraCSFFile("new_strings_v2_6_7.csf"); +} + +static std::vector WordWrapText(const std::wstring& text, size_t maxCharsPerLine = 46) +{ + std::vector lines; + if (text.empty()) + return lines; + + std::wstring currentLine; + std::wstring word; + + for (size_t i = 0; i <= text.length(); ++i) + { + wchar_t c = (i < text.length()) ? text[i] : L'\0'; + if (c == L' ' || c == L'\t' || c == L'\n' || c == L'\r' || c == L'\0') + { + if (!word.empty()) + { + if (currentLine.empty()) + { + currentLine = word; + } + else if (currentLine.length() + 1 + word.length() <= maxCharsPerLine) + { + currentLine += L" " + word; + } + else + { + lines.push_back(currentLine); + currentLine = word; + } + word.clear(); + } + if (c == L'\n') + { + if (!currentLine.empty()) + { + lines.push_back(currentLine); + currentLine.clear(); + } + } + } + else + { + word.push_back(c); + } + } + if (!currentLine.empty()) + { + lines.push_back(currentLine); + } + return lines; +} + +static ColorStruct ParseColorString(const char* pStr, const ColorStruct& defaultColor) +{ + if (!pStr || !pStr[0]) + return defaultColor; + + int r = 0, g = 0, b = 0; + if (sscanf_s(pStr, "%d,%d,%d", &r, &g, &b) == 3) + { + return ColorStruct { static_cast(std::clamp(r, 0, 255)), + static_cast(std::clamp(g, 0, 255)), + static_cast(std::clamp(b, 0, 255)) }; + } + return defaultColor; +} + +static std::wstring ResolveCSFOrText(const std::string& inputKey) +{ + if (inputKey.empty()) + return L""; + + std::string key = inputKey; + while (!key.empty() && (key.back() == ' ' || key.back() == '\t' || key.back() == '\r' || key.back() == '\n')) + key.pop_back(); + while (!key.empty() && (key.front() == ' ' || key.front() == '\t')) + key.erase(key.begin()); + + if (key.empty()) + return L""; + + EnsureExtraCSFLoaded(); + + // Try directly as CSF label in main StringTable + const wchar_t* pCsf = StringTable::TryFetchString(key.c_str(), nullptr); + if (pCsf && pCsf[0]) + return pCsf; + + // Try in Extra CSF Map (stringtable00.csf, etc.) + auto it = g_ExtraCSFMap.find(key); + if (it != g_ExtraCSFMap.end()) + return it->second; + + // Try common prefixes: loadbrief:, briefvideo:, birefvideo:, brief:, name:, DESC:, TXT_ + const char* testPrefixes[] = { + "loadbrief:", "LOADBRIEF:", "briefvideo:", "BRIEFVIDEO:", "birefvideo:", "BIREFVIDEO:", + "Brief:", "brief:", "BRIEF:", "name:", "NAME:", "DESC:", "desc:", "TXT_" + }; + for (const auto& pref : testPrefixes) + { + std::string alt = pref + key; + pCsf = StringTable::TryFetchString(alt.c_str(), nullptr); + if (pCsf && pCsf[0]) + return pCsf; + + it = g_ExtraCSFMap.find(alt); + if (it != g_ExtraCSFMap.end()) + return it->second; + } + + // If input is integer (e.g. 769), try TXT_ + if (std::all_of(key.begin(), key.end(), ::isdigit)) + { + std::string txtSym = "TXT_" + key; + pCsf = StringTable::TryFetchString(txtSym.c_str(), nullptr); + if (pCsf && pCsf[0]) + return pCsf; + + it = g_ExtraCSFMap.find(txtSym); + if (it != g_ExtraCSFMap.end()) + return it->second; + } + + // Return as wide string directly if not found in CSF + return std::wstring(key.begin(), key.end()); +} + +MapSelectionClass::MapSelectionClass() = default; + +MapSelectionClass::~MapSelectionClass() +{ + if (pPalette) + { + delete pPalette; + pPalette = nullptr; + } + if (pOverlayPalette) + { + delete pOverlayPalette; + pOverlayPalette = nullptr; + } +} + +static SHPStruct* LoadMapSelSHP(const char* pFileName) +{ + if (!pFileName || !pFileName[0]) + return nullptr; + + SHPStruct* pSHP = static_cast(LoadGameFile(pFileName, true)); + if (!pSHP) + { + CCFileClass file(pFileName); + if (file.Exists()) + { + pSHP = static_cast(file.ReadWholeFile()); + } + } + + Debug::Log("[MapSelection] LoadMapSelSHP '%s' -> %p\n", pFileName, pSHP); + return pSHP; +} + +static ConvertClass* LoadMapSelPalette(const char* pFileName, DSurface* pSurface) +{ + if (!pFileName || !pFileName[0]) + return nullptr; + + const ColorStruct* rawPal = static_cast(LoadGameFile(pFileName, false)); + std::vector localBuf; + if (!rawPal) + { + CCFileClass file(pFileName); + if (file.Exists() && file.Open(FileAccessMode::Read)) + { + localBuf.resize(sizeof(ColorStruct) * 256); + file.ReadBytes(localBuf.data(), sizeof(ColorStruct) * 256); + file.Close(); + rawPal = reinterpret_cast(localBuf.data()); + } + } + + if (!rawPal) + { + Debug::Log("[MapSelection] LoadMapSelPalette '%s' not found.\n", pFileName); + return nullptr; + } + + BytePalette colorData; + for (int i = 0; i < 256; ++i) + { + colorData[i].R = static_cast(rawPal[i].R << 2); + colorData[i].G = static_cast(rawPal[i].G << 2); + colorData[i].B = static_cast(rawPal[i].B << 2); + } + + ConvertClass* pConvert = GameCreate(colorData, FileSystem::TEMPERAT_PAL, pSurface, 1, false); + Debug::Log("[MapSelection] LoadMapSelPalette '%s' created ConvertClass %p\n", pFileName, pConvert); + return pConvert; +} + +static bool LoadMapSelPCXData(const char* pFileName, MapSelectPCX& out) +{ + if (!pFileName || !pFileName[0]) + return false; + + std::vector raw; + CCFileClass file(pFileName); + if (file.Exists() && file.Open(FileAccessMode::Read)) + { + int sz = file.GetFileSize(); + if (sz >= 128) + { + raw.resize(sz); + file.ReadBytes(raw.data(), sz); + file.Close(); + } + } + + if (raw.size() < 128) + return false; + + int xmin = raw[4] | (raw[5] << 8); + int ymin = raw[6] | (raw[7] << 8); + int xmax = raw[8] | (raw[9] << 8); + int ymax = raw[10] | (raw[11] << 8); + + int width = xmax - xmin + 1; + int height = ymax - ymin + 1; + int bytesPerLine = raw[66] | (raw[67] << 8); + + if (width <= 0 || height <= 0 || bytesPerLine <= 0) + return false; + + out.Width = width; + out.Height = height; + out.Pixels.resize(width * height, 0); + + size_t pos = 128; + for (int y = 0; y < height; ++y) + { + int x = 0; + while (x < bytesPerLine && pos < raw.size()) + { + BYTE b = raw[pos++]; + int count = 1; + BYTE val = b; + if ((b & 0xC0) == 0xC0) + { + count = b & 0x3F; + if (pos < raw.size()) + val = raw[pos++]; + } + for (int c = 0; c < count && x < bytesPerLine; ++c, ++x) + { + if (x < width) + { + out.Pixels[y * width + x] = val; + } + } + } + } + + Debug::Log("[MapSelection] Decoded clickmap PCX '%s' (%dx%d, %zu pixels)\n", pFileName, width, height, out.Pixels.size()); + return true; +} + +static bool MatchScenarioNames(const char* a, const char* b) +{ + if (!a || !b || !a[0] || !b[0]) + return false; + + if (_stricmp(a, b) == 0) + return true; + + const char* baseA = strrchr(a, '/'); + if (!baseA) baseA = strrchr(a, '\\'); + baseA = baseA ? baseA + 1 : a; + + const char* baseB = strrchr(b, '/'); + if (!baseB) baseB = strrchr(b, '\\'); + baseB = baseB ? baseB + 1 : b; + + return _stricmp(baseA, baseB) == 0; +} + +static bool DrawElement( + DSurface* pSurface, + const RectangleStruct& destRect, + BSurface* pPCXSurface, + SHPStruct* pSHP, + ConvertClass* pPalette, + int frameIndex = 0, + int zAdjust = -2, + BlitterFlags blitFlags = BlitterFlags::None) +{ + if (!pSurface || (!pPCXSurface && !pSHP)) + return false; + + if (pPCXSurface) + { + PCX::Instance.BlitToSurface(const_cast(&destRect), pSurface, pPCXSurface); + return true; + } + else if (pSHP && pPalette) + { + Point2D drawPos = { destRect.X, destRect.Y }; + RectangleStruct bounds = pSurface->GetRect(); + CC_Draw_Shape( + pSurface, + pPalette, + pSHP, + frameIndex, + &drawPos, + &bounds, + blitFlags, + 0, zAdjust, ZGradient::Ground, 1000, 0, nullptr, 0, 0, 0 + ); + return true; + } + return false; +} + +static void EnsureMixFileLoaded(const char* pMixName) +{ + if (!pMixName || !pMixName[0]) + return; + + for (GenericNode* pNode = (GenericNode*)MixFileClass::MIXes.First(); pNode && pNode->IsValid(); pNode = pNode->Next()) + { + auto pMix = reinterpret_cast(pNode); + if (pMix->FileName && _stricmp(pMix->FileName, pMixName) == 0) + { + return; + } + } + + CCFileClass testFile(pMixName); + if (testFile.Exists()) + { + auto pMix = GameCreate(pMixName); + Debug::Log("[MapSelection] Mounted Mix '%s' (%p, CountFiles=%d)\n", pMixName, pMix, pMix ? pMix->CountFiles : 0); + } +} + +bool MapSelectionClass::OpenMapSelectionWindow(ScenarioClass* pScenario) +{ + if (!pScenario) + return false; + + Debug::Log("[MapSelection] OpenMapSelectionWindow called for '%s'\n", pScenario->FileName); + + MapSelectionClass mapSelect; + if (mapSelect.Initialize(pScenario)) + { + return mapSelect.Run(); + } + + Debug::Log("[MapSelection] Initialize returned false, map selection aborted.\n"); + return false; +} + +bool MapSelectionClass::Initialize(ScenarioClass* pScenario) +{ + if (!pScenario) + return false; + + EnsureMixFileLoaded("mapsel.mix"); + EnsureMixFileLoaded("expandmd03.mix"); + + this->pCurrentScenario = pScenario; + this->LoadConfig(pScenario); + + if (this->choices.empty()) + { + Debug::Log("[MapSelection] No choices configured for '%s', bypassing.\n", pScenario->FileName); + return false; + } + + this->LoadAssets(); + this->isInitialized = true; + + return true; +} + +void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) +{ + CCINIClass ini; + bool loaded = false; + + CCFileClass fileMD("mapselmd.ini"); + if (fileMD.Exists() && fileMD.Open(FileAccessMode::Read)) + { + ini.ReadCCFile(&fileMD); + loaded = true; + Debug::Log("[MapSelection] Loaded mapselmd.ini successfully.\n"); + } + + if (!loaded) + { + Debug::Log("[MapSelection] Could not open mapselmd.ini\n"); + return; + } + + CCINIClass battleIni; + CCFileClass battleFile("battlemd.ini"); + if (battleFile.Exists() && battleFile.Open(FileAccessMode::Read)) + { + battleIni.ReadCCFile(&battleFile); + } + + CCINIClass missionIni; + CCFileClass missionFile("missionmd.ini"); + if (missionFile.Exists() && missionFile.Open(FileAccessMode::Read)) + { + missionIni.ReadCCFile(&missionFile); + } + + // Active side section + std::string sideSection = "GDI"; + int sideIdx = pScenario->PlayerSideIndex; + if (sideIdx < 0 && HouseClass::CurrentPlayer) + sideIdx = HouseClass::CurrentPlayer->SideIndex; + + if (sideIdx >= 0) + { + const SideClass* pSide = SideClass::Array.GetItemOrDefault(sideIdx); + if (pSide && pSide->ID && pSide->ID[0]) + { + if (ini.GetSection(pSide->ID)) + sideSection = pSide->ID; + } + + if (ini.GetSection("Sides")) + { + char keyName[16]; + sprintf_s(keyName, "%d", sideIdx); + char buf[64] = { 0 }; + ini.ReadString("Sides", keyName, "", buf, sizeof(buf)); + if (buf[0] && ini.GetSection(buf)) + sideSection = buf; + } + } + + Debug::Log("[MapSelection] Active side section determined as: '[%s]'\n", sideSection.c_str()); + + // Find stage progression + std::string targetStage {}; + char stageBuf[128] = { 0 }; + + const char* curScen = pScenario->FileName; + const char* nextScen = pScenario->NextScenario; + + Debug::Log("[MapSelection] Matching stage for curScen='%s', nextScen='%s'\n", curScen ? curScen : "", nextScen ? nextScen : ""); + + if (nextScen && nextScen[0] && ini.GetSection(nextScen)) + { + targetStage = nextScen; + } + else if (curScen && curScen[0] && ini.GetSection(curScen)) + { + targetStage = curScen; + } + + if (targetStage.empty()) + { + int keyCount = ini.GetKeyCount(sideSection.c_str()); + for (int i = 1; i <= keyCount; ++i) + { + char keyName[32]; + sprintf_s(keyName, "%d", i); + ini.ReadString(sideSection.c_str(), keyName, "", stageBuf, sizeof(stageBuf)); + if (!stageBuf[0]) + continue; + + char scenVal[260] = { 0 }; + ini.ReadString(stageBuf, "Scenario", "", scenVal, sizeof(scenVal)); + + if (curScen && curScen[0] && MatchScenarioNames(scenVal, curScen)) + { + targetStage = stageBuf; + break; + } + else if (nextScen && nextScen[0] && MatchScenarioNames(scenVal, nextScen)) + { + targetStage = stageBuf; + break; + } + else if (nextScen && nextScen[0] && _stricmp(stageBuf, nextScen) == 0) + { + targetStage = stageBuf; + break; + } + } + } + + if (targetStage.empty()) + { + int keyCount = ini.GetKeyCount(sideSection.c_str()); + if (keyCount > 0) + { + ini.ReadString(sideSection.c_str(), "1", "", stageBuf, sizeof(stageBuf)); + targetStage = stageBuf; + } + } + + this->currentStageName = targetStage; + Debug::Log("[MapSelection] Selected active stage: '[%s]'\n", this->currentStageName.c_str()); + + if (this->currentStageName.empty()) + return; + + const char* pStage = this->currentStageName.c_str(); + + char clickMapBuf[128] = { 0 }; + ini.ReadString(pStage, "ClickMap", "", clickMapBuf, sizeof(clickMapBuf)); + this->clickMapFileName = clickMapBuf; + + char mapVQBuf[128] = { 0 }; + ini.ReadString(pStage, "MapVQ", "", mapVQBuf, sizeof(mapVQBuf)); + this->mapVQFileName = mapVQBuf; + + char voiceOverBuf[128] = { 0 }; + ini.ReadString(pStage, "VoiceOver", "", voiceOverBuf, sizeof(voiceOverBuf)); + this->voiceOverFileName = voiceOverBuf; + + char themeBuf[128] = { 0 }; + ini.ReadString(pStage, "Theme", "", themeBuf, sizeof(themeBuf)); + this->themeName = themeBuf; + + // Determine default text color from player's house + ColorStruct playerHouseColor { 255, 239, 99 }; + HouseClass* pHouse = HouseClass::CurrentPlayer; + if (!pHouse && pScenario && pScenario->HumanPlayerHouseTypeIndex >= 0 && pScenario->HumanPlayerHouseTypeIndex < HouseClass::Array.Count) + { + pHouse = HouseClass::Array.GetItemOrDefault(pScenario->HumanPlayerHouseTypeIndex); + } + if (pHouse) + { + int schemeIdx = pHouse->ColorSchemeIndex; + ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(schemeIdx); + if (pScheme) + { + playerHouseColor = pScheme->BaseColor; + } + } + else if (_stricmp(sideSection.c_str(), "Nod") == 0) + { + playerHouseColor = ColorStruct { 255, 50, 50 }; + } + + char colorBuf[64] = { 0 }; + ini.ReadString(pStage, "TextColor", "", colorBuf, sizeof(colorBuf)); + this->defaultTextColor = ParseColorString(colorBuf, playerHouseColor); + + char sfxBuf[128] = { 0 }; + ini.ReadString(pStage, "OverlaySound", "", sfxBuf, sizeof(sfxBuf)); + this->overlaySound = sfxBuf; + + ini.ReadString(pStage, "TargetSound", "", sfxBuf, sizeof(sfxBuf)); + this->targetFlyInSound = sfxBuf; + + ini.ReadString(pStage, "EnterRegionSound", "", sfxBuf, sizeof(sfxBuf)); + this->enterRegionSound = sfxBuf; + + ini.ReadString(pStage, "ExitRegionSound", "", sfxBuf, sizeof(sfxBuf)); + this->exitRegionSound = sfxBuf; + + ini.ReadString(pStage, "ClickSound", "", sfxBuf, sizeof(sfxBuf)); + this->clickRegionSound = sfxBuf; + + ini.ReadString(pStage, "MouseOnMapSound", "", sfxBuf, sizeof(sfxBuf)); + this->mouseOnMapSound = sfxBuf; + + ini.ReadString(pStage, "MouseOffMapSound", "", sfxBuf, sizeof(sfxBuf)); + this->mouseOffMapSound = sfxBuf; + + char palBuf[128] = { 0 }; + ini.ReadString(pStage, "Palette", "mapsel.pal", palBuf, sizeof(palBuf)); + this->paletteFileName = palBuf[0] ? palBuf : "mapsel.pal"; + + char ovrPalBuf[128] = { 0 }; + ini.ReadString(pStage, "OverlayPalette", "msovrly.pal", ovrPalBuf, sizeof(ovrPalBuf)); + this->overlayPaletteFileName = ovrPalBuf[0] ? ovrPalBuf : "msovrly.pal"; + + char bgPCXBuf[128] = { 0 }; + ini.ReadString(pStage, "MapPCX", "", bgPCXBuf, sizeof(bgPCXBuf)); + this->backgroundPCXFileName = bgPCXBuf; + + char bgSHPBuf[128] = { 0 }; + ini.ReadString(pStage, "Map", "", bgSHPBuf, sizeof(bgSHPBuf)); + if (!bgSHPBuf[0]) + ini.ReadString(pStage, "MapVQ", "", bgSHPBuf, sizeof(bgSHPBuf)); + this->backgroundFileName = bgSHPBuf; + + char rectBuf[64] = { 0 }; + ini.ReadString(pStage, "TextRect", "", rectBuf, sizeof(rectBuf)); + if (rectBuf[0]) + { + int rx = 0, ry = 0, rw = 0, rh = 0; + if (sscanf_s(rectBuf, "%d,%d,%d,%d", &rx, &ry, &rw, &rh) == 4) + { + this->textRectangle = { rx, ry, rw, rh }; + } + } + + // Overlays (PCX takes precedence over SHP if provided) + char overlaysPCXBuf[512] = { 0 }; + ini.ReadString(pStage, "OverlaysPCX", "", overlaysPCXBuf, sizeof(overlaysPCXBuf)); + if (overlaysPCXBuf[0]) + { + char* context = nullptr; + char* token = strtok_s(overlaysPCXBuf, ",", &context); + while (token) + { + while (*token == ' ') token++; + if (*token) + { + this->overlayPCXNames.push_back(token); + } + token = strtok_s(nullptr, ",", &context); + } + } + + char overlaysBuf[512] = { 0 }; + ini.ReadString(pStage, "Overlays", "", overlaysBuf, sizeof(overlaysBuf)); + if (overlaysBuf[0]) + { + char* context = nullptr; + char* token = strtok_s(overlaysBuf, ",", &context); + while (token) + { + while (*token == ' ') token++; + if (*token) + { + this->overlaySHPNames.push_back(token); + } + token = strtok_s(nullptr, ",", &context); + } + } + + // Target fly-in and target marker custom assets + char flyInPCXBuf[512] = { 0 }; + ini.ReadString(pStage, "TargetFlyInPCX", "", flyInPCXBuf, sizeof(flyInPCXBuf)); + if (flyInPCXBuf[0]) + { + char* context = nullptr; + char* token = strtok_s(flyInPCXBuf, ",", &context); + while (token) + { + while (*token == ' ') token++; + if (*token) + { + this->targetFlyInPCXNames.push_back(token); + } + token = strtok_s(nullptr, ",", &context); + } + } + + char flyInBuf[128] = { 0 }; + ini.ReadString(pStage, "TargetFlyIn", "", flyInBuf, sizeof(flyInBuf)); + this->targetFlyInFileName = flyInBuf; + + char markerPCXBuf[512] = { 0 }; + ini.ReadString(pStage, "TargetMarkerPCX", "", markerPCXBuf, sizeof(markerPCXBuf)); + if (markerPCXBuf[0]) + { + char* context = nullptr; + char* token = strtok_s(markerPCXBuf, ",", &context); + while (token) + { + while (*token == ' ') token++; + if (*token) + { + this->targetMarkerPCXNames.push_back(token); + } + token = strtok_s(nullptr, ",", &context); + } + } + + char markerBuf[128] = { 0 }; + ini.ReadString(pStage, "TargetMarker", "", markerBuf, sizeof(markerBuf)); + this->targetMarkerFileName = markerBuf; + + // Parse stage Targets list: Targets=,,,,,... + std::vector stageTargets; + char stageTargetsBuf[256] = { 0 }; + ini.ReadString(pStage, "Targets", "", stageTargetsBuf, sizeof(stageTargetsBuf)); + if (stageTargetsBuf[0]) + { + char* context = nullptr; + char* token = strtok_s(stageTargetsBuf, ",", &context); + if (token) + { + int count = atoi(token); + for (int t = 0; t < count; ++t) + { + char* tokX = strtok_s(nullptr, ",", &context); + char* tokY = strtok_s(nullptr, ",", &context); + if (tokX && tokY) + { + stageTargets.push_back({ atoi(tokX), atoi(tokY) }); + } + } + } + } + + // Choices: iterate in strict numerical order (matching OpenTS mschoice.cpp) + char scenBuf[260] = { 0 }; + ini.ReadString(pStage, "Scenario", "", scenBuf, sizeof(scenBuf)); + + for (int choiceIndex = 1; choiceIndex < 256; ++choiceIndex) + { + char keyName[16]; + sprintf_s(keyName, "%d", choiceIndex); + + char choiceStage[128] = { 0 }; + ini.ReadString(pStage, keyName, "", choiceStage, sizeof(choiceStage)); + if (!choiceStage[0]) + continue; + + MapSelectChoice choice; + choice.Index = choiceIndex; + choice.StageName = choiceStage; + + // Map corresponding target coordinate from stage Targets list + size_t choiceIdxInStage = this->choices.size(); + if (choiceIdxInStage < stageTargets.size()) + { + choice.TargetCoord = stageTargets[choiceIdxInStage]; + choice.HasTargetCoord = true; + } + + char buf[512] = { 0 }; + if (ini.GetSection(choiceStage)) + { + ini.ReadString(choiceStage, "Scenario", "", buf, sizeof(buf)); + choice.ScenarioPath = buf; + + ini.ReadString(choiceStage, "Description", "", buf, sizeof(buf)); + choice.Description = buf; + + ini.ReadString(choiceStage, "Summary", "", buf, sizeof(buf)); + choice.Summary = buf; + + ini.ReadString(choiceStage, "VoiceOver", "", buf, sizeof(buf)); + choice.VoiceOver = buf; + + ini.ReadString(choiceStage, "HoverSound", "", buf, sizeof(buf)); + choice.HoverSound = buf; + + ini.ReadString(choiceStage, "ClickSound", "", buf, sizeof(buf)); + if (buf[0]) choice.ClickSound = buf; + else choice.ClickSound = this->clickRegionSound; + + ini.ReadString(choiceStage, "TextColor", "", buf, sizeof(buf)); + if (buf[0]) + { + choice.TextColor = ParseColorString(buf, this->defaultTextColor); + choice.HasCustomTextColor = true; + } + else + { + choice.TextColor = this->defaultTextColor; + } + + // Only fallback to sub-stage Targets if the active stage had no target for this choice + if (!choice.HasTargetCoord) + { + ini.ReadString(choiceStage, "Targets", "", buf, sizeof(buf)); + if (buf[0]) + { + int count = 0, tx = 0, ty = 0; + if (sscanf_s(buf, "%d,%d,%d", &count, &tx, &ty) >= 3) + { + choice.TargetCoord = { tx, ty }; + choice.HasTargetCoord = true; + } + } + } + } + else + { + choice.ScenarioPath = choiceStage; + } + + if (choice.Description.empty() && battleIni.GetSection(choiceStage)) + { + battleIni.ReadString(choiceStage, "Description", "", buf, sizeof(buf)); + choice.Description = buf; + battleIni.ReadString(choiceStage, "Summary", "", buf, sizeof(buf)); + choice.Summary = buf; + } + + if (choice.Description.empty() && missionIni.GetSection(choice.ScenarioPath.c_str())) + { + missionIni.ReadString(choice.ScenarioPath.c_str(), "UIName", "", buf, sizeof(buf)); + choice.Description = buf; + missionIni.ReadString(choice.ScenarioPath.c_str(), "Briefing", "", buf, sizeof(buf)); + choice.Summary = buf; + } + + if (!choice.ScenarioPath.empty()) + { + Debug::Log("[MapSelection] Added choice %d (index=%d): Stage='%s', Scenario='%s', Target=(%d,%d)\n", + (int)this->choices.size() + 1, choice.Index, choice.StageName.c_str(), choice.ScenarioPath.c_str(), choice.TargetCoord.X, choice.TargetCoord.Y); + this->choices.push_back(choice); + } + } + + if (this->choices.empty() && scenBuf[0]) + { + MapSelectChoice choice; + choice.Index = 1; + choice.StageName = this->currentStageName; + choice.ScenarioPath = scenBuf; + + char buf[512] = { 0 }; + ini.ReadString(pStage, "Description", "", buf, sizeof(buf)); + choice.Description = buf; + + ini.ReadString(pStage, "Summary", "", buf, sizeof(buf)); + choice.Summary = buf; + + ini.ReadString(pStage, "VoiceOver", "", buf, sizeof(buf)); + choice.VoiceOver = buf; + + ini.ReadString(pStage, "HoverSound", "", buf, sizeof(buf)); + choice.HoverSound = buf; + + ini.ReadString(pStage, "Targets", "", buf, sizeof(buf)); + if (buf[0]) + { + int count = 0, tx = 0, ty = 0; + if (sscanf_s(buf, "%d,%d,%d", &count, &tx, &ty) >= 3) + { + choice.TargetCoord = { tx, ty }; + choice.HasTargetCoord = true; + } + } + + Debug::Log("[MapSelection] Added single default choice: Stage='%s', Scenario='%s'\n", choice.StageName.c_str(), choice.ScenarioPath.c_str()); + this->choices.push_back(choice); + } + + // Parse animations from stage section + for (int i = 1; i <= 32; ++i) + { + char keyName[32]; + + // Stage PCX animation (AnimPCX) + sprintf_s(keyName, "Anim%dPCX", i); + char animPCXDef[512] = { 0 }; + ini.ReadString(pStage, keyName, "", animPCXDef, sizeof(animPCXDef)); + if (animPCXDef[0]) + { + char pcxFile[128] = { 0 }; + int ax = 0, ay = 0, arate = 5; + if (sscanf_s(animPCXDef, "%127[^,],%d,%d,%d", pcxFile, (unsigned)_countof(pcxFile), &ax, &ay, &arate) >= 3) + { + auto pFrames = GeneralUtils::GetAnimationPCX(pcxFile); + if (pFrames && !pFrames->empty()) + { + MapSelectAnim anim; + anim.X = ax; + anim.Y = ay; + anim.FrameDelay = (arate > 0) ? arate : 5; + for (const auto& frame : *pFrames) + { + if (frame.Exists()) + anim.PCXFrames.push_back(frame.GetSurface()); + } + if (!anim.PCXFrames.empty()) + { + anim.TotalFrames = static_cast(anim.PCXFrames.size()); + anim.CurrentFrame = 0; + anim.Timer.Start(anim.FrameDelay); + this->backgroundAnims.push_back(anim); + continue; + } + } + } + } + + // Stage SHP animation (Anim) + sprintf_s(keyName, "Anim%d", i); + char animSHPDef[256] = { 0 }; + ini.ReadString(pStage, keyName, "", animSHPDef, sizeof(animSHPDef)); + if (animSHPDef[0]) + { + char shpName[64] = { 0 }; + char animPal[64] = { 0 }; + int ax = 0, ay = 0, arate = 5; + int parsed = sscanf_s(animSHPDef, "%63[^,],%d,%d,%d,%63s", shpName, (unsigned)_countof(shpName), &ax, &ay, &arate, animPal, (unsigned)_countof(animPal)); + if (parsed >= 3) + { + SHPStruct* pSHP = LoadMapSelSHP(shpName); + if (pSHP) + { + MapSelectAnim anim; + anim.SHP = pSHP; + anim.X = ax; + anim.Y = ay; + anim.FrameDelay = (arate > 0) ? arate : 5; + anim.CurrentFrame = 0; + anim.TotalFrames = pSHP->Frames; + anim.Timer.Start(anim.FrameDelay); + if (parsed >= 5 && animPal[0]) + { + anim.PaletteName = animPal; + } + this->backgroundAnims.push_back(anim); + } + } + } + } + + // Global PCX animations from [AnimsPCX] + if (ini.GetSection("AnimsPCX")) + { + for (int i = 1; i <= 32; ++i) + { + char keyName[16]; + sprintf_s(keyName, "%d", i); + char animPCXDef[512] = { 0 }; + ini.ReadString("AnimsPCX", keyName, "", animPCXDef, sizeof(animPCXDef)); + if (animPCXDef[0]) + { + char pcxFile[128] = { 0 }; + int ax = 0, ay = 0, arate = 5; + if (sscanf_s(animPCXDef, "%127[^,],%d,%d,%d", pcxFile, (unsigned)_countof(pcxFile), &ax, &ay, &arate) >= 3) + { + auto pFrames = GeneralUtils::GetAnimationPCX(pcxFile); + if (pFrames && !pFrames->empty()) + { + MapSelectAnim anim; + anim.X = ax; + anim.Y = ay; + anim.FrameDelay = (arate > 0) ? arate : 5; + for (const auto& frame : *pFrames) + { + if (frame.Exists()) + anim.PCXFrames.push_back(frame.GetSurface()); + } + if (!anim.PCXFrames.empty()) + { + anim.TotalFrames = static_cast(anim.PCXFrames.size()); + anim.CurrentFrame = 0; + anim.Timer.Start(anim.FrameDelay); + this->backgroundAnims.push_back(anim); + } + } + } + } + } + } + + // Global SHP animations from [Anims] + if (ini.GetSection("Anims")) + { + for (int i = 1; i <= 32; ++i) + { + char keyName[16]; + sprintf_s(keyName, "%d", i); + char animSHPDef[256] = { 0 }; + ini.ReadString("Anims", keyName, "", animSHPDef, sizeof(animSHPDef)); + if (animSHPDef[0]) + { + char shpName[64] = { 0 }; + char animPal[64] = { 0 }; + int ax = 0, ay = 0, arate = 5; + int parsed = sscanf_s(animSHPDef, "%63[^,],%d,%d,%d,%63s", shpName, (unsigned)_countof(shpName), &ax, &ay, &arate, animPal, (unsigned)_countof(animPal)); + if (parsed >= 3) + { + SHPStruct* pSHP = LoadMapSelSHP(shpName); + if (pSHP) + { + MapSelectAnim anim; + anim.SHP = pSHP; + anim.X = ax; + anim.Y = ay; + anim.FrameDelay = (arate > 0) ? arate : 5; + anim.CurrentFrame = 0; + anim.TotalFrames = pSHP->Frames; + anim.Timer.Start(anim.FrameDelay); + if (parsed >= 5 && animPal[0]) + { + anim.PaletteName = animPal; + } + this->backgroundAnims.push_back(anim); + } + } + } + } + } +} + +void MapSelectionClass::LoadAssets() +{ + DSurface* pSurface = DSurface::Hidden ? DSurface::Hidden : DSurface::Composite; + + // Palette for UI & Animations (default: MAPSEL.PAL) + const char* palName = !this->paletteFileName.empty() ? this->paletteFileName.c_str() : "mapsel.pal"; + this->pPalette = LoadMapSelPalette(palName, pSurface); + + // Palette for Overlays (default: MSOVRLY.PAL) + const char* ovrPalName = !this->overlayPaletteFileName.empty() ? this->overlayPaletteFileName.c_str() : "msovrly.pal"; + this->pOverlayPalette = LoadMapSelPalette(ovrPalName, pSurface); + if (!this->pOverlayPalette) + this->pOverlayPalette = this->pPalette; + + // Background PCX (MapPCX takes preference over Map SHP) + if (!this->backgroundPCXFileName.empty()) + { + PhobosPCXFile pcx(this->backgroundPCXFileName.c_str()); + if (pcx.Exists()) + this->pBackgroundPCX = pcx.GetSurface(); + } + + // Background SHP (Map) + if (!this->pBackgroundPCX && !this->backgroundFileName.empty()) + { + this->pBackgroundSHP = LoadMapSelSHP(this->backgroundFileName.c_str()); + } + + // Load custom palettes for animations if specified + for (auto& anim : this->backgroundAnims) + { + if (!anim.PaletteName.empty()) + { + anim.Palette = LoadMapSelPalette(anim.PaletteName.c_str(), pSurface); + } + } + + // If no background was loaded, try deriving a map PCX from clickmap name + if (!this->pBackgroundPCX && !this->pBackgroundSHP && !this->clickMapFileName.empty()) + { + std::string mapPCX = this->clickMapFileName; + std::transform(mapPCX.begin(), mapPCX.end(), mapPCX.begin(), ::toupper); + + // GDICLK01.PCX -> GDIMAP01.PCX, NODCLK01.PCX -> NODMAP01.PCX + size_t clkPos = mapPCX.find("CLK"); + if (clkPos != std::string::npos) + { + mapPCX.replace(clkPos, 3, "MAP"); + Debug::Log("[MapSelection] Trying derived background PCX '%s'\n", mapPCX.c_str()); + PhobosPCXFile pcx(mapPCX.c_str()); + if (pcx.Exists()) + this->pBackgroundPCX = pcx.GetSurface(); + } + + // Last resort: use the clickmap PCX itself as visual background + if (!this->pBackgroundPCX) + { + Debug::Log("[MapSelection] Using clickmap '%s' as visual background fallback\n", this->clickMapFileName.c_str()); + PhobosPCXFile pcx(this->clickMapFileName.c_str()); + if (pcx.Exists()) + this->pBackgroundPCX = pcx.GetSurface(); + } + } + + // ClickMap PCX (decoded for hit-testing) + if (!this->clickMapFileName.empty()) + { + LoadMapSelPCXData(this->clickMapFileName.c_str(), this->clickMapData); + } + if (this->clickMapData.Pixels.empty()) + { + LoadMapSelPCXData("GDICLK01.PCX", this->clickMapData); + } + + // Overlays (PCX takes preference over SHP) + if (!this->overlayPCXNames.empty()) + { + for (const auto& name : this->overlayPCXNames) + { + PhobosPCXFile pcx(name.c_str()); + if (pcx.Exists()) + { + this->overlayPCXs.push_back(pcx.GetSurface()); + } + } + } + if (this->overlayPCXs.empty() && !this->overlaySHPNames.empty()) + { + for (const auto& name : this->overlaySHPNames) + { + SHPStruct* pSHP = LoadMapSelSHP(name.c_str()); + if (pSHP) + { + this->overlaySHPs.push_back(pSHP); + } + } + } + + // Target Fly-In (PCX takes preference over SHP) + if (!this->targetFlyInPCXNames.empty()) + { + for (const auto& name : this->targetFlyInPCXNames) + { + auto pFrames = GeneralUtils::GetAnimationPCX(name); + if (pFrames && !pFrames->empty()) + { + for (const auto& frame : *pFrames) + { + if (frame.Exists()) + this->targetFlyInPCXSurfaces.push_back(frame.GetSurface()); + } + } + } + } + if (this->targetFlyInPCXSurfaces.empty()) + { + const char* flyInName = !this->targetFlyInFileName.empty() ? this->targetFlyInFileName.c_str() : "TARGET1.SHP"; + this->pTargetFlyInSHP = LoadMapSelSHP(flyInName); + } + + // Target Marker (PCX takes preference over SHP) + if (!this->targetMarkerPCXNames.empty()) + { + for (const auto& name : this->targetMarkerPCXNames) + { + auto pFrames = GeneralUtils::GetAnimationPCX(name); + if (pFrames && !pFrames->empty()) + { + for (const auto& frame : *pFrames) + { + if (frame.Exists()) + this->targetMarkerPCXSurfaces.push_back(frame.GetSurface()); + } + } + } + } + if (this->targetMarkerPCXSurfaces.empty()) + { + const char* markerName = !this->targetMarkerFileName.empty() ? this->targetMarkerFileName.c_str() : "TARGET2.SHP"; + this->pTargetMarkerSHP = LoadMapSelSHP(markerName); + } + + // Target crosshairs animation timer + this->targetAnimTimer.Start(4); +} + +void MapSelectionClass::CalculateLayout(DSurface* pSurface) +{ + if (!pSurface) + return; + + int bgWidth = 640; + int bgHeight = 400; + + if (this->pBackgroundPCX) + { + bgWidth = this->pBackgroundPCX->Width; + bgHeight = this->pBackgroundPCX->Height; + } + else if (this->pBackgroundSHP) + { + bgWidth = this->pBackgroundSHP->Width; + bgHeight = this->pBackgroundSHP->Height; + } + else if (this->clickMapData.Width > 0 && this->clickMapData.Height > 0) + { + bgWidth = this->clickMapData.Width; + bgHeight = this->clickMapData.Height; + } + + int screenWidth = pSurface->Width; + int screenHeight = pSurface->Height; + + this->windowRectangle.X = (screenWidth - bgWidth) / 2; + this->windowRectangle.Y = (screenHeight - bgHeight) / 2; + this->windowRectangle.Width = bgWidth; + this->windowRectangle.Height = bgHeight; + + Debug::Log("[MapSelection] Layout: Screen=(%dx%d), WindowRect=(%d,%d,%d,%d), TextRect=(%d,%d,%d,%d)\n", + screenWidth, screenHeight, + this->windowRectangle.X, this->windowRectangle.Y, this->windowRectangle.Width, this->windowRectangle.Height, + this->textRectangle.X, this->textRectangle.Y, this->textRectangle.Width, this->textRectangle.Height); +} + +int MapSelectionClass::GetChoiceIndexAtPoint(int screenX, int screenY) +{ + int relX = screenX - this->windowRectangle.X; + int relY = screenY - this->windowRectangle.Y; + + if (relX < 0 || relX >= this->windowRectangle.Width || relY < 0 || relY >= this->windowRectangle.Height) + return -1; + + // Clickmap pixel color index (exact OpenTS behavior) + if (!this->clickMapData.Pixels.empty()) + { + if (relX >= 0 && relX < this->clickMapData.Width && relY >= 0 && relY < this->clickMapData.Height) + { + BYTE pixelColor = this->clickMapData.Pixels[relY * this->clickMapData.Width + relX]; + // 0 and 255 are unselectable background/ocean pixels + if (pixelColor > 0 && pixelColor < 255) + { + for (size_t i = 0; i < this->choices.size(); ++i) + { + if (this->choices[i].Index == static_cast(pixelColor)) + return static_cast(i); + } + } + } + // Never select anything outside the designated mission color regions + return -1; + } + + return -1; +} + +void MapSelectionClass::UpdateAnimations() +{ + for (auto& anim : this->backgroundAnims) + { + if (anim.Timer.Completed() && anim.TotalFrames > 1) + { + anim.CurrentFrame = (anim.CurrentFrame + 1) % anim.TotalFrames; + anim.Timer.Start(anim.FrameDelay); + this->repaintAll = true; + } + } + + if (this->targetAnimTimer.Completed()) + { + if (!this->targetMarkerPCXSurfaces.empty()) + { + int count = static_cast(this->targetMarkerPCXSurfaces.size()); + if (count > 0) + { + this->idleTargetAnimFrame = (this->idleTargetAnimFrame + 1) % count; + this->activeTargetAnimFrame = (this->activeTargetAnimFrame + 1) % count; + } + } + else + { + this->idleTargetAnimFrame = (this->idleTargetAnimFrame + 1) % 32; + this->activeTargetAnimFrame = 32 + ((this->activeTargetAnimFrame - 32 + 1) % 32); + } + this->targetAnimTimer.Start(4); + this->repaintAll = true; + } + + // Briefing typewriter text advance + if (this->hoveredChoiceIdx >= 0 && this->typewriterTimer.Completed()) + { + size_t totalChars = 0; + for (const auto& l : this->briefingLines) + totalChars += l.length(); + + if (static_cast(this->typewriterCharCount) < totalChars) + { + this->typewriterCharCount += 1; + this->typewriterTimer.Start(1); + this->repaintAll = true; + + // Play typing sound through game DirectSound mixer (never cuts off VoiceOver) + if (this->typeSoundTimer.Completed()) + { + if (RulesClass::Instance && RulesClass::Instance->MessageCharTyped >= 0) + { + VocClass::PlayGlobal(RulesClass::Instance->MessageCharTyped, 0x2000, 0.4f); + } + else + { + int typeIdx = VocClass::FindIndex("Type"); + if (typeIdx >= 0) + { + VocClass::PlayGlobal(typeIdx, 0x2000, 0.4f); + } + } + this->typeSoundTimer.Start(3); + } + } + } +} + +void MapSelectionClass::Render(DSurface* pSurface) +{ + if (!pSurface) + return; + + // Fill background black for letterboxing + pSurface->Fill(0); + + ConvertClass* pDrawer = this->pPalette ? this->pPalette : FileSystem::PALETTE_PAL; + ConvertClass* pOverlayDrawer = this->pOverlayPalette ? this->pOverlayPalette : pDrawer; + + // Draw Background (PCX or SHP) + DrawElement(pSurface, this->windowRectangle, this->pBackgroundPCX, this->pBackgroundSHP, pDrawer, 0, -2); + + // Draw Overlays (World map / territory borders) + if (!this->overlayPCXs.empty()) + { + for (auto pPCX : this->overlayPCXs) + { + if (pPCX) + { + DrawElement(pSurface, this->windowRectangle, pPCX, nullptr, nullptr, 0, -2); + } + } + } + else + { + for (auto pOverlaySHP : this->overlaySHPs) + { + if (pOverlaySHP) + { + DrawElement(pSurface, this->windowRectangle, nullptr, pOverlaySHP, pOverlayDrawer, 0, -2); + } + } + } + + // Draw Animation SHPs or PCX sequences (Faction Logo, Rotating Globe, Compass) + for (const auto& anim : this->backgroundAnims) + { + if (!anim.PCXFrames.empty()) + { + int frame = (anim.CurrentFrame >= 0 && anim.CurrentFrame < static_cast(anim.PCXFrames.size())) ? anim.CurrentFrame : 0; + BSurface* pFramePCX = anim.PCXFrames[frame]; + if (pFramePCX) + { + RectangleStruct animRect = { + this->windowRectangle.X + anim.X, + this->windowRectangle.Y + anim.Y, + pFramePCX->Width, + pFramePCX->Height + }; + DrawElement(pSurface, animRect, pFramePCX, nullptr, nullptr, 0, -2); + } + } + else if (anim.SHP) + { + RectangleStruct animRect = { + this->windowRectangle.X + anim.X, + this->windowRectangle.Y + anim.Y, + anim.SHP->Width, + anim.SHP->Height + }; + ConvertClass* pAnimDrawer = anim.Palette ? anim.Palette : pDrawer; + DrawElement(pSurface, animRect, nullptr, anim.SHP, pAnimDrawer, anim.CurrentFrame, -2); + } + } + + // Draw target markers / highlights for choices + for (size_t i = 0; i < this->choices.size(); ++i) + { + const auto& choice = this->choices[i]; + if (choice.HasTargetCoord) + { + int drawX = this->windowRectangle.X + choice.TargetCoord.X; + int drawY = this->windowRectangle.Y + choice.TargetCoord.Y; + + if (!this->targetMarkerPCXSurfaces.empty()) + { + int frame = (static_cast(i) == this->hoveredChoiceIdx) ? this->activeTargetAnimFrame : this->idleTargetAnimFrame; + if (frame >= 0 && frame < static_cast(this->targetMarkerPCXSurfaces.size())) + { + BSurface* pPCX = this->targetMarkerPCXSurfaces[frame]; + if (pPCX) + { + int halfW = pPCX->Width / 2; + int halfH = pPCX->Height / 2; + RectangleStruct markerRect = { drawX - halfW, drawY - halfH, pPCX->Width, pPCX->Height }; + DrawElement(pSurface, markerRect, pPCX, nullptr, nullptr, 0, -2); + } + } + } + else if (this->pTargetMarkerSHP) + { + int halfW = this->pTargetMarkerSHP->Width / 2; + int halfH = this->pTargetMarkerSHP->Height / 2; + RectangleStruct markerRect = { drawX - halfW, drawY - halfH, this->pTargetMarkerSHP->Width, this->pTargetMarkerSHP->Height }; + int frame = (static_cast(i) == this->hoveredChoiceIdx) ? this->activeTargetAnimFrame : this->idleTargetAnimFrame; + DrawElement(pSurface, markerRect, nullptr, this->pTargetMarkerSHP, pDrawer, frame, -2); + } + else + { + COLORREF color = (static_cast(i) == this->hoveredChoiceIdx) + ? Drawing::RGB_To_Int(255, 255, 80) + : Drawing::RGB_To_Int(0, 255, 0); + + RectangleStruct markerRect = { drawX - 8, drawY - 8, 16, 16 }; + pSurface->DrawRect(&markerRect, color); + } + } + } + + // Draw briefing text box + this->DrawBriefing(pSurface); +} + +void MapSelectionClass::DrawBriefing(DSurface* pSurface) +{ + if (!pSurface || this->hoveredChoiceIdx < 0 || this->briefingLines.empty()) + return; + + const auto& choice = this->choices[this->hoveredChoiceIdx]; + + int drawX = this->windowRectangle.X + this->textRectangle.X; + int drawY = this->windowRectangle.Y + this->textRectangle.Y; + + // Text rendering on the metallic HUD plate (TextRect: 92, 322, 332, 78) + COLORREF textColor = Drawing::RGB_To_Int(choice.TextColor.R, choice.TextColor.G, choice.TextColor.B); + TextPrintType style = (TextPrintType::FullShadow | TextPrintType::Point6Grad); + + int remainingChars = this->typewriterCharCount; + int lineHeight = 13; + + for (size_t i = 0; i < this->briefingLines.size(); ++i) + { + if (remainingChars <= 0) + break; + + const std::wstring& line = this->briefingLines[i]; + std::wstring visiblePart; + + if (static_cast(line.length()) <= remainingChars) + { + visiblePart = line; + remainingChars -= static_cast(line.length()); + } + else + { + visiblePart = line.substr(0, remainingChars); + remainingChars = 0; + } + + if (!visiblePart.empty()) + { + RectangleStruct lineBounds = { + drawX, + drawY + static_cast(i) * lineHeight, + this->textRectangle.Width, + lineHeight + 4 + }; + Point2D relPoint = { 0, 0 }; + pSurface->DrawText(visiblePart.c_str(), &lineBounds, &relPoint, textColor, 0, style); + } + } +} + +void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) +{ + if (!pSurface) + return; + + ConvertClass* pDrawer = this->pPalette ? this->pPalette : FileSystem::PALETTE_PAL; + ConvertClass* pOverlayDrawer = this->pOverlayPalette ? this->pOverlayPalette : pDrawer; + + auto pumpAndCheckSkip = [&]() -> bool + { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + if (msg.message == WM_QUIT) + return true; + TranslateMessage(&msg); + DispatchMessage(&msg); + } + Game::CallBack(); + HWND hGameWnd = Game::hWnd; + HWND hActiveWnd = GetForegroundWindow(); + bool isWindowActive = (hGameWnd && (hActiveWnd == hGameWnd || IsChild(hGameWnd, hActiveWnd))); + if (isWindowActive && ((GetAsyncKeyState(VK_LBUTTON) & 0x8000) || (GetAsyncKeyState(VK_ESCAPE) & 0x8000))) + { + return true; + } + return false; + }; + + auto renderBase = [&](int completedOverlays, int completedTargets) + { + pSurface->Fill(0); + + // Background + DrawElement(pSurface, this->windowRectangle, this->pBackgroundPCX, this->pBackgroundSHP, pDrawer, 0, -2); + + // Completed Overlays (drawn solid) + if (!this->overlayPCXs.empty()) + { + for (int o = 0; o < completedOverlays && o < static_cast(this->overlayPCXs.size()); ++o) + { + if (this->overlayPCXs[o]) + { + DrawElement(pSurface, this->windowRectangle, this->overlayPCXs[o], nullptr, nullptr, 0, -2); + } + } + } + else + { + for (int o = 0; o < completedOverlays && o < static_cast(this->overlaySHPs.size()); ++o) + { + if (this->overlaySHPs[o]) + { + DrawElement(pSurface, this->windowRectangle, nullptr, this->overlaySHPs[o], pOverlayDrawer, 0, -2); + } + } + } + + // Background Anims + for (const auto& anim : this->backgroundAnims) + { + if (!anim.PCXFrames.empty()) + { + int frame = (anim.CurrentFrame >= 0 && anim.CurrentFrame < static_cast(anim.PCXFrames.size())) ? anim.CurrentFrame : 0; + BSurface* pFramePCX = anim.PCXFrames[frame]; + if (pFramePCX) + { + RectangleStruct animRect = { + this->windowRectangle.X + anim.X, + this->windowRectangle.Y + anim.Y, + pFramePCX->Width, + pFramePCX->Height + }; + DrawElement(pSurface, animRect, pFramePCX, nullptr, nullptr, 0, -2); + } + } + else if (anim.SHP) + { + RectangleStruct animRect = { + this->windowRectangle.X + anim.X, + this->windowRectangle.Y + anim.Y, + anim.SHP->Width, + anim.SHP->Height + }; + ConvertClass* pAnimDrawer = anim.Palette ? anim.Palette : pDrawer; + DrawElement(pSurface, animRect, nullptr, anim.SHP, pAnimDrawer, anim.CurrentFrame, -2); + } + } + + // Completed Targets + for (int t = 0; t < completedTargets && t < static_cast(this->choices.size()); ++t) + { + const auto& choice = this->choices[t]; + if (choice.HasTargetCoord) + { + int drawX = this->windowRectangle.X + choice.TargetCoord.X; + int drawY = this->windowRectangle.Y + choice.TargetCoord.Y; + + if (!this->targetMarkerPCXSurfaces.empty()) + { + if (this->idleTargetAnimFrame >= 0 && this->idleTargetAnimFrame < static_cast(this->targetMarkerPCXSurfaces.size())) + { + BSurface* pPCX = this->targetMarkerPCXSurfaces[this->idleTargetAnimFrame]; + if (pPCX) + { + int halfW = pPCX->Width / 2; + int halfH = pPCX->Height / 2; + RectangleStruct markerRect = { drawX - halfW, drawY - halfH, pPCX->Width, pPCX->Height }; + DrawElement(pSurface, markerRect, pPCX, nullptr, nullptr, 0, -2); + } + } + } + else if (this->pTargetMarkerSHP) + { + int halfW = this->pTargetMarkerSHP->Width / 2; + int halfH = this->pTargetMarkerSHP->Height / 2; + RectangleStruct markerRect = { drawX - halfW, drawY - halfH, this->pTargetMarkerSHP->Width, this->pTargetMarkerSHP->Height }; + DrawElement(pSurface, markerRect, nullptr, this->pTargetMarkerSHP, pDrawer, this->idleTargetAnimFrame, -2); + } + } + } + }; + + const BlitterFlags fadeFlags[] = { + BlitterFlags::TransLucent75, + BlitterFlags::TransLucent50, + BlitterFlags::TransLucent25, + BlitterFlags::None + }; + + size_t totalOverlays = !this->overlayPCXs.empty() ? this->overlayPCXs.size() : this->overlaySHPs.size(); + + // Animate Overlays fade-in + for (size_t o = 0; o < totalOverlays; ++o) + { + if (!this->overlaySound.empty()) + { + PlayMapSelAudio(this->overlaySound.c_str()); + } + + for (int stage = 0; stage < 4; ++stage) + { + if (pumpAndCheckSkip()) + return; + + this->UpdateAnimations(); + renderBase(static_cast(o), 0); + + if (!this->overlayPCXs.empty()) + { + if (this->overlayPCXs[o]) + { + DrawElement(pSurface, this->windowRectangle, this->overlayPCXs[o], nullptr, nullptr, 0, -2); + } + } + else if (o < this->overlaySHPs.size() && this->overlaySHPs[o]) + { + DrawElement(pSurface, this->windowRectangle, nullptr, this->overlaySHPs[o], pOverlayDrawer, 0, -2, fadeFlags[stage]); + } + + GScreenClass::Instance.DoBlit(true, pSurface, nullptr); + Sleep(50); + } + + // Small delay between overlays + for (int d = 0; d < 5; ++d) + { + if (pumpAndCheckSkip()) + return; + this->UpdateAnimations(); + renderBase(static_cast(o + 1), 0); + GScreenClass::Instance.DoBlit(true, pSurface, nullptr); + Sleep(40); + } + } + + // Animate Target Fly-Ins + for (size_t t = 0; t < this->choices.size(); ++t) + { + const auto& choice = this->choices[t]; + if (!choice.HasTargetCoord) + continue; + + if (!this->targetFlyInSound.empty()) + { + PlayMapSelAudio(this->targetFlyInSound.c_str()); + } + + int drawX = this->windowRectangle.X + choice.TargetCoord.X; + int drawY = this->windowRectangle.Y + choice.TargetCoord.Y; + + if (!this->targetFlyInPCXSurfaces.empty()) + { + int maxFrames = static_cast(this->targetFlyInPCXSurfaces.size()); + for (int f = 0; f < maxFrames; ++f) + { + if (pumpAndCheckSkip()) + return; + + this->UpdateAnimations(); + renderBase(static_cast(totalOverlays), static_cast(t)); + + BSurface* pFlyInPCX = this->targetFlyInPCXSurfaces[f]; + if (pFlyInPCX) + { + int halfW = pFlyInPCX->Width / 2; + int halfH = pFlyInPCX->Height / 2; + RectangleStruct flyInRect = { drawX - halfW, drawY - halfH, pFlyInPCX->Width, pFlyInPCX->Height }; + DrawElement(pSurface, flyInRect, pFlyInPCX, nullptr, nullptr, 0, -2); + } + + GScreenClass::Instance.DoBlit(true, pSurface, nullptr); + Sleep(35); + } + } + else if (this->pTargetFlyInSHP && this->pTargetFlyInSHP->Frames > 0) + { + int halfW = this->pTargetFlyInSHP->Width / 2; + int halfH = this->pTargetFlyInSHP->Height / 2; + RectangleStruct flyInRect = { drawX - halfW, drawY - halfH, this->pTargetFlyInSHP->Width, this->pTargetFlyInSHP->Height }; + + int maxFrames = this->pTargetFlyInSHP->Frames; + for (int f = 0; f < maxFrames; ++f) + { + if (pumpAndCheckSkip()) + return; + + this->UpdateAnimations(); + renderBase(static_cast(totalOverlays), static_cast(t)); + + // Draw fly-in frame + BlitterFlags flag = (f < 4) ? fadeFlags[f] : BlitterFlags::None; + DrawElement(pSurface, flyInRect, nullptr, this->pTargetFlyInSHP, pDrawer, f, -2, flag); + + GScreenClass::Instance.DoBlit(true, pSurface, nullptr); + Sleep(35); + } + } + + // Small delay after target settles + for (int d = 0; d < 3; ++d) + { + if (pumpAndCheckSkip()) + return; + this->UpdateAnimations(); + renderBase(static_cast(totalOverlays), static_cast(t + 1)); + GScreenClass::Instance.DoBlit(true, pSurface, nullptr); + Sleep(35); + } + } +} + +bool MapSelectionClass::Run() +{ + DSurface* pSurface = DSurface::Hidden; + if (!pSurface) + pSurface = DSurface::Composite; + if (!pSurface) + pSurface = DSurface::Primary; + if (!pSurface) + return false; + + // Play Bink video if MapVQ is specified and exists + if (!this->mapVQFileName.empty()) + { + PlayMapSelBinkVideo(this->mapVQFileName.c_str()); + + // Drain any leftover input (e.g. ESC or mouse clicks used to skip video) + MSG drainMsg; + while (PeekMessage(&drainMsg, NULL, 0, 0, PM_REMOVE)) + { + TranslateMessage(&drainMsg); + DispatchMessage(&drainMsg); + } + while ((GetAsyncKeyState(VK_LBUTTON) & 0x8000) || (GetAsyncKeyState(VK_ESCAPE) & 0x8000) || (GetAsyncKeyState(VK_SPACE) & 0x8000)) + { + Sleep(10); + } + } + + // Play stage VoiceOver audio if present + if (!this->voiceOverFileName.empty()) + { + PlayMapSelAudio(this->voiceOverFileName.c_str()); + } + + pSurface->Fill(0); + this->CalculateLayout(pSurface); + + // Play animated intro sequence (Overlays sweep fade-in and TARGET1 fly-ins) + this->PlayIntroSequence(pSurface); + + // Play Map Selection Theme Song (only if Theme= is explicitly specified) + if (!this->themeName.empty()) + { + int themeIdx = ThemeClass::Instance.FindIndex(this->themeName.c_str()); + if (themeIdx >= 0) + { + ThemeClass::Instance.Play(themeIdx); + } + } + + // Drain any input after intro sequence + { + MSG drainMsg; + while (PeekMessage(&drainMsg, NULL, 0, 0, PM_REMOVE)) + { + TranslateMessage(&drainMsg); + DispatchMessage(&drainMsg); + } + while ((GetAsyncKeyState(VK_LBUTTON) & 0x8000) || (GetAsyncKeyState(VK_ESCAPE) & 0x8000)) + { + Sleep(10); + } + } + + bool wasLButtonDown = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; + bool wasMouseOnMap = false; + + this->hoveredChoiceIdx = -1; + this->lastHoveredChoiceIdx = -1; + this->selectedChoiceIdx = -1; + + Debug::Log("[MapSelection] Entering modal Run loop (choices count: %zu)\n", this->choices.size()); + + while (this->selectedChoiceIdx < 0) + { + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + if (msg.message == WM_QUIT) + return false; + + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + Game::CallBack(); + + POINT mousePos = { 0, 0 }; + GetCursorPos(&mousePos); + HWND hGameWnd = Game::hWnd; + if (hGameWnd) + { + ScreenToClient(hGameWnd, &mousePos); + + RECT clientRect; + if (GetClientRect(hGameWnd, &clientRect)) + { + int clientW = clientRect.right - clientRect.left; + int clientH = clientRect.bottom - clientRect.top; + if (clientW > 0 && clientH > 0 && pSurface->Width > 0 && pSurface->Height > 0) + { + if (clientW != pSurface->Width || clientH != pSurface->Height) + { + mousePos.x = static_cast(mousePos.x * (static_cast(pSurface->Width) / clientW)); + mousePos.y = static_cast(mousePos.y * (static_cast(pSurface->Height) / clientH)); + } + } + } + } + + bool isMouseOnMap = (mousePos.x >= this->windowRectangle.X && mousePos.x < this->windowRectangle.X + this->windowRectangle.Width && + mousePos.y >= this->windowRectangle.Y && mousePos.y < this->windowRectangle.Y + this->windowRectangle.Height); + + if (isMouseOnMap != wasMouseOnMap) + { + if (isMouseOnMap && !this->mouseOnMapSound.empty()) + { + PlayMapSelAudio(this->mouseOnMapSound.c_str()); + } + else if (!isMouseOnMap && !this->mouseOffMapSound.empty()) + { + PlayMapSelAudio(this->mouseOffMapSound.c_str()); + } + wasMouseOnMap = isMouseOnMap; + } + + this->hoveredChoiceIdx = isMouseOnMap ? this->GetChoiceIndexAtPoint(mousePos.x, mousePos.y) : -1; + + if (this->hoveredChoiceIdx != this->lastHoveredChoiceIdx) + { + // Stop previous VoiceOver if moving off or switching regions + PlaySoundA(NULL, NULL, 0); + VoxClass::DeleteAll(); + + this->typewriterCharCount = 0; + this->typewriterTimer.Start(1); + this->typeSoundTimer.Start(3); + + if (this->hoveredChoiceIdx >= 0 && this->hoveredChoiceIdx < static_cast(this->choices.size())) + { + const auto& choice = this->choices[this->hoveredChoiceIdx]; + std::wstring fullText = ResolveCSFOrText(choice.Description); + if (fullText.empty()) + fullText = ResolveCSFOrText(choice.Summary); + if (fullText.empty()) + fullText = std::wstring(choice.StageName.begin(), choice.StageName.end()); + + this->briefingLines = WordWrapText(fullText, 46); + + if (!choice.VoiceOver.empty()) + { + PlayMapSelAudio(choice.VoiceOver.c_str()); + } + else if (!choice.HoverSound.empty()) + { + PlayMapSelAudio(choice.HoverSound.c_str()); + } + else if (!this->enterRegionSound.empty()) + { + PlayMapSelAudio(this->enterRegionSound.c_str()); + } + } + else + { + this->briefingLines.clear(); + if (this->lastHoveredChoiceIdx >= 0 && !this->exitRegionSound.empty()) + { + PlayMapSelAudio(this->exitRegionSound.c_str()); + } + } + + this->lastHoveredChoiceIdx = this->hoveredChoiceIdx; + this->repaintAll = true; + } + + bool isLButtonDown = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; + if (isLButtonDown && !wasLButtonDown) + { + // Only select if the mouse is hovering over a valid choice on the map + if (this->hoveredChoiceIdx >= 0 && this->hoveredChoiceIdx < static_cast(this->choices.size())) + { + int clickedChoice = this->GetChoiceIndexAtPoint(mousePos.x, mousePos.y); + if (clickedChoice == this->hoveredChoiceIdx) + { + this->selectedChoiceIdx = clickedChoice; + Debug::Log("[MapSelection] Region clicked on hovered choice %d ('%s')\n", + this->selectedChoiceIdx, this->choices[this->selectedChoiceIdx].StageName.c_str()); + + // Immediately cut off VoiceOver and any background Windows speech + PlaySoundA(NULL, NULL, 0); + VoxClass::DeleteAll(); + + const auto& chosen = this->choices[this->selectedChoiceIdx]; + if (!chosen.ClickSound.empty()) + { + PlayMapSelAudio(chosen.ClickSound.c_str()); + } + else if (!this->clickRegionSound.empty()) + { + PlayMapSelAudio(this->clickRegionSound.c_str()); + } + break; + } + } + } + wasLButtonDown = isLButtonDown; + + this->UpdateAnimations(); + + this->Render(pSurface); + + MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false); + GScreenClass::Instance.DoBlit(true, pSurface, nullptr); + + Sleep(1); + } + + // Ensure any playing VoiceOver audio and custom theme music are stopped when loading begins + PlaySoundA(NULL, NULL, 0); + VoxClass::DeleteAll(); + if (!this->themeName.empty()) + { + ThemeClass::Instance.Stop(true); + } + + if (this->selectedChoiceIdx >= 0 && this->selectedChoiceIdx < static_cast(this->choices.size())) + { + const auto& choice = this->choices[this->selectedChoiceIdx]; + Debug::Log("[MapSelection] Final selected mission: '%s' (Scenario: '%s')\n", choice.StageName.c_str(), choice.ScenarioPath.c_str()); + + if (this->pCurrentScenario && !choice.ScenarioPath.empty()) + { + this->pCurrentScenario->Stage = 0; + strncpy(this->pCurrentScenario->FileName, choice.ScenarioPath.c_str(), 0x104u); + this->pCurrentScenario->FileName[259] = '\0'; + strncpy(this->pCurrentScenario->NextScenario, choice.ScenarioPath.c_str(), 0x104u); + this->pCurrentScenario->NextScenario[259] = '\0'; + } + return true; + } + + return false; +} diff --git a/src/Misc/MapSelectionClass.h b/src/Misc/MapSelectionClass.h new file mode 100644 index 0000000000..7f7a31269e --- /dev/null +++ b/src/Misc/MapSelectionClass.h @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include +#include +#include + +struct SHPStruct; +class BSurface; +class DSurface; +class ConvertClass; +class ScenarioClass; + +struct MapSelectChoice +{ + int Index { 0 }; + std::string StageName {}; + std::string ScenarioPath {}; + std::string Description {}; + std::string Summary {}; + std::string VoiceOver {}; + std::string HoverSound {}; + std::string ClickSound {}; + Point2D TargetCoord { 0, 0 }; + bool HasTargetCoord { false }; + ColorStruct TextColor { 255, 239, 99 }; + bool HasCustomTextColor { false }; +}; + +struct MapSelectAnim +{ + SHPStruct* SHP { nullptr }; + std::vector PCXFrames; + std::string PaletteName {}; + ConvertClass* Palette { nullptr }; + int X { 0 }; + int Y { 0 }; + int FrameDelay { 5 }; + int CurrentFrame { 0 }; + int TotalFrames { 0 }; + SysTimerClass Timer {}; +}; + +struct MapSelectPCX +{ + int Width { 0 }; + int Height { 0 }; + std::vector Pixels; +}; + +class MapSelectionClass +{ +public: + MapSelectionClass(); + ~MapSelectionClass(); + + static bool OpenMapSelectionWindow(ScenarioClass* pScenario); + + bool Initialize(ScenarioClass* pScenario); + bool Run(); + +private: + void LoadConfig(ScenarioClass* pScenario); + void LoadAssets(); + void CalculateLayout(DSurface* pSurface); + int GetChoiceIndexAtPoint(int screenX, int screenY); + void UpdateAnimations(); + void Render(DSurface* pSurface); + void DrawBriefing(DSurface* pSurface); + void PlayIntroSequence(DSurface* pSurface); + + ScenarioClass* pCurrentScenario { nullptr }; + std::string currentStageName {}; + std::string selectedScenarioPath {}; + + // Layout & resolution + RectangleStruct windowRectangle { 0, 0, 800, 600 }; + RectangleStruct textRectangle { 92, 322, 332, 78 }; + + // Background & Visual Assets + std::string backgroundFileName {}; + std::string backgroundPCXFileName {}; + std::string clickMapFileName {}; + std::string paletteFileName { "mapsel.pal" }; + std::string overlayPaletteFileName { "msovrly.pal" }; + ConvertClass* pPalette { nullptr }; + ConvertClass* pOverlayPalette { nullptr }; + BSurface* pBackgroundPCX { nullptr }; + SHPStruct* pBackgroundSHP { nullptr }; + + std::string targetMarkerFileName {}; + std::string targetFlyInFileName {}; + std::vector targetMarkerPCXNames; + std::vector targetFlyInPCXNames; + SHPStruct* pTargetMarkerSHP { nullptr }; + SHPStruct* pTargetFlyInSHP { nullptr }; + std::vector targetMarkerPCXSurfaces; + std::vector targetFlyInPCXSurfaces; + + int idleTargetAnimFrame { 0 }; + int activeTargetAnimFrame { 32 }; + SysTimerClass targetAnimTimer {}; + MapSelectPCX clickMapData {}; + + std::vector overlaySHPNames; + std::vector overlayPCXNames; + std::vector overlaySHPs; + std::vector overlayPCXs; + + std::vector backgroundAnims; + + // Sound & Audio & Video + std::string mapVQFileName {}; + std::string voiceOverFileName {}; + std::string overlaySound {}; + std::string targetFlyInSound {}; + std::string enterRegionSound {}; + std::string exitRegionSound {}; + std::string clickRegionSound {}; + std::string mouseOnMapSound {}; + std::string mouseOffMapSound {}; + std::string themeName {}; + + // Choices / Progression + std::vector choices; + int hoveredChoiceIdx { -1 }; + int selectedChoiceIdx { -1 }; + int lastHoveredChoiceIdx { -2 }; + + // Briefing typewriter animation + int typewriterCharCount { 0 }; + SysTimerClass typewriterTimer {}; + SysTimerClass typeSoundTimer {}; + std::vector briefingLines; + + ColorStruct defaultTextColor { 255, 239, 99 }; + + // State + bool repaintAll { true }; + bool isInitialized { false }; +}; diff --git a/src/Utilities/GeneralUtils.cpp b/src/Utilities/GeneralUtils.cpp index 7e7d79ba0d..09728d0dc4 100644 --- a/src/Utilities/GeneralUtils.cpp +++ b/src/Utilities/GeneralUtils.cpp @@ -329,3 +329,76 @@ int GeneralUtils::SafeMultiply(int value, double mult) return static_cast(product); } + +std::unique_ptr> GeneralUtils::GetAnimationPCX(const std::string& baseFilename) +{ + auto animationFrames = std::make_unique>(); + + std::string filenameBase = baseFilename; + std::string extension = ".PCX"; + + // Find the position of the last dot to separate the extension + size_t lastDot = baseFilename.find_last_of('.'); + if (lastDot != std::string::npos) + { + filenameBase = baseFilename.substr(0, lastDot); + extension = baseFilename.substr(lastDot); + } + + // Check if the part before the extension was a frame number and remove it if so + if (filenameBase.length() > 5 && filenameBase[filenameBase.length() - 5] == ' ') + { + std::string frameNumberStr = filenameBase.substr(filenameBase.length() - 4); + bool isNumeric = true; + for (char c : frameNumberStr) + { + if (!isdigit(c)) + { + isNumeric = false; + break; + } + } + if (isNumeric) + { + filenameBase = filenameBase.substr(0, filenameBase.length() - 5); + } + } + + // Try loading frame 0 as " 0000." first + char frame0Filename[256]; + _snprintf_s(frame0Filename, sizeof(frame0Filename), "%s 0000%s", filenameBase.c_str(), extension.c_str()); + PhobosPCXFile frame0(frame0Filename); + if (frame0.Exists()) + { + animationFrames->emplace_back(std::move(frame0)); + } + else + { + // If " 0000." doesn't exist, try loading the exact filename as provided (e.g. "TARGET1.PCX") + PhobosPCXFile exactFile(baseFilename.c_str()); + if (exactFile.Exists()) + { + animationFrames->emplace_back(std::move(exactFile)); + } + else + { + return animationFrames; + } + } + + // Loop to find and load the subsequent frames, starting from frame 1 + for (int i = 1; i < 10000; ++i) + { + char currentFilename[256]; + _snprintf_s(currentFilename, sizeof(currentFilename), "%s %04d%s", filenameBase.c_str(), i, extension.c_str()); + + PhobosPCXFile filePCX(currentFilename); + if (filePCX.Exists()) + animationFrames->emplace_back(std::move(filePCX)); + else + break; + } + + return animationFrames; +} + diff --git a/src/Utilities/GeneralUtils.h b/src/Utilities/GeneralUtils.h index b86a4ec21b..f53e994f0a 100644 --- a/src/Utilities/GeneralUtils.h +++ b/src/Utilities/GeneralUtils.h @@ -43,6 +43,7 @@ class GeneralUtils static int SafeMultiply(int value, int mult); static int SafeMultiply(int value, double mult); static DynamicVectorClass* BuildPalette(const char* paletteFileName); + static std::unique_ptr> GetAnimationPCX(const std::string& baseFilename); template static constexpr T FastPow(T x, size_t n) From c66c835f342ca2ac7a26a239179f75f84ebf530d Mon Sep 17 00:00:00 2001 From: FS-21 Date: Wed, 2 Sep 2026 23:40:58 +0200 Subject: [PATCH 2/5] Enhance MapSelection: native cursor handling, target anim tags and doc updates - Adopt WWMouseClass reactivation and native cursor pattern from Dropship Loadout - Rename target graphics tags to TargetFlyInAnim/TargetFlyInAnimPCX and TargetMarkerAnim/TargetMarkerAnimPCX, freeing TargetFlyIn strictly for audio - Dynamically split TargetMarkerAnim frames into idle and hover halves based on file frame count - Remove obsolete battlemd.ini and missionmd.ini reads from mission selection - Prevent double free in pOverlayPalette and clean up custom animation palettes - Update User-Interface documentation with target animation details and sounds --- docs/User-Interface.md | 93 ++- src/Misc/MapSelectionClass.cpp | 996 +++++++++++++++++++++++++-------- src/Misc/MapSelectionClass.h | 1 + 3 files changed, 831 insertions(+), 259 deletions(-) diff --git a/docs/User-Interface.md b/docs/User-Interface.md index addbc5e590..de42e4706a 100644 --- a/docs/User-Interface.md +++ b/docs/User-Interface.md @@ -668,24 +668,32 @@ DisableEmptySpawnPositions=false ; boolean - `Overlays` specifies a comma-separated list of territory overlay SHP files that sweep into the map after the intro video. - `OverlaysPCX` specifies a comma-separated list of PCX overlay files (taking precedence over `Overlays`). - `Targets` defines the count and (X,Y) coordinates of animated mission target crosshairs on the map. - - `TargetFlyIn` specifies the target zoom-in SHP animation (defaults to `TARGET1.SHP`). - - `TargetFlyInPCX` specifies a base PCX filename for the target zoom-in animation (declaring `TARGET1.PCX` or `TARGET1 0000.PCX` automatically loads the sequence of frame files `TARGET1 0000.PCX`, `TARGET1 0001.PCX`, etc., taking precedence over `TargetFlyIn`). - - `TargetMarker` specifies the target crosshair marker SHP animation (defaults to `TARGET2.SHP`). - - `TargetMarkerPCX` specifies a base PCX filename for the target crosshair marker animation (declaring `TARGET2.PCX` or `TARGET2 0000.PCX` automatically loads the sequence of frame files `TARGET2 0000.PCX`, `TARGET2 0001.PCX`, etc., taking precedence over `TargetMarker`). + - `TargetFlyInAnim` specifies the target zoom-in SHP animation (defaults to `TARGET1.SHP`). + - `TargetFlyInAnimPCX` specifies a base PCX filename for the target zoom-in animation (declaring `TARGET1.PCX` or `TARGET1 0000.PCX` automatically loads the sequence of frame files `TARGET1 0000.PCX`, `TARGET1 0001.PCX`, etc., taking precedence over `TargetFlyInAnim`). + - `TargetMarkerAnim` specifies the target crosshair marker SHP animation (defaults to `TARGET2.SHP`). The first half of the file's total frames are looped while the target is in idle state, and the second half of the frames are looped while the cursor hovers over the corresponding region. + - `TargetMarkerAnimPCX` specifies a base PCX filename for the target crosshair marker animation (declaring `TARGET2.PCX` or `TARGET2 0000.PCX` automatically loads the sequence of frame files `TARGET2 0000.PCX`, `TARGET2 0001.PCX`, etc., taking precedence over `TargetMarkerAnim`). Just like the SHP version, the first half of frames are looped in idle and the second half in hover. - `Anim` defines stage-specific decorative SHP animations (`,,,[,]`) on the map interface (logos, radar sweeps, spinning globes, beacons). An optional custom palette filename can be provided (defaulting to `Palette`). - - `AnimPCX` defines stage-specific decorative multi-frame PCX animations (`,,,`). Declaring `` (or ``) automatically loads the sequence of frame files (`base 0000.PCX`, `base 0001.PCX`, etc.) and takes precedence over `Anim`. - `[Anims]` defines global SHP animations shared across all stages (`=,,,[,]`). An optional custom palette filename can be provided (defaulting to `Palette`). + - `[Anims]` / `[Anims]` defines side-specific global SHP animations (which can also be referenced via `Anims=` under `[SOMESIDE]`, or declared directly as `Anim` inside `[SOMESIDE]`). - `[AnimsPCX]` defines global multi-frame PCX animations shared across all stages (`=,,,`). + - `[AnimsPCX]` / `[AnimsPCX]` defines side-specific global multi-frame PCX animations (which can also be referenced via `AnimsPCX=` under `[SOMESIDE]`, or declared directly as `AnimPCX` inside `[SOMESIDE]`). - `ClickMap` specifies an 8-bit indexed 256-color PCX mask image. Color indices `0` and `255` represent non-clickable background areas (oceans, borders, or transparent zones), while palette color indices `1` through `254` represent clickable territory zones (referenced by `=`). - `=` binds a palette color index from the `ClickMap` PCX to a mission choice section (e.g. `3=NODB03` and `4=NODA03`). When the player hovers over pixels painted with palette color index `3`, the game highlights and displays the briefing for `[NODB03]`, and launches its scenario upon clicking. Up to 254 unique clickable territory zones per screen are supported. - - `VoiceOver` defines the spoken audio brief played when hovering over a mission (or opening the stage). - - `HoverSound` defines an optional SFX sound when hovering over a mission choice. - - `ClickSound` defines the sound played when confirming a mission selection. - - `OverlaySound` defines the sound played during the territory overlay sweep. - - `TargetSound` defines the sound played during target crosshair zoom-in animation. + - `Sounds` specifies an external sound configuration section (e.g. `Sounds=GDISFX` or `Sounds=NODSFX`). This allows centralizing and sharing a common sound palette across multiple stages or sides instead of repeating audio tags in every stage section. Individual stages can still override any sound locally. + - `[Sounds]` defines optional global sound effects shared across all sides. + - `[Sounds]` / `[Sounds]` defines optional side-specific global sound effects (e.g. `[Sounds0]` or `[SoundsGDI]`). + - All sound tags support an optional volume percentage suffix: `[,]` (e.g. `BESTBOX.WAV,75` or `EFFICIEN.WAV,40`). Volume defaults to 100% if omitted. + - Sound tags accept either a sound entry ID from `soundmd.ini` or a direct WAV filename (from disk or `.MIX` archives). Specifying an explicit `.wav` extension prioritizes searching for the standalone audio file, whereas omitting the extension prioritizes looking up the sound ID in `soundmd.ini` (falling back to `.wav` if not found). + - `VoiceOver` defines the spoken audio brief played when hovering over a mission choice (supports WAV filename or sound ID, with optional volume). + - `EnterRegion` defines the sound played when entering/hovering over a territory. + - `ClickRegion` defines the sound played when confirming a mission selection. + - `Overlay` defines the sound played during the territory overlay sweep. + - `TargetFlyIn` defines the sound played during target crosshair zoom-in animation. + - `ExitRegion` defines the sound played when cursor exits a territory. - `MouseOnMapSound` / `MouseOffMapSound` defines sounds played when cursor enters or leaves the map window. - - `Theme` specifies an optional soundtrack theme played during map selection. - - `TextColor` specifies the text color (`R,G,B`) for the mission briefing (defaults to the active player's house color). + - `Theme` specifies an optional soundtrack theme played during map selection (silent by default unless specified). + - `TypeSound` specifies the typing sound effect from `soundmd.ini` (e.g. `TypeSound=TextBleep` or `TypeSound=none` to disable; defaults to `TextBleep`). + - `TextColor` specifies the text color (`R,G,B`) for the mission briefing (defaults to the active side's `MessageTextColor` in `rulesmd.ini`, or the country's `Color` scheme, falling back to white). - `TextRect` specifies an optional custom bounding rectangle (`,,,`) for the mission briefing text, relative to the map window (defaults to `92,322,332,78` matching Tiberian Sun's HUD plate). - `Description` specifies the CSF string key or text for the mission briefing, displayed in the metallic briefing panel with automatic multi-line word wrapping and typewriter animation. @@ -696,6 +704,28 @@ In `mapselmd.ini`: [SOMESIDE] ; Side = ; Stage +Sounds= ; Section name +Anims= ; Section name +AnimsPCX= ; Section name +Anim= ; Animation SHP (file.shp,x,y,delay[,palette.pal]) +AnimPCX= ; Animation PCX (base.pcx,x,y,delay) + +[Sounds] ; Optional global sound effects for all sides +TargetFlyIn= ; Sound ID or WAV filename[,volume] +Overlay= ; Sound ID or WAV filename[,volume] +EnterRegion= ; Sound ID or WAV filename[,volume] +ExitRegion= ; Sound ID or WAV filename[,volume] +ClickRegion= ; Sound ID or WAV filename[,volume] +MouseOnMapSound= ; Sound ID or WAV filename[,volume] +MouseOffMapSound= ; Sound ID or WAV filename[,volume] +TypeSound= ; Sound ID or WAV filename[,volume] + +[Sounds] ; Optional global sound effects for a specific side +TargetFlyIn= ; Sound ID or WAV filename[,volume] +Overlay= ; Sound ID or WAV filename[,volume] +EnterRegion= ; Sound ID or WAV filename[,volume] +ExitRegion= ; Sound ID or WAV filename[,volume] +ClickRegion= ; Sound ID or WAV filename[,volume] [SOMESTAGE] ; Stage Scenario= ; Scenario filename @@ -708,31 +738,38 @@ MapVQ= ; video filename Overlays= ; list of SHP filenames OverlaysPCX= ; list of PCX filenames Targets= ; integer, point list -TargetFlyIn=TARGET1.SHP ; SHP filename -TargetFlyInPCX= ; PCX filename -TargetMarker=TARGET2.SHP ; SHP filename -TargetMarkerPCX= ; PCX filename +TargetFlyInAnim=TARGET1.SHP ; SHP filename +TargetFlyInAnimPCX= ; PCX filename +TargetMarkerAnim=TARGET2.SHP ; SHP filename +TargetMarkerAnimPCX= ; PCX filename ClickMap= ; PCX filename Theme= ; Soundtrack theme ID -VoiceOver= ; WAV sound filename -OverlaySound= ; WAV sound filename -TargetSound= ; WAV sound filename -HoverSound= ; WAV sound filename -ClickSound= ; WAV sound filename -EnterRegionSound= ; WAV sound filename -ExitRegionSound= ; WAV sound filename -MouseOnMapSound= ; WAV sound filename -MouseOffMapSound= ; WAV sound filename -TextColor= ; ColorStruct (R,G,B) +Sounds= ; Section name +VoiceOver= ; Sound ID or WAV filename[,volume] +Overlay= ; Sound ID or WAV filename[,volume] +TargetFlyIn= ; Sound ID or WAV filename[,volume] +EnterRegion= ; Sound ID or WAV filename[,volume] +ClickRegion= ; Sound ID or WAV filename[,volume] +ExitRegion= ; Sound ID or WAV filename[,volume] +MouseOnMapSound= ; Sound ID or WAV filename[,volume] +MouseOffMapSound= ; Sound ID or WAV filename[,volume] +TypeSound= ; Sound ID or WAV filename[,volume] +TextColor= ; ColorStruct (R,G,B) TextRect=92,322,332,78 ; Rectangle (x,y,width,height) Anim= ; Animation SHP (file.shp,x,y,delay[,palette.pal]) AnimPCX= ; Animation PCX (base.pcx,x,y,delay) = ; Choice Stage -[Anims] ; Optional global SHP animations +[Anims] ; Optional global SHP animations for all sides = ; Animation SHP (file.shp,x,y,delay[,palette.pal]) -[AnimsPCX] ; Optional global PCX animations +[Anims] ; Optional global SHP animations for a specific side += ; Animation SHP (file.shp,x,y,delay[,palette.pal]) + +[AnimsPCX] ; Optional global PCX animations for all sides += ; Animation PCX (base.pcx,x,y,delay) + +[AnimsPCX] ; Optional global PCX animations for a specific side = ; Animation PCX (base.pcx,x,y,delay) ``` diff --git a/src/Misc/MapSelectionClass.cpp b/src/Misc/MapSelectionClass.cpp index fe8a512505..21b5934eee 100644 --- a/src/Misc/MapSelectionClass.cpp +++ b/src/Misc/MapSelectionClass.cpp @@ -2,16 +2,20 @@ #include #include +#include #include +#include #include #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -37,105 +41,240 @@ static inline void* LoadGameFile(const char* pFileName, bool bLoadAsSHP = false) return pFunc(pFileName, bLoadAsSHP); } +static void StopMapSelAudio() +{ + PlaySoundA(NULL, NULL, 0); + AudioStream* pStream = AudioStream::Instance; + if (pStream) + { + pStream->PlayWAV("", false); + } +} + +static std::string_view Trim(std::string_view sv) +{ + auto s = sv.find_first_not_of(" \t\r\n"); + if (s == std::string_view::npos) + return {}; + + auto e = sv.find_last_not_of(" \t\r\n"); + return sv.substr(s, e - s + 1); +} + static void PlayMapSelAudio(const char* pAudioName) { if (!pAudioName || !pAudioName[0]) return; - std::string baseName = pAudioName; - // Strip volume percentage if present (e.g. "GSWEEP.AUD,60" -> "GSWEEP.AUD") - size_t commaPos = baseName.find(','); - if (commaPos != std::string::npos) + std::string soundName; + float volumeFloat = 1.0f; + std::string_view rawAudio { pAudioName }; + size_t commaPos = rawAudio.find(','); + if (commaPos != std::string_view::npos) { - baseName = baseName.substr(0, commaPos); + int vol = atoi(std::string(Trim(rawAudio.substr(commaPos + 1))).c_str()); + if (vol > 0) + { + volumeFloat = std::clamp(vol, 1, 100) / 100.0f; + } + soundName = Trim(rawAudio.substr(0, commaPos)); + } + else + { + soundName = Trim(rawAudio); } - while (!baseName.empty() && (baseName.back() == ' ' || baseName.back() == '\t' || baseName.back() == '\r' || baseName.back() == '\n')) - baseName.pop_back(); - while (!baseName.empty() && (baseName.front() == ' ' || baseName.front() == '\t')) - baseName.erase(baseName.begin()); - if (baseName.empty()) + if (soundName.empty() || _stricmp(soundName.c_str(), "none") == 0 || _stricmp(soundName.c_str(), "no") == 0) return; - // Strip any legacy extension (.aud, .wav) - std::string rawName = baseName; - size_t dotPos = rawName.find_last_of('.'); + StopMapSelAudio(); + + // If it has a file extension (e.g. .wav), search and play as audio file without requiring soundmd.ini + size_t dotPos = soundName.find_last_of('.'); if (dotPos != std::string::npos) { - std::string ext = rawName.substr(dotPos); - if (_stricmp(ext.c_str(), ".aud") == 0 || _stricmp(ext.c_str(), ".wav") == 0) + if (GetFileAttributesA(soundName.c_str()) != INVALID_FILE_ATTRIBUTES) { - rawName = rawName.substr(0, dotPos); + PlaySoundA(soundName.c_str(), NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT); + return; } - } - std::string wavNameLower = rawName + ".wav"; - std::string wavNameUpper = rawName + ".WAV"; + static std::vector s_AudioMemBuf; + CCFileClass testFile(soundName.c_str()); + if (testFile.Exists() && testFile.Open(FileAccessMode::Read)) + { + int sz = testFile.GetFileSize(); + if (sz > 44) + { + s_AudioMemBuf.resize(sz); + testFile.ReadBytes(s_AudioMemBuf.data(), sz); + testFile.Close(); + PlaySoundA(reinterpret_cast(s_AudioMemBuf.data()), NULL, SND_MEMORY | SND_ASYNC | SND_NODEFAULT); + return; + } + testFile.Close(); + } + } - // Try Windows PlaySound on disk file first (100% reliable, async, non-blocking) - if (GetFileAttributesA(wavNameLower.c_str()) != INVALID_FILE_ATTRIBUTES) + // If no extension or not found as direct file, look up in soundmd.ini via VocClass + int vocIdx = VocClass::FindIndex(soundName.c_str()); + if (vocIdx >= 0) { - Debug::Log("[MapSelection] Playing WAV via PlaySound '%s'\n", wavNameLower.c_str()); - PlaySoundA(wavNameLower.c_str(), NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT); + VocClass::PlayGlobal(vocIdx, 0x2000, volumeFloat); return; } - if (GetFileAttributesA(wavNameUpper.c_str()) != INVALID_FILE_ATTRIBUTES) + + // Try appending .wav if sound name was provided without extension and wasn't in soundmd.ini + std::string withWav = soundName + ".wav"; + if (GetFileAttributesA(withWav.c_str()) != INVALID_FILE_ATTRIBUTES) { - Debug::Log("[MapSelection] Playing WAV via PlaySound '%s'\n", wavNameUpper.c_str()); - PlaySoundA(wavNameUpper.c_str(), NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT); + PlaySoundA(withWav.c_str(), NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT); return; } - // Try playing .wav via AudioStream (from MIX files or memory) - AudioStream* pStream = AudioStream::Instance; - if (pStream) + static std::vector s_AudioMemBuf2; + CCFileClass testWav(withWav.c_str()); + if (testWav.Exists() && testWav.Open(FileAccessMode::Read)) { - CCFileClass testFileLower(wavNameLower.c_str()); - CCFileClass testFileUpper(wavNameUpper.c_str()); - - if (testFileLower.Exists()) + int sz = testWav.GetFileSize(); + if (sz > 44) { - Debug::Log("[MapSelection] Playing WAV via AudioStream '%s'\n", wavNameLower.c_str()); - typedef bool(__thiscall* PlayWAV_t)(void* pThis, const char* pFileName, bool bLoop); - auto playWAV = reinterpret_cast(0x407B60); - playWAV(pStream, wavNameLower.c_str(), false); + s_AudioMemBuf2.resize(sz); + testWav.ReadBytes(s_AudioMemBuf2.data(), sz); + testWav.Close(); + PlaySoundA(reinterpret_cast(s_AudioMemBuf2.data()), NULL, SND_MEMORY | SND_ASYNC | SND_NODEFAULT); return; } - else if (testFileUpper.Exists()) + testWav.Close(); + } +} + +static void PlayMapSelSFX(const char* pAudioName) +{ + if (!pAudioName || !pAudioName[0]) + return; + + std::string soundName; + float volumeFloat = 1.0f; + std::string_view rawAudio { pAudioName }; + size_t commaPos = rawAudio.find(','); + if (commaPos != std::string_view::npos) + { + int vol = atoi(std::string(Trim(rawAudio.substr(commaPos + 1))).c_str()); + if (vol > 0) { - Debug::Log("[MapSelection] Playing WAV via AudioStream '%s'\n", wavNameUpper.c_str()); - typedef bool(__thiscall* PlayWAV_t)(void* pThis, const char* pFileName, bool bLoop); - auto playWAV = reinterpret_cast(0x407B60); - playWAV(pStream, wavNameUpper.c_str(), false); - return; + volumeFloat = std::clamp(vol, 1, 100) / 100.0f; } + soundName = Trim(rawAudio.substr(0, commaPos)); + } + else + { + soundName = Trim(rawAudio); } - // Fallback to VocClass (Sound.ini sound entry) + if (soundName.empty() || _stricmp(soundName.c_str(), "none") == 0 || _stricmp(soundName.c_str(), "no") == 0) + return; + + // For SFX: try VocClass (soundmd.ini) first so it doesn't interrupt VoiceOver + std::string rawName = soundName; + size_t dotPos = rawName.find_last_of('.'); + if (dotPos != std::string::npos && _stricmp(rawName.substr(dotPos).c_str(), ".wav") == 0) + rawName = rawName.substr(0, dotPos); + int vocIdx = VocClass::FindIndex(rawName.c_str()); if (vocIdx < 0) - vocIdx = VocClass::FindIndex(baseName.c_str()); + vocIdx = VocClass::FindIndex(soundName.c_str()); if (vocIdx >= 0) { - Debug::Log("[MapSelection] Playing Voc sound '%s' (idx %d)\n", rawName.c_str(), vocIdx); - VocClass::PlayGlobal(vocIdx, 0x2000, 1.0f); + VocClass::PlayGlobal(vocIdx, 0x2000, volumeFloat); return; } - // Fallback to VoxClass (EVA messages in eva.ini) - int voxIdx = VoxClass::FindIndex(rawName.c_str()); - if (voxIdx < 0) - voxIdx = VoxClass::FindIndex(baseName.c_str()); + PlayMapSelAudio(pAudioName); +} - if (voxIdx >= 0) +static int GetTypewriterVocIndex(const std::string& customTypeSound, float& outVolume) +{ + outVolume = 0.5f; + if (customTypeSound.empty()) { - Debug::Log("[MapSelection] Playing Vox EVA sound '%s' (idx %d)\n", rawName.c_str(), voxIdx); - VoxClass::PlayIndex(voxIdx, -1, -1); - return; + // Default to TextBleep (official RA2/YR typing sound) + static int s_DefaultTypeVocIdx = -2; + if (s_DefaultTypeVocIdx != -2) + return s_DefaultTypeVocIdx; + + if (RulesClass::Instance && RulesClass::Instance->MessageCharTyped >= 0) + { + s_DefaultTypeVocIdx = RulesClass::Instance->MessageCharTyped; + return s_DefaultTypeVocIdx; + } + + const char* typingSoundNames[] = { + "TextBleep", "MessageText", "KeyClick", "Typing", "Type", "TextType", + "MessageCharTyped", "MessageClick", "ScoreScreenTick", "Button" + }; + for (const char* name : typingSoundNames) + { + int idx = VocClass::FindIndex(name); + if (idx >= 0) + { + s_DefaultTypeVocIdx = idx; + Debug::Log("[MapSelection] Found typewriter sound '%s' (idx %d)\n", name, idx); + return s_DefaultTypeVocIdx; + } + } + s_DefaultTypeVocIdx = -1; + return s_DefaultTypeVocIdx; + } + + std::string soundName; + std::string_view rawAudio { customTypeSound }; + size_t commaPos = rawAudio.find(','); + if (commaPos != std::string_view::npos) + { + int vol = atoi(std::string(Trim(rawAudio.substr(commaPos + 1))).c_str()); + if (vol > 0) + { + outVolume = std::clamp(vol, 1, 100) / 100.0f; + } + soundName = Trim(rawAudio.substr(0, commaPos)); + } + else + { + soundName = Trim(rawAudio); } - Debug::Log("[MapSelection] Audio '%s' / '%s.wav' not found anywhere.\n", baseName.c_str(), rawName.c_str()); + if (soundName.empty() || _stricmp(soundName.c_str(), "none") == 0 || _stricmp(soundName.c_str(), "no") == 0) + return -1; + + return VocClass::FindIndex(soundName.c_str()); +} + +static bool g_InMapSelectionBinkVideo = false; + +DEFINE_HOOK(0x432D03, BinkMovie_CheckInput_MapSelection, 0x8) +{ + if (g_InMapSelectionBinkVideo) + { + // Only skip movie if ESC key is physically pressed (ignore mouse clicks) + if ((GetAsyncKeyState(VK_ESCAPE) & 0x8000) != 0) + { + return 0x432E27; // Skip movie + } + else + { + return 0x432D0B; // Continue playing movie + } + } + + // Default game behavior + if (R->EAX() == 0) + { + return 0x432E27; + } + + return 0x432D0B; } static bool PlayMapSelBinkVideo(const char* pVideoName) @@ -162,8 +301,10 @@ static bool PlayMapSelBinkVideo(const char* pVideoName) if (testBikLower.Exists() || testBikUpper.Exists()) { - Debug::Log("[MapSelection] Playing Bink video '%s'\n", movieName.c_str()); + Debug::Log("[MapSelection] Playing Bink video '%s' (ESC to skip)\n", movieName.c_str()); + g_InMapSelectionBinkVideo = true; Game::PlayMovie(movieName.c_str(), -1, -1, -1, -1, -1); + g_InMapSelectionBinkVideo = false; return true; } @@ -321,18 +462,54 @@ static std::vector WordWrapText(const std::wstring& text, size_t m return lines; } +static ColorStruct ConvertSchemeColorToRGB(const ColorStruct& hslColor) +{ + ColorStruct rgbColor { 255, 255, 255 }; + reinterpret_cast(0x517440)(&hslColor, &rgbColor); + return rgbColor; +} + static ColorStruct ParseColorString(const char* pStr, const ColorStruct& defaultColor) { if (!pStr || !pStr[0]) return defaultColor; + std::string s { Trim(pStr) }; + + if (s.empty()) + return defaultColor; + int r = 0, g = 0, b = 0; - if (sscanf_s(pStr, "%d,%d,%d", &r, &g, &b) == 3) + if (sscanf_s(s.c_str(), "%d,%d,%d", &r, &g, &b) == 3) { return ColorStruct { static_cast(std::clamp(r, 0, 255)), static_cast(std::clamp(g, 0, 255)), static_cast(std::clamp(b, 0, 255)) }; } + + int schemeIdx = ColorScheme::FindIndex(s.c_str()); + if (schemeIdx >= 0 && schemeIdx < ColorScheme::Array.Count) + { + ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(schemeIdx); + if (pScheme) + { + return ConvertSchemeColorToRGB(pScheme->BaseColor); + } + } + + if (isdigit(static_cast(s[0]))) + { + int idx = atoi(s.c_str()); + if (idx >= 0 && idx < ColorScheme::Array.Count) + { + ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(idx); + if (pScheme) + { + return ConvertSchemeColorToRGB(pScheme->BaseColor); + } + } + } + return defaultColor; } @@ -341,11 +518,7 @@ static std::wstring ResolveCSFOrText(const std::string& inputKey) if (inputKey.empty()) return L""; - std::string key = inputKey; - while (!key.empty() && (key.back() == ' ' || key.back() == '\t' || key.back() == '\r' || key.back() == '\n')) - key.pop_back(); - while (!key.empty() && (key.front() == ' ' || key.front() == '\t')) - key.erase(key.begin()); + std::string key { Trim(inputKey) }; if (key.empty()) return L""; @@ -400,15 +573,23 @@ MapSelectionClass::MapSelectionClass() = default; MapSelectionClass::~MapSelectionClass() { - if (pPalette) + if (this->pPalette) { - delete pPalette; - pPalette = nullptr; + delete this->pPalette; + this->pPalette = nullptr; } - if (pOverlayPalette) + if (this->pOverlayPalette && this->pOverlayPalette != this->pPalette) { - delete pOverlayPalette; - pOverlayPalette = nullptr; + delete this->pOverlayPalette; + this->pOverlayPalette = nullptr; + } + for (auto& anim : this->backgroundAnims) + { + if (anim.Palette && anim.Palette != this->pPalette && anim.Palette != this->pOverlayPalette) + { + delete anim.Palette; + anim.Palette = nullptr; + } } } @@ -672,20 +853,6 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) return; } - CCINIClass battleIni; - CCFileClass battleFile("battlemd.ini"); - if (battleFile.Exists() && battleFile.Open(FileAccessMode::Read)) - { - battleIni.ReadCCFile(&battleFile); - } - - CCINIClass missionIni; - CCFileClass missionFile("missionmd.ini"); - if (missionFile.Exists() && missionFile.Open(FileAccessMode::Read)) - { - missionIni.ReadCCFile(&missionFile); - } - // Active side section std::string sideSection = "GDI"; int sideIdx = pScenario->PlayerSideIndex; @@ -798,52 +965,204 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) ini.ReadString(pStage, "Theme", "", themeBuf, sizeof(themeBuf)); this->themeName = themeBuf; - // Determine default text color from player's house - ColorStruct playerHouseColor { 255, 239, 99 }; - HouseClass* pHouse = HouseClass::CurrentPlayer; - if (!pHouse && pScenario && pScenario->HumanPlayerHouseTypeIndex >= 0 && pScenario->HumanPlayerHouseTypeIndex < HouseClass::Array.Count) + // Determine default text color from active side rules (MessageTextColor -> Color -> White fallback) + ColorStruct sideTextColor { 255, 255, 255 }; // White fallback + bool foundSideColor = false; + + SideClass* pSide = nullptr; + if (sideIdx >= 0 && sideIdx < SideClass::Array.Count) { - pHouse = HouseClass::Array.GetItemOrDefault(pScenario->HumanPlayerHouseTypeIndex); + pSide = SideClass::Array.GetItemOrDefault(sideIdx); } - if (pHouse) + if (!pSide) { - int schemeIdx = pHouse->ColorSchemeIndex; - ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(schemeIdx); - if (pScheme) + pSide = SideClass::Find(sideSection.c_str()); + } + if (!pSide) + { + for (int s = 0; s < SideClass::Array.Count; ++s) + { + SideClass* pCheck = SideClass::Array.GetItemOrDefault(s); + if (!pCheck) continue; + if ((pCheck->ID && _stricmp(pCheck->ID, sideSection.c_str()) == 0) || + (pCheck->Name && _stricmp(pCheck->Name, sideSection.c_str()) == 0)) + { + pSide = pCheck; + break; + } + } + } + if (!pSide) + { + if (_stricmp(sideSection.c_str(), "Nod") == 0) + { + pSide = SideClass::Find("TSNodSide"); + if (!pSide) pSide = SideClass::Find("Nod"); + if (!pSide) pSide = SideClass::Find("Soviets"); + if (!pSide) pSide = SideClass::Find("Soviet"); + } + else if (_stricmp(sideSection.c_str(), "GDI") == 0) + { + pSide = SideClass::Find("TSGDISide"); + if (!pSide) pSide = SideClass::Find("GDI"); + if (!pSide) pSide = SideClass::Find("Allies"); + if (!pSide) pSide = SideClass::Find("Allied"); + } + } + + if (pSide) + { + // Check MessageTextColor from rulesmd.ini via SideExt + auto pSideExt = SideExt::TryFetch(pSide); + if (pSideExt && pSideExt->MessageTextColor >= 0 && pSideExt->MessageTextColor < ColorScheme::Array.Count) + { + ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(pSideExt->MessageTextColor); + if (pScheme) + { + sideTextColor = ConvertSchemeColorToRGB(pScheme->BaseColor); + foundSideColor = true; + Debug::Log("[MapSelection] Text color from SideExt MessageTextColor: RGB(%d,%d,%d) (Side: '%s')\n", + sideTextColor.R, sideTextColor.G, sideTextColor.B, pSide->ID ? pSide->ID : ""); + } + } + + // If MessageTextColor not set, check primary country Color= tag from rules + if (!foundSideColor && pSide->HouseTypes.Count > 0) + { + for (int h = 0; h < pSide->HouseTypes.Count; ++h) + { + int houseIdx = pSide->HouseTypes.GetItem(h); + HouseTypeClass* pHouseType = HouseTypeClass::Array.GetItemOrDefault(houseIdx); + if (pHouseType && pHouseType->ColorSchemeIndex >= 0 && pHouseType->ColorSchemeIndex < ColorScheme::Array.Count) + { + ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(pHouseType->ColorSchemeIndex); + if (pScheme) + { + sideTextColor = ConvertSchemeColorToRGB(pScheme->BaseColor); + foundSideColor = true; + Debug::Log("[MapSelection] Text color from HouseType '%s' ColorScheme: RGB(%d,%d,%d)\n", + pHouseType->ID, sideTextColor.R, sideTextColor.G, sideTextColor.B); + break; + } + } + } + } + } + + // Check active player House ColorScheme if still not found + if (!foundSideColor) + { + HouseClass* pHouse = HouseClass::CurrentPlayer; + if (!pHouse && pScenario && pScenario->HumanPlayerHouseTypeIndex >= 0 && pScenario->HumanPlayerHouseTypeIndex < HouseClass::Array.Count) + { + pHouse = HouseClass::Array.GetItemOrDefault(pScenario->HumanPlayerHouseTypeIndex); + } + if (pHouse && pHouse->ColorSchemeIndex >= 0 && pHouse->ColorSchemeIndex < ColorScheme::Array.Count) { - playerHouseColor = pScheme->BaseColor; + ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(pHouse->ColorSchemeIndex); + if (pScheme) + { + sideTextColor = ConvertSchemeColorToRGB(pScheme->BaseColor); + foundSideColor = true; + } } } - else if (_stricmp(sideSection.c_str(), "Nod") == 0) + + // Direct rules check for [TSNodSide] / [TSGDISide] / [Nod] / [GDI] if still not found + if (!foundSideColor) { - playerHouseColor = ColorStruct { 255, 50, 50 }; + CCINIClass rulesIni; + CCFileClass rulesFile("rulesmd.ini"); + if (rulesFile.Exists() && rulesFile.Open(FileAccessMode::Read)) + { + rulesIni.ReadCCFile(&rulesFile); + char msgColorBuf[64] = { 0 }; + const char* checkSections[] = { + sideSection.c_str(), + (_stricmp(sideSection.c_str(), "Nod") == 0) ? "TSNodSide" : "TSGDISide", + (_stricmp(sideSection.c_str(), "Nod") == 0) ? "Nod" : "GDI", + (_stricmp(sideSection.c_str(), "Nod") == 0) ? "Soviet" : "Allies", + (_stricmp(sideSection.c_str(), "Nod") == 0) ? "Soviets" : "Allied" + }; + for (const char* sec : checkSections) + { + rulesIni.ReadString(sec, "MessageTextColor", "", msgColorBuf, sizeof(msgColorBuf)); + if (!msgColorBuf[0]) + rulesIni.ReadString(sec, "Color", "", msgColorBuf, sizeof(msgColorBuf)); + + if (msgColorBuf[0]) + { + sideTextColor = ParseColorString(msgColorBuf, sideTextColor); + foundSideColor = true; + Debug::Log("[MapSelection] Text color resolved from rulesmd.ini [%s] '%s': RGB(%d,%d,%d)\n", + sec, msgColorBuf, sideTextColor.R, sideTextColor.G, sideTextColor.B); + break; + } + } + } } char colorBuf[64] = { 0 }; ini.ReadString(pStage, "TextColor", "", colorBuf, sizeof(colorBuf)); - this->defaultTextColor = ParseColorString(colorBuf, playerHouseColor); + this->defaultTextColor = ParseColorString(colorBuf, sideTextColor); - char sfxBuf[128] = { 0 }; - ini.ReadString(pStage, "OverlaySound", "", sfxBuf, sizeof(sfxBuf)); - this->overlaySound = sfxBuf; + // Check Sounds section from stage, side, side-specific [Sounds], or global [Sounds] + char soundsSecBuf[128] = { 0 }; + ini.ReadString(pStage, "Sounds", "", soundsSecBuf, sizeof(soundsSecBuf)); + if (!soundsSecBuf[0]) + ini.ReadString(sideSection.c_str(), "Sounds", "", soundsSecBuf, sizeof(soundsSecBuf)); - ini.ReadString(pStage, "TargetSound", "", sfxBuf, sizeof(sfxBuf)); - this->targetFlyInSound = sfxBuf; + std::string resolvedSoundsSec; + if (soundsSecBuf[0]) + { + resolvedSoundsSec = soundsSecBuf; + } + else + { + std::string sideSoundsNamed = "Sounds" + sideSection; + char sideSoundsIndexed[32] = { 0 }; + if (sideIdx >= 0) + sprintf_s(sideSoundsIndexed, "Sounds%d", sideIdx); - ini.ReadString(pStage, "EnterRegionSound", "", sfxBuf, sizeof(sfxBuf)); - this->enterRegionSound = sfxBuf; + if (ini.GetSection(sideSoundsNamed.c_str())) + { + resolvedSoundsSec = sideSoundsNamed; + } + else if (sideSoundsIndexed[0] && ini.GetSection(sideSoundsIndexed)) + { + resolvedSoundsSec = sideSoundsIndexed; + } + else if (ini.GetSection("Sounds")) + { + resolvedSoundsSec = "Sounds"; + } + } + const char* pSoundsSec = !resolvedSoundsSec.empty() ? resolvedSoundsSec.c_str() : nullptr; - ini.ReadString(pStage, "ExitRegionSound", "", sfxBuf, sizeof(sfxBuf)); - this->exitRegionSound = sfxBuf; + auto readSoundKey = [&](const char* key) -> std::string { + char buf[128] = { 0 }; + ini.ReadString(pStage, key, "", buf, sizeof(buf)); - ini.ReadString(pStage, "ClickSound", "", sfxBuf, sizeof(sfxBuf)); - this->clickRegionSound = sfxBuf; + if (!buf[0] && pSoundsSec) + { + ini.ReadString(pSoundsSec, key, "", buf, sizeof(buf)); + } - ini.ReadString(pStage, "MouseOnMapSound", "", sfxBuf, sizeof(sfxBuf)); - this->mouseOnMapSound = sfxBuf; + if (!buf[0] && ini.GetSection("Sounds")) + { + ini.ReadString("Sounds", key, "", buf, sizeof(buf)); + } + return std::string(buf); + }; - ini.ReadString(pStage, "MouseOffMapSound", "", sfxBuf, sizeof(sfxBuf)); - this->mouseOffMapSound = sfxBuf; + this->overlaySound = readSoundKey("Overlay"); + this->targetFlyInSound = readSoundKey("TargetFlyIn"); + this->enterRegionSound = readSoundKey("EnterRegion"); + this->exitRegionSound = readSoundKey("ExitRegion"); + this->clickRegionSound = readSoundKey("ClickRegion"); + this->mouseOnMapSound = readSoundKey("MouseOnMapSound"); + this->mouseOffMapSound = readSoundKey("MouseOffMapSound"); + this->typeSound = readSoundKey("TypeSound"); char palBuf[128] = { 0 }; ini.ReadString(pStage, "Palette", "mapsel.pal", palBuf, sizeof(palBuf)); @@ -911,7 +1230,9 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) // Target fly-in and target marker custom assets char flyInPCXBuf[512] = { 0 }; - ini.ReadString(pStage, "TargetFlyInPCX", "", flyInPCXBuf, sizeof(flyInPCXBuf)); + ini.ReadString(pStage, "TargetFlyInAnimPCX", "", flyInPCXBuf, sizeof(flyInPCXBuf)); + if (!flyInPCXBuf[0]) + ini.ReadString(pStage, "TargetFlyInPCX", "", flyInPCXBuf, sizeof(flyInPCXBuf)); if (flyInPCXBuf[0]) { char* context = nullptr; @@ -928,11 +1249,13 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) } char flyInBuf[128] = { 0 }; - ini.ReadString(pStage, "TargetFlyIn", "", flyInBuf, sizeof(flyInBuf)); + ini.ReadString(pStage, "TargetFlyInAnim", "", flyInBuf, sizeof(flyInBuf)); this->targetFlyInFileName = flyInBuf; char markerPCXBuf[512] = { 0 }; - ini.ReadString(pStage, "TargetMarkerPCX", "", markerPCXBuf, sizeof(markerPCXBuf)); + ini.ReadString(pStage, "TargetMarkerAnimPCX", "", markerPCXBuf, sizeof(markerPCXBuf)); + if (!markerPCXBuf[0]) + ini.ReadString(pStage, "TargetMarkerPCX", "", markerPCXBuf, sizeof(markerPCXBuf)); if (markerPCXBuf[0]) { char* context = nullptr; @@ -949,7 +1272,9 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) } char markerBuf[128] = { 0 }; - ini.ReadString(pStage, "TargetMarker", "", markerBuf, sizeof(markerBuf)); + ini.ReadString(pStage, "TargetMarkerAnim", "", markerBuf, sizeof(markerBuf)); + if (!markerBuf[0]) + ini.ReadString(pStage, "TargetMarker", "", markerBuf, sizeof(markerBuf)); this->targetMarkerFileName = markerBuf; // Parse stage Targets list: Targets=,,,,,... @@ -1016,12 +1341,11 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) ini.ReadString(choiceStage, "VoiceOver", "", buf, sizeof(buf)); choice.VoiceOver = buf; - ini.ReadString(choiceStage, "HoverSound", "", buf, sizeof(buf)); - choice.HoverSound = buf; + ini.ReadString(choiceStage, "EnterRegion", "", buf, sizeof(buf)); + choice.HoverSound = buf[0] ? buf : this->enterRegionSound; - ini.ReadString(choiceStage, "ClickSound", "", buf, sizeof(buf)); - if (buf[0]) choice.ClickSound = buf; - else choice.ClickSound = this->clickRegionSound; + ini.ReadString(choiceStage, "ClickRegion", "", buf, sizeof(buf)); + choice.ClickSound = buf[0] ? buf : this->clickRegionSound; ini.ReadString(choiceStage, "TextColor", "", buf, sizeof(buf)); if (buf[0]) @@ -1054,22 +1378,6 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) choice.ScenarioPath = choiceStage; } - if (choice.Description.empty() && battleIni.GetSection(choiceStage)) - { - battleIni.ReadString(choiceStage, "Description", "", buf, sizeof(buf)); - choice.Description = buf; - battleIni.ReadString(choiceStage, "Summary", "", buf, sizeof(buf)); - choice.Summary = buf; - } - - if (choice.Description.empty() && missionIni.GetSection(choice.ScenarioPath.c_str())) - { - missionIni.ReadString(choice.ScenarioPath.c_str(), "UIName", "", buf, sizeof(buf)); - choice.Description = buf; - missionIni.ReadString(choice.ScenarioPath.c_str(), "Briefing", "", buf, sizeof(buf)); - choice.Summary = buf; - } - if (!choice.ScenarioPath.empty()) { Debug::Log("[MapSelection] Added choice %d (index=%d): Stage='%s', Scenario='%s', Target=(%d,%d)\n", @@ -1095,8 +1403,22 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) ini.ReadString(pStage, "VoiceOver", "", buf, sizeof(buf)); choice.VoiceOver = buf; - ini.ReadString(pStage, "HoverSound", "", buf, sizeof(buf)); - choice.HoverSound = buf; + ini.ReadString(pStage, "EnterRegion", "", buf, sizeof(buf)); + choice.HoverSound = buf[0] ? buf : this->enterRegionSound; + + ini.ReadString(pStage, "ClickRegion", "", buf, sizeof(buf)); + choice.ClickSound = buf[0] ? buf : this->clickRegionSound; + + ini.ReadString(pStage, "TextColor", "", buf, sizeof(buf)); + if (buf[0]) + { + choice.TextColor = ParseColorString(buf, this->defaultTextColor); + choice.HasCustomTextColor = true; + } + else + { + choice.TextColor = this->defaultTextColor; + } ini.ReadString(pStage, "Targets", "", buf, sizeof(buf)); if (buf[0]) @@ -1185,15 +1507,17 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) } } - // Global PCX animations from [AnimsPCX] - if (ini.GetSection("AnimsPCX")) + auto parsePCXAnimSection = [&](const char* pSecName) { + if (!ini.GetSection(pSecName)) + return; + for (int i = 1; i <= 32; ++i) { char keyName[16]; sprintf_s(keyName, "%d", i); char animPCXDef[512] = { 0 }; - ini.ReadString("AnimsPCX", keyName, "", animPCXDef, sizeof(animPCXDef)); + ini.ReadString(pSecName, keyName, "", animPCXDef, sizeof(animPCXDef)); if (animPCXDef[0]) { char pcxFile[128] = { 0 }; @@ -1223,17 +1547,121 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) } } } - } + }; - // Global SHP animations from [Anims] - if (ini.GetSection("Anims")) + auto parseSHPAnimSection = [&](const char* pSecName) { + if (!ini.GetSection(pSecName)) + return; + for (int i = 1; i <= 32; ++i) { char keyName[16]; sprintf_s(keyName, "%d", i); char animSHPDef[256] = { 0 }; - ini.ReadString("Anims", keyName, "", animSHPDef, sizeof(animSHPDef)); + ini.ReadString(pSecName, keyName, "", animSHPDef, sizeof(animSHPDef)); + if (animSHPDef[0]) + { + char shpName[64] = { 0 }; + char animPal[64] = { 0 }; + int ax = 0, ay = 0, arate = 5; + int parsed = sscanf_s(animSHPDef, "%63[^,],%d,%d,%d,%63s", shpName, (unsigned)_countof(shpName), &ax, &ay, &arate, animPal, (unsigned)_countof(animPal)); + if (parsed >= 3) + { + SHPStruct* pSHP = LoadMapSelSHP(shpName); + if (pSHP) + { + MapSelectAnim anim; + anim.SHP = pSHP; + anim.X = ax; + anim.Y = ay; + anim.FrameDelay = (arate > 0) ? arate : 5; + anim.CurrentFrame = 0; + anim.TotalFrames = pSHP->Frames; + anim.Timer.Start(anim.FrameDelay); + if (parsed >= 5 && animPal[0]) + { + anim.PaletteName = animPal; + } + this->backgroundAnims.push_back(anim); + } + } + } + } + }; + + // 1. Global animations across all sides + parsePCXAnimSection("AnimsPCX"); + parseSHPAnimSection("Anims"); + + // 2. Side-specific global animations: [Anims], [AnimsPCX], [Anims], [AnimsPCX] + if (sideIdx >= 0) + { + char sideSecBuf[64]; + sprintf_s(sideSecBuf, "Anims%dPCX", sideIdx); + parsePCXAnimSection(sideSecBuf); + + sprintf_s(sideSecBuf, "Anims%d", sideIdx); + parseSHPAnimSection(sideSecBuf); + } + if (!sideSection.empty()) + { + std::string sideNameSecPCX = "Anims" + sideSection + "PCX"; + parsePCXAnimSection(sideNameSecPCX.c_str()); + + std::string sideNameSecSHP = "Anims" + sideSection; + parseSHPAnimSection(sideNameSecSHP.c_str()); + + // Anims= and AnimsPCX= section pointers under [SOMESIDE] + char animsSecBuf[128] = { 0 }; + ini.ReadString(sideSection.c_str(), "Anims", "", animsSecBuf, sizeof(animsSecBuf)); + if (animsSecBuf[0]) + parseSHPAnimSection(animsSecBuf); + + char animsPCXSecBuf[128] = { 0 }; + ini.ReadString(sideSection.c_str(), "AnimsPCX", "", animsPCXSecBuf, sizeof(animsPCXSecBuf)); + if (animsPCXSecBuf[0]) + parsePCXAnimSection(animsPCXSecBuf); + + // Direct Anim / AnimPCX defined directly inside [SOMESIDE] + for (int i = 1; i <= 32; ++i) + { + char keyName[32]; + sprintf_s(keyName, "Anim%dPCX", i); + char animPCXDef[512] = { 0 }; + ini.ReadString(sideSection.c_str(), keyName, "", animPCXDef, sizeof(animPCXDef)); + if (animPCXDef[0]) + { + char pcxFile[128] = { 0 }; + int ax = 0, ay = 0, arate = 5; + if (sscanf_s(animPCXDef, "%127[^,],%d,%d,%d", pcxFile, (unsigned)_countof(pcxFile), &ax, &ay, &arate) >= 3) + { + auto pFrames = GeneralUtils::GetAnimationPCX(pcxFile); + if (pFrames && !pFrames->empty()) + { + MapSelectAnim anim; + anim.X = ax; + anim.Y = ay; + anim.FrameDelay = (arate > 0) ? arate : 5; + for (const auto& frame : *pFrames) + { + if (frame.Exists()) + anim.PCXFrames.push_back(frame.GetSurface()); + } + if (!anim.PCXFrames.empty()) + { + anim.TotalFrames = static_cast(anim.PCXFrames.size()); + anim.CurrentFrame = 0; + anim.Timer.Start(anim.FrameDelay); + this->backgroundAnims.push_back(anim); + } + } + } + } + + sprintf_s(keyName, "Anim%d", i); + char animSHPDef[256] = { 0 }; + ini.ReadString(sideSection.c_str(), keyName, "", animSHPDef, sizeof(animSHPDef)); if (animSHPDef[0]) { char shpName[64] = { 0 }; @@ -1494,19 +1922,27 @@ void MapSelectionClass::UpdateAnimations() if (this->targetAnimTimer.Completed()) { + int totalFrames = 0; if (!this->targetMarkerPCXSurfaces.empty()) { - int count = static_cast(this->targetMarkerPCXSurfaces.size()); - if (count > 0) - { - this->idleTargetAnimFrame = (this->idleTargetAnimFrame + 1) % count; - this->activeTargetAnimFrame = (this->activeTargetAnimFrame + 1) % count; - } + totalFrames = static_cast(this->targetMarkerPCXSurfaces.size()); + } + else if (this->pTargetMarkerSHP) + { + totalFrames = static_cast(this->pTargetMarkerSHP->Frames); + } + + if (totalFrames > 1) + { + int half = totalFrames / 2; + int activeCount = totalFrames - half; + this->idleTargetAnimFrame = (this->idleTargetAnimFrame + 1) % half; + this->activeTargetAnimFrame = half + ((this->activeTargetAnimFrame - half + 1) % activeCount); } else { - this->idleTargetAnimFrame = (this->idleTargetAnimFrame + 1) % 32; - this->activeTargetAnimFrame = 32 + ((this->activeTargetAnimFrame - 32 + 1) % 32); + this->idleTargetAnimFrame = 0; + this->activeTargetAnimFrame = 0; } this->targetAnimTimer.Start(4); this->repaintAll = true; @@ -1528,17 +1964,15 @@ void MapSelectionClass::UpdateAnimations() // Play typing sound through game DirectSound mixer (never cuts off VoiceOver) if (this->typeSoundTimer.Completed()) { - if (RulesClass::Instance && RulesClass::Instance->MessageCharTyped >= 0) + float typeVol = 0.5f; + int typeIdx = GetTypewriterVocIndex(this->typeSound, typeVol); + if (typeIdx >= 0) { - VocClass::PlayGlobal(RulesClass::Instance->MessageCharTyped, 0x2000, 0.4f); + VocClass::PlayGlobal(typeIdx, 0x2000, typeVol); } - else + else if (!this->typeSound.empty() && _stricmp(this->typeSound.c_str(), "none") != 0 && _stricmp(this->typeSound.c_str(), "no") != 0) { - int typeIdx = VocClass::FindIndex("Type"); - if (typeIdx >= 0) - { - VocClass::PlayGlobal(typeIdx, 0x2000, 0.4f); - } + PlayMapSelSFX(this->typeSound.c_str()); } this->typeSoundTimer.Start(3); } @@ -1673,41 +2107,132 @@ void MapSelectionClass::DrawBriefing(DSurface* pSurface) // Text rendering on the metallic HUD plate (TextRect: 92, 322, 332, 78) COLORREF textColor = Drawing::RGB_To_Int(choice.TextColor.R, choice.TextColor.G, choice.TextColor.B); + COLORREF glowWhite = Drawing::RGB_To_Int(255, 255, 255); + COLORREF glowFade = Drawing::RGB_To_Int( + (static_cast(choice.TextColor.R) + 255) / 2, + (static_cast(choice.TextColor.G) + 255) / 2, + (static_cast(choice.TextColor.B) + 255) / 2 + ); TextPrintType style = (TextPrintType::FullShadow | TextPrintType::Point6Grad); - int remainingChars = this->typewriterCharCount; + size_t totalBriefingChars = 0; + for (const auto& l : this->briefingLines) + totalBriefingChars += l.length(); + + bool isTypingInProgress = static_cast(this->typewriterCharCount) < totalBriefingChars; + int lineHeight = 13; + int maxVisibleLines = (this->textRectangle.Height > 0) ? (this->textRectangle.Height / lineHeight) : 6; + if (maxVisibleLines < 1) + maxVisibleLines = 1; + + // Determine which line the typing cursor is currently on + int activeLine = 0; + int charAcc = 0; + for (size_t l = 0; l < this->briefingLines.size(); ++l) + { + charAcc += static_cast(this->briefingLines[l].length()); + activeLine = static_cast(l); + if (this->typewriterCharCount <= charAcc) + break; + } + + // Auto-scroll / roll upward: old lines disappear as new lines appear below + int startLine = (activeLine >= maxVisibleLines) ? (activeLine - maxVisibleLines + 1) : 0; + int endLine = std::min(static_cast(this->briefingLines.size()), startLine + maxVisibleLines); - for (size_t i = 0; i < this->briefingLines.size(); ++i) + int charsAccountedFor = 0; + for (int l = 0; l < startLine; ++l) + charsAccountedFor += static_cast(this->briefingLines[l].length()); + + for (int l = startLine; l < endLine; ++l) { - if (remainingChars <= 0) - break; + int lineLen = static_cast(this->briefingLines[l].length()); + int charsToDrawOnLine = std::clamp(this->typewriterCharCount - charsAccountedFor, 0, lineLen); + charsAccountedFor += lineLen; - const std::wstring& line = this->briefingLines[i]; - std::wstring visiblePart; + if (charsToDrawOnLine <= 0) + continue; - if (static_cast(line.length()) <= remainingChars) - { - visiblePart = line; - remainingChars -= static_cast(line.length()); - } - else - { - visiblePart = line.substr(0, remainingChars); - remainingChars = 0; - } + const std::wstring& line = this->briefingLines[l]; + std::wstring visiblePart = line.substr(0, charsToDrawOnLine); + bool isActiveTypingLine = (l == activeLine && isTypingInProgress); - if (!visiblePart.empty()) + int screenRow = l - startLine; + int lineY = drawY + screenRow * lineHeight; + + if (!isActiveTypingLine) { RectangleStruct lineBounds = { drawX, - drawY + static_cast(i) * lineHeight, + lineY, this->textRectangle.Width, lineHeight + 4 }; Point2D relPoint = { 0, 0 }; pSurface->DrawText(visiblePart.c_str(), &lineBounds, &relPoint, textColor, 0, style); } + else + { + int len = static_cast(visiblePart.length()); + if (len >= 3) + { + std::wstring settled = visiblePart.substr(0, len - 2); + std::wstring fadeChar = visiblePart.substr(len - 2, 1); + std::wstring headChar = visiblePart.substr(len - 1, 1); + + int wSettled = 0, hDummy = 0; + if (BitFont::Instance) + BitFont::Instance->GetTextDimension(settled.c_str(), &wSettled, &hDummy, 1000); + + int wFade = 0; + if (BitFont::Instance) + { + std::wstring settledPlusFade = visiblePart.substr(0, len - 1); + BitFont::Instance->GetTextDimension(settledPlusFade.c_str(), &wFade, &hDummy, 1000); + } + + if (!settled.empty()) + { + RectangleStruct r1 = { drawX, lineY, this->textRectangle.Width, lineHeight + 4 }; + Point2D p1 = { 0, 0 }; + pSurface->DrawText(settled.c_str(), &r1, &p1, textColor, 0, style); + } + { + RectangleStruct r2 = { drawX + wSettled, lineY, this->textRectangle.Width - wSettled, lineHeight + 4 }; + Point2D p2 = { 0, 0 }; + pSurface->DrawText(fadeChar.c_str(), &r2, &p2, glowFade, 0, style); + } + { + RectangleStruct r3 = { drawX + wFade, lineY, this->textRectangle.Width - wFade, lineHeight + 4 }; + Point2D p3 = { 0, 0 }; + pSurface->DrawText(headChar.c_str(), &r3, &p3, glowWhite, 0, style); + } + } + else if (len == 2) + { + std::wstring fadeChar = visiblePart.substr(0, 1); + std::wstring headChar = visiblePart.substr(1, 1); + + int wFade = 0, hDummy = 0; + if (BitFont::Instance) + BitFont::Instance->GetTextDimension(fadeChar.c_str(), &wFade, &hDummy, 1000); + + RectangleStruct r1 = { drawX, lineY, this->textRectangle.Width, lineHeight + 4 }; + Point2D p1 = { 0, 0 }; + pSurface->DrawText(fadeChar.c_str(), &r1, &p1, glowFade, 0, style); + + RectangleStruct r2 = { drawX + wFade, lineY, this->textRectangle.Width - wFade, lineHeight + 4 }; + Point2D p2 = { 0, 0 }; + pSurface->DrawText(headChar.c_str(), &r2, &p2, glowWhite, 0, style); + } + else if (len == 1) + { + RectangleStruct r = { drawX, lineY, this->textRectangle.Width, lineHeight + 4 }; + Point2D p = { 0, 0 }; + pSurface->DrawText(visiblePart.c_str(), &r, &p, glowWhite, 0, style); + } + } } } @@ -1848,7 +2373,7 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) { if (!this->overlaySound.empty()) { - PlayMapSelAudio(this->overlaySound.c_str()); + PlayMapSelSFX(this->overlaySound.c_str()); } for (int stage = 0; stage < 4; ++stage) @@ -1896,7 +2421,7 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) if (!this->targetFlyInSound.empty()) { - PlayMapSelAudio(this->targetFlyInSound.c_str()); + PlayMapSelSFX(this->targetFlyInSound.c_str()); } int drawX = this->windowRectangle.X + choice.TargetCoord.X; @@ -1991,12 +2516,6 @@ bool MapSelectionClass::Run() } } - // Play stage VoiceOver audio if present - if (!this->voiceOverFileName.empty()) - { - PlayMapSelAudio(this->voiceOverFileName.c_str()); - } - pSurface->Fill(0); this->CalculateLayout(pSurface); @@ -2027,6 +2546,26 @@ bool MapSelectionClass::Run() } } + // Ensure any audio is stopped before interactive loop starts + StopMapSelAudio(); + + if (DisplayClass::Instance.CurrentSWTypeIndex != -1) + DisplayClass::Instance.CurrentSWTypeIndex = -1; + + if (Unsorted::CurrentSWType != -1) + Unsorted::CurrentSWType = -1; + + if (WWMouseClass::Instance) + { + WWMouseClass::Instance->HideCursor(); + WWMouseClass::Instance->ShowCursor(); + WWMouseClass::Instance->CaptureMouse(); + WWMouseClass::Instance->RefCount = 0; + } + + MouseClass::Instance.SetCursor(MouseCursorType::Default, false); + MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false); + bool wasLButtonDown = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; bool wasMouseOnMap = false; @@ -2049,58 +2588,51 @@ bool MapSelectionClass::Run() } Game::CallBack(); + MouseClass::Instance.UpdateCursor(MouseCursorType::Default, false); - POINT mousePos = { 0, 0 }; - GetCursorPos(&mousePos); - HWND hGameWnd = Game::hWnd; - if (hGameWnd) + Point2D mousePos = { 0, 0 }; + if (WWMouseClass::Instance) { - ScreenToClient(hGameWnd, &mousePos); - - RECT clientRect; - if (GetClientRect(hGameWnd, &clientRect)) - { - int clientW = clientRect.right - clientRect.left; - int clientH = clientRect.bottom - clientRect.top; - if (clientW > 0 && clientH > 0 && pSurface->Width > 0 && pSurface->Height > 0) - { - if (clientW != pSurface->Width || clientH != pSurface->Height) - { - mousePos.x = static_cast(mousePos.x * (static_cast(pSurface->Width) / clientW)); - mousePos.y = static_cast(mousePos.y * (static_cast(pSurface->Height) / clientH)); - } - } - } + mousePos.X = WWMouseClass::Instance->GetX(); + mousePos.Y = WWMouseClass::Instance->GetY(); } - bool isMouseOnMap = (mousePos.x >= this->windowRectangle.X && mousePos.x < this->windowRectangle.X + this->windowRectangle.Width && - mousePos.y >= this->windowRectangle.Y && mousePos.y < this->windowRectangle.Y + this->windowRectangle.Height); + bool isMouseOnMap = (mousePos.X >= this->windowRectangle.X && + mousePos.X < (this->windowRectangle.X + this->windowRectangle.Width) && + mousePos.Y >= this->windowRectangle.Y && + mousePos.Y < (this->windowRectangle.Y + this->windowRectangle.Height)); if (isMouseOnMap != wasMouseOnMap) { if (isMouseOnMap && !this->mouseOnMapSound.empty()) { - PlayMapSelAudio(this->mouseOnMapSound.c_str()); + PlayMapSelSFX(this->mouseOnMapSound.c_str()); } else if (!isMouseOnMap && !this->mouseOffMapSound.empty()) { - PlayMapSelAudio(this->mouseOffMapSound.c_str()); + PlayMapSelSFX(this->mouseOffMapSound.c_str()); } wasMouseOnMap = isMouseOnMap; } - this->hoveredChoiceIdx = isMouseOnMap ? this->GetChoiceIndexAtPoint(mousePos.x, mousePos.y) : -1; + this->hoveredChoiceIdx = isMouseOnMap ? this->GetChoiceIndexAtPoint(mousePos.X, mousePos.Y) : -1; if (this->hoveredChoiceIdx != this->lastHoveredChoiceIdx) { - // Stop previous VoiceOver if moving off or switching regions - PlaySoundA(NULL, NULL, 0); - VoxClass::DeleteAll(); + // Stop previous VoiceOver immediately if moving off or switching regions + StopMapSelAudio(); this->typewriterCharCount = 0; this->typewriterTimer.Start(1); this->typeSoundTimer.Start(3); + // Always restart active target marker animation from its initial frame upon hover enter + int totalMarkerFrames = !this->targetMarkerPCXSurfaces.empty() + ? static_cast(this->targetMarkerPCXSurfaces.size()) + : (this->pTargetMarkerSHP ? static_cast(this->pTargetMarkerSHP->Frames) : 0); + this->activeTargetAnimFrame = totalMarkerFrames > 1 ? (totalMarkerFrames / 2) : 0; + this->targetAnimTimer.Start(4); + if (this->hoveredChoiceIdx >= 0 && this->hoveredChoiceIdx < static_cast(this->choices.size())) { const auto& choice = this->choices[this->hoveredChoiceIdx]; @@ -2110,19 +2642,23 @@ bool MapSelectionClass::Run() if (fullText.empty()) fullText = std::wstring(choice.StageName.begin(), choice.StageName.end()); - this->briefingLines = WordWrapText(fullText, 46); + size_t charsPerLine = (this->textRectangle.Width > 0) ? std::max(20, static_cast(this->textRectangle.Width / 7)) : 46; + this->briefingLines = WordWrapText(fullText, charsPerLine); - if (!choice.VoiceOver.empty()) + // Play hover / enter region SFX sound first + if (!choice.HoverSound.empty()) { - PlayMapSelAudio(choice.VoiceOver.c_str()); + PlayMapSelSFX(choice.HoverSound.c_str()); } - else if (!choice.HoverSound.empty()) + else if (!this->enterRegionSound.empty()) { - PlayMapSelAudio(choice.HoverSound.c_str()); + PlayMapSelSFX(this->enterRegionSound.c_str()); } - else if (!this->enterRegionSound.empty()) + + // Play VoiceOver speech + if (!choice.VoiceOver.empty()) { - PlayMapSelAudio(this->enterRegionSound.c_str()); + PlayMapSelAudio(choice.VoiceOver.c_str()); } } else @@ -2130,7 +2666,7 @@ bool MapSelectionClass::Run() this->briefingLines.clear(); if (this->lastHoveredChoiceIdx >= 0 && !this->exitRegionSound.empty()) { - PlayMapSelAudio(this->exitRegionSound.c_str()); + PlayMapSelSFX(this->exitRegionSound.c_str()); } } @@ -2144,25 +2680,24 @@ bool MapSelectionClass::Run() // Only select if the mouse is hovering over a valid choice on the map if (this->hoveredChoiceIdx >= 0 && this->hoveredChoiceIdx < static_cast(this->choices.size())) { - int clickedChoice = this->GetChoiceIndexAtPoint(mousePos.x, mousePos.y); + int clickedChoice = this->GetChoiceIndexAtPoint(mousePos.X, mousePos.Y); if (clickedChoice == this->hoveredChoiceIdx) { this->selectedChoiceIdx = clickedChoice; Debug::Log("[MapSelection] Region clicked on hovered choice %d ('%s')\n", this->selectedChoiceIdx, this->choices[this->selectedChoiceIdx].StageName.c_str()); - // Immediately cut off VoiceOver and any background Windows speech - PlaySoundA(NULL, NULL, 0); - VoxClass::DeleteAll(); + // Immediately cut off VoiceOver speech + StopMapSelAudio(); const auto& chosen = this->choices[this->selectedChoiceIdx]; if (!chosen.ClickSound.empty()) { - PlayMapSelAudio(chosen.ClickSound.c_str()); + PlayMapSelSFX(chosen.ClickSound.c_str()); } else if (!this->clickRegionSound.empty()) { - PlayMapSelAudio(this->clickRegionSound.c_str()); + PlayMapSelSFX(this->clickRegionSound.c_str()); } break; } @@ -2181,8 +2716,7 @@ bool MapSelectionClass::Run() } // Ensure any playing VoiceOver audio and custom theme music are stopped when loading begins - PlaySoundA(NULL, NULL, 0); - VoxClass::DeleteAll(); + StopMapSelAudio(); if (!this->themeName.empty()) { ThemeClass::Instance.Stop(true); diff --git a/src/Misc/MapSelectionClass.h b/src/Misc/MapSelectionClass.h index 7f7a31269e..9c3455a5c9 100644 --- a/src/Misc/MapSelectionClass.h +++ b/src/Misc/MapSelectionClass.h @@ -120,6 +120,7 @@ class MapSelectionClass std::string clickRegionSound {}; std::string mouseOnMapSound {}; std::string mouseOffMapSound {}; + std::string typeSound {}; std::string themeName {}; // Choices / Progression From e845945515a3f30f484f37b76af340bb91aa1d0f Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 3 Sep 2026 07:50:22 +0200 Subject: [PATCH 3/5] MapSelection: match OpenTS target timing and ensure cursor is hidden during intro - Sync target fly-in timing to 50ms per frame matching OpenTS Rate=3 @ 60Hz - Suppress OS and in-game cursors completely during intro presentation - Ensure definitive cursor only appears once all targets have settled on map - Follow Phobos code styleguide for single-line braceless blocks and spacing --- src/Misc/MapSelectionClass.cpp | 43 ++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/src/Misc/MapSelectionClass.cpp b/src/Misc/MapSelectionClass.cpp index 21b5934eee..10a21b9d21 100644 --- a/src/Misc/MapSelectionClass.cpp +++ b/src/Misc/MapSelectionClass.cpp @@ -2244,8 +2244,16 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) ConvertClass* pDrawer = this->pPalette ? this->pPalette : FileSystem::PALETTE_PAL; ConvertClass* pOverlayDrawer = this->pOverlayPalette ? this->pOverlayPalette : pDrawer; + if (WWMouseClass::Instance) + WWMouseClass::Instance->HideCursor(); + + while (ShowCursor(FALSE) >= 0); + + ::SetCursor(NULL); + auto pumpAndCheckSkip = [&]() -> bool { + ::SetCursor(NULL); MSG msg; while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { @@ -2448,7 +2456,7 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) } GScreenClass::Instance.DoBlit(true, pSurface, nullptr); - Sleep(35); + Sleep(50); } } else if (this->pTargetFlyInSHP && this->pTargetFlyInSHP->Frames > 0) @@ -2471,20 +2479,9 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) DrawElement(pSurface, flyInRect, nullptr, this->pTargetFlyInSHP, pDrawer, f, -2, flag); GScreenClass::Instance.DoBlit(true, pSurface, nullptr); - Sleep(35); + Sleep(50); } } - - // Small delay after target settles - for (int d = 0; d < 3; ++d) - { - if (pumpAndCheckSkip()) - return; - this->UpdateAnimations(); - renderBase(static_cast(totalOverlays), static_cast(t + 1)); - GScreenClass::Instance.DoBlit(true, pSurface, nullptr); - Sleep(35); - } } } @@ -2498,6 +2495,14 @@ bool MapSelectionClass::Run() if (!pSurface) return false; + // Hide mouse cursor during video playback, overlays sweep, and target fly-ins (matching OpenTS Hide_Mouse()) + if (WWMouseClass::Instance) + WWMouseClass::Instance->HideCursor(); + + while (ShowCursor(FALSE) >= 0); + + ::SetCursor(NULL); + // Play Bink video if MapVQ is specified and exists if (!this->mapVQFileName.empty()) { @@ -2549,6 +2554,10 @@ bool MapSelectionClass::Run() // Ensure any audio is stopped before interactive loop starts StopMapSelAudio(); + // Intro presentation is finished and all target markers are placed on map; now show definitive cursor + while (ShowCursor(FALSE) >= 0); + ::SetCursor(NULL); + if (DisplayClass::Instance.CurrentSWTypeIndex != -1) DisplayClass::Instance.CurrentSWTypeIndex = -1; @@ -2722,6 +2731,14 @@ bool MapSelectionClass::Run() ThemeClass::Instance.Stop(true); } + // Hide mouse cursor when leaving map selection (matching OpenTS Hide_Mouse()) + if (WWMouseClass::Instance) + WWMouseClass::Instance->HideCursor(); + + while (ShowCursor(FALSE) >= 0); + + ::SetCursor(NULL); + if (this->selectedChoiceIdx >= 0 && this->selectedChoiceIdx < static_cast(this->choices.size())) { const auto& choice = this->choices[this->selectedChoiceIdx]; From 52fe2f87597ff7a79c8efc5e708262190b318e9d Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 3 Sep 2026 08:00:02 +0200 Subject: [PATCH 4/5] Refactor MapSelection to strictly follow Phobos code styleguide - Class members and fields renamed from camelCase to PascalCase - Enforce anonymous enum for return addresses in BinkMovie_CheckInput_MapSelection - Replace auto on primitive types and pointers with explicit types - Add space between empty curly braces { } - Consistent bracing across if-else branches and no nested braceless blocks - Mandatory blank lines separating return statements, variable declarations and code blocks - Enforce tab indentation and Allman brace formatting --- src/Misc/MapSelectionClass.cpp | 673 +++++++++++++++++---------------- src/Misc/MapSelectionClass.h | 132 +++---- 2 files changed, 411 insertions(+), 394 deletions(-) diff --git a/src/Misc/MapSelectionClass.cpp b/src/Misc/MapSelectionClass.cpp index 10a21b9d21..9eab5f8502 100644 --- a/src/Misc/MapSelectionClass.cpp +++ b/src/Misc/MapSelectionClass.cpp @@ -37,7 +37,7 @@ using LoadFile_FastCall_t = void* (__fastcall*)(const char* pFileName, bool bLoa static inline void* LoadGameFile(const char* pFileName, bool bLoadAsSHP = false) { - auto const pFunc = reinterpret_cast(0x5B40B0); + LoadFile_FastCall_t const pFunc = reinterpret_cast(0x5B40B0); return pFunc(pFileName, bLoadAsSHP); } @@ -53,11 +53,13 @@ static void StopMapSelAudio() static std::string_view Trim(std::string_view sv) { - auto s = sv.find_first_not_of(" \t\r\n"); + size_t s = sv.find_first_not_of(" \t\r\n"); + if (s == std::string_view::npos) - return {}; + return { }; + + size_t e = sv.find_last_not_of(" \t\r\n"); - auto e = sv.find_last_not_of(" \t\r\n"); return sv.substr(s, e - s + 1); } @@ -96,6 +98,7 @@ static void PlayMapSelAudio(const char* pAudioName) if (GetFileAttributesA(soundName.c_str()) != INVALID_FILE_ATTRIBUTES) { PlaySoundA(soundName.c_str(), NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT); + return; } @@ -110,6 +113,7 @@ static void PlayMapSelAudio(const char* pAudioName) testFile.ReadBytes(s_AudioMemBuf.data(), sz); testFile.Close(); PlaySoundA(reinterpret_cast(s_AudioMemBuf.data()), NULL, SND_MEMORY | SND_ASYNC | SND_NODEFAULT); + return; } testFile.Close(); @@ -121,6 +125,7 @@ static void PlayMapSelAudio(const char* pAudioName) if (vocIdx >= 0) { VocClass::PlayGlobal(vocIdx, 0x2000, volumeFloat); + return; } @@ -129,6 +134,7 @@ static void PlayMapSelAudio(const char* pAudioName) if (GetFileAttributesA(withWav.c_str()) != INVALID_FILE_ATTRIBUTES) { PlaySoundA(withWav.c_str(), NULL, SND_FILENAME | SND_ASYNC | SND_NODEFAULT); + return; } @@ -143,6 +149,7 @@ static void PlayMapSelAudio(const char* pAudioName) testWav.ReadBytes(s_AudioMemBuf2.data(), sz); testWav.Close(); PlaySoundA(reinterpret_cast(s_AudioMemBuf2.data()), NULL, SND_MEMORY | SND_ASYNC | SND_NODEFAULT); + return; } testWav.Close(); @@ -188,6 +195,7 @@ static void PlayMapSelSFX(const char* pAudioName) if (vocIdx >= 0) { VocClass::PlayGlobal(vocIdx, 0x2000, volumeFloat); + return; } @@ -255,26 +263,22 @@ static bool g_InMapSelectionBinkVideo = false; DEFINE_HOOK(0x432D03, BinkMovie_CheckInput_MapSelection, 0x8) { + enum { SkipMovie = 0x432E27, ContinueMovie = 0x432D0B }; + if (g_InMapSelectionBinkVideo) { // Only skip movie if ESC key is physically pressed (ignore mouse clicks) if ((GetAsyncKeyState(VK_ESCAPE) & 0x8000) != 0) - { - return 0x432E27; // Skip movie - } + return SkipMovie; else - { - return 0x432D0B; // Continue playing movie - } + return ContinueMovie; } // Default game behavior if (R->EAX() == 0) - { - return 0x432E27; - } + return SkipMovie; - return 0x432D0B; + return ContinueMovie; } static bool PlayMapSelBinkVideo(const char* pVideoName) @@ -305,10 +309,12 @@ static bool PlayMapSelBinkVideo(const char* pVideoName) g_InMapSelectionBinkVideo = true; Game::PlayMovie(movieName.c_str(), -1, -1, -1, -1, -1); g_InMapSelectionBinkVideo = false; + return true; } Debug::Log("[MapSelection] Bink video '%s' not found, skipping video playback.\n", bikNameLower.c_str()); + return false; } @@ -466,6 +472,7 @@ static ColorStruct ConvertSchemeColorToRGB(const ColorStruct& hslColor) { ColorStruct rgbColor { 255, 255, 255 }; reinterpret_cast(0x517440)(&hslColor, &rgbColor); + return rgbColor; } @@ -573,19 +580,19 @@ MapSelectionClass::MapSelectionClass() = default; MapSelectionClass::~MapSelectionClass() { - if (this->pPalette) + if (this->Palette) { - delete this->pPalette; - this->pPalette = nullptr; + delete this->Palette; + this->Palette = nullptr; } - if (this->pOverlayPalette && this->pOverlayPalette != this->pPalette) + if (this->OverlayPalette && this->OverlayPalette != this->Palette) { - delete this->pOverlayPalette; - this->pOverlayPalette = nullptr; + delete this->OverlayPalette; + this->OverlayPalette = nullptr; } - for (auto& anim : this->backgroundAnims) + for (auto& anim : this->BackgroundAnims) { - if (anim.Palette && anim.Palette != this->pPalette && anim.Palette != this->pOverlayPalette) + if (anim.Palette && anim.Palette != this->Palette && anim.Palette != this->OverlayPalette) { delete anim.Palette; anim.Palette = nullptr; @@ -609,6 +616,7 @@ static SHPStruct* LoadMapSelSHP(const char* pFileName) } Debug::Log("[MapSelection] LoadMapSelSHP '%s' -> %p\n", pFileName, pSHP); + return pSHP; } @@ -647,6 +655,7 @@ static ConvertClass* LoadMapSelPalette(const char* pFileName, DSurface* pSurface ConvertClass* pConvert = GameCreate(colorData, FileSystem::TEMPERAT_PAL, pSurface, 1, false); Debug::Log("[MapSelection] LoadMapSelPalette '%s' created ConvertClass %p\n", pFileName, pConvert); + return pConvert; } @@ -713,6 +722,7 @@ static bool LoadMapSelPCXData(const char* pFileName, MapSelectPCX& out) } Debug::Log("[MapSelection] Decoded clickmap PCX '%s' (%dx%d, %zu pixels)\n", pFileName, width, height, out.Pixels.size()); + return true; } @@ -777,19 +787,20 @@ static void EnsureMixFileLoaded(const char* pMixName) if (!pMixName || !pMixName[0]) return; - for (GenericNode* pNode = (GenericNode*)MixFileClass::MIXes.First(); pNode && pNode->IsValid(); pNode = pNode->Next()) + for (int i = 0; i < MixFileClass::Array.Count; ++i) { - auto pMix = reinterpret_cast(pNode); - if (pMix->FileName && _stricmp(pMix->FileName, pMixName) == 0) - { + MixFileClass* pMix = MixFileClass::Array.GetItemOrDefault(i); + + if (pMix && pMix->FileName && _stricmp(pMix->FileName, pMixName) == 0) return; - } } CCFileClass testFile(pMixName); + if (testFile.Exists()) { - auto pMix = GameCreate(pMixName); + MixFileClass* pMix = GameCreate(pMixName); + Debug::Log("[MapSelection] Mounted Mix '%s' (%p, CountFiles=%d)\n", pMixName, pMix, pMix ? pMix->CountFiles : 0); } } @@ -808,6 +819,7 @@ bool MapSelectionClass::OpenMapSelectionWindow(ScenarioClass* pScenario) } Debug::Log("[MapSelection] Initialize returned false, map selection aborted.\n"); + return false; } @@ -819,17 +831,17 @@ bool MapSelectionClass::Initialize(ScenarioClass* pScenario) EnsureMixFileLoaded("mapsel.mix"); EnsureMixFileLoaded("expandmd03.mix"); - this->pCurrentScenario = pScenario; + this->CurrentScenario = pScenario; this->LoadConfig(pScenario); - if (this->choices.empty()) + if (this->Choices.empty()) { Debug::Log("[MapSelection] No choices configured for '%s', bypassing.\n", pScenario->FileName); return false; } this->LoadAssets(); - this->isInitialized = true; + this->IsInitialized = true; return true; } @@ -882,7 +894,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) Debug::Log("[MapSelection] Active side section determined as: '[%s]'\n", sideSection.c_str()); // Find stage progression - std::string targetStage {}; + std::string targetStage { }; char stageBuf[128] = { 0 }; const char* curScen = pScenario->FileName; @@ -941,29 +953,29 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) } } - this->currentStageName = targetStage; - Debug::Log("[MapSelection] Selected active stage: '[%s]'\n", this->currentStageName.c_str()); + this->CurrentStageName = targetStage; + Debug::Log("[MapSelection] Selected active stage: '[%s]'\n", this->CurrentStageName.c_str()); - if (this->currentStageName.empty()) + if (this->CurrentStageName.empty()) return; - const char* pStage = this->currentStageName.c_str(); + const char* pStage = this->CurrentStageName.c_str(); char clickMapBuf[128] = { 0 }; ini.ReadString(pStage, "ClickMap", "", clickMapBuf, sizeof(clickMapBuf)); - this->clickMapFileName = clickMapBuf; + this->ClickMapFileName = clickMapBuf; char mapVQBuf[128] = { 0 }; ini.ReadString(pStage, "MapVQ", "", mapVQBuf, sizeof(mapVQBuf)); - this->mapVQFileName = mapVQBuf; + this->MapVQFileName = mapVQBuf; char voiceOverBuf[128] = { 0 }; ini.ReadString(pStage, "VoiceOver", "", voiceOverBuf, sizeof(voiceOverBuf)); - this->voiceOverFileName = voiceOverBuf; + this->VoiceOverFileName = voiceOverBuf; char themeBuf[128] = { 0 }; ini.ReadString(pStage, "Theme", "", themeBuf, sizeof(themeBuf)); - this->themeName = themeBuf; + this->ThemeName = themeBuf; // Determine default text color from active side rules (MessageTextColor -> Color -> White fallback) ColorStruct sideTextColor { 255, 255, 255 }; // White fallback @@ -1013,7 +1025,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) if (pSide) { // Check MessageTextColor from rulesmd.ini via SideExt - auto pSideExt = SideExt::TryFetch(pSide); + SideExt* pSideExt = SideExt::TryFetch(pSide); if (pSideExt && pSideExt->MessageTextColor >= 0 && pSideExt->MessageTextColor < ColorScheme::Array.Count) { ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(pSideExt->MessageTextColor); @@ -1104,7 +1116,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) char colorBuf[64] = { 0 }; ini.ReadString(pStage, "TextColor", "", colorBuf, sizeof(colorBuf)); - this->defaultTextColor = ParseColorString(colorBuf, sideTextColor); + this->DefaultTextColor = ParseColorString(colorBuf, sideTextColor); // Check Sounds section from stage, side, side-specific [Sounds], or global [Sounds] char soundsSecBuf[128] = { 0 }; @@ -1155,32 +1167,32 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) return std::string(buf); }; - this->overlaySound = readSoundKey("Overlay"); - this->targetFlyInSound = readSoundKey("TargetFlyIn"); - this->enterRegionSound = readSoundKey("EnterRegion"); - this->exitRegionSound = readSoundKey("ExitRegion"); - this->clickRegionSound = readSoundKey("ClickRegion"); - this->mouseOnMapSound = readSoundKey("MouseOnMapSound"); - this->mouseOffMapSound = readSoundKey("MouseOffMapSound"); - this->typeSound = readSoundKey("TypeSound"); + this->OverlaySound = readSoundKey("Overlay"); + this->TargetFlyInSound = readSoundKey("TargetFlyIn"); + this->EnterRegionSound = readSoundKey("EnterRegion"); + this->ExitRegionSound = readSoundKey("ExitRegion"); + this->ClickRegionSound = readSoundKey("ClickRegion"); + this->MouseOnMapSound = readSoundKey("MouseOnMapSound"); + this->MouseOffMapSound = readSoundKey("MouseOffMapSound"); + this->TypeSound = readSoundKey("TypeSound"); char palBuf[128] = { 0 }; ini.ReadString(pStage, "Palette", "mapsel.pal", palBuf, sizeof(palBuf)); - this->paletteFileName = palBuf[0] ? palBuf : "mapsel.pal"; + this->PaletteFileName = palBuf[0] ? palBuf : "mapsel.pal"; char ovrPalBuf[128] = { 0 }; ini.ReadString(pStage, "OverlayPalette", "msovrly.pal", ovrPalBuf, sizeof(ovrPalBuf)); - this->overlayPaletteFileName = ovrPalBuf[0] ? ovrPalBuf : "msovrly.pal"; + this->OverlayPaletteFileName = ovrPalBuf[0] ? ovrPalBuf : "msovrly.pal"; char bgPCXBuf[128] = { 0 }; ini.ReadString(pStage, "MapPCX", "", bgPCXBuf, sizeof(bgPCXBuf)); - this->backgroundPCXFileName = bgPCXBuf; + this->BackgroundPCXFileName = bgPCXBuf; char bgSHPBuf[128] = { 0 }; ini.ReadString(pStage, "Map", "", bgSHPBuf, sizeof(bgSHPBuf)); if (!bgSHPBuf[0]) ini.ReadString(pStage, "MapVQ", "", bgSHPBuf, sizeof(bgSHPBuf)); - this->backgroundFileName = bgSHPBuf; + this->BackgroundFileName = bgSHPBuf; char rectBuf[64] = { 0 }; ini.ReadString(pStage, "TextRect", "", rectBuf, sizeof(rectBuf)); @@ -1189,7 +1201,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) int rx = 0, ry = 0, rw = 0, rh = 0; if (sscanf_s(rectBuf, "%d,%d,%d,%d", &rx, &ry, &rw, &rh) == 4) { - this->textRectangle = { rx, ry, rw, rh }; + this->TextRectangle = { rx, ry, rw, rh }; } } @@ -1202,10 +1214,11 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) char* token = strtok_s(overlaysPCXBuf, ",", &context); while (token) { - while (*token == ' ') token++; + while (*token == ' ') + token++; if (*token) { - this->overlayPCXNames.push_back(token); + this->OverlayPCXNames.push_back(token); } token = strtok_s(nullptr, ",", &context); } @@ -1219,10 +1232,11 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) char* token = strtok_s(overlaysBuf, ",", &context); while (token) { - while (*token == ' ') token++; + while (*token == ' ') + token++; if (*token) { - this->overlaySHPNames.push_back(token); + this->OverlaySHPNames.push_back(token); } token = strtok_s(nullptr, ",", &context); } @@ -1239,10 +1253,11 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) char* token = strtok_s(flyInPCXBuf, ",", &context); while (token) { - while (*token == ' ') token++; + while (*token == ' ') + token++; if (*token) { - this->targetFlyInPCXNames.push_back(token); + this->TargetFlyInPCXNames.push_back(token); } token = strtok_s(nullptr, ",", &context); } @@ -1250,7 +1265,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) char flyInBuf[128] = { 0 }; ini.ReadString(pStage, "TargetFlyInAnim", "", flyInBuf, sizeof(flyInBuf)); - this->targetFlyInFileName = flyInBuf; + this->TargetFlyInFileName = flyInBuf; char markerPCXBuf[512] = { 0 }; ini.ReadString(pStage, "TargetMarkerAnimPCX", "", markerPCXBuf, sizeof(markerPCXBuf)); @@ -1262,10 +1277,11 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) char* token = strtok_s(markerPCXBuf, ",", &context); while (token) { - while (*token == ' ') token++; + while (*token == ' ') + token++; if (*token) { - this->targetMarkerPCXNames.push_back(token); + this->TargetMarkerPCXNames.push_back(token); } token = strtok_s(nullptr, ",", &context); } @@ -1275,7 +1291,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) ini.ReadString(pStage, "TargetMarkerAnim", "", markerBuf, sizeof(markerBuf)); if (!markerBuf[0]) ini.ReadString(pStage, "TargetMarker", "", markerBuf, sizeof(markerBuf)); - this->targetMarkerFileName = markerBuf; + this->TargetMarkerFileName = markerBuf; // Parse stage Targets list: Targets=,,,,,... std::vector stageTargets; @@ -1319,7 +1335,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) choice.StageName = choiceStage; // Map corresponding target coordinate from stage Targets list - size_t choiceIdxInStage = this->choices.size(); + size_t choiceIdxInStage = this->Choices.size(); if (choiceIdxInStage < stageTargets.size()) { choice.TargetCoord = stageTargets[choiceIdxInStage]; @@ -1342,20 +1358,20 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) choice.VoiceOver = buf; ini.ReadString(choiceStage, "EnterRegion", "", buf, sizeof(buf)); - choice.HoverSound = buf[0] ? buf : this->enterRegionSound; + choice.HoverSound = buf[0] ? buf : this->EnterRegionSound; ini.ReadString(choiceStage, "ClickRegion", "", buf, sizeof(buf)); - choice.ClickSound = buf[0] ? buf : this->clickRegionSound; + choice.ClickSound = buf[0] ? buf : this->ClickRegionSound; ini.ReadString(choiceStage, "TextColor", "", buf, sizeof(buf)); if (buf[0]) { - choice.TextColor = ParseColorString(buf, this->defaultTextColor); + choice.TextColor = ParseColorString(buf, this->DefaultTextColor); choice.HasCustomTextColor = true; } else { - choice.TextColor = this->defaultTextColor; + choice.TextColor = this->DefaultTextColor; } // Only fallback to sub-stage Targets if the active stage had no target for this choice @@ -1381,16 +1397,16 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) if (!choice.ScenarioPath.empty()) { Debug::Log("[MapSelection] Added choice %d (index=%d): Stage='%s', Scenario='%s', Target=(%d,%d)\n", - (int)this->choices.size() + 1, choice.Index, choice.StageName.c_str(), choice.ScenarioPath.c_str(), choice.TargetCoord.X, choice.TargetCoord.Y); - this->choices.push_back(choice); + (int)this->Choices.size() + 1, choice.Index, choice.StageName.c_str(), choice.ScenarioPath.c_str(), choice.TargetCoord.X, choice.TargetCoord.Y); + this->Choices.push_back(choice); } } - if (this->choices.empty() && scenBuf[0]) + if (this->Choices.empty() && scenBuf[0]) { MapSelectChoice choice; choice.Index = 1; - choice.StageName = this->currentStageName; + choice.StageName = this->CurrentStageName; choice.ScenarioPath = scenBuf; char buf[512] = { 0 }; @@ -1404,20 +1420,20 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) choice.VoiceOver = buf; ini.ReadString(pStage, "EnterRegion", "", buf, sizeof(buf)); - choice.HoverSound = buf[0] ? buf : this->enterRegionSound; + choice.HoverSound = buf[0] ? buf : this->EnterRegionSound; ini.ReadString(pStage, "ClickRegion", "", buf, sizeof(buf)); - choice.ClickSound = buf[0] ? buf : this->clickRegionSound; + choice.ClickSound = buf[0] ? buf : this->ClickRegionSound; ini.ReadString(pStage, "TextColor", "", buf, sizeof(buf)); if (buf[0]) { - choice.TextColor = ParseColorString(buf, this->defaultTextColor); + choice.TextColor = ParseColorString(buf, this->DefaultTextColor); choice.HasCustomTextColor = true; } else { - choice.TextColor = this->defaultTextColor; + choice.TextColor = this->DefaultTextColor; } ini.ReadString(pStage, "Targets", "", buf, sizeof(buf)); @@ -1432,7 +1448,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) } Debug::Log("[MapSelection] Added single default choice: Stage='%s', Scenario='%s'\n", choice.StageName.c_str(), choice.ScenarioPath.c_str()); - this->choices.push_back(choice); + this->Choices.push_back(choice); } // Parse animations from stage section @@ -1467,7 +1483,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) anim.TotalFrames = static_cast(anim.PCXFrames.size()); anim.CurrentFrame = 0; anim.Timer.Start(anim.FrameDelay); - this->backgroundAnims.push_back(anim); + this->BackgroundAnims.push_back(anim); continue; } } @@ -1501,7 +1517,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) { anim.PaletteName = animPal; } - this->backgroundAnims.push_back(anim); + this->BackgroundAnims.push_back(anim); } } } @@ -1541,7 +1557,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) anim.TotalFrames = static_cast(anim.PCXFrames.size()); anim.CurrentFrame = 0; anim.Timer.Start(anim.FrameDelay); - this->backgroundAnims.push_back(anim); + this->BackgroundAnims.push_back(anim); } } } @@ -1583,7 +1599,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) { anim.PaletteName = animPal; } - this->backgroundAnims.push_back(anim); + this->BackgroundAnims.push_back(anim); } } } @@ -1653,7 +1669,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) anim.TotalFrames = static_cast(anim.PCXFrames.size()); anim.CurrentFrame = 0; anim.Timer.Start(anim.FrameDelay); - this->backgroundAnims.push_back(anim); + this->BackgroundAnims.push_back(anim); } } } @@ -1685,7 +1701,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) { anim.PaletteName = animPal; } - this->backgroundAnims.push_back(anim); + this->BackgroundAnims.push_back(anim); } } } @@ -1698,31 +1714,31 @@ void MapSelectionClass::LoadAssets() DSurface* pSurface = DSurface::Hidden ? DSurface::Hidden : DSurface::Composite; // Palette for UI & Animations (default: MAPSEL.PAL) - const char* palName = !this->paletteFileName.empty() ? this->paletteFileName.c_str() : "mapsel.pal"; - this->pPalette = LoadMapSelPalette(palName, pSurface); + const char* palName = !this->PaletteFileName.empty() ? this->PaletteFileName.c_str() : "mapsel.pal"; + this->Palette = LoadMapSelPalette(palName, pSurface); // Palette for Overlays (default: MSOVRLY.PAL) - const char* ovrPalName = !this->overlayPaletteFileName.empty() ? this->overlayPaletteFileName.c_str() : "msovrly.pal"; - this->pOverlayPalette = LoadMapSelPalette(ovrPalName, pSurface); - if (!this->pOverlayPalette) - this->pOverlayPalette = this->pPalette; + const char* ovrPalName = !this->OverlayPaletteFileName.empty() ? this->OverlayPaletteFileName.c_str() : "msovrly.pal"; + this->OverlayPalette = LoadMapSelPalette(ovrPalName, pSurface); + if (!this->OverlayPalette) + this->OverlayPalette = this->Palette; // Background PCX (MapPCX takes preference over Map SHP) - if (!this->backgroundPCXFileName.empty()) + if (!this->BackgroundPCXFileName.empty()) { - PhobosPCXFile pcx(this->backgroundPCXFileName.c_str()); + PhobosPCXFile pcx(this->BackgroundPCXFileName.c_str()); if (pcx.Exists()) - this->pBackgroundPCX = pcx.GetSurface(); + this->BackgroundPCX = pcx.GetSurface(); } // Background SHP (Map) - if (!this->pBackgroundPCX && !this->backgroundFileName.empty()) + if (!this->BackgroundPCX && !this->BackgroundFileName.empty()) { - this->pBackgroundSHP = LoadMapSelSHP(this->backgroundFileName.c_str()); + this->BackgroundSHP = LoadMapSelSHP(this->BackgroundFileName.c_str()); } // Load custom palettes for animations if specified - for (auto& anim : this->backgroundAnims) + for (auto& anim : this->BackgroundAnims) { if (!anim.PaletteName.empty()) { @@ -1731,9 +1747,9 @@ void MapSelectionClass::LoadAssets() } // If no background was loaded, try deriving a map PCX from clickmap name - if (!this->pBackgroundPCX && !this->pBackgroundSHP && !this->clickMapFileName.empty()) + if (!this->BackgroundPCX && !this->BackgroundSHP && !this->ClickMapFileName.empty()) { - std::string mapPCX = this->clickMapFileName; + std::string mapPCX = this->ClickMapFileName; std::transform(mapPCX.begin(), mapPCX.end(), mapPCX.begin(), ::toupper); // GDICLK01.PCX -> GDIMAP01.PCX, NODCLK01.PCX -> NODMAP01.PCX @@ -1744,57 +1760,57 @@ void MapSelectionClass::LoadAssets() Debug::Log("[MapSelection] Trying derived background PCX '%s'\n", mapPCX.c_str()); PhobosPCXFile pcx(mapPCX.c_str()); if (pcx.Exists()) - this->pBackgroundPCX = pcx.GetSurface(); + this->BackgroundPCX = pcx.GetSurface(); } // Last resort: use the clickmap PCX itself as visual background - if (!this->pBackgroundPCX) + if (!this->BackgroundPCX) { - Debug::Log("[MapSelection] Using clickmap '%s' as visual background fallback\n", this->clickMapFileName.c_str()); - PhobosPCXFile pcx(this->clickMapFileName.c_str()); + Debug::Log("[MapSelection] Using clickmap '%s' as visual background fallback\n", this->ClickMapFileName.c_str()); + PhobosPCXFile pcx(this->ClickMapFileName.c_str()); if (pcx.Exists()) - this->pBackgroundPCX = pcx.GetSurface(); + this->BackgroundPCX = pcx.GetSurface(); } } // ClickMap PCX (decoded for hit-testing) - if (!this->clickMapFileName.empty()) + if (!this->ClickMapFileName.empty()) { - LoadMapSelPCXData(this->clickMapFileName.c_str(), this->clickMapData); + LoadMapSelPCXData(this->ClickMapFileName.c_str(), this->ClickMapData); } - if (this->clickMapData.Pixels.empty()) + if (this->ClickMapData.Pixels.empty()) { - LoadMapSelPCXData("GDICLK01.PCX", this->clickMapData); + LoadMapSelPCXData("GDICLK01.PCX", this->ClickMapData); } // Overlays (PCX takes preference over SHP) - if (!this->overlayPCXNames.empty()) + if (!this->OverlayPCXNames.empty()) { - for (const auto& name : this->overlayPCXNames) + for (const auto& name : this->OverlayPCXNames) { PhobosPCXFile pcx(name.c_str()); if (pcx.Exists()) { - this->overlayPCXs.push_back(pcx.GetSurface()); + this->OverlayPCXs.push_back(pcx.GetSurface()); } } } - if (this->overlayPCXs.empty() && !this->overlaySHPNames.empty()) + if (this->OverlayPCXs.empty() && !this->OverlaySHPNames.empty()) { - for (const auto& name : this->overlaySHPNames) + for (const auto& name : this->OverlaySHPNames) { SHPStruct* pSHP = LoadMapSelSHP(name.c_str()); if (pSHP) { - this->overlaySHPs.push_back(pSHP); + this->OverlaySHPs.push_back(pSHP); } } } // Target Fly-In (PCX takes preference over SHP) - if (!this->targetFlyInPCXNames.empty()) + if (!this->TargetFlyInPCXNames.empty()) { - for (const auto& name : this->targetFlyInPCXNames) + for (const auto& name : this->TargetFlyInPCXNames) { auto pFrames = GeneralUtils::GetAnimationPCX(name); if (pFrames && !pFrames->empty()) @@ -1802,21 +1818,21 @@ void MapSelectionClass::LoadAssets() for (const auto& frame : *pFrames) { if (frame.Exists()) - this->targetFlyInPCXSurfaces.push_back(frame.GetSurface()); + this->TargetFlyInPCXSurfaces.push_back(frame.GetSurface()); } } } } - if (this->targetFlyInPCXSurfaces.empty()) + if (this->TargetFlyInPCXSurfaces.empty()) { - const char* flyInName = !this->targetFlyInFileName.empty() ? this->targetFlyInFileName.c_str() : "TARGET1.SHP"; - this->pTargetFlyInSHP = LoadMapSelSHP(flyInName); + const char* flyInName = !this->TargetFlyInFileName.empty() ? this->TargetFlyInFileName.c_str() : "TARGET1.SHP"; + this->TargetFlyInSHP = LoadMapSelSHP(flyInName); } // Target Marker (PCX takes preference over SHP) - if (!this->targetMarkerPCXNames.empty()) + if (!this->TargetMarkerPCXNames.empty()) { - for (const auto& name : this->targetMarkerPCXNames) + for (const auto& name : this->TargetMarkerPCXNames) { auto pFrames = GeneralUtils::GetAnimationPCX(name); if (pFrames && !pFrames->empty()) @@ -1824,19 +1840,19 @@ void MapSelectionClass::LoadAssets() for (const auto& frame : *pFrames) { if (frame.Exists()) - this->targetMarkerPCXSurfaces.push_back(frame.GetSurface()); + this->TargetMarkerPCXSurfaces.push_back(frame.GetSurface()); } } } } - if (this->targetMarkerPCXSurfaces.empty()) + if (this->TargetMarkerPCXSurfaces.empty()) { - const char* markerName = !this->targetMarkerFileName.empty() ? this->targetMarkerFileName.c_str() : "TARGET2.SHP"; - this->pTargetMarkerSHP = LoadMapSelSHP(markerName); + const char* markerName = !this->TargetMarkerFileName.empty() ? this->TargetMarkerFileName.c_str() : "TARGET2.SHP"; + this->TargetMarkerSHP = LoadMapSelSHP(markerName); } // Target crosshairs animation timer - this->targetAnimTimer.Start(4); + this->TargetAnimTimer.Start(4); } void MapSelectionClass::CalculateLayout(DSurface* pSurface) @@ -1847,56 +1863,56 @@ void MapSelectionClass::CalculateLayout(DSurface* pSurface) int bgWidth = 640; int bgHeight = 400; - if (this->pBackgroundPCX) + if (this->BackgroundPCX) { - bgWidth = this->pBackgroundPCX->Width; - bgHeight = this->pBackgroundPCX->Height; + bgWidth = this->BackgroundPCX->Width; + bgHeight = this->BackgroundPCX->Height; } - else if (this->pBackgroundSHP) + else if (this->BackgroundSHP) { - bgWidth = this->pBackgroundSHP->Width; - bgHeight = this->pBackgroundSHP->Height; + bgWidth = this->BackgroundSHP->Width; + bgHeight = this->BackgroundSHP->Height; } - else if (this->clickMapData.Width > 0 && this->clickMapData.Height > 0) + else if (this->ClickMapData.Width > 0 && this->ClickMapData.Height > 0) { - bgWidth = this->clickMapData.Width; - bgHeight = this->clickMapData.Height; + bgWidth = this->ClickMapData.Width; + bgHeight = this->ClickMapData.Height; } int screenWidth = pSurface->Width; int screenHeight = pSurface->Height; - this->windowRectangle.X = (screenWidth - bgWidth) / 2; - this->windowRectangle.Y = (screenHeight - bgHeight) / 2; - this->windowRectangle.Width = bgWidth; - this->windowRectangle.Height = bgHeight; + this->WindowRectangle.X = (screenWidth - bgWidth) / 2; + this->WindowRectangle.Y = (screenHeight - bgHeight) / 2; + this->WindowRectangle.Width = bgWidth; + this->WindowRectangle.Height = bgHeight; Debug::Log("[MapSelection] Layout: Screen=(%dx%d), WindowRect=(%d,%d,%d,%d), TextRect=(%d,%d,%d,%d)\n", screenWidth, screenHeight, - this->windowRectangle.X, this->windowRectangle.Y, this->windowRectangle.Width, this->windowRectangle.Height, - this->textRectangle.X, this->textRectangle.Y, this->textRectangle.Width, this->textRectangle.Height); + this->WindowRectangle.X, this->WindowRectangle.Y, this->WindowRectangle.Width, this->WindowRectangle.Height, + this->TextRectangle.X, this->TextRectangle.Y, this->TextRectangle.Width, this->TextRectangle.Height); } int MapSelectionClass::GetChoiceIndexAtPoint(int screenX, int screenY) { - int relX = screenX - this->windowRectangle.X; - int relY = screenY - this->windowRectangle.Y; + int relX = screenX - this->WindowRectangle.X; + int relY = screenY - this->WindowRectangle.Y; - if (relX < 0 || relX >= this->windowRectangle.Width || relY < 0 || relY >= this->windowRectangle.Height) + if (relX < 0 || relX >= this->WindowRectangle.Width || relY < 0 || relY >= this->WindowRectangle.Height) return -1; // Clickmap pixel color index (exact OpenTS behavior) - if (!this->clickMapData.Pixels.empty()) + if (!this->ClickMapData.Pixels.empty()) { - if (relX >= 0 && relX < this->clickMapData.Width && relY >= 0 && relY < this->clickMapData.Height) + if (relX >= 0 && relX < this->ClickMapData.Width && relY >= 0 && relY < this->ClickMapData.Height) { - BYTE pixelColor = this->clickMapData.Pixels[relY * this->clickMapData.Width + relX]; + BYTE pixelColor = this->ClickMapData.Pixels[relY * this->ClickMapData.Width + relX]; // 0 and 255 are unselectable background/ocean pixels if (pixelColor > 0 && pixelColor < 255) { - for (size_t i = 0; i < this->choices.size(); ++i) + for (size_t i = 0; i < this->Choices.size(); ++i) { - if (this->choices[i].Index == static_cast(pixelColor)) + if (this->Choices[i].Index == static_cast(pixelColor)) return static_cast(i); } } @@ -1910,71 +1926,71 @@ int MapSelectionClass::GetChoiceIndexAtPoint(int screenX, int screenY) void MapSelectionClass::UpdateAnimations() { - for (auto& anim : this->backgroundAnims) + for (auto& anim : this->BackgroundAnims) { if (anim.Timer.Completed() && anim.TotalFrames > 1) { anim.CurrentFrame = (anim.CurrentFrame + 1) % anim.TotalFrames; anim.Timer.Start(anim.FrameDelay); - this->repaintAll = true; + this->RepaintAll = true; } } - if (this->targetAnimTimer.Completed()) + if (this->TargetAnimTimer.Completed()) { int totalFrames = 0; - if (!this->targetMarkerPCXSurfaces.empty()) + if (!this->TargetMarkerPCXSurfaces.empty()) { - totalFrames = static_cast(this->targetMarkerPCXSurfaces.size()); + totalFrames = static_cast(this->TargetMarkerPCXSurfaces.size()); } - else if (this->pTargetMarkerSHP) + else if (this->TargetMarkerSHP) { - totalFrames = static_cast(this->pTargetMarkerSHP->Frames); + totalFrames = static_cast(this->TargetMarkerSHP->Frames); } if (totalFrames > 1) { int half = totalFrames / 2; int activeCount = totalFrames - half; - this->idleTargetAnimFrame = (this->idleTargetAnimFrame + 1) % half; - this->activeTargetAnimFrame = half + ((this->activeTargetAnimFrame - half + 1) % activeCount); + this->IdleTargetAnimFrame = (this->IdleTargetAnimFrame + 1) % half; + this->ActiveTargetAnimFrame = half + ((this->ActiveTargetAnimFrame - half + 1) % activeCount); } else { - this->idleTargetAnimFrame = 0; - this->activeTargetAnimFrame = 0; + this->IdleTargetAnimFrame = 0; + this->ActiveTargetAnimFrame = 0; } - this->targetAnimTimer.Start(4); - this->repaintAll = true; + this->TargetAnimTimer.Start(4); + this->RepaintAll = true; } // Briefing typewriter text advance - if (this->hoveredChoiceIdx >= 0 && this->typewriterTimer.Completed()) + if (this->HoveredChoiceIdx >= 0 && this->TypewriterTimer.Completed()) { size_t totalChars = 0; - for (const auto& l : this->briefingLines) + for (const auto& l : this->BriefingLines) totalChars += l.length(); - if (static_cast(this->typewriterCharCount) < totalChars) + if (static_cast(this->TypewriterCharCount) < totalChars) { - this->typewriterCharCount += 1; - this->typewriterTimer.Start(1); - this->repaintAll = true; + this->TypewriterCharCount += 1; + this->TypewriterTimer.Start(1); + this->RepaintAll = true; // Play typing sound through game DirectSound mixer (never cuts off VoiceOver) - if (this->typeSoundTimer.Completed()) + if (this->TypeSoundTimer.Completed()) { float typeVol = 0.5f; - int typeIdx = GetTypewriterVocIndex(this->typeSound, typeVol); + int typeIdx = GetTypewriterVocIndex(this->TypeSound, typeVol); if (typeIdx >= 0) { VocClass::PlayGlobal(typeIdx, 0x2000, typeVol); } - else if (!this->typeSound.empty() && _stricmp(this->typeSound.c_str(), "none") != 0 && _stricmp(this->typeSound.c_str(), "no") != 0) + else if (!this->TypeSound.empty() && _stricmp(this->TypeSound.c_str(), "none") != 0 && _stricmp(this->TypeSound.c_str(), "no") != 0) { - PlayMapSelSFX(this->typeSound.c_str()); + PlayMapSelSFX(this->TypeSound.c_str()); } - this->typeSoundTimer.Start(3); + this->TypeSoundTimer.Start(3); } } } @@ -1988,36 +2004,36 @@ void MapSelectionClass::Render(DSurface* pSurface) // Fill background black for letterboxing pSurface->Fill(0); - ConvertClass* pDrawer = this->pPalette ? this->pPalette : FileSystem::PALETTE_PAL; - ConvertClass* pOverlayDrawer = this->pOverlayPalette ? this->pOverlayPalette : pDrawer; + ConvertClass* pDrawer = this->Palette ? this->Palette : FileSystem::PALETTE_PAL; + ConvertClass* pOverlayDrawer = this->OverlayPalette ? this->OverlayPalette : pDrawer; // Draw Background (PCX or SHP) - DrawElement(pSurface, this->windowRectangle, this->pBackgroundPCX, this->pBackgroundSHP, pDrawer, 0, -2); + DrawElement(pSurface, this->WindowRectangle, this->BackgroundPCX, this->BackgroundSHP, pDrawer, 0, -2); // Draw Overlays (World map / territory borders) - if (!this->overlayPCXs.empty()) + if (!this->OverlayPCXs.empty()) { - for (auto pPCX : this->overlayPCXs) + for (BSurface* pPCX : this->OverlayPCXs) { if (pPCX) { - DrawElement(pSurface, this->windowRectangle, pPCX, nullptr, nullptr, 0, -2); + DrawElement(pSurface, this->WindowRectangle, pPCX, nullptr, nullptr, 0, -2); } } } else { - for (auto pOverlaySHP : this->overlaySHPs) + for (SHPStruct* pOverlaySHP : this->OverlaySHPs) { if (pOverlaySHP) { - DrawElement(pSurface, this->windowRectangle, nullptr, pOverlaySHP, pOverlayDrawer, 0, -2); + DrawElement(pSurface, this->WindowRectangle, nullptr, pOverlaySHP, pOverlayDrawer, 0, -2); } } } // Draw Animation SHPs or PCX sequences (Faction Logo, Rotating Globe, Compass) - for (const auto& anim : this->backgroundAnims) + for (const auto& anim : this->BackgroundAnims) { if (!anim.PCXFrames.empty()) { @@ -2026,8 +2042,8 @@ void MapSelectionClass::Render(DSurface* pSurface) if (pFramePCX) { RectangleStruct animRect = { - this->windowRectangle.X + anim.X, - this->windowRectangle.Y + anim.Y, + this->WindowRectangle.X + anim.X, + this->WindowRectangle.Y + anim.Y, pFramePCX->Width, pFramePCX->Height }; @@ -2037,8 +2053,8 @@ void MapSelectionClass::Render(DSurface* pSurface) else if (anim.SHP) { RectangleStruct animRect = { - this->windowRectangle.X + anim.X, - this->windowRectangle.Y + anim.Y, + this->WindowRectangle.X + anim.X, + this->WindowRectangle.Y + anim.Y, anim.SHP->Width, anim.SHP->Height }; @@ -2048,20 +2064,20 @@ void MapSelectionClass::Render(DSurface* pSurface) } // Draw target markers / highlights for choices - for (size_t i = 0; i < this->choices.size(); ++i) + for (size_t i = 0; i < this->Choices.size(); ++i) { - const auto& choice = this->choices[i]; + const auto& choice = this->Choices[i]; if (choice.HasTargetCoord) { - int drawX = this->windowRectangle.X + choice.TargetCoord.X; - int drawY = this->windowRectangle.Y + choice.TargetCoord.Y; + int drawX = this->WindowRectangle.X + choice.TargetCoord.X; + int drawY = this->WindowRectangle.Y + choice.TargetCoord.Y; - if (!this->targetMarkerPCXSurfaces.empty()) + if (!this->TargetMarkerPCXSurfaces.empty()) { - int frame = (static_cast(i) == this->hoveredChoiceIdx) ? this->activeTargetAnimFrame : this->idleTargetAnimFrame; - if (frame >= 0 && frame < static_cast(this->targetMarkerPCXSurfaces.size())) + int frame = (static_cast(i) == this->HoveredChoiceIdx) ? this->ActiveTargetAnimFrame : this->IdleTargetAnimFrame; + if (frame >= 0 && frame < static_cast(this->TargetMarkerPCXSurfaces.size())) { - BSurface* pPCX = this->targetMarkerPCXSurfaces[frame]; + BSurface* pPCX = this->TargetMarkerPCXSurfaces[frame]; if (pPCX) { int halfW = pPCX->Width / 2; @@ -2071,17 +2087,17 @@ void MapSelectionClass::Render(DSurface* pSurface) } } } - else if (this->pTargetMarkerSHP) + else if (this->TargetMarkerSHP) { - int halfW = this->pTargetMarkerSHP->Width / 2; - int halfH = this->pTargetMarkerSHP->Height / 2; - RectangleStruct markerRect = { drawX - halfW, drawY - halfH, this->pTargetMarkerSHP->Width, this->pTargetMarkerSHP->Height }; - int frame = (static_cast(i) == this->hoveredChoiceIdx) ? this->activeTargetAnimFrame : this->idleTargetAnimFrame; - DrawElement(pSurface, markerRect, nullptr, this->pTargetMarkerSHP, pDrawer, frame, -2); + int halfW = this->TargetMarkerSHP->Width / 2; + int halfH = this->TargetMarkerSHP->Height / 2; + RectangleStruct markerRect = { drawX - halfW, drawY - halfH, this->TargetMarkerSHP->Width, this->TargetMarkerSHP->Height }; + int frame = (static_cast(i) == this->HoveredChoiceIdx) ? this->ActiveTargetAnimFrame : this->IdleTargetAnimFrame; + DrawElement(pSurface, markerRect, nullptr, this->TargetMarkerSHP, pDrawer, frame, -2); } else { - COLORREF color = (static_cast(i) == this->hoveredChoiceIdx) + COLORREF color = (static_cast(i) == this->HoveredChoiceIdx) ? Drawing::RGB_To_Int(255, 255, 80) : Drawing::RGB_To_Int(0, 255, 0); @@ -2097,13 +2113,13 @@ void MapSelectionClass::Render(DSurface* pSurface) void MapSelectionClass::DrawBriefing(DSurface* pSurface) { - if (!pSurface || this->hoveredChoiceIdx < 0 || this->briefingLines.empty()) + if (!pSurface || this->HoveredChoiceIdx < 0 || this->BriefingLines.empty()) return; - const auto& choice = this->choices[this->hoveredChoiceIdx]; + const auto& choice = this->Choices[this->HoveredChoiceIdx]; - int drawX = this->windowRectangle.X + this->textRectangle.X; - int drawY = this->windowRectangle.Y + this->textRectangle.Y; + int drawX = this->WindowRectangle.X + this->TextRectangle.X; + int drawY = this->WindowRectangle.Y + this->TextRectangle.Y; // Text rendering on the metallic HUD plate (TextRect: 92, 322, 332, 78) COLORREF textColor = Drawing::RGB_To_Int(choice.TextColor.R, choice.TextColor.G, choice.TextColor.B); @@ -2116,45 +2132,45 @@ void MapSelectionClass::DrawBriefing(DSurface* pSurface) TextPrintType style = (TextPrintType::FullShadow | TextPrintType::Point6Grad); size_t totalBriefingChars = 0; - for (const auto& l : this->briefingLines) + for (const auto& l : this->BriefingLines) totalBriefingChars += l.length(); - bool isTypingInProgress = static_cast(this->typewriterCharCount) < totalBriefingChars; + bool isTypingInProgress = static_cast(this->TypewriterCharCount) < totalBriefingChars; int lineHeight = 13; - int maxVisibleLines = (this->textRectangle.Height > 0) ? (this->textRectangle.Height / lineHeight) : 6; + int maxVisibleLines = (this->TextRectangle.Height > 0) ? (this->TextRectangle.Height / lineHeight) : 6; if (maxVisibleLines < 1) maxVisibleLines = 1; // Determine which line the typing cursor is currently on int activeLine = 0; int charAcc = 0; - for (size_t l = 0; l < this->briefingLines.size(); ++l) + for (size_t l = 0; l < this->BriefingLines.size(); ++l) { - charAcc += static_cast(this->briefingLines[l].length()); + charAcc += static_cast(this->BriefingLines[l].length()); activeLine = static_cast(l); - if (this->typewriterCharCount <= charAcc) + if (this->TypewriterCharCount <= charAcc) break; } // Auto-scroll / roll upward: old lines disappear as new lines appear below int startLine = (activeLine >= maxVisibleLines) ? (activeLine - maxVisibleLines + 1) : 0; - int endLine = std::min(static_cast(this->briefingLines.size()), startLine + maxVisibleLines); + int endLine = std::min(static_cast(this->BriefingLines.size()), startLine + maxVisibleLines); int charsAccountedFor = 0; for (int l = 0; l < startLine; ++l) - charsAccountedFor += static_cast(this->briefingLines[l].length()); + charsAccountedFor += static_cast(this->BriefingLines[l].length()); for (int l = startLine; l < endLine; ++l) { - int lineLen = static_cast(this->briefingLines[l].length()); - int charsToDrawOnLine = std::clamp(this->typewriterCharCount - charsAccountedFor, 0, lineLen); + int lineLen = static_cast(this->BriefingLines[l].length()); + int charsToDrawOnLine = std::clamp(this->TypewriterCharCount - charsAccountedFor, 0, lineLen); charsAccountedFor += lineLen; if (charsToDrawOnLine <= 0) continue; - const std::wstring& line = this->briefingLines[l]; + const std::wstring& line = this->BriefingLines[l]; std::wstring visiblePart = line.substr(0, charsToDrawOnLine); bool isActiveTypingLine = (l == activeLine && isTypingInProgress); @@ -2166,7 +2182,7 @@ void MapSelectionClass::DrawBriefing(DSurface* pSurface) RectangleStruct lineBounds = { drawX, lineY, - this->textRectangle.Width, + this->TextRectangle.Width, lineHeight + 4 }; Point2D relPoint = { 0, 0 }; @@ -2194,17 +2210,17 @@ void MapSelectionClass::DrawBriefing(DSurface* pSurface) if (!settled.empty()) { - RectangleStruct r1 = { drawX, lineY, this->textRectangle.Width, lineHeight + 4 }; + RectangleStruct r1 = { drawX, lineY, this->TextRectangle.Width, lineHeight + 4 }; Point2D p1 = { 0, 0 }; pSurface->DrawText(settled.c_str(), &r1, &p1, textColor, 0, style); } { - RectangleStruct r2 = { drawX + wSettled, lineY, this->textRectangle.Width - wSettled, lineHeight + 4 }; + RectangleStruct r2 = { drawX + wSettled, lineY, this->TextRectangle.Width - wSettled, lineHeight + 4 }; Point2D p2 = { 0, 0 }; pSurface->DrawText(fadeChar.c_str(), &r2, &p2, glowFade, 0, style); } { - RectangleStruct r3 = { drawX + wFade, lineY, this->textRectangle.Width - wFade, lineHeight + 4 }; + RectangleStruct r3 = { drawX + wFade, lineY, this->TextRectangle.Width - wFade, lineHeight + 4 }; Point2D p3 = { 0, 0 }; pSurface->DrawText(headChar.c_str(), &r3, &p3, glowWhite, 0, style); } @@ -2218,17 +2234,17 @@ void MapSelectionClass::DrawBriefing(DSurface* pSurface) if (BitFont::Instance) BitFont::Instance->GetTextDimension(fadeChar.c_str(), &wFade, &hDummy, 1000); - RectangleStruct r1 = { drawX, lineY, this->textRectangle.Width, lineHeight + 4 }; + RectangleStruct r1 = { drawX, lineY, this->TextRectangle.Width, lineHeight + 4 }; Point2D p1 = { 0, 0 }; pSurface->DrawText(fadeChar.c_str(), &r1, &p1, glowFade, 0, style); - RectangleStruct r2 = { drawX + wFade, lineY, this->textRectangle.Width - wFade, lineHeight + 4 }; + RectangleStruct r2 = { drawX + wFade, lineY, this->TextRectangle.Width - wFade, lineHeight + 4 }; Point2D p2 = { 0, 0 }; pSurface->DrawText(headChar.c_str(), &r2, &p2, glowWhite, 0, style); } else if (len == 1) { - RectangleStruct r = { drawX, lineY, this->textRectangle.Width, lineHeight + 4 }; + RectangleStruct r = { drawX, lineY, this->TextRectangle.Width, lineHeight + 4 }; Point2D p = { 0, 0 }; pSurface->DrawText(visiblePart.c_str(), &r, &p, glowWhite, 0, style); } @@ -2241,8 +2257,8 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) if (!pSurface) return; - ConvertClass* pDrawer = this->pPalette ? this->pPalette : FileSystem::PALETTE_PAL; - ConvertClass* pOverlayDrawer = this->pOverlayPalette ? this->pOverlayPalette : pDrawer; + ConvertClass* pDrawer = this->Palette ? this->Palette : FileSystem::PALETTE_PAL; + ConvertClass* pOverlayDrawer = this->OverlayPalette ? this->OverlayPalette : pDrawer; if (WWMouseClass::Instance) WWMouseClass::Instance->HideCursor(); @@ -2278,32 +2294,32 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) pSurface->Fill(0); // Background - DrawElement(pSurface, this->windowRectangle, this->pBackgroundPCX, this->pBackgroundSHP, pDrawer, 0, -2); + DrawElement(pSurface, this->WindowRectangle, this->BackgroundPCX, this->BackgroundSHP, pDrawer, 0, -2); // Completed Overlays (drawn solid) - if (!this->overlayPCXs.empty()) + if (!this->OverlayPCXs.empty()) { - for (int o = 0; o < completedOverlays && o < static_cast(this->overlayPCXs.size()); ++o) + for (int o = 0; o < completedOverlays && o < static_cast(this->OverlayPCXs.size()); ++o) { - if (this->overlayPCXs[o]) + if (this->OverlayPCXs[o]) { - DrawElement(pSurface, this->windowRectangle, this->overlayPCXs[o], nullptr, nullptr, 0, -2); + DrawElement(pSurface, this->WindowRectangle, this->OverlayPCXs[o], nullptr, nullptr, 0, -2); } } } else { - for (int o = 0; o < completedOverlays && o < static_cast(this->overlaySHPs.size()); ++o) + for (int o = 0; o < completedOverlays && o < static_cast(this->OverlaySHPs.size()); ++o) { - if (this->overlaySHPs[o]) + if (this->OverlaySHPs[o]) { - DrawElement(pSurface, this->windowRectangle, nullptr, this->overlaySHPs[o], pOverlayDrawer, 0, -2); + DrawElement(pSurface, this->WindowRectangle, nullptr, this->OverlaySHPs[o], pOverlayDrawer, 0, -2); } } } // Background Anims - for (const auto& anim : this->backgroundAnims) + for (const auto& anim : this->BackgroundAnims) { if (!anim.PCXFrames.empty()) { @@ -2312,8 +2328,8 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) if (pFramePCX) { RectangleStruct animRect = { - this->windowRectangle.X + anim.X, - this->windowRectangle.Y + anim.Y, + this->WindowRectangle.X + anim.X, + this->WindowRectangle.Y + anim.Y, pFramePCX->Width, pFramePCX->Height }; @@ -2323,8 +2339,8 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) else if (anim.SHP) { RectangleStruct animRect = { - this->windowRectangle.X + anim.X, - this->windowRectangle.Y + anim.Y, + this->WindowRectangle.X + anim.X, + this->WindowRectangle.Y + anim.Y, anim.SHP->Width, anim.SHP->Height }; @@ -2334,19 +2350,19 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) } // Completed Targets - for (int t = 0; t < completedTargets && t < static_cast(this->choices.size()); ++t) + for (int t = 0; t < completedTargets && t < static_cast(this->Choices.size()); ++t) { - const auto& choice = this->choices[t]; + const auto& choice = this->Choices[t]; if (choice.HasTargetCoord) { - int drawX = this->windowRectangle.X + choice.TargetCoord.X; - int drawY = this->windowRectangle.Y + choice.TargetCoord.Y; + int drawX = this->WindowRectangle.X + choice.TargetCoord.X; + int drawY = this->WindowRectangle.Y + choice.TargetCoord.Y; - if (!this->targetMarkerPCXSurfaces.empty()) + if (!this->TargetMarkerPCXSurfaces.empty()) { - if (this->idleTargetAnimFrame >= 0 && this->idleTargetAnimFrame < static_cast(this->targetMarkerPCXSurfaces.size())) + if (this->IdleTargetAnimFrame >= 0 && this->IdleTargetAnimFrame < static_cast(this->TargetMarkerPCXSurfaces.size())) { - BSurface* pPCX = this->targetMarkerPCXSurfaces[this->idleTargetAnimFrame]; + BSurface* pPCX = this->TargetMarkerPCXSurfaces[this->IdleTargetAnimFrame]; if (pPCX) { int halfW = pPCX->Width / 2; @@ -2356,12 +2372,12 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) } } } - else if (this->pTargetMarkerSHP) + else if (this->TargetMarkerSHP) { - int halfW = this->pTargetMarkerSHP->Width / 2; - int halfH = this->pTargetMarkerSHP->Height / 2; - RectangleStruct markerRect = { drawX - halfW, drawY - halfH, this->pTargetMarkerSHP->Width, this->pTargetMarkerSHP->Height }; - DrawElement(pSurface, markerRect, nullptr, this->pTargetMarkerSHP, pDrawer, this->idleTargetAnimFrame, -2); + int halfW = this->TargetMarkerSHP->Width / 2; + int halfH = this->TargetMarkerSHP->Height / 2; + RectangleStruct markerRect = { drawX - halfW, drawY - halfH, this->TargetMarkerSHP->Width, this->TargetMarkerSHP->Height }; + DrawElement(pSurface, markerRect, nullptr, this->TargetMarkerSHP, pDrawer, this->IdleTargetAnimFrame, -2); } } } @@ -2374,14 +2390,14 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) BlitterFlags::None }; - size_t totalOverlays = !this->overlayPCXs.empty() ? this->overlayPCXs.size() : this->overlaySHPs.size(); + size_t totalOverlays = !this->OverlayPCXs.empty() ? this->OverlayPCXs.size() : this->OverlaySHPs.size(); // Animate Overlays fade-in for (size_t o = 0; o < totalOverlays; ++o) { - if (!this->overlaySound.empty()) + if (!this->OverlaySound.empty()) { - PlayMapSelSFX(this->overlaySound.c_str()); + PlayMapSelSFX(this->OverlaySound.c_str()); } for (int stage = 0; stage < 4; ++stage) @@ -2392,16 +2408,16 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) this->UpdateAnimations(); renderBase(static_cast(o), 0); - if (!this->overlayPCXs.empty()) + if (!this->OverlayPCXs.empty()) { - if (this->overlayPCXs[o]) + if (this->OverlayPCXs[o]) { - DrawElement(pSurface, this->windowRectangle, this->overlayPCXs[o], nullptr, nullptr, 0, -2); + DrawElement(pSurface, this->WindowRectangle, this->OverlayPCXs[o], nullptr, nullptr, 0, -2); } } - else if (o < this->overlaySHPs.size() && this->overlaySHPs[o]) + else if (o < this->OverlaySHPs.size() && this->OverlaySHPs[o]) { - DrawElement(pSurface, this->windowRectangle, nullptr, this->overlaySHPs[o], pOverlayDrawer, 0, -2, fadeFlags[stage]); + DrawElement(pSurface, this->WindowRectangle, nullptr, this->OverlaySHPs[o], pOverlayDrawer, 0, -2, fadeFlags[stage]); } GScreenClass::Instance.DoBlit(true, pSurface, nullptr); @@ -2421,23 +2437,23 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) } // Animate Target Fly-Ins - for (size_t t = 0; t < this->choices.size(); ++t) + for (size_t t = 0; t < this->Choices.size(); ++t) { - const auto& choice = this->choices[t]; + const auto& choice = this->Choices[t]; if (!choice.HasTargetCoord) continue; - if (!this->targetFlyInSound.empty()) + if (!this->TargetFlyInSound.empty()) { - PlayMapSelSFX(this->targetFlyInSound.c_str()); + PlayMapSelSFX(this->TargetFlyInSound.c_str()); } - int drawX = this->windowRectangle.X + choice.TargetCoord.X; - int drawY = this->windowRectangle.Y + choice.TargetCoord.Y; + int drawX = this->WindowRectangle.X + choice.TargetCoord.X; + int drawY = this->WindowRectangle.Y + choice.TargetCoord.Y; - if (!this->targetFlyInPCXSurfaces.empty()) + if (!this->TargetFlyInPCXSurfaces.empty()) { - int maxFrames = static_cast(this->targetFlyInPCXSurfaces.size()); + int maxFrames = static_cast(this->TargetFlyInPCXSurfaces.size()); for (int f = 0; f < maxFrames; ++f) { if (pumpAndCheckSkip()) @@ -2446,7 +2462,7 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) this->UpdateAnimations(); renderBase(static_cast(totalOverlays), static_cast(t)); - BSurface* pFlyInPCX = this->targetFlyInPCXSurfaces[f]; + BSurface* pFlyInPCX = this->TargetFlyInPCXSurfaces[f]; if (pFlyInPCX) { int halfW = pFlyInPCX->Width / 2; @@ -2459,13 +2475,13 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) Sleep(50); } } - else if (this->pTargetFlyInSHP && this->pTargetFlyInSHP->Frames > 0) + else if (this->TargetFlyInSHP && this->TargetFlyInSHP->Frames > 0) { - int halfW = this->pTargetFlyInSHP->Width / 2; - int halfH = this->pTargetFlyInSHP->Height / 2; - RectangleStruct flyInRect = { drawX - halfW, drawY - halfH, this->pTargetFlyInSHP->Width, this->pTargetFlyInSHP->Height }; + int halfW = this->TargetFlyInSHP->Width / 2; + int halfH = this->TargetFlyInSHP->Height / 2; + RectangleStruct flyInRect = { drawX - halfW, drawY - halfH, this->TargetFlyInSHP->Width, this->TargetFlyInSHP->Height }; - int maxFrames = this->pTargetFlyInSHP->Frames; + int maxFrames = this->TargetFlyInSHP->Frames; for (int f = 0; f < maxFrames; ++f) { if (pumpAndCheckSkip()) @@ -2476,7 +2492,7 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) // Draw fly-in frame BlitterFlags flag = (f < 4) ? fadeFlags[f] : BlitterFlags::None; - DrawElement(pSurface, flyInRect, nullptr, this->pTargetFlyInSHP, pDrawer, f, -2, flag); + DrawElement(pSurface, flyInRect, nullptr, this->TargetFlyInSHP, pDrawer, f, -2, flag); GScreenClass::Instance.DoBlit(true, pSurface, nullptr); Sleep(50); @@ -2504,9 +2520,9 @@ bool MapSelectionClass::Run() ::SetCursor(NULL); // Play Bink video if MapVQ is specified and exists - if (!this->mapVQFileName.empty()) + if (!this->MapVQFileName.empty()) { - PlayMapSelBinkVideo(this->mapVQFileName.c_str()); + PlayMapSelBinkVideo(this->MapVQFileName.c_str()); // Drain any leftover input (e.g. ESC or mouse clicks used to skip video) MSG drainMsg; @@ -2528,9 +2544,9 @@ bool MapSelectionClass::Run() this->PlayIntroSequence(pSurface); // Play Map Selection Theme Song (only if Theme= is explicitly specified) - if (!this->themeName.empty()) + if (!this->ThemeName.empty()) { - int themeIdx = ThemeClass::Instance.FindIndex(this->themeName.c_str()); + int themeIdx = ThemeClass::Instance.FindIndex(this->ThemeName.c_str()); if (themeIdx >= 0) { ThemeClass::Instance.Play(themeIdx); @@ -2578,13 +2594,13 @@ bool MapSelectionClass::Run() bool wasLButtonDown = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; bool wasMouseOnMap = false; - this->hoveredChoiceIdx = -1; - this->lastHoveredChoiceIdx = -1; - this->selectedChoiceIdx = -1; + this->HoveredChoiceIdx = -1; + this->LastHoveredChoiceIdx = -1; + this->SelectedChoiceIdx = -1; - Debug::Log("[MapSelection] Entering modal Run loop (choices count: %zu)\n", this->choices.size()); + Debug::Log("[MapSelection] Entering modal Run loop (choices count: %zu)\n", this->Choices.size()); - while (this->selectedChoiceIdx < 0) + while (this->SelectedChoiceIdx < 0) { MSG msg; while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) @@ -2606,62 +2622,62 @@ bool MapSelectionClass::Run() mousePos.Y = WWMouseClass::Instance->GetY(); } - bool isMouseOnMap = (mousePos.X >= this->windowRectangle.X && - mousePos.X < (this->windowRectangle.X + this->windowRectangle.Width) && - mousePos.Y >= this->windowRectangle.Y && - mousePos.Y < (this->windowRectangle.Y + this->windowRectangle.Height)); + bool isMouseOnMap = (mousePos.X >= this->WindowRectangle.X && + mousePos.X < (this->WindowRectangle.X + this->WindowRectangle.Width) && + mousePos.Y >= this->WindowRectangle.Y && + mousePos.Y < (this->WindowRectangle.Y + this->WindowRectangle.Height)); if (isMouseOnMap != wasMouseOnMap) { - if (isMouseOnMap && !this->mouseOnMapSound.empty()) + if (isMouseOnMap && !this->MouseOnMapSound.empty()) { - PlayMapSelSFX(this->mouseOnMapSound.c_str()); + PlayMapSelSFX(this->MouseOnMapSound.c_str()); } - else if (!isMouseOnMap && !this->mouseOffMapSound.empty()) + else if (!isMouseOnMap && !this->MouseOffMapSound.empty()) { - PlayMapSelSFX(this->mouseOffMapSound.c_str()); + PlayMapSelSFX(this->MouseOffMapSound.c_str()); } wasMouseOnMap = isMouseOnMap; } - this->hoveredChoiceIdx = isMouseOnMap ? this->GetChoiceIndexAtPoint(mousePos.X, mousePos.Y) : -1; + this->HoveredChoiceIdx = isMouseOnMap ? this->GetChoiceIndexAtPoint(mousePos.X, mousePos.Y) : -1; - if (this->hoveredChoiceIdx != this->lastHoveredChoiceIdx) + if (this->HoveredChoiceIdx != this->LastHoveredChoiceIdx) { // Stop previous VoiceOver immediately if moving off or switching regions StopMapSelAudio(); - this->typewriterCharCount = 0; - this->typewriterTimer.Start(1); - this->typeSoundTimer.Start(3); + this->TypewriterCharCount = 0; + this->TypewriterTimer.Start(1); + this->TypeSoundTimer.Start(3); // Always restart active target marker animation from its initial frame upon hover enter - int totalMarkerFrames = !this->targetMarkerPCXSurfaces.empty() - ? static_cast(this->targetMarkerPCXSurfaces.size()) - : (this->pTargetMarkerSHP ? static_cast(this->pTargetMarkerSHP->Frames) : 0); - this->activeTargetAnimFrame = totalMarkerFrames > 1 ? (totalMarkerFrames / 2) : 0; - this->targetAnimTimer.Start(4); + int totalMarkerFrames = !this->TargetMarkerPCXSurfaces.empty() + ? static_cast(this->TargetMarkerPCXSurfaces.size()) + : (this->TargetMarkerSHP ? static_cast(this->TargetMarkerSHP->Frames) : 0); + this->ActiveTargetAnimFrame = totalMarkerFrames > 1 ? (totalMarkerFrames / 2) : 0; + this->TargetAnimTimer.Start(4); - if (this->hoveredChoiceIdx >= 0 && this->hoveredChoiceIdx < static_cast(this->choices.size())) + if (this->HoveredChoiceIdx >= 0 && this->HoveredChoiceIdx < static_cast(this->Choices.size())) { - const auto& choice = this->choices[this->hoveredChoiceIdx]; + const auto& choice = this->Choices[this->HoveredChoiceIdx]; std::wstring fullText = ResolveCSFOrText(choice.Description); if (fullText.empty()) fullText = ResolveCSFOrText(choice.Summary); if (fullText.empty()) fullText = std::wstring(choice.StageName.begin(), choice.StageName.end()); - size_t charsPerLine = (this->textRectangle.Width > 0) ? std::max(20, static_cast(this->textRectangle.Width / 7)) : 46; - this->briefingLines = WordWrapText(fullText, charsPerLine); + size_t charsPerLine = (this->TextRectangle.Width > 0) ? std::max(20, static_cast(this->TextRectangle.Width / 7)) : 46; + this->BriefingLines = WordWrapText(fullText, charsPerLine); // Play hover / enter region SFX sound first if (!choice.HoverSound.empty()) { PlayMapSelSFX(choice.HoverSound.c_str()); } - else if (!this->enterRegionSound.empty()) + else if (!this->EnterRegionSound.empty()) { - PlayMapSelSFX(this->enterRegionSound.c_str()); + PlayMapSelSFX(this->EnterRegionSound.c_str()); } // Play VoiceOver speech @@ -2672,41 +2688,41 @@ bool MapSelectionClass::Run() } else { - this->briefingLines.clear(); - if (this->lastHoveredChoiceIdx >= 0 && !this->exitRegionSound.empty()) + this->BriefingLines.clear(); + if (this->LastHoveredChoiceIdx >= 0 && !this->ExitRegionSound.empty()) { - PlayMapSelSFX(this->exitRegionSound.c_str()); + PlayMapSelSFX(this->ExitRegionSound.c_str()); } } - this->lastHoveredChoiceIdx = this->hoveredChoiceIdx; - this->repaintAll = true; + this->LastHoveredChoiceIdx = this->HoveredChoiceIdx; + this->RepaintAll = true; } bool isLButtonDown = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; if (isLButtonDown && !wasLButtonDown) { // Only select if the mouse is hovering over a valid choice on the map - if (this->hoveredChoiceIdx >= 0 && this->hoveredChoiceIdx < static_cast(this->choices.size())) + if (this->HoveredChoiceIdx >= 0 && this->HoveredChoiceIdx < static_cast(this->Choices.size())) { int clickedChoice = this->GetChoiceIndexAtPoint(mousePos.X, mousePos.Y); - if (clickedChoice == this->hoveredChoiceIdx) + if (clickedChoice == this->HoveredChoiceIdx) { - this->selectedChoiceIdx = clickedChoice; + this->SelectedChoiceIdx = clickedChoice; Debug::Log("[MapSelection] Region clicked on hovered choice %d ('%s')\n", - this->selectedChoiceIdx, this->choices[this->selectedChoiceIdx].StageName.c_str()); + this->SelectedChoiceIdx, this->Choices[this->SelectedChoiceIdx].StageName.c_str()); // Immediately cut off VoiceOver speech StopMapSelAudio(); - const auto& chosen = this->choices[this->selectedChoiceIdx]; + const auto& chosen = this->Choices[this->SelectedChoiceIdx]; if (!chosen.ClickSound.empty()) { PlayMapSelSFX(chosen.ClickSound.c_str()); } - else if (!this->clickRegionSound.empty()) + else if (!this->ClickRegionSound.empty()) { - PlayMapSelSFX(this->clickRegionSound.c_str()); + PlayMapSelSFX(this->ClickRegionSound.c_str()); } break; } @@ -2726,7 +2742,7 @@ bool MapSelectionClass::Run() // Ensure any playing VoiceOver audio and custom theme music are stopped when loading begins StopMapSelAudio(); - if (!this->themeName.empty()) + if (!this->ThemeName.empty()) { ThemeClass::Instance.Stop(true); } @@ -2739,19 +2755,20 @@ bool MapSelectionClass::Run() ::SetCursor(NULL); - if (this->selectedChoiceIdx >= 0 && this->selectedChoiceIdx < static_cast(this->choices.size())) + if (this->SelectedChoiceIdx >= 0 && this->SelectedChoiceIdx < static_cast(this->Choices.size())) { - const auto& choice = this->choices[this->selectedChoiceIdx]; + const auto& choice = this->Choices[this->SelectedChoiceIdx]; Debug::Log("[MapSelection] Final selected mission: '%s' (Scenario: '%s')\n", choice.StageName.c_str(), choice.ScenarioPath.c_str()); - if (this->pCurrentScenario && !choice.ScenarioPath.empty()) + if (this->CurrentScenario && !choice.ScenarioPath.empty()) { - this->pCurrentScenario->Stage = 0; - strncpy(this->pCurrentScenario->FileName, choice.ScenarioPath.c_str(), 0x104u); - this->pCurrentScenario->FileName[259] = '\0'; - strncpy(this->pCurrentScenario->NextScenario, choice.ScenarioPath.c_str(), 0x104u); - this->pCurrentScenario->NextScenario[259] = '\0'; + this->CurrentScenario->Stage = 0; + strncpy(this->CurrentScenario->FileName, choice.ScenarioPath.c_str(), 0x104u); + this->CurrentScenario->FileName[259] = '\0'; + strncpy(this->CurrentScenario->NextScenario, choice.ScenarioPath.c_str(), 0x104u); + this->CurrentScenario->NextScenario[259] = '\0'; } + return true; } diff --git a/src/Misc/MapSelectionClass.h b/src/Misc/MapSelectionClass.h index 9c3455a5c9..f11f2d7596 100644 --- a/src/Misc/MapSelectionClass.h +++ b/src/Misc/MapSelectionClass.h @@ -15,13 +15,13 @@ class ScenarioClass; struct MapSelectChoice { int Index { 0 }; - std::string StageName {}; - std::string ScenarioPath {}; - std::string Description {}; - std::string Summary {}; - std::string VoiceOver {}; - std::string HoverSound {}; - std::string ClickSound {}; + std::string StageName { }; + std::string ScenarioPath { }; + std::string Description { }; + std::string Summary { }; + std::string VoiceOver { }; + std::string HoverSound { }; + std::string ClickSound { }; Point2D TargetCoord { 0, 0 }; bool HasTargetCoord { false }; ColorStruct TextColor { 255, 239, 99 }; @@ -32,14 +32,14 @@ struct MapSelectAnim { SHPStruct* SHP { nullptr }; std::vector PCXFrames; - std::string PaletteName {}; + std::string PaletteName { }; ConvertClass* Palette { nullptr }; int X { 0 }; int Y { 0 }; int FrameDelay { 5 }; int CurrentFrame { 0 }; int TotalFrames { 0 }; - SysTimerClass Timer {}; + SysTimerClass Timer { }; }; struct MapSelectPCX @@ -70,74 +70,74 @@ class MapSelectionClass void DrawBriefing(DSurface* pSurface); void PlayIntroSequence(DSurface* pSurface); - ScenarioClass* pCurrentScenario { nullptr }; - std::string currentStageName {}; - std::string selectedScenarioPath {}; + ScenarioClass* CurrentScenario { nullptr }; + std::string CurrentStageName { }; + std::string SelectedScenarioPath { }; // Layout & resolution - RectangleStruct windowRectangle { 0, 0, 800, 600 }; - RectangleStruct textRectangle { 92, 322, 332, 78 }; + RectangleStruct WindowRectangle { 0, 0, 800, 600 }; + RectangleStruct TextRectangle { 92, 322, 332, 78 }; // Background & Visual Assets - std::string backgroundFileName {}; - std::string backgroundPCXFileName {}; - std::string clickMapFileName {}; - std::string paletteFileName { "mapsel.pal" }; - std::string overlayPaletteFileName { "msovrly.pal" }; - ConvertClass* pPalette { nullptr }; - ConvertClass* pOverlayPalette { nullptr }; - BSurface* pBackgroundPCX { nullptr }; - SHPStruct* pBackgroundSHP { nullptr }; - - std::string targetMarkerFileName {}; - std::string targetFlyInFileName {}; - std::vector targetMarkerPCXNames; - std::vector targetFlyInPCXNames; - SHPStruct* pTargetMarkerSHP { nullptr }; - SHPStruct* pTargetFlyInSHP { nullptr }; - std::vector targetMarkerPCXSurfaces; - std::vector targetFlyInPCXSurfaces; - - int idleTargetAnimFrame { 0 }; - int activeTargetAnimFrame { 32 }; - SysTimerClass targetAnimTimer {}; - MapSelectPCX clickMapData {}; - - std::vector overlaySHPNames; - std::vector overlayPCXNames; - std::vector overlaySHPs; - std::vector overlayPCXs; - - std::vector backgroundAnims; + std::string BackgroundFileName { }; + std::string BackgroundPCXFileName { }; + std::string ClickMapFileName { }; + std::string PaletteFileName { "mapsel.pal" }; + std::string OverlayPaletteFileName { "msovrly.pal" }; + ConvertClass* Palette { nullptr }; + ConvertClass* OverlayPalette { nullptr }; + BSurface* BackgroundPCX { nullptr }; + SHPStruct* BackgroundSHP { nullptr }; + + std::string TargetMarkerFileName { }; + std::string TargetFlyInFileName { }; + std::vector TargetMarkerPCXNames; + std::vector TargetFlyInPCXNames; + SHPStruct* TargetMarkerSHP { nullptr }; + SHPStruct* TargetFlyInSHP { nullptr }; + std::vector TargetMarkerPCXSurfaces; + std::vector TargetFlyInPCXSurfaces; + + int IdleTargetAnimFrame { 0 }; + int ActiveTargetAnimFrame { 32 }; + SysTimerClass TargetAnimTimer { }; + MapSelectPCX ClickMapData { }; + + std::vector OverlaySHPNames; + std::vector OverlayPCXNames; + std::vector OverlaySHPs; + std::vector OverlayPCXs; + + std::vector BackgroundAnims; // Sound & Audio & Video - std::string mapVQFileName {}; - std::string voiceOverFileName {}; - std::string overlaySound {}; - std::string targetFlyInSound {}; - std::string enterRegionSound {}; - std::string exitRegionSound {}; - std::string clickRegionSound {}; - std::string mouseOnMapSound {}; - std::string mouseOffMapSound {}; - std::string typeSound {}; - std::string themeName {}; + std::string MapVQFileName { }; + std::string VoiceOverFileName { }; + std::string OverlaySound { }; + std::string TargetFlyInSound { }; + std::string EnterRegionSound { }; + std::string ExitRegionSound { }; + std::string ClickRegionSound { }; + std::string MouseOnMapSound { }; + std::string MouseOffMapSound { }; + std::string TypeSound { }; + std::string ThemeName { }; // Choices / Progression - std::vector choices; - int hoveredChoiceIdx { -1 }; - int selectedChoiceIdx { -1 }; - int lastHoveredChoiceIdx { -2 }; + std::vector Choices; + int HoveredChoiceIdx { -1 }; + int SelectedChoiceIdx { -1 }; + int LastHoveredChoiceIdx { -2 }; // Briefing typewriter animation - int typewriterCharCount { 0 }; - SysTimerClass typewriterTimer {}; - SysTimerClass typeSoundTimer {}; - std::vector briefingLines; + int TypewriterCharCount { 0 }; + SysTimerClass TypewriterTimer { }; + SysTimerClass TypeSoundTimer { }; + std::vector BriefingLines; - ColorStruct defaultTextColor { 255, 239, 99 }; + ColorStruct DefaultTextColor { 255, 239, 99 }; // State - bool repaintAll { true }; - bool isInitialized { false }; + bool RepaintAll { true }; + bool IsInitialized { false }; }; From f24138ce823cbea335f11e91785d882e4086d02b Mon Sep 17 00:00:00 2001 From: FS-21 Date: Thu, 3 Sep 2026 08:14:33 +0200 Subject: [PATCH 5/5] MapSelection: enforce braceless single-line blocks and spacing per styleguide --- src/Misc/MapSelectionClass.cpp | 227 +++++++++++++-------------------- 1 file changed, 88 insertions(+), 139 deletions(-) diff --git a/src/Misc/MapSelectionClass.cpp b/src/Misc/MapSelectionClass.cpp index 9eab5f8502..e145c96751 100644 --- a/src/Misc/MapSelectionClass.cpp +++ b/src/Misc/MapSelectionClass.cpp @@ -46,9 +46,7 @@ static void StopMapSelAudio() PlaySoundA(NULL, NULL, 0); AudioStream* pStream = AudioStream::Instance; if (pStream) - { pStream->PlayWAV("", false); - } } static std::string_view Trim(std::string_view sv) @@ -76,9 +74,8 @@ static void PlayMapSelAudio(const char* pAudioName) { int vol = atoi(std::string(Trim(rawAudio.substr(commaPos + 1))).c_str()); if (vol > 0) - { volumeFloat = std::clamp(vol, 1, 100) / 100.0f; - } + soundName = Trim(rawAudio.substr(0, commaPos)); } else @@ -116,6 +113,7 @@ static void PlayMapSelAudio(const char* pAudioName) return; } + testFile.Close(); } } @@ -152,6 +150,7 @@ static void PlayMapSelAudio(const char* pAudioName) return; } + testWav.Close(); } } @@ -169,9 +168,8 @@ static void PlayMapSelSFX(const char* pAudioName) { int vol = atoi(std::string(Trim(rawAudio.substr(commaPos + 1))).c_str()); if (vol > 0) - { volumeFloat = std::clamp(vol, 1, 100) / 100.0f; - } + soundName = Trim(rawAudio.substr(0, commaPos)); } else @@ -232,6 +230,7 @@ static int GetTypewriterVocIndex(const std::string& customTypeSound, float& outV return s_DefaultTypeVocIdx; } } + s_DefaultTypeVocIdx = -1; return s_DefaultTypeVocIdx; } @@ -243,9 +242,8 @@ static int GetTypewriterVocIndex(const std::string& customTypeSound, float& outV { int vol = atoi(std::string(Trim(rawAudio.substr(commaPos + 1))).c_str()); if (vol > 0) - { outVolume = std::clamp(vol, 1, 100) / 100.0f; - } + soundName = Trim(rawAudio.substr(0, commaPos)); } else @@ -292,9 +290,7 @@ static bool PlayMapSelBinkVideo(const char* pVideoName) { std::string ext = movieName.substr(dotPos); if (_stricmp(ext.c_str(), ".vqa") == 0 || _stricmp(ext.c_str(), ".bik") == 0) - { movieName = movieName.substr(0, dotPos); - } } std::string bikNameLower = movieName + ".bik"; @@ -346,6 +342,7 @@ static void LoadExtraCSFFile(const char* pFileName) file.Close(); } } + if (raw.size() < 24) return; @@ -385,6 +382,7 @@ static void LoadExtraCSFFile(const char* pFileName) WORD ch = *reinterpret_cast(&raw[pos + c * 2]); value.push_back(static_cast(~ch)); } + pos += strLen * 2; if (strSig == 0x53545257) // "WRTS" @@ -397,11 +395,10 @@ static void LoadExtraCSFFile(const char* pFileName) } if (s == 0) - { g_ExtraCSFMap[labelName] = value; - } } } + Debug::Log("[MapSelection] Loaded %zu strings from extra CSF '%s'\n", g_ExtraCSFMap.size(), pFileName); } @@ -409,6 +406,7 @@ static void EnsureExtraCSFLoaded() { if (g_ExtraCSFLoaded) return; + g_ExtraCSFLoaded = true; LoadExtraCSFFile("stringtable00.csf"); @@ -433,20 +431,18 @@ static std::vector WordWrapText(const std::wstring& text, size_t m if (!word.empty()) { if (currentLine.empty()) - { currentLine = word; - } else if (currentLine.length() + 1 + word.length() <= maxCharsPerLine) - { currentLine += L" " + word; - } else { lines.push_back(currentLine); currentLine = word; } + word.clear(); } + if (c == L'\n') { if (!currentLine.empty()) @@ -461,10 +457,10 @@ static std::vector WordWrapText(const std::wstring& text, size_t m word.push_back(c); } } + if (!currentLine.empty()) - { lines.push_back(currentLine); - } + return lines; } @@ -499,9 +495,7 @@ static ColorStruct ParseColorString(const char* pStr, const ColorStruct& default { ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(schemeIdx); if (pScheme) - { return ConvertSchemeColorToRGB(pScheme->BaseColor); - } } if (isdigit(static_cast(s[0]))) @@ -511,9 +505,7 @@ static ColorStruct ParseColorString(const char* pStr, const ColorStruct& default { ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(idx); if (pScheme) - { return ConvertSchemeColorToRGB(pScheme->BaseColor); - } } } @@ -585,11 +577,13 @@ MapSelectionClass::~MapSelectionClass() delete this->Palette; this->Palette = nullptr; } + if (this->OverlayPalette && this->OverlayPalette != this->Palette) { delete this->OverlayPalette; this->OverlayPalette = nullptr; } + for (auto& anim : this->BackgroundAnims) { if (anim.Palette && anim.Palette != this->Palette && anim.Palette != this->OverlayPalette) @@ -610,9 +604,7 @@ static SHPStruct* LoadMapSelSHP(const char* pFileName) { CCFileClass file(pFileName); if (file.Exists()) - { pSHP = static_cast(file.ReadWholeFile()); - } } Debug::Log("[MapSelection] LoadMapSelSHP '%s' -> %p\n", pFileName, pSHP); @@ -711,12 +703,11 @@ static bool LoadMapSelPCXData(const char* pFileName, MapSelectPCX& out) if (pos < raw.size()) val = raw[pos++]; } + for (int c = 0; c < count && x < bytesPerLine; ++c, ++x) { if (x < width) - { out.Pixels[y * width + x] = val; - } } } } @@ -735,11 +726,15 @@ static bool MatchScenarioNames(const char* a, const char* b) return true; const char* baseA = strrchr(a, '/'); - if (!baseA) baseA = strrchr(a, '\\'); + if (!baseA) + baseA = strrchr(a, '\\'); + baseA = baseA ? baseA + 1 : a; const char* baseB = strrchr(b, '/'); - if (!baseB) baseB = strrchr(b, '\\'); + if (!baseB) + baseB = strrchr(b, '\\'); + baseB = baseB ? baseB + 1 : b; return _stricmp(baseA, baseB) == 0; @@ -779,6 +774,7 @@ static bool DrawElement( ); return true; } + return false; } @@ -814,9 +810,7 @@ bool MapSelectionClass::OpenMapSelectionWindow(ScenarioClass* pScenario) MapSelectionClass mapSelect; if (mapSelect.Initialize(pScenario)) - { return mapSelect.Run(); - } Debug::Log("[MapSelection] Initialize returned false, map selection aborted.\n"); @@ -903,13 +897,9 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) Debug::Log("[MapSelection] Matching stage for curScen='%s', nextScen='%s'\n", curScen ? curScen : "", nextScen ? nextScen : ""); if (nextScen && nextScen[0] && ini.GetSection(nextScen)) - { targetStage = nextScen; - } else if (curScen && curScen[0] && ini.GetSection(curScen)) - { targetStage = curScen; - } if (targetStage.empty()) { @@ -983,42 +973,54 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) SideClass* pSide = nullptr; if (sideIdx >= 0 && sideIdx < SideClass::Array.Count) - { pSide = SideClass::Array.GetItemOrDefault(sideIdx); - } + if (!pSide) - { pSide = SideClass::Find(sideSection.c_str()); - } + if (!pSide) { for (int s = 0; s < SideClass::Array.Count; ++s) { SideClass* pCheck = SideClass::Array.GetItemOrDefault(s); - if (!pCheck) continue; + if (!pCheck) + continue; + if ((pCheck->ID && _stricmp(pCheck->ID, sideSection.c_str()) == 0) || (pCheck->Name && _stricmp(pCheck->Name, sideSection.c_str()) == 0)) + { pSide = pCheck; break; } } } + if (!pSide) { if (_stricmp(sideSection.c_str(), "Nod") == 0) { pSide = SideClass::Find("TSNodSide"); - if (!pSide) pSide = SideClass::Find("Nod"); - if (!pSide) pSide = SideClass::Find("Soviets"); - if (!pSide) pSide = SideClass::Find("Soviet"); + if (!pSide) + pSide = SideClass::Find("Nod"); + + if (!pSide) + pSide = SideClass::Find("Soviets"); + + if (!pSide) + pSide = SideClass::Find("Soviet"); } else if (_stricmp(sideSection.c_str(), "GDI") == 0) { pSide = SideClass::Find("TSGDISide"); - if (!pSide) pSide = SideClass::Find("GDI"); - if (!pSide) pSide = SideClass::Find("Allies"); - if (!pSide) pSide = SideClass::Find("Allied"); + if (!pSide) + pSide = SideClass::Find("GDI"); + + if (!pSide) + pSide = SideClass::Find("Allies"); + + if (!pSide) + pSide = SideClass::Find("Allied"); } } @@ -1066,9 +1068,8 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) { HouseClass* pHouse = HouseClass::CurrentPlayer; if (!pHouse && pScenario && pScenario->HumanPlayerHouseTypeIndex >= 0 && pScenario->HumanPlayerHouseTypeIndex < HouseClass::Array.Count) - { pHouse = HouseClass::Array.GetItemOrDefault(pScenario->HumanPlayerHouseTypeIndex); - } + if (pHouse && pHouse->ColorSchemeIndex >= 0 && pHouse->ColorSchemeIndex < ColorScheme::Array.Count) { ColorScheme* pScheme = ColorScheme::Array.GetItemOrDefault(pHouse->ColorSchemeIndex); @@ -1126,9 +1127,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) std::string resolvedSoundsSec; if (soundsSecBuf[0]) - { resolvedSoundsSec = soundsSecBuf; - } else { std::string sideSoundsNamed = "Sounds" + sideSection; @@ -1137,18 +1136,13 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) sprintf_s(sideSoundsIndexed, "Sounds%d", sideIdx); if (ini.GetSection(sideSoundsNamed.c_str())) - { resolvedSoundsSec = sideSoundsNamed; - } else if (sideSoundsIndexed[0] && ini.GetSection(sideSoundsIndexed)) - { resolvedSoundsSec = sideSoundsIndexed; - } else if (ini.GetSection("Sounds")) - { resolvedSoundsSec = "Sounds"; - } } + const char* pSoundsSec = !resolvedSoundsSec.empty() ? resolvedSoundsSec.c_str() : nullptr; auto readSoundKey = [&](const char* key) -> std::string { @@ -1156,14 +1150,11 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) ini.ReadString(pStage, key, "", buf, sizeof(buf)); if (!buf[0] && pSoundsSec) - { ini.ReadString(pSoundsSec, key, "", buf, sizeof(buf)); - } if (!buf[0] && ini.GetSection("Sounds")) - { ini.ReadString("Sounds", key, "", buf, sizeof(buf)); - } + return std::string(buf); }; @@ -1192,6 +1183,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) ini.ReadString(pStage, "Map", "", bgSHPBuf, sizeof(bgSHPBuf)); if (!bgSHPBuf[0]) ini.ReadString(pStage, "MapVQ", "", bgSHPBuf, sizeof(bgSHPBuf)); + this->BackgroundFileName = bgSHPBuf; char rectBuf[64] = { 0 }; @@ -1200,9 +1192,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) { int rx = 0, ry = 0, rw = 0, rh = 0; if (sscanf_s(rectBuf, "%d,%d,%d,%d", &rx, &ry, &rw, &rh) == 4) - { this->TextRectangle = { rx, ry, rw, rh }; - } } // Overlays (PCX takes precedence over SHP if provided) @@ -1216,10 +1206,10 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) { while (*token == ' ') token++; + if (*token) - { this->OverlayPCXNames.push_back(token); - } + token = strtok_s(nullptr, ",", &context); } } @@ -1234,10 +1224,10 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) { while (*token == ' ') token++; + if (*token) - { this->OverlaySHPNames.push_back(token); - } + token = strtok_s(nullptr, ",", &context); } } @@ -1247,6 +1237,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) ini.ReadString(pStage, "TargetFlyInAnimPCX", "", flyInPCXBuf, sizeof(flyInPCXBuf)); if (!flyInPCXBuf[0]) ini.ReadString(pStage, "TargetFlyInPCX", "", flyInPCXBuf, sizeof(flyInPCXBuf)); + if (flyInPCXBuf[0]) { char* context = nullptr; @@ -1255,10 +1246,10 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) { while (*token == ' ') token++; + if (*token) - { this->TargetFlyInPCXNames.push_back(token); - } + token = strtok_s(nullptr, ",", &context); } } @@ -1271,6 +1262,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) ini.ReadString(pStage, "TargetMarkerAnimPCX", "", markerPCXBuf, sizeof(markerPCXBuf)); if (!markerPCXBuf[0]) ini.ReadString(pStage, "TargetMarkerPCX", "", markerPCXBuf, sizeof(markerPCXBuf)); + if (markerPCXBuf[0]) { char* context = nullptr; @@ -1279,10 +1271,10 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) { while (*token == ' ') token++; + if (*token) - { this->TargetMarkerPCXNames.push_back(token); - } + token = strtok_s(nullptr, ",", &context); } } @@ -1291,6 +1283,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) ini.ReadString(pStage, "TargetMarkerAnim", "", markerBuf, sizeof(markerBuf)); if (!markerBuf[0]) ini.ReadString(pStage, "TargetMarker", "", markerBuf, sizeof(markerBuf)); + this->TargetMarkerFileName = markerBuf; // Parse stage Targets list: Targets=,,,,,... @@ -1309,9 +1302,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) char* tokX = strtok_s(nullptr, ",", &context); char* tokY = strtok_s(nullptr, ",", &context); if (tokX && tokY) - { stageTargets.push_back({ atoi(tokX), atoi(tokY) }); - } } } } @@ -1478,6 +1469,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) if (frame.Exists()) anim.PCXFrames.push_back(frame.GetSurface()); } + if (!anim.PCXFrames.empty()) { anim.TotalFrames = static_cast(anim.PCXFrames.size()); @@ -1514,9 +1506,8 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) anim.TotalFrames = pSHP->Frames; anim.Timer.Start(anim.FrameDelay); if (parsed >= 5 && animPal[0]) - { anim.PaletteName = animPal; - } + this->BackgroundAnims.push_back(anim); } } @@ -1552,6 +1543,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) if (frame.Exists()) anim.PCXFrames.push_back(frame.GetSurface()); } + if (!anim.PCXFrames.empty()) { anim.TotalFrames = static_cast(anim.PCXFrames.size()); @@ -1596,9 +1588,8 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) anim.TotalFrames = pSHP->Frames; anim.Timer.Start(anim.FrameDelay); if (parsed >= 5 && animPal[0]) - { anim.PaletteName = animPal; - } + this->BackgroundAnims.push_back(anim); } } @@ -1620,6 +1611,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) sprintf_s(sideSecBuf, "Anims%d", sideIdx); parseSHPAnimSection(sideSecBuf); } + if (!sideSection.empty()) { std::string sideNameSecPCX = "Anims" + sideSection + "PCX"; @@ -1664,6 +1656,7 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) if (frame.Exists()) anim.PCXFrames.push_back(frame.GetSurface()); } + if (!anim.PCXFrames.empty()) { anim.TotalFrames = static_cast(anim.PCXFrames.size()); @@ -1698,9 +1691,8 @@ void MapSelectionClass::LoadConfig(ScenarioClass* pScenario) anim.TotalFrames = pSHP->Frames; anim.Timer.Start(anim.FrameDelay); if (parsed >= 5 && animPal[0]) - { anim.PaletteName = animPal; - } + this->BackgroundAnims.push_back(anim); } } @@ -1733,17 +1725,13 @@ void MapSelectionClass::LoadAssets() // Background SHP (Map) if (!this->BackgroundPCX && !this->BackgroundFileName.empty()) - { this->BackgroundSHP = LoadMapSelSHP(this->BackgroundFileName.c_str()); - } // Load custom palettes for animations if specified for (auto& anim : this->BackgroundAnims) { if (!anim.PaletteName.empty()) - { anim.Palette = LoadMapSelPalette(anim.PaletteName.c_str(), pSurface); - } } // If no background was loaded, try deriving a map PCX from clickmap name @@ -1775,13 +1763,10 @@ void MapSelectionClass::LoadAssets() // ClickMap PCX (decoded for hit-testing) if (!this->ClickMapFileName.empty()) - { LoadMapSelPCXData(this->ClickMapFileName.c_str(), this->ClickMapData); - } + if (this->ClickMapData.Pixels.empty()) - { LoadMapSelPCXData("GDICLK01.PCX", this->ClickMapData); - } // Overlays (PCX takes preference over SHP) if (!this->OverlayPCXNames.empty()) @@ -1790,20 +1775,17 @@ void MapSelectionClass::LoadAssets() { PhobosPCXFile pcx(name.c_str()); if (pcx.Exists()) - { this->OverlayPCXs.push_back(pcx.GetSurface()); - } } } + if (this->OverlayPCXs.empty() && !this->OverlaySHPNames.empty()) { for (const auto& name : this->OverlaySHPNames) { SHPStruct* pSHP = LoadMapSelSHP(name.c_str()); if (pSHP) - { this->OverlaySHPs.push_back(pSHP); - } } } @@ -1823,6 +1805,7 @@ void MapSelectionClass::LoadAssets() } } } + if (this->TargetFlyInPCXSurfaces.empty()) { const char* flyInName = !this->TargetFlyInFileName.empty() ? this->TargetFlyInFileName.c_str() : "TARGET1.SHP"; @@ -1845,6 +1828,7 @@ void MapSelectionClass::LoadAssets() } } } + if (this->TargetMarkerPCXSurfaces.empty()) { const char* markerName = !this->TargetMarkerFileName.empty() ? this->TargetMarkerFileName.c_str() : "TARGET2.SHP"; @@ -1917,6 +1901,7 @@ int MapSelectionClass::GetChoiceIndexAtPoint(int screenX, int screenY) } } } + // Never select anything outside the designated mission color regions return -1; } @@ -1940,13 +1925,9 @@ void MapSelectionClass::UpdateAnimations() { int totalFrames = 0; if (!this->TargetMarkerPCXSurfaces.empty()) - { totalFrames = static_cast(this->TargetMarkerPCXSurfaces.size()); - } else if (this->TargetMarkerSHP) - { totalFrames = static_cast(this->TargetMarkerSHP->Frames); - } if (totalFrames > 1) { @@ -1960,6 +1941,7 @@ void MapSelectionClass::UpdateAnimations() this->IdleTargetAnimFrame = 0; this->ActiveTargetAnimFrame = 0; } + this->TargetAnimTimer.Start(4); this->RepaintAll = true; } @@ -1983,13 +1965,10 @@ void MapSelectionClass::UpdateAnimations() float typeVol = 0.5f; int typeIdx = GetTypewriterVocIndex(this->TypeSound, typeVol); if (typeIdx >= 0) - { VocClass::PlayGlobal(typeIdx, 0x2000, typeVol); - } else if (!this->TypeSound.empty() && _stricmp(this->TypeSound.c_str(), "none") != 0 && _stricmp(this->TypeSound.c_str(), "no") != 0) - { PlayMapSelSFX(this->TypeSound.c_str()); - } + this->TypeSoundTimer.Start(3); } } @@ -2016,9 +1995,7 @@ void MapSelectionClass::Render(DSurface* pSurface) for (BSurface* pPCX : this->OverlayPCXs) { if (pPCX) - { DrawElement(pSurface, this->WindowRectangle, pPCX, nullptr, nullptr, 0, -2); - } } } else @@ -2026,9 +2003,7 @@ void MapSelectionClass::Render(DSurface* pSurface) for (SHPStruct* pOverlaySHP : this->OverlaySHPs) { if (pOverlaySHP) - { DrawElement(pSurface, this->WindowRectangle, nullptr, pOverlaySHP, pOverlayDrawer, 0, -2); - } } } @@ -2214,11 +2189,13 @@ void MapSelectionClass::DrawBriefing(DSurface* pSurface) Point2D p1 = { 0, 0 }; pSurface->DrawText(settled.c_str(), &r1, &p1, textColor, 0, style); } + { RectangleStruct r2 = { drawX + wSettled, lineY, this->TextRectangle.Width - wSettled, lineHeight + 4 }; Point2D p2 = { 0, 0 }; pSurface->DrawText(fadeChar.c_str(), &r2, &p2, glowFade, 0, style); } + { RectangleStruct r3 = { drawX + wFade, lineY, this->TextRectangle.Width - wFade, lineHeight + 4 }; Point2D p3 = { 0, 0 }; @@ -2275,17 +2252,18 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) { if (msg.message == WM_QUIT) return true; + TranslateMessage(&msg); DispatchMessage(&msg); } + Game::CallBack(); HWND hGameWnd = Game::hWnd; HWND hActiveWnd = GetForegroundWindow(); bool isWindowActive = (hGameWnd && (hActiveWnd == hGameWnd || IsChild(hGameWnd, hActiveWnd))); if (isWindowActive && ((GetAsyncKeyState(VK_LBUTTON) & 0x8000) || (GetAsyncKeyState(VK_ESCAPE) & 0x8000))) - { return true; - } + return false; }; @@ -2302,9 +2280,7 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) for (int o = 0; o < completedOverlays && o < static_cast(this->OverlayPCXs.size()); ++o) { if (this->OverlayPCXs[o]) - { DrawElement(pSurface, this->WindowRectangle, this->OverlayPCXs[o], nullptr, nullptr, 0, -2); - } } } else @@ -2312,9 +2288,7 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) for (int o = 0; o < completedOverlays && o < static_cast(this->OverlaySHPs.size()); ++o) { if (this->OverlaySHPs[o]) - { DrawElement(pSurface, this->WindowRectangle, nullptr, this->OverlaySHPs[o], pOverlayDrawer, 0, -2); - } } } @@ -2396,9 +2370,7 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) for (size_t o = 0; o < totalOverlays; ++o) { if (!this->OverlaySound.empty()) - { PlayMapSelSFX(this->OverlaySound.c_str()); - } for (int stage = 0; stage < 4; ++stage) { @@ -2411,14 +2383,10 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) if (!this->OverlayPCXs.empty()) { if (this->OverlayPCXs[o]) - { DrawElement(pSurface, this->WindowRectangle, this->OverlayPCXs[o], nullptr, nullptr, 0, -2); - } } else if (o < this->OverlaySHPs.size() && this->OverlaySHPs[o]) - { DrawElement(pSurface, this->WindowRectangle, nullptr, this->OverlaySHPs[o], pOverlayDrawer, 0, -2, fadeFlags[stage]); - } GScreenClass::Instance.DoBlit(true, pSurface, nullptr); Sleep(50); @@ -2429,6 +2397,7 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) { if (pumpAndCheckSkip()) return; + this->UpdateAnimations(); renderBase(static_cast(o + 1), 0); GScreenClass::Instance.DoBlit(true, pSurface, nullptr); @@ -2444,9 +2413,7 @@ void MapSelectionClass::PlayIntroSequence(DSurface* pSurface) continue; if (!this->TargetFlyInSound.empty()) - { PlayMapSelSFX(this->TargetFlyInSound.c_str()); - } int drawX = this->WindowRectangle.X + choice.TargetCoord.X; int drawY = this->WindowRectangle.Y + choice.TargetCoord.Y; @@ -2506,8 +2473,10 @@ bool MapSelectionClass::Run() DSurface* pSurface = DSurface::Hidden; if (!pSurface) pSurface = DSurface::Composite; + if (!pSurface) pSurface = DSurface::Primary; + if (!pSurface) return false; @@ -2532,9 +2501,7 @@ bool MapSelectionClass::Run() DispatchMessage(&drainMsg); } while ((GetAsyncKeyState(VK_LBUTTON) & 0x8000) || (GetAsyncKeyState(VK_ESCAPE) & 0x8000) || (GetAsyncKeyState(VK_SPACE) & 0x8000)) - { Sleep(10); - } } pSurface->Fill(0); @@ -2548,9 +2515,7 @@ bool MapSelectionClass::Run() { int themeIdx = ThemeClass::Instance.FindIndex(this->ThemeName.c_str()); if (themeIdx >= 0) - { ThemeClass::Instance.Play(themeIdx); - } } // Drain any input after intro sequence @@ -2562,9 +2527,7 @@ bool MapSelectionClass::Run() DispatchMessage(&drainMsg); } while ((GetAsyncKeyState(VK_LBUTTON) & 0x8000) || (GetAsyncKeyState(VK_ESCAPE) & 0x8000)) - { Sleep(10); - } } // Ensure any audio is stopped before interactive loop starts @@ -2630,13 +2593,10 @@ bool MapSelectionClass::Run() if (isMouseOnMap != wasMouseOnMap) { if (isMouseOnMap && !this->MouseOnMapSound.empty()) - { PlayMapSelSFX(this->MouseOnMapSound.c_str()); - } else if (!isMouseOnMap && !this->MouseOffMapSound.empty()) - { PlayMapSelSFX(this->MouseOffMapSound.c_str()); - } + wasMouseOnMap = isMouseOnMap; } @@ -2664,6 +2624,7 @@ bool MapSelectionClass::Run() std::wstring fullText = ResolveCSFOrText(choice.Description); if (fullText.empty()) fullText = ResolveCSFOrText(choice.Summary); + if (fullText.empty()) fullText = std::wstring(choice.StageName.begin(), choice.StageName.end()); @@ -2672,27 +2633,19 @@ bool MapSelectionClass::Run() // Play hover / enter region SFX sound first if (!choice.HoverSound.empty()) - { PlayMapSelSFX(choice.HoverSound.c_str()); - } else if (!this->EnterRegionSound.empty()) - { PlayMapSelSFX(this->EnterRegionSound.c_str()); - } // Play VoiceOver speech if (!choice.VoiceOver.empty()) - { PlayMapSelAudio(choice.VoiceOver.c_str()); - } } else { this->BriefingLines.clear(); if (this->LastHoveredChoiceIdx >= 0 && !this->ExitRegionSound.empty()) - { PlayMapSelSFX(this->ExitRegionSound.c_str()); - } } this->LastHoveredChoiceIdx = this->HoveredChoiceIdx; @@ -2717,17 +2670,15 @@ bool MapSelectionClass::Run() const auto& chosen = this->Choices[this->SelectedChoiceIdx]; if (!chosen.ClickSound.empty()) - { PlayMapSelSFX(chosen.ClickSound.c_str()); - } else if (!this->ClickRegionSound.empty()) - { PlayMapSelSFX(this->ClickRegionSound.c_str()); - } + break; } } } + wasLButtonDown = isLButtonDown; this->UpdateAnimations(); @@ -2743,9 +2694,7 @@ bool MapSelectionClass::Run() // Ensure any playing VoiceOver audio and custom theme music are stopped when loading begins StopMapSelAudio(); if (!this->ThemeName.empty()) - { ThemeClass::Instance.Stop(true); - } // Hide mouse cursor when leaving map selection (matching OpenTS Hide_Mouse()) if (WWMouseClass::Instance)