diff --git a/src/android/android_main.cpp b/src/android/android_main.cpp index 03b06beecf..e60ec49765 100644 --- a/src/android/android_main.cpp +++ b/src/android/android_main.cpp @@ -1,159 +1,236 @@ -#include - -#ifdef CONF_PLATFORM_ANDROID -#include -#include +#include "android_main.h" #include #include +#include #include + #include + #include #include -extern "C" __attribute__((visibility("default"))) void InitAndroid(); - -static int gs_AndroidStarted = false; - -void InitAndroid() +static bool UnpackAsset(const char *pFilename) { - if(gs_AndroidStarted) + char aAssetFilename[IO_MAX_PATH_LENGTH]; + str_copy(aAssetFilename, "asset_integrity_files/"); + str_append(aAssetFilename, pFilename); + + // This uses SDL_RWFromFile because it can read Android assets, + // which are files stored in the app's APK file. All data files + // are stored as assets and unpacked to the external storage. + SDL_RWops *pAssetFile = SDL_RWFromFile(aAssetFilename, "rb"); + if(!pAssetFile) { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "DDNet", "The app was started, but not closed properly, this causes bugs. Please restart or manually delete this task.", SDL_GL_GetCurrentWindow()); - std::exit(0); + log_error("android", "Failed to open asset '%s' for reading", pFilename); + return false; } - gs_AndroidStarted = true; + const long int FileLength = SDL_RWsize(pAssetFile); + if(FileLength < 0) + { + SDL_RWclose(pAssetFile); + log_error("android", "Failed to determine length of asset '%s'", pFilename); + return false; + } - // change current path to a writable directory - const char *pPath = SDL_AndroidGetExternalStoragePath(); - chdir(pPath); - dbg_msg("client", "changed path to %s", pPath); + char *pData = static_cast(malloc(FileLength)); + const size_t ReadLength = SDL_RWread(pAssetFile, pData, 1, FileLength); + SDL_RWclose(pAssetFile); - // copy integrity files + if(ReadLength != (size_t)FileLength) { - SDL_RWops *pF = SDL_RWFromFile("asset_integrity_files/integrity.txt", "rb"); - if(!pF) - { - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "DDNet", "integrity.txt not found, consider reinstalling", SDL_GL_GetCurrentWindow()); - std::exit(0); - } + free(pData); + log_error("android", "Failed to read asset '%s' (read %" PRIzu ", wanted %ld)", pFilename, ReadLength, FileLength); + return false; + } + + IOHANDLE TargetFile = io_open(pFilename, IOFLAG_WRITE); + if(!TargetFile) + { + free(pData); + log_error("android", "Failed to open '%s' for writing", pFilename); + return false; + } - long int length; - SDL_RWseek(pF, 0, RW_SEEK_END); - length = SDL_RWtell(pF); - SDL_RWseek(pF, 0, RW_SEEK_SET); + const size_t WriteLength = io_write(TargetFile, pData, FileLength); + io_close(TargetFile); + free(pData); + + if(WriteLength != (size_t)FileLength) + { + log_error("android", "Failed to write data to '%s' (wrote %" PRIzu ", wanted %ld)", pFilename, WriteLength, FileLength); + return false; + } - char *pAl = (char *)malloc(length); - SDL_RWread(pF, pAl, 1, length); + return true; +} - SDL_RWclose(pF); +constexpr const char *INTEGRITY_INDEX = "integrity.txt"; +constexpr const char *INTEGRITY_INDEX_SAVE = "integrity_save.txt"; - mkdir("data", 0755); +// The first line of each integrity file contains the combined hash for all files, +// if the hashes match then we assume that the unpacked data folder is up-to-date. +static bool EqualIntegrityFiles(const char *pAssetFilename, const char *pStorageFilename) +{ + IOHANDLE StorageFile = io_open(pStorageFilename, IOFLAG_READ); + if(!StorageFile) + { + return false; + } - dbg_msg("integrity", "copying integrity.txt with size: %ld", length); + char aStorageMainSha256[SHA256_MAXSTRSIZE]; + const size_t StorageReadLength = io_read(StorageFile, aStorageMainSha256, sizeof(aStorageMainSha256) - 1); + io_close(StorageFile); + if(StorageReadLength != sizeof(aStorageMainSha256) - 1) + { + return false; + } + aStorageMainSha256[sizeof(aStorageMainSha256) - 1] = '\0'; - IOHANDLE pIO = io_open("integrity.txt", IOFLAG_WRITE); - io_write(pIO, pAl, length); - io_close(pIO); + char aAssetFilename[IO_MAX_PATH_LENGTH]; + str_copy(aAssetFilename, "asset_integrity_files/"); + str_append(aAssetFilename, pAssetFilename); - free(pAl); + SDL_RWops *pAssetFile = SDL_RWFromFile(aAssetFilename, "rb"); + if(!pAssetFile) + { + return false; } - IOHANDLE pIO = io_open("integrity.txt", IOFLAG_READ); - CLineReader LineReader; - LineReader.Init(pIO); - const char *pReadLine = NULL; - std::vector vLines; - while((pReadLine = LineReader.Get())) + char aAssetMainSha256[SHA256_MAXSTRSIZE]; + const size_t AssetReadLength = SDL_RWread(pAssetFile, aAssetMainSha256, 1, sizeof(aAssetMainSha256) - 1); + SDL_RWclose(pAssetFile); + if(AssetReadLength != sizeof(aAssetMainSha256) - 1) { - vLines.push_back(pReadLine); + return false; } - io_close(pIO); + aAssetMainSha256[sizeof(aAssetMainSha256) - 1] = '\0'; + + return str_comp(aStorageMainSha256, aAssetMainSha256) == 0; +} + +class CIntegrityFileLine +{ +public: + char m_aFilename[IO_MAX_PATH_LENGTH]; + SHA256_DIGEST m_Sha256; +}; - // first line is the whole hash - std::string AllAsOne; - for(size_t i = 1; i < vLines.size(); ++i) +static std::vector ReadIntegrityFile(const char *pFilename) +{ + CLineReader LineReader; + if(!LineReader.OpenFile(io_open(pFilename, IOFLAG_READ))) { - AllAsOne.append(vLines[i]); - AllAsOne.append("\n"); + return {}; } - SHA256_DIGEST ShaAll; - bool GotSHA = false; + + std::vector vLines; + while(const char *pReadLine = LineReader.Get()) { - IOHANDLE pIOR = io_open("integrity_save.txt", IOFLAG_READ); - if(pIOR != NULL) + const char *pSpaceInLine = str_rchr(pReadLine, ' '); + CIntegrityFileLine Line; + char aSha256[SHA256_MAXSTRSIZE]; + if(pSpaceInLine == nullptr) { - CLineReader LineReader; - LineReader.Init(pIOR); - const char *pLine = LineReader.Get(); - if(pLine != NULL) + if(!vLines.empty()) { - sha256_from_str(&ShaAll, pLine); - GotSHA = true; + // Only the first line is allowed to not contain a filename + log_error("android", "Failed to parse line %" PRIzu " of '%s': line does not contain space", vLines.size() + 1, pFilename); + return {}; } + Line.m_aFilename[0] = '\0'; + str_copy(aSha256, pReadLine); } + else + { + str_truncate(Line.m_aFilename, sizeof(Line.m_aFilename), pReadLine, pSpaceInLine - pReadLine); + str_copy(aSha256, pSpaceInLine + 1); + } + if(sha256_from_str(&Line.m_Sha256, aSha256) != 0) + { + log_error("android", "Failed to parse line %" PRIzu " of '%s': invalid SHA256 string", vLines.size() + 1, pFilename); + return {}; + } + vLines.emplace_back(std::move(Line)); } - SHA256_DIGEST ShaAllFile; - sha256_from_str(&ShaAllFile, vLines[0].c_str()); + return vLines; +} - // TODO: check files individually - if(!GotSHA || ShaAllFile != ShaAll) +const char *InitAndroid() +{ + // Change current working directory to our external storage location + const char *pPath = SDL_AndroidGetExternalStoragePath(); + if(pPath == nullptr) { - // then the files - for(size_t i = 1; i < vLines.size(); ++i) - { - std::string FileName, Hash; - std::string::size_type n = 0; - std::string::size_type c = 0; - while((c = vLines[i].find(' ', n)) != std::string::npos) - n = c + 1; - FileName = vLines[i].substr(0, n - 1); - Hash = vLines[i].substr(n + 1); - - std::string AssetFileName = std::string("asset_integrity_files/") + FileName; - SDL_RWops *pF = SDL_RWFromFile(AssetFileName.c_str(), "rb"); - - dbg_msg("Integrity", "Copying from assets: %s", FileName.c_str()); - - std::string FileNamePath = FileName; - std::string FileNamePathSub; - c = 0; - while((c = FileNamePath.find('/', c)) != std::string::npos) - { - FileNamePathSub = FileNamePath.substr(0, c); - fs_makedir(FileNamePathSub.c_str()); - ++c; - } + return "The external storage is not available."; + } + if(fs_chdir(pPath) != 0) + { + return "Failed to change current directory to external storage."; + } + log_info("android", "Changed current directory to '%s'", pPath); + + if(fs_makedir("data") != 0 || fs_makedir("user") != 0) + { + return "Failed to create 'data' and 'user' directories in external storage."; + } - long int length; - SDL_RWseek(pF, 0, RW_SEEK_END); - length = SDL_RWtell(pF); - SDL_RWseek(pF, 0, RW_SEEK_SET); + if(EqualIntegrityFiles(INTEGRITY_INDEX, INTEGRITY_INDEX_SAVE)) + { + return nullptr; + } + + if(!UnpackAsset(INTEGRITY_INDEX)) + { + return "Failed to unpack the integrity index file. Consider reinstalling the app."; + } - char *pAl = (char *)malloc(length); - SDL_RWread(pF, pAl, 1, length); + std::vector vIntegrityLines = ReadIntegrityFile(INTEGRITY_INDEX); + if(vIntegrityLines.empty()) + { + return "Failed to load the integrity index file. Consider reinstalling the app."; + } - SDL_RWclose(pF); + std::vector vIntegritySaveLines = ReadIntegrityFile(INTEGRITY_INDEX_SAVE); - IOHANDLE pIO = io_open(FileName.c_str(), IOFLAG_WRITE); - io_write(pIO, pAl, length); - io_close(pIO); + // The remaining lines of each integrity file list all assets and their hashes + for(size_t i = 1; i < vIntegrityLines.size(); ++i) + { + const CIntegrityFileLine &IntegrityLine = vIntegrityLines[i]; - free(pAl); + // Check if the asset is unchanged from the last unpacking + const auto IntegritySaveLine = std::find_if(vIntegritySaveLines.begin(), vIntegritySaveLines.end(), [&](const CIntegrityFileLine &Line) { + return str_comp(Line.m_aFilename, IntegrityLine.m_aFilename) == 0; + }); + if(IntegritySaveLine != vIntegritySaveLines.end() && IntegritySaveLine->m_Sha256 == IntegrityLine.m_Sha256) + { + continue; } - IOHANDLE pIOR = io_open("integrity_save.txt", IOFLAG_WRITE); - if(pIOR != NULL) + if(fs_makedir_rec_for(IntegrityLine.m_aFilename) != 0 || !UnpackAsset(IntegrityLine.m_aFilename)) { - char aFileSHA[SHA256_MAXSTRSIZE]; - sha256_str(ShaAllFile, aFileSHA, sizeof(aFileSHA)); - io_write(pIOR, aFileSHA, str_length(aFileSHA)); - io_close(pIOR); + return "Failed to unpack game assets, consider reinstalling the app."; } } + + // The integrity file will be unpacked every time when launching, + // so we can simply rename it to update the saved integrity file. + if((fs_is_file(INTEGRITY_INDEX_SAVE) && fs_remove(INTEGRITY_INDEX_SAVE) != 0) || fs_rename(INTEGRITY_INDEX, INTEGRITY_INDEX_SAVE) != 0) + { + return "Failed to update the saved integrity index file."; + } + + return nullptr; } -#endif +// See NativeMain.java +constexpr uint32_t COMMAND_USER = 0x8000; +constexpr uint32_t COMMAND_RESTART_APP = COMMAND_USER + 1; + +void RestartAndroidApp() +{ + SDL_AndroidSendMessage(COMMAND_RESTART_APP, 0); +} diff --git a/src/android/android_main.h b/src/android/android_main.h new file mode 100644 index 0000000000..c7e3a3e88d --- /dev/null +++ b/src/android/android_main.h @@ -0,0 +1,31 @@ +#ifndef ANDROID_ANDROID_MAIN_H +#define ANDROID_ANDROID_MAIN_H + +#include +#if !defined(CONF_PLATFORM_ANDROID) +#error "This header should only be included when compiling for Android" +#endif + +/** + * Initializes the Android storage. Must be called on Android-systems + * before using any of the I/O and storage functions. + * + * This will change the current working directory to the app specific external + * storage location and unpack the assets from the APK file to the `data` folder. + * The folder `user` is created in the external storage to store the user data. + * + * Failure must be handled by exiting the app. + * + * @return `nullptr` on success, error message on failure. + */ +const char *InitAndroid(); + +/** + * Sends an intent to the Android system to restart the app. + * + * This will restart the main activity in a new task. The current process + * must immediately terminate after this function is called. + */ +void RestartAndroidApp(); + +#endif // ANDROID_ANDROID_MAIN_H diff --git a/src/antibot/antibot_data.h b/src/antibot/antibot_data.h index f65f626287..5751db6163 100644 --- a/src/antibot/antibot_data.h +++ b/src/antibot/antibot_data.h @@ -87,10 +87,10 @@ struct CAntibotData int64_t m_Now; int64_t m_Freq; - void (*m_pfnKick)(int ClientID, const char *pMessage, void *pUser); + void (*m_pfnKick)(int ClientId, const char *pMessage, void *pUser); void (*m_pfnLog)(const char *pMessage, void *pUser); - void (*m_pfnReport)(int ClientID, const char *pMessage, void *pUser); - void (*m_pfnSend)(int ClientID, const void *pData, int DataSize, int Flags, void *pUser); + void (*m_pfnReport)(int ClientId, const char *pMessage, void *pUser); + void (*m_pfnSend)(int ClientId, const void *pData, int DataSize, int Flags, void *pUser); void (*m_pfnTeehistorian)(const void *pData, int DataSize, void *pUser); void *m_pUser; }; diff --git a/src/antibot/antibot_interface.h b/src/antibot/antibot_interface.h index a982802c9e..e98e7cd636 100644 --- a/src/antibot/antibot_interface.h +++ b/src/antibot/antibot_interface.h @@ -17,23 +17,23 @@ ANTIBOTAPI void AntibotRoundEnd(void); ANTIBOTAPI void AntibotUpdateData(void); ANTIBOTAPI void AntibotDestroy(void); ANTIBOTAPI void AntibotConsoleCommand(const char *pCommand); -ANTIBOTAPI void AntibotOnPlayerInit(int ClientID); -ANTIBOTAPI void AntibotOnPlayerDestroy(int ClientID); -ANTIBOTAPI void AntibotOnSpawn(int ClientID); -ANTIBOTAPI void AntibotOnHammerFireReloading(int ClientID); -ANTIBOTAPI void AntibotOnHammerFire(int ClientID); -ANTIBOTAPI void AntibotOnHammerHit(int ClientID, int TargetID); -ANTIBOTAPI void AntibotOnDirectInput(int ClientID); -ANTIBOTAPI void AntibotOnCharacterTick(int ClientID); -ANTIBOTAPI void AntibotOnHookAttach(int ClientID, bool Player); +ANTIBOTAPI void AntibotOnPlayerInit(int ClientId); +ANTIBOTAPI void AntibotOnPlayerDestroy(int ClientId); +ANTIBOTAPI void AntibotOnSpawn(int ClientId); +ANTIBOTAPI void AntibotOnHammerFireReloading(int ClientId); +ANTIBOTAPI void AntibotOnHammerFire(int ClientId); +ANTIBOTAPI void AntibotOnHammerHit(int ClientId, int TargetId); +ANTIBOTAPI void AntibotOnDirectInput(int ClientId); +ANTIBOTAPI void AntibotOnCharacterTick(int ClientId); +ANTIBOTAPI void AntibotOnHookAttach(int ClientId, bool Player); ANTIBOTAPI void AntibotOnEngineTick(void); -ANTIBOTAPI void AntibotOnEngineClientJoin(int ClientID, bool Sixup); -ANTIBOTAPI void AntibotOnEngineClientDrop(int ClientID, const char *pReason); +ANTIBOTAPI void AntibotOnEngineClientJoin(int ClientId, bool Sixup); +ANTIBOTAPI void AntibotOnEngineClientDrop(int ClientId, const char *pReason); // Returns true if the message shouldn't be processed by the server. -ANTIBOTAPI bool AntibotOnEngineClientMessage(int ClientID, const void *pData, int Size, int Flags); -ANTIBOTAPI bool AntibotOnEngineServerMessage(int ClientID, const void *pData, int Size, int Flags); +ANTIBOTAPI bool AntibotOnEngineClientMessage(int ClientId, const void *pData, int Size, int Flags); +ANTIBOTAPI bool AntibotOnEngineServerMessage(int ClientId, const void *pData, int Size, int Flags); // Returns true if the server should simulate receiving a client message. -ANTIBOTAPI bool AntibotOnEngineSimulateClientMessage(int *pClientID, void *pBuffer, int BufferSize, int *pOutSize, int *pFlags); +ANTIBOTAPI bool AntibotOnEngineSimulateClientMessage(int *pClientId, void *pBuffer, int BufferSize, int *pOutSize, int *pFlags); } #endif // ANTIBOT_ANTIBOT_INTERFACE_H diff --git a/src/antibot/antibot_null.cpp b/src/antibot/antibot_null.cpp index 6a70c0387a..2db022c097 100644 --- a/src/antibot/antibot_null.cpp +++ b/src/antibot/antibot_null.cpp @@ -32,19 +32,19 @@ void AntibotConsoleCommand(const char *pCommand) g_pData->m_pfnLog("unknown command", g_pData->m_pUser); } } -void AntibotOnPlayerInit(int /*ClientID*/) {} -void AntibotOnPlayerDestroy(int /*ClientID*/) {} -void AntibotOnSpawn(int /*ClientID*/) {} -void AntibotOnHammerFireReloading(int /*ClientID*/) {} -void AntibotOnHammerFire(int /*ClientID*/) {} -void AntibotOnHammerHit(int /*ClientID*/, int /*TargetID*/) {} -void AntibotOnDirectInput(int /*ClientID*/) {} -void AntibotOnCharacterTick(int /*ClientID*/) {} -void AntibotOnHookAttach(int /*ClientID*/, bool /*Player*/) {} +void AntibotOnPlayerInit(int /*ClientId*/) {} +void AntibotOnPlayerDestroy(int /*ClientId*/) {} +void AntibotOnSpawn(int /*ClientId*/) {} +void AntibotOnHammerFireReloading(int /*ClientId*/) {} +void AntibotOnHammerFire(int /*ClientId*/) {} +void AntibotOnHammerHit(int /*ClientId*/, int /*TargetId*/) {} +void AntibotOnDirectInput(int /*ClientId*/) {} +void AntibotOnCharacterTick(int /*ClientId*/) {} +void AntibotOnHookAttach(int /*ClientId*/, bool /*Player*/) {} void AntibotOnEngineTick(void) {} -void AntibotOnEngineClientJoin(int /*ClientID*/, bool /*Sixup*/) {} -void AntibotOnEngineClientDrop(int /*ClientID*/, const char * /*pReason*/) {} -bool AntibotOnEngineClientMessage(int /*ClientID*/, const void * /*pData*/, int /*Size*/, int /*Flags*/) { return false; } -bool AntibotOnEngineServerMessage(int /*ClientID*/, const void * /*pData*/, int /*Size*/, int /*Flags*/) { return false; } -bool AntibotOnEngineSimulateClientMessage(int * /*pClientID*/, void * /*pBuffer*/, int /*BufferSize*/, int * /*pOutSize*/, int * /*pFlags*/) { return false; } +void AntibotOnEngineClientJoin(int /*ClientId*/, bool /*Sixup*/) {} +void AntibotOnEngineClientDrop(int /*ClientId*/, const char * /*pReason*/) {} +bool AntibotOnEngineClientMessage(int /*ClientId*/, const void * /*pData*/, int /*Size*/, int /*Flags*/) { return false; } +bool AntibotOnEngineServerMessage(int /*ClientId*/, const void * /*pData*/, int /*Size*/, int /*Flags*/) { return false; } +bool AntibotOnEngineSimulateClientMessage(int * /*pClientId*/, void * /*pBuffer*/, int /*BufferSize*/, int * /*pOutSize*/, int * /*pFlags*/) { return false; } } diff --git a/src/base/.clang-tidy b/src/base/.clang-tidy new file mode 100644 index 0000000000..6d6f9e5d52 --- /dev/null +++ b/src/base/.clang-tidy @@ -0,0 +1 @@ +Checks: '-readability-identifier-naming' diff --git a/src/base/Cargo.toml b/src/base/Cargo.toml index 709a35c8e3..c72aa91b3a 100644 --- a/src/base/Cargo.toml +++ b/src/base/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ddnet-base" version = "0.0.1" -edition = "2018" +edition = "2021" publish = false license = "Zlib" diff --git a/src/base/color.h b/src/base/color.h index 4e84645f43..0480183069 100644 --- a/src/base/color.h +++ b/src/base/color.h @@ -137,6 +137,16 @@ class color4_base return col; } + DerivedT Multiply(const DerivedT &Other) const + { + DerivedT Color(static_cast(*this)); + Color.x *= Other.x; + Color.y *= Other.y; + Color.z *= Other.z; + Color.a *= Other.a; + return Color; + } + template static UnpackT UnpackAlphaLast(unsigned Color, bool Alpha = true) { @@ -166,8 +176,9 @@ class ColorHSLA : public color4_base ColorHSLA(){}; constexpr static const float DARKEST_LGT = 0.5f; + constexpr static const float DARKEST_LGT7 = 61.0f / 255.0f; - ColorHSLA UnclampLighting(float Darkest = DARKEST_LGT) const + ColorHSLA UnclampLighting(float Darkest) const { ColorHSLA col = *this; col.l = Darkest + col.l * (1.0f - Darkest); diff --git a/src/base/detect.h b/src/base/detect.h index 8bceed0861..28b9982cb7 100644 --- a/src/base/detect.h +++ b/src/base/detect.h @@ -163,19 +163,19 @@ #define CONF_ARCH_ARM 1 #define CONF_ARCH_STRING "arm" #define CONF_ARCH_ENDIAN_BIG 1 -#endif - -#if defined(__ARMEL__) +#elif defined(__ARMEL__) #define CONF_ARCH_ARM 1 #define CONF_ARCH_STRING "arm" #define CONF_ARCH_ENDIAN_LITTLE 1 -#endif - -#if defined(__aarch64__) || defined(__arm64__) +#elif defined(__aarch64__) || defined(__arm64__) || defined(__ARM_ARCH_ISA_A64) #define CONF_ARCH_ARM64 1 #define CONF_ARCH_STRING "arm64" +#if defined(__ARM_BIG_ENDIAN) +#define CONF_ARCH_ENDIAN_BIG 1 +#else #define CONF_ARCH_ENDIAN_LITTLE 1 #endif +#endif #ifndef CONF_FAMILY_STRING #define CONF_FAMILY_STRING "unknown" diff --git a/src/base/log.cpp b/src/base/log.cpp index b0b167c68f..989238007b 100644 --- a/src/base/log.cpp +++ b/src/base/log.cpp @@ -8,9 +8,8 @@ #include #if defined(CONF_FAMILY_WINDOWS) -#define WIN32_LEAN_AND_MEAN -#undef _WIN32_WINNT -#define _WIN32_WINNT 0x0501 /* required for mingw to get getaddrinfo to work */ +#include +#include #include #else #include @@ -49,7 +48,10 @@ void log_set_global_logger_default() #else logger = log_logger_stdout(); #endif - log_set_global_logger(logger.release()); + if(logger) + { + log_set_global_logger(logger.release()); + } } ILogger *log_get_scope_logger() @@ -233,19 +235,16 @@ class CLoggerAsync : public ILogger return; } aio_lock(m_pAio); - if(m_AnsiTruecolor) + if(m_AnsiTruecolor && pMessage->m_HaveColor) { // https://en.wikipedia.org/w/index.php?title=ANSI_escape_code&oldid=1077146479#24-bit char aAnsi[32]; - if(pMessage->m_HaveColor) - { - str_format(aAnsi, sizeof(aAnsi), - "\x1b[38;2;%d;%d;%dm", - pMessage->m_Color.r, - pMessage->m_Color.g, - pMessage->m_Color.b); - aio_write_unlocked(m_pAio, aAnsi, str_length(aAnsi)); - } + str_format(aAnsi, sizeof(aAnsi), + "\x1b[38;2;%d;%d;%dm", + pMessage->m_Color.r, + pMessage->m_Color.g, + pMessage->m_Color.b); + aio_write_unlocked(m_pAio, aAnsi, str_length(aAnsi)); } aio_write_unlocked(m_pAio, pMessage->m_aLine, pMessage->m_LineLength); if(m_AnsiTruecolor && pMessage->m_HaveColor) @@ -317,14 +316,16 @@ static int color_hsv_to_windows_console_color(const ColorHSVA &Hsv) class CWindowsConsoleLogger : public ILogger { HANDLE m_pConsole; + bool m_EnableColor; int m_BackgroundColor; int m_ForegroundColor; CLock m_OutputLock; bool m_Finished = false; public: - CWindowsConsoleLogger(HANDLE pConsole) : - m_pConsole(pConsole) + CWindowsConsoleLogger(HANDLE pConsole, bool EnableColor) : + m_pConsole(pConsole), + m_EnableColor(EnableColor) { CONSOLE_SCREEN_BUFFER_INFO ConsoleInfo; if(GetConsoleScreenBufferInfo(pConsole, &ConsoleInfo)) @@ -344,15 +345,12 @@ class CWindowsConsoleLogger : public ILogger { return; } - const std::wstring WideMessage = windows_utf8_to_wide(pMessage->m_aLine) + L"\r\n"; + const std::wstring WideMessage = windows_utf8_to_wide(pMessage->m_aLine); int Color = m_BackgroundColor; - if(pMessage->m_HaveColor) + if(m_EnableColor && pMessage->m_HaveColor) { - ColorRGBA Rgba(1.0, 1.0, 1.0, 1.0); - Rgba.r = pMessage->m_Color.r / 255.0; - Rgba.g = pMessage->m_Color.g / 255.0; - Rgba.b = pMessage->m_Color.b / 255.0; + const ColorRGBA Rgba(pMessage->m_Color.r / 255.0f, pMessage->m_Color.g / 255.0f, pMessage->m_Color.b / 255.0f); Color |= color_hsv_to_windows_console_color(color_cast(Rgba)); } else @@ -362,7 +360,8 @@ class CWindowsConsoleLogger : public ILogger if(!m_Finished) { SetConsoleTextAttribute(m_pConsole, Color); - WriteConsoleW(m_pConsole, WideMessage.c_str(), WideMessage.length(), NULL, NULL); + WriteConsoleW(m_pConsole, WideMessage.c_str(), WideMessage.length(), nullptr, nullptr); + WriteConsoleW(m_pConsole, L"\r\n", 2, nullptr, nullptr); } } void GlobalFinish() override REQUIRES(!m_OutputLock) @@ -373,28 +372,15 @@ class CWindowsConsoleLogger : public ILogger m_Finished = true; } }; -class CWindowsFileLogger : public ILogger -{ - HANDLE m_pFile; - CLock m_OutputLock; -public: - CWindowsFileLogger(HANDLE pFile) : - m_pFile(pFile) - { - } - void Log(const CLogMessage *pMessage) override REQUIRES(!m_OutputLock) - { - if(m_Filter.Filters(pMessage)) - { - return; - } - const CLockScope LockScope(m_OutputLock); - DWORD Written; // we don't care about the value, but Windows 7 crashes if we pass NULL - WriteFile(m_pFile, pMessage->m_aLine, pMessage->m_LineLength, &Written, NULL); - WriteFile(m_pFile, "\r\n", 2, &Written, NULL); - } -}; +static IOHANDLE ConvertWindowsHandle(HANDLE pHandle, int OpenFlags) +{ + int FileDescriptor = _open_osfhandle(reinterpret_cast(pHandle), OpenFlags); + dbg_assert(FileDescriptor != -1, "_open_osfhandle failure"); + IOHANDLE FileStream = _wfdopen(FileDescriptor, L"w"); + dbg_assert(FileStream != nullptr, "_wfdopen failure"); + return FileStream; +} #endif std::unique_ptr log_logger_stdout() @@ -402,18 +388,71 @@ std::unique_ptr log_logger_stdout() #if !defined(CONF_FAMILY_WINDOWS) // TODO: Only enable true color when COLORTERM contains "truecolor". // https://github.com/termstandard/colors/tree/65bf0cd1ece7c15fa33a17c17528b02c99f1ae0b#checking-for-colorterm - const bool colors = getenv("NO_COLOR") == nullptr && isatty(STDOUT_FILENO); - return std::make_unique(io_stdout(), colors, false); + const bool Colors = getenv("NO_COLOR") == nullptr && isatty(STDOUT_FILENO); + return std::make_unique(io_stdout(), Colors, false); #else + // If we currently have no stdout (console, file, pipe), + // try to attach to the console of the parent process. if(GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)) == FILE_TYPE_UNKNOWN) + { AttachConsole(ATTACH_PARENT_PROCESS); + } + HANDLE pOutput = GetStdHandle(STD_OUTPUT_HANDLE); - switch(GetFileType(pOutput)) + if(pOutput == nullptr) { - case FILE_TYPE_CHAR: return std::make_unique(pOutput); - case FILE_TYPE_PIPE: [[fallthrough]]; // writing to pipe works the same as writing to a file - case FILE_TYPE_DISK: return std::make_unique(pOutput); - default: return std::make_unique(io_stdout(), false, false); + // There is no console, file or pipe that we can output to. + return nullptr; + } + dbg_assert(pOutput != INVALID_HANDLE_VALUE, "GetStdHandle failure"); + + const DWORD OutputType = GetFileType(pOutput); + if(OutputType == FILE_TYPE_CHAR) + { + DWORD OldConsoleMode = 0; + if(!GetConsoleMode(pOutput, &OldConsoleMode)) + { + // GetConsoleMode can fail with ERROR_INVALID_HANDLE when redirecting output to "nul", + // which is considered a character file but cannot be used as a console. + dbg_assert(GetLastError() == ERROR_INVALID_HANDLE, "GetConsoleMode failure"); + return nullptr; + } + + const bool Colors = _wgetenv(L"NO_COLOR") == nullptr; + + // Try to enable virtual terminal processing in the Windows console. + // See https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences + if(!SetConsoleMode(pOutput, OldConsoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN)) + { + // Try to downgrade mode gracefully when failing to set both. + if(!SetConsoleMode(pOutput, OldConsoleMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) + { + // Fallback to old, slower Windows logging API, when failing to enable virtual terminal processing. + return std::make_unique(pOutput, Colors); + } + } + + // Virtual terminal processing was enabled successfully. We can + // use the async logger with ANSI escape codes for colors now. + // We need to set the output encoding to UTF-8 manually and + // convert the HANDLE to an IOHANDLE to use the async logger. + // We assume UTF-8 is available when virtual terminal processing is. + dbg_assert(SetConsoleOutputCP(CP_UTF8) != 0, "SetConsoleOutputCP failure"); + return std::make_unique(ConvertWindowsHandle(pOutput, _O_TEXT), Colors, false); + } + else if(OutputType == FILE_TYPE_DISK || OutputType == FILE_TYPE_PIPE) + { + // Writing to a pipe works the same as writing to a file. + // We can use the async logger to write to files and pipes + // by converting the HANDLE to an IOHANDLE. + // For pipes there does not seem to be any way to determine + // whether the console supports ANSI escape codes. + return std::make_unique(ConvertWindowsHandle(pOutput, _O_APPEND), false, false); + } + else + { + dbg_assert(false, "GetFileType failure"); + dbg_break(); } #endif } @@ -444,6 +483,19 @@ std::unique_ptr log_logger_windows_debugger() } #endif +class CLoggerNoOp : public ILogger +{ +public: + void Log(const CLogMessage *pMessage) override + { + // no-op + } +}; +std::unique_ptr log_logger_noop() +{ + return std::make_unique(); +} + void CFutureLogger::Set(std::shared_ptr pLogger) { const CLockScope LockScope(m_PendingLock); diff --git a/src/base/logger.h b/src/base/logger.h index cc6575b49a..b43d0468c0 100644 --- a/src/base/logger.h +++ b/src/base/logger.h @@ -210,6 +210,13 @@ std::unique_ptr log_logger_stdout(); */ std::unique_ptr log_logger_windows_debugger(); +/** + * @ingroup Log + * + * Logger which discards all logs. + */ +std::unique_ptr log_logger_noop(); + /** * @ingroup Log * diff --git a/src/base/system.cpp b/src/base/system.cpp index 4255afa228..f42341453c 100644 --- a/src/base/system.cpp +++ b/src/base/system.cpp @@ -9,7 +9,9 @@ #include #include #include +#include // std::get_time #include // std::size +#include // std::istringstream #include #include "lock.h" @@ -60,11 +62,6 @@ #endif #elif defined(CONF_FAMILY_WINDOWS) -#define WIN32_LEAN_AND_MEAN -#undef _WIN32_WINNT -// 0x0501 (Windows XP) is required for mingw to get getaddrinfo to work -// 0x0600 (Windows Vista) is required to use RegGetValueW and RegDeleteTreeW -#define _WIN32_WINNT 0x0600 #include #include #include @@ -87,58 +84,6 @@ #include #endif -IOHANDLE io_stdin() -{ - return stdin; -} - -IOHANDLE io_stdout() -{ - return stdout; -} - -IOHANDLE io_stderr() -{ - return stderr; -} - -IOHANDLE io_current_exe() -{ - // From https://stackoverflow.com/a/1024937. -#if defined(CONF_FAMILY_WINDOWS) - wchar_t wide_path[IO_MAX_PATH_LENGTH]; - if(GetModuleFileNameW(NULL, wide_path, std::size(wide_path)) == 0 || GetLastError() != ERROR_SUCCESS) - { - return 0; - } - const std::optional path = windows_wide_to_utf8(wide_path); - return path.has_value() ? io_open(path.value().c_str(), IOFLAG_READ) : 0; -#elif defined(CONF_PLATFORM_MACOS) - char path[IO_MAX_PATH_LENGTH]; - uint32_t path_size = sizeof(path); - if(_NSGetExecutablePath(path, &path_size)) - { - return 0; - } - return io_open(path, IOFLAG_READ); -#else - static const char *NAMES[] = { - "/proc/self/exe", // Linux, Android - "/proc/curproc/exe", // NetBSD - "/proc/curproc/file", // DragonFly - }; - for(auto &name : NAMES) - { - IOHANDLE result = io_open(name, IOFLAG_READ); - if(result) - { - return result; - } - } - return 0; -#endif -} - static NETSTATS network_stats = {0}; #define VLEN 128 @@ -254,9 +199,9 @@ bool mem_has_null(const void *block, size_t size) return false; } -IOHANDLE io_open_impl(const char *filename, int flags) +IOHANDLE io_open(const char *filename, int flags) { - dbg_assert(flags == (IOFLAG_READ | IOFLAG_SKIP_BOM) || flags == IOFLAG_READ || flags == IOFLAG_WRITE || flags == IOFLAG_APPEND, "flags must be read, read+skipbom, write or append"); + dbg_assert(flags == IOFLAG_READ || flags == IOFLAG_WRITE || flags == IOFLAG_APPEND, "flags must be read, write or append"); #if defined(CONF_FAMILY_WINDOWS) const std::wstring wide_filename = windows_utf8_to_wide(filename); DWORD desired_access; @@ -271,7 +216,7 @@ IOHANDLE io_open_impl(const char *filename, int flags) else if(flags == IOFLAG_WRITE) { desired_access = FILE_WRITE_DATA; - creation_disposition = OPEN_ALWAYS; + creation_disposition = CREATE_ALWAYS; open_mode = "wb"; } else if(flags == IOFLAG_APPEND) @@ -316,21 +261,6 @@ IOHANDLE io_open_impl(const char *filename, int flags) #endif } -IOHANDLE io_open(const char *filename, int flags) -{ - IOHANDLE result = io_open_impl(filename, flags); - unsigned char buf[3]; - if((flags & IOFLAG_SKIP_BOM) == 0 || !result) - { - return result; - } - if(io_read(result, buf, sizeof(buf)) != 3 || buf[0] != 0xef || buf[1] != 0xbb || buf[2] != 0xbf) - { - io_seek(result, 0, IOSEEK_START); - } - return result; -} - unsigned io_read(IOHANDLE io, void *buffer, unsigned size) { return fread(buffer, 1, size, (FILE *)io); @@ -421,11 +351,6 @@ long int io_length(IOHANDLE io) return length; } -int io_error(IOHANDLE io) -{ - return ferror((FILE *)io); -} - unsigned io_write(IOHANDLE io, const void *buffer, unsigned size) { return fwrite(buffer, 1, size, (FILE *)io); @@ -463,6 +388,63 @@ int io_sync(IOHANDLE io) #endif } +int io_error(IOHANDLE io) +{ + return ferror((FILE *)io); +} + +IOHANDLE io_stdin() +{ + return stdin; +} + +IOHANDLE io_stdout() +{ + return stdout; +} + +IOHANDLE io_stderr() +{ + return stderr; +} + +IOHANDLE io_current_exe() +{ + // From https://stackoverflow.com/a/1024937. +#if defined(CONF_FAMILY_WINDOWS) + wchar_t wide_path[IO_MAX_PATH_LENGTH]; + if(GetModuleFileNameW(NULL, wide_path, std::size(wide_path)) == 0 || GetLastError() != ERROR_SUCCESS) + { + return 0; + } + const std::optional path = windows_wide_to_utf8(wide_path); + return path.has_value() ? io_open(path.value().c_str(), IOFLAG_READ) : 0; +#elif defined(CONF_PLATFORM_MACOS) + char path[IO_MAX_PATH_LENGTH]; + uint32_t path_size = sizeof(path); + if(_NSGetExecutablePath(path, &path_size)) + { + return 0; + } + return io_open(path, IOFLAG_READ); +#else + static const char *NAMES[] = { + "/proc/self/exe", // Linux, Android + "/proc/curproc/exe", // NetBSD + "/proc/curproc/file", // DragonFly + }; + for(auto &name : NAMES) + { + IOHANDLE result = io_open(name, IOFLAG_READ); + if(result) + { + return result; + } + } + return 0; +#endif +} + #define ASYNC_BUFSIZE (8 * 1024) #define ASYNC_LOCAL_BUFSIZE (64 * 1024) @@ -737,17 +719,6 @@ int aio_error(ASYNCIO *aio) return aio->error; } -void aio_free(ASYNCIO *aio) -{ - aio->lock.lock(); - if(aio->thread) - { - thread_detach(aio->thread); - aio->thread = 0; - } - aio_handle_free_and_unlock(aio); -} - void aio_close(ASYNCIO *aio) { { @@ -773,6 +744,17 @@ void aio_wait(ASYNCIO *aio) thread_wait(thread); } +void aio_free(ASYNCIO *aio) +{ + aio->lock.lock(); + if(aio->thread) + { + thread_detach(aio->thread); + aio->thread = 0; + } + aio_handle_free_and_unlock(aio); +} + struct THREAD_RUN { void (*threadfunc)(void *); @@ -805,12 +787,12 @@ void *thread_init(void (*threadfunc)(void *), void *u, const char *name) data->u = u; #if defined(CONF_FAMILY_UNIX) { - pthread_t id; pthread_attr_t attr; - pthread_attr_init(&attr); + dbg_assert(pthread_attr_init(&attr) == 0, "pthread_attr_init failure"); #if defined(CONF_PLATFORM_MACOS) && defined(__MAC_10_10) && __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_10 - pthread_attr_set_qos_class_np(&attr, QOS_CLASS_USER_INTERACTIVE, 0); + dbg_assert(pthread_attr_set_qos_class_np(&attr, QOS_CLASS_USER_INTERACTIVE, 0) == 0, "pthread_attr_set_qos_class_np failure"); #endif + pthread_t id; dbg_assert(pthread_create(&id, &attr, thread_run, data) == 0, "pthread_create failure"); return (void *)id; } @@ -827,12 +809,10 @@ void *thread_init(void (*threadfunc)(void *), void *u, const char *name) void thread_wait(void *thread) { #if defined(CONF_FAMILY_UNIX) - int result = pthread_join((pthread_t)thread, NULL); - if(result != 0) - dbg_msg("thread", "!! %d", result); + dbg_assert(pthread_join((pthread_t)thread, nullptr) == 0, "pthread_join failure"); #elif defined(CONF_FAMILY_WINDOWS) - WaitForSingleObject((HANDLE)thread, INFINITE); - CloseHandle(thread); + dbg_assert(WaitForSingleObject((HANDLE)thread, INFINITE) == WAIT_OBJECT_0, "WaitForSingleObject failure"); + dbg_assert(CloseHandle(thread), "CloseHandle failure"); #else #error not implemented #endif @@ -841,9 +821,7 @@ void thread_wait(void *thread) void thread_yield() { #if defined(CONF_FAMILY_UNIX) - int result = sched_yield(); - if(result != 0) - dbg_msg("thread", "yield failed: %d", errno); + dbg_assert(sched_yield() == 0, "sched_yield failure"); #elif defined(CONF_FAMILY_WINDOWS) Sleep(0); #else @@ -854,76 +832,92 @@ void thread_yield() void thread_detach(void *thread) { #if defined(CONF_FAMILY_UNIX) - int result = pthread_detach((pthread_t)(thread)); - if(result != 0) - dbg_msg("thread", "detach failed: %d", result); + dbg_assert(pthread_detach((pthread_t)thread) == 0, "pthread_detach failure"); #elif defined(CONF_FAMILY_WINDOWS) - CloseHandle(thread); + dbg_assert(CloseHandle(thread), "CloseHandle failure"); #else #error not implemented #endif } -bool thread_init_and_detach(void (*threadfunc)(void *), void *u, const char *name) +void thread_init_and_detach(void (*threadfunc)(void *), void *u, const char *name) { void *thread = thread_init(threadfunc, u, name); - if(thread) - thread_detach(thread); - return thread != nullptr; + thread_detach(thread); } #if defined(CONF_FAMILY_WINDOWS) void sphore_init(SEMAPHORE *sem) { - *sem = CreateSemaphore(0, 0, 10000, 0); + *sem = CreateSemaphoreW(nullptr, 0, std::numeric_limits::max(), nullptr); + dbg_assert(*sem != nullptr, "CreateSemaphoreW failure"); +} +void sphore_wait(SEMAPHORE *sem) +{ + dbg_assert(WaitForSingleObject((HANDLE)*sem, INFINITE) == WAIT_OBJECT_0, "WaitForSingleObject failure"); +} +void sphore_signal(SEMAPHORE *sem) +{ + dbg_assert(ReleaseSemaphore((HANDLE)*sem, 1, nullptr), "ReleaseSemaphore failure"); +} +void sphore_destroy(SEMAPHORE *sem) +{ + dbg_assert(CloseHandle((HANDLE)*sem), "CloseHandle failure"); } -void sphore_wait(SEMAPHORE *sem) { WaitForSingleObject((HANDLE)*sem, INFINITE); } -void sphore_signal(SEMAPHORE *sem) { ReleaseSemaphore((HANDLE)*sem, 1, NULL); } -void sphore_destroy(SEMAPHORE *sem) { CloseHandle((HANDLE)*sem); } #elif defined(CONF_PLATFORM_MACOS) void sphore_init(SEMAPHORE *sem) { char aBuf[64]; - str_format(aBuf, sizeof(aBuf), "%p", (void *)sem); + str_format(aBuf, sizeof(aBuf), "/%d.%p", pid(), (void *)sem); *sem = sem_open(aBuf, O_CREAT | O_EXCL, S_IRWXU | S_IRWXG, 0); if(*sem == SEM_FAILED) - dbg_msg("sphore", "init failed: %d", errno); + { + char aError[128]; + str_format(aError, sizeof(aError), "sem_open failure, errno=%d, name='%s'", errno, aBuf); + dbg_assert(false, aError); + } +} +void sphore_wait(SEMAPHORE *sem) +{ + while(true) + { + if(sem_wait(*sem) == 0) + break; + dbg_assert(errno == EINTR, "sem_wait failure"); + } +} +void sphore_signal(SEMAPHORE *sem) +{ + dbg_assert(sem_post(*sem) == 0, "sem_post failure"); } -void sphore_wait(SEMAPHORE *sem) { sem_wait(*sem); } -void sphore_signal(SEMAPHORE *sem) { sem_post(*sem); } void sphore_destroy(SEMAPHORE *sem) { + dbg_assert(sem_close(*sem) == 0, "sem_close failure"); char aBuf[64]; - sem_close(*sem); - str_format(aBuf, sizeof(aBuf), "%p", (void *)sem); - sem_unlink(aBuf); + str_format(aBuf, sizeof(aBuf), "/%d.%p", pid(), (void *)sem); + dbg_assert(sem_unlink(aBuf) == 0, "sem_unlink failure"); } #elif defined(CONF_FAMILY_UNIX) void sphore_init(SEMAPHORE *sem) { - if(sem_init(sem, 0, 0) != 0) - dbg_msg("sphore", "init failed: %d", errno); + dbg_assert(sem_init(sem, 0, 0) == 0, "sem_init failure"); } - void sphore_wait(SEMAPHORE *sem) { - do + while(true) { - errno = 0; - if(sem_wait(sem) != 0) - dbg_msg("sphore", "wait failed: %d", errno); - } while(errno == EINTR); + if(sem_wait(sem) == 0) + break; + dbg_assert(errno == EINTR, "sem_wait failure"); + } } - void sphore_signal(SEMAPHORE *sem) { - if(sem_post(sem) != 0) - dbg_msg("sphore", "post failed: %d", errno); + dbg_assert(sem_post(sem) == 0, "sem_post failure"); } void sphore_destroy(SEMAPHORE *sem) { - if(sem_destroy(sem) != 0) - dbg_msg("sphore", "destroy failed: %d", errno); + dbg_assert(sem_destroy(sem) == 0, "sem_destroy failure"); } #endif @@ -969,7 +963,7 @@ const NETADDR NETADDR_ZEROED = {NETTYPE_INVALID, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, static void netaddr_to_sockaddr_in(const NETADDR *src, struct sockaddr_in *dest) { mem_zero(dest, sizeof(struct sockaddr_in)); - if(src->type != NETTYPE_IPV4 && src->type != NETTYPE_WEBSOCKET_IPV4) + if(!(src->type & NETTYPE_IPV4) && !(src->type & NETTYPE_WEBSOCKET_IPV4)) { dbg_msg("system", "couldn't convert NETADDR of type %d to ipv4", src->type); return; @@ -983,9 +977,9 @@ static void netaddr_to_sockaddr_in(const NETADDR *src, struct sockaddr_in *dest) static void netaddr_to_sockaddr_in6(const NETADDR *src, struct sockaddr_in6 *dest) { mem_zero(dest, sizeof(struct sockaddr_in6)); - if(src->type != NETTYPE_IPV6) + if(!(src->type & NETTYPE_IPV6)) { - dbg_msg("system", "couldn't not convert NETADDR of type %d to ipv6", src->type); + dbg_msg("system", "couldn't convert NETADDR of type %d to ipv6", src->type); return; } @@ -1109,16 +1103,16 @@ void net_addr_str_v6(const unsigned short ip[8], int port, char *buffer, int buf } } -void net_addr_str(const NETADDR *addr, char *string, int max_length, int add_port) +bool net_addr_str(const NETADDR *addr, char *string, int max_length, int add_port) { - if(addr->type == NETTYPE_IPV4 || addr->type == NETTYPE_WEBSOCKET_IPV4) + if(addr->type & NETTYPE_IPV4 || addr->type & NETTYPE_WEBSOCKET_IPV4) { if(add_port != 0) str_format(string, max_length, "%d.%d.%d.%d:%d", addr->ip[0], addr->ip[1], addr->ip[2], addr->ip[3], addr->port); else str_format(string, max_length, "%d.%d.%d.%d", addr->ip[0], addr->ip[1], addr->ip[2], addr->ip[3]); } - else if(addr->type == NETTYPE_IPV6) + else if(addr->type & NETTYPE_IPV6) { int port = -1; unsigned short ip[8]; @@ -1134,7 +1128,11 @@ void net_addr_str(const NETADDR *addr, char *string, int max_length, int add_por net_addr_str_v6(ip, port, string, max_length); } else + { str_format(string, max_length, "unknown type %d", addr->type); + return false; + } + return true; } static int priv_net_extract(const char *hostname, char *host, int max_host, int *port) @@ -1233,7 +1231,7 @@ static int parse_int(int *out, const char **str) { int i = 0; *out = 0; - if(**str < '0' || **str > '9') + if(!str_isnum(**str)) return -1; i = **str - '0'; @@ -1241,7 +1239,7 @@ static int parse_int(int *out, const char **str) while(true) { - if(**str < '0' || **str > '9') + if(!str_isnum(**str)) { *out = i; return 0; @@ -1286,19 +1284,20 @@ static int parse_uint16(unsigned short *out, const char **str) int net_addr_from_url(NETADDR *addr, const char *string, char *host_buf, size_t host_buf_size) { - char host[128]; - int length; - int start = 0; - int end; - int failure; + bool sixup = false; + mem_zero(addr, sizeof(*addr)); const char *str = str_startswith(string, "tw-0.6+udp://"); + if(!str && (str = str_startswith(string, "tw-0.7+udp://"))) + { + addr->type |= NETTYPE_TW7; + sixup = true; + } if(!str) return 1; - mem_zero(addr, sizeof(*addr)); - - length = str_length(str); - end = length; + int length = str_length(str); + int start = 0; + int end = length; for(int i = 0; i < length; i++) { if(str[i] == '@') @@ -1316,13 +1315,19 @@ int net_addr_from_url(NETADDR *addr, const char *string, char *host_buf, size_t break; } } + + char host[128]; str_truncate(host, sizeof(host), str + start, end - start); if(host_buf) str_copy(host_buf, host, host_buf_size); - if((failure = net_addr_from_str(addr, host))) + int failure = net_addr_from_str(addr, host); + if(failure) return failure; + if(sixup) + addr->type |= NETTYPE_TW7; + return failure; } @@ -1532,47 +1537,45 @@ NETSOCKET net_udp_create(NETADDR bindaddr) { NETSOCKET sock = (NETSOCKET_INTERNAL *)malloc(sizeof(*sock)); *sock = invalid_socket; - NETADDR tmpbindaddr = bindaddr; - int broadcast = 1; - int socket = -1; if(bindaddr.type & NETTYPE_IPV4) { struct sockaddr_in addr; - - /* bind, we should check for error */ + NETADDR tmpbindaddr = bindaddr; tmpbindaddr.type = NETTYPE_IPV4; netaddr_to_sockaddr_in(&tmpbindaddr, &addr); - socket = priv_net_create_socket(AF_INET, SOCK_DGRAM, (struct sockaddr *)&addr, sizeof(addr)); + int socket = priv_net_create_socket(AF_INET, SOCK_DGRAM, (struct sockaddr *)&addr, sizeof(addr)); if(socket >= 0) { sock->type |= NETTYPE_IPV4; sock->ipv4sock = socket; /* set broadcast */ + int broadcast = 1; if(setsockopt(socket, SOL_SOCKET, SO_BROADCAST, (const char *)&broadcast, sizeof(broadcast)) != 0) - dbg_msg("socket", "Setting BROADCAST on ipv4 failed: %d", errno); + { + dbg_msg("socket", "Setting BROADCAST on ipv4 failed: %d", net_errno()); + } { /* set DSCP/TOS */ int iptos = 0x10 /* IPTOS_LOWDELAY */; - //int iptos = 46; /* High Priority */ if(setsockopt(socket, IPPROTO_IP, IP_TOS, (char *)&iptos, sizeof(iptos)) != 0) - dbg_msg("socket", "Setting TOS on ipv4 failed: %d", errno); + { + dbg_msg("socket", "Setting TOS on ipv4 failed: %d", net_errno()); + } } } } + #if defined(CONF_WEBSOCKETS) if(bindaddr.type & NETTYPE_WEBSOCKET_IPV4) { char addr_str[NETADDR_MAXSTRSIZE]; - - /* bind, we should check for error */ + NETADDR tmpbindaddr = bindaddr; tmpbindaddr.type = NETTYPE_WEBSOCKET_IPV4; - net_addr_str(&tmpbindaddr, addr_str, sizeof(addr_str), 0); - socket = websocket_create(addr_str, tmpbindaddr.port); - + int socket = websocket_create(addr_str, tmpbindaddr.port); if(socket >= 0) { sock->type |= NETTYPE_WEBSOCKET_IPV4; @@ -1584,44 +1587,47 @@ NETSOCKET net_udp_create(NETADDR bindaddr) if(bindaddr.type & NETTYPE_IPV6) { struct sockaddr_in6 addr; - - /* bind, we should check for error */ + NETADDR tmpbindaddr = bindaddr; tmpbindaddr.type = NETTYPE_IPV6; netaddr_to_sockaddr_in6(&tmpbindaddr, &addr); - socket = priv_net_create_socket(AF_INET6, SOCK_DGRAM, (struct sockaddr *)&addr, sizeof(addr)); + int socket = priv_net_create_socket(AF_INET6, SOCK_DGRAM, (struct sockaddr *)&addr, sizeof(addr)); if(socket >= 0) { sock->type |= NETTYPE_IPV6; sock->ipv6sock = socket; /* set broadcast */ + int broadcast = 1; if(setsockopt(socket, SOL_SOCKET, SO_BROADCAST, (const char *)&broadcast, sizeof(broadcast)) != 0) - dbg_msg("socket", "Setting BROADCAST on ipv6 failed: %d", errno); + { + dbg_msg("socket", "Setting BROADCAST on ipv6 failed: %d", net_errno()); + } + // TODO: setting IP_TOS on ipv6 with setsockopt is not supported on Windows, see https://github.com/ddnet/ddnet/issues/7605 +#if !defined(CONF_FAMILY_WINDOWS) { /* set DSCP/TOS */ int iptos = 0x10 /* IPTOS_LOWDELAY */; - //int iptos = 46; /* High Priority */ if(setsockopt(socket, IPPROTO_IP, IP_TOS, (char *)&iptos, sizeof(iptos)) != 0) - dbg_msg("socket", "Setting TOS on ipv6 failed: %d", errno); + { + dbg_msg("socket", "Setting TOS on ipv6 failed: %d", net_errno()); + } } +#endif } } - if(socket < 0) + if(sock->type == NETTYPE_INVALID) { free(sock); sock = nullptr; } else { - /* set non-blocking */ net_set_non_blocking(sock); - net_buffer_init(&sock->buffer); } - /* return */ return sock; } @@ -1837,46 +1843,41 @@ NETSOCKET net_tcp_create(NETADDR bindaddr) { NETSOCKET sock = (NETSOCKET_INTERNAL *)malloc(sizeof(*sock)); *sock = invalid_socket; - NETADDR tmpbindaddr = bindaddr; - int socket = -1; if(bindaddr.type & NETTYPE_IPV4) { struct sockaddr_in addr; - - /* bind, we should check for error */ + NETADDR tmpbindaddr = bindaddr; tmpbindaddr.type = NETTYPE_IPV4; netaddr_to_sockaddr_in(&tmpbindaddr, &addr); - socket = priv_net_create_socket(AF_INET, SOCK_STREAM, (struct sockaddr *)&addr, sizeof(addr)); - if(socket >= 0) + int socket4 = priv_net_create_socket(AF_INET, SOCK_STREAM, (struct sockaddr *)&addr, sizeof(addr)); + if(socket4 >= 0) { sock->type |= NETTYPE_IPV4; - sock->ipv4sock = socket; + sock->ipv4sock = socket4; } } if(bindaddr.type & NETTYPE_IPV6) { struct sockaddr_in6 addr; - - /* bind, we should check for error */ + NETADDR tmpbindaddr = bindaddr; tmpbindaddr.type = NETTYPE_IPV6; netaddr_to_sockaddr_in6(&tmpbindaddr, &addr); - socket = priv_net_create_socket(AF_INET6, SOCK_STREAM, (struct sockaddr *)&addr, sizeof(addr)); - if(socket >= 0) + int socket6 = priv_net_create_socket(AF_INET6, SOCK_STREAM, (struct sockaddr *)&addr, sizeof(addr)); + if(socket6 >= 0) { sock->type |= NETTYPE_IPV6; - sock->ipv6sock = socket; + sock->ipv6sock = socket6; } } - if(socket < 0) + if(sock->type == NETTYPE_INVALID) { free(sock); sock = nullptr; } - /* return */ return sock; } @@ -1978,6 +1979,8 @@ int net_tcp_connect(NETSOCKET sock, const NETADDR *a) { struct sockaddr_in addr; netaddr_to_sockaddr_in(a, &addr); + if(sock->ipv4sock < 0) + return -2; return connect(sock->ipv4sock, (struct sockaddr *)&addr, sizeof(addr)); } @@ -1985,6 +1988,8 @@ int net_tcp_connect(NETSOCKET sock, const NETADDR *a) { struct sockaddr_in6 addr; netaddr_to_sockaddr_in6(a, &addr); + if(sock->ipv6sock < 0) + return -2; return connect(sock->ipv6sock, (struct sockaddr *)&addr, sizeof(addr)); } @@ -2285,17 +2290,20 @@ int fs_storage_path(const char *appname, char *path, int max) int fs_makedir_rec_for(const char *path) { - char buffer[1024 * 2]; - char *p; + char buffer[IO_MAX_PATH_LENGTH]; str_copy(buffer, path); - for(p = buffer + 1; *p != '\0'; p++) + for(int index = 1; buffer[index] != '\0'; ++index) { - if(*p == '/' && *(p + 1) != '\0') + // Do not try to create folder for drive letters on Windows, + // as this is not necessary and may fail for system drives. + if((buffer[index] == '/' || buffer[index] == '\\') && buffer[index + 1] != '\0' && buffer[index - 1] != ':') { - *p = '\0'; + buffer[index] = '\0'; if(fs_makedir(buffer) < 0) + { return -1; - *p = '/'; + } + buffer[index] = '/'; } } return 0; @@ -2378,17 +2386,12 @@ int fs_is_relative_path(const char *path) int fs_chdir(const char *path) { - if(fs_is_dir(path)) - { #if defined(CONF_FAMILY_WINDOWS) - const std::wstring wide_path = windows_utf8_to_wide(path); - return SetCurrentDirectoryW(wide_path.c_str()) != 0 ? 0 : 1; + const std::wstring wide_path = windows_utf8_to_wide(path); + return SetCurrentDirectoryW(wide_path.c_str()) != 0 ? 0 : 1; #else - return chdir(path) ? 1 : 0; + return chdir(path) ? 1 : 0; #endif - } - else - return 1; } char *fs_getcwd(char *buffer, int buffer_size) @@ -2396,15 +2399,7 @@ char *fs_getcwd(char *buffer, int buffer_size) #if defined(CONF_FAMILY_WINDOWS) const DWORD size_needed = GetCurrentDirectoryW(0, nullptr); std::wstring wide_current_dir(size_needed, L'0'); - DWORD result = GetCurrentDirectoryW(size_needed, wide_current_dir.data()); - if(result == 0) - { - const DWORD LastError = GetLastError(); - const std::string ErrorMsg = windows_format_system_message(LastError); - dbg_msg("filesystem", "GetCurrentDirectoryW failed: %ld %s", LastError, ErrorMsg.c_str()); - buffer[0] = '\0'; - return nullptr; - } + dbg_assert(GetCurrentDirectoryW(size_needed, wide_current_dir.data()) == size_needed - 1, "GetCurrentDirectoryW failure"); const std::optional current_dir = windows_wide_to_utf8(wide_current_dir.c_str()); if(!current_dir.has_value()) { @@ -2602,18 +2597,29 @@ int net_socket_read_wait(NETSOCKET sock, int time) return 0; } -int time_timestamp() +int64_t time_timestamp() { return time(0); } +static struct tm *time_localtime_threadlocal(time_t *time_data) +{ +#if defined(CONF_FAMILY_WINDOWS) + // The result of localtime is thread-local on Windows + // https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/localtime-localtime32-localtime64 + return localtime(time_data); +#else + // Thread-local buffer for the result of localtime_r + thread_local struct tm time_info_buf; + return localtime_r(time_data, &time_info_buf); +#endif +} + int time_houroftheday() { time_t time_data; - struct tm *time_info; - time(&time_data); - time_info = localtime(&time_data); + struct tm *time_info = time_localtime_threadlocal(&time_data); return time_info->tm_hour; } @@ -2640,7 +2646,7 @@ static bool time_iseasterday(time_t time_data, struct tm *time_info) for(int day_offset = -1; day_offset <= 2; day_offset++) { time_data = time_data + day_offset * 60 * 60 * 24; - time_info = localtime(&time_data); + time_info = time_localtime_threadlocal(&time_data); if(time_info->tm_mon == month - 1 && time_info->tm_mday == day) return true; } @@ -2651,7 +2657,7 @@ ETimeSeason time_season() { time_t time_data; time(&time_data); - struct tm *time_info = localtime(&time_data); + struct tm *time_info = time_localtime_threadlocal(&time_data); if((time_info->tm_mon == 11 && time_info->tm_mday == 31) || (time_info->tm_mon == 0 && time_info->tm_mday == 1)) { @@ -2762,6 +2768,15 @@ int str_format_v(char *buffer, int buffer_size, const char *format, va_list args return str_utf8_fix_truncation(buffer); } +int str_format_int(char *buffer, size_t buffer_size, int value) +{ + buffer[0] = '\0'; // Fix false positive clang-analyzer-core.UndefinedBinaryOperatorResult when using result + auto result = std::to_chars(buffer, buffer + buffer_size - 1, value); + result.ptr[0] = '\0'; + return result.ptr - buffer; +} + +#undef str_format int str_format(char *buffer, int buffer_size, const char *format, ...) { va_list args; @@ -2770,6 +2785,9 @@ int str_format(char *buffer, int buffer_size, const char *format, ...) va_end(args); return length; } +#if !defined(CONF_DEBUG) +#define str_format str_format_opt +#endif const char *str_trim_words(const char *str, int words) { @@ -2928,7 +2946,7 @@ int str_comp_filenames(const char *a, const char *b) for(; *a && *b; ++a, ++b) { - if(*a >= '0' && *a <= '9' && *b >= '0' && *b <= '9') + if(str_isnum(*a) && str_isnum(*b)) { result = 0; do @@ -2937,11 +2955,11 @@ int str_comp_filenames(const char *a, const char *b) result = *a - *b; ++a; ++b; - } while(*a >= '0' && *a <= '9' && *b >= '0' && *b <= '9'); + } while(str_isnum(*a) && str_isnum(*b)); - if(*a >= '0' && *a <= '9') + if(str_isnum(*a)) return 1; - else if(*b >= '0' && *b <= '9') + else if(str_isnum(*b)) return -1; else if(result || *a == '\0' || *b == '\0') return result; @@ -3144,6 +3162,38 @@ const char *str_find(const char *haystack, const char *needle) return 0; } +bool str_delimiters_around_offset(const char *haystack, const char *delim, int offset, int *start, int *end) +{ + bool found = true; + const char *search = haystack; + const int delim_len = str_length(delim); + *start = 0; + while(str_find(search, delim)) + { + const char *test = str_find(search, delim) + delim_len; + int distance = test - haystack; + if(distance > offset) + break; + + *start = distance; + search = test; + } + if(search == haystack) + found = false; + + if(str_find(search, delim)) + { + *end = str_find(search, delim) - haystack; + } + else + { + *end = str_length(haystack); + found = false; + } + + return found; +} + const char *str_rchr(const char *haystack, char needle) { return strrchr(haystack, needle); @@ -3420,8 +3470,7 @@ int str_base64_decode(void *dst_raw, int dst_size, const char *data) #endif void str_timestamp_ex(time_t time_data, char *buffer, int buffer_size, const char *format) { - struct tm *time_info; - time_info = localtime(&time_data); + struct tm *time_info = time_localtime_threadlocal(&time_data); strftime(buffer, buffer_size, format, time_info); buffer[buffer_size - 1] = 0; /* assure null termination */ } @@ -3437,6 +3486,22 @@ void str_timestamp(char *buffer, int buffer_size) { str_timestamp_format(buffer, buffer_size, FORMAT_NOSPACE); } + +bool timestamp_from_str(const char *string, const char *format, time_t *timestamp) +{ + std::tm tm{}; + std::istringstream ss(string); + ss >> std::get_time(&tm, format); + if(ss.fail() || !ss.eof()) + return false; + + time_t result = mktime(&tm); + if(result < 0) + return false; + + *timestamp = result; + return true; +} #ifdef __GNUC__ #pragma GCC diagnostic pop #endif @@ -3526,11 +3591,16 @@ char str_uppercase(char c) return c; } +bool str_isnum(char c) +{ + return c >= '0' && c <= '9'; +} + int str_isallnum(const char *str) { while(*str) { - if(!(*str >= '0' && *str <= '9')) + if(!str_isnum(*str)) return 0; str++; } @@ -3541,7 +3611,7 @@ int str_isallnum_hex(const char *str) { while(*str) { - if(!(*str >= '0' && *str <= '9') && !(*str >= 'a' && *str <= 'f') && !(*str >= 'A' && *str <= 'F')) + if(!str_isnum(*str) && !(*str >= 'a' && *str <= 'f') && !(*str >= 'A' && *str <= 'F')) return 0; str++; } @@ -3553,6 +3623,18 @@ int str_toint(const char *str) return str_toint_base(str, 10); } +bool str_toint(const char *str, int *out) +{ + // returns true if conversion was successful + char *end; + int value = strtol(str, &end, 10); + if(*end != '\0') + return false; + if(out != nullptr) + *out = value; + return true; +} + int str_toint_base(const char *str, int base) { return strtol(str, nullptr, base); @@ -3573,11 +3655,16 @@ float str_tofloat(const char *str) return strtod(str, nullptr); } -void str_from_int(int value, char *buffer, size_t buffer_size) +bool str_tofloat(const char *str, float *out) { - buffer[0] = '\0'; // Fix false positive clang-analyzer-core.UndefinedBinaryOperatorResult when using result - auto result = std::to_chars(buffer, buffer + buffer_size - 1, value); - result.ptr[0] = '\0'; + // returns true if conversion was successful + char *end; + float value = strtod(str, &end); + if(*end != '\0') + return false; + if(out != nullptr) + *out = value; + return true; } int str_utf8_comp_nocase(const char *a, const char *b) @@ -3873,6 +3960,24 @@ int str_utf8_check(const char *str) return 1; } +void str_utf8_copy_num(char *dst, const char *src, int dst_size, int num) +{ + int new_cursor; + int cursor = 0; + + while(src[cursor] && num > 0) + { + new_cursor = str_utf8_forward(src, cursor); + if(new_cursor >= dst_size) // reserve 1 byte for the null termination + break; + else + cursor = new_cursor; + --num; + } + + str_copy(dst, src, cursor < dst_size ? cursor + 1 : dst_size); +} + void str_utf8_stats(const char *str, size_t max_size, size_t max_count, size_t *size, size_t *count) { const char *cursor = str; @@ -4045,7 +4150,8 @@ void cmdline_free(int argc, const char **argv) #endif } -PROCESS shell_execute(const char *file) +#if !defined(CONF_PLATFORM_ANDROID) +PROCESS shell_execute(const char *file, EShellExecuteWindowState window_state) { #if defined(CONF_FAMILY_WINDOWS) const std::wstring wide_file = windows_utf8_to_wide(file); @@ -4055,7 +4161,18 @@ PROCESS shell_execute(const char *file) info.cbSize = sizeof(SHELLEXECUTEINFOW); info.lpVerb = L"open"; info.lpFile = wide_file.c_str(); - info.nShow = SW_SHOWMINNOACTIVE; + switch(window_state) + { + case EShellExecuteWindowState::FOREGROUND: + info.nShow = SW_SHOW; + break; + case EShellExecuteWindowState::BACKGROUND: + info.nShow = SW_SHOWMINNOACTIVE; + break; + default: + dbg_assert(false, "window_state invalid"); + dbg_break(); + } info.fMask = SEE_MASK_NOCLOSEPROCESS; // Save and restore the FPU control word because ShellExecute might change it fenv_t floating_point_environment; @@ -4177,6 +4294,7 @@ int open_file(const char *path) return open_link(buf); #endif } +#endif // !defined(CONF_PLATFORM_ANDROID) struct SECURE_RANDOM_DATA { @@ -4474,7 +4592,7 @@ void os_locale_str(char *locale, size_t length) { locale[i] = '-'; } - else if(locale[i] != '-' && !(locale[i] >= 'a' && locale[i] <= 'z') && !(locale[i] >= 'A' && locale[i] <= 'Z') && !(locale[i] >= '0' && locale[i] <= '9')) + else if(locale[i] != '-' && !(locale[i] >= 'a' && locale[i] <= 'z') && !(locale[i] >= 'A' && locale[i] <= 'Z') && !(str_isnum(locale[i]))) { locale[i] = '\0'; break; diff --git a/src/base/system.h b/src/base/system.h index c575949e62..cf75ce3092 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -219,13 +219,42 @@ bool mem_has_null(const void *block, size_t size); */ enum { + /** + * Open file for reading. + * + * @see io_open + */ IOFLAG_READ = 1, + /** + * Open file for writing. + * + * @see io_open + */ IOFLAG_WRITE = 2, + /** + * Open file for appending at the end. + * + * @see io_open + */ IOFLAG_APPEND = 4, - IOFLAG_SKIP_BOM = 8, + /** + * Start seeking from the beginning of the file. + * + * @see io_seek + */ IOSEEK_START = 0, + /** + * Start seeking from the current position. + * + * @see io_seek + */ IOSEEK_CUR = 1, + /** + * Start seeking from the end of the file. + * + * @see io_seek + */ IOSEEK_END = 2, }; @@ -237,10 +266,9 @@ enum * @param File to open. * @param flags A set of IOFLAG flags. * - * @sa IOFLAG_READ, IOFLAG_WRITE, IOFLAG_APPEND, IOFLAG_SKIP_BOM. - * - * @return A handle to the file on success and 0 on failure. + * @see IOFLAG_READ, IOFLAG_WRITE, IOFLAG_APPEND. * + * @return A handle to the file on success, or `nullptr` on failure. */ IOHANDLE io_open(const char *filename, int flags); @@ -254,7 +282,6 @@ IOHANDLE io_open(const char *filename, int flags); * @param size Number of bytes to read from the file. * * @return Number of bytes read. - * */ unsigned io_read(IOHANDLE io, void *buffer, unsigned size); @@ -280,7 +307,7 @@ void io_read_all(IOHANDLE io, void **result, unsigned *result_len); * * @param io Handle to the file to read data from. * - * @return The file's remaining contents or null on failure. + * @return The file's remaining contents, or `nullptr` on failure. * * @remark Guarantees that there are no internal null bytes. * @remark Guarantees that result will contain zero-termination. @@ -301,63 +328,63 @@ char *io_read_all_str(IOHANDLE io); int io_skip(IOHANDLE io, int size); /** - * Writes data from a buffer to file. + * Seeks to a specified offset in the file. * * @ingroup File-IO * * @param io Handle to the file. - * @param buffer Pointer to the data that should be written. - * @param size Number of bytes to write. + * @param offset Offset from position to search. + * @param origin Position to start searching from. * - * @return Number of bytes written. + * @return `0` on success. */ -unsigned io_write(IOHANDLE io, const void *buffer, unsigned size); +int io_seek(IOHANDLE io, int offset, int origin); /** - * Writes a platform dependent newline to file. + * Gets the current position in the file. * * @ingroup File-IO * * @param io Handle to the file. * - * @return true on success, false on failure. + * @return The current position, or `-1` on failure. */ -bool io_write_newline(IOHANDLE io); +long int io_tell(IOHANDLE io); /** - * Seeks to a specified offset in the file. + * Gets the total length of the file. Resets cursor to the beginning. * * @ingroup File-IO * * @param io Handle to the file. - * @param offset Offset from pos to stop. - * @param origin Position to start searching from. * - * @return 0 on success. + * @return The total size, or `-1` on failure. */ -int io_seek(IOHANDLE io, int offset, int origin); +long int io_length(IOHANDLE io); /** - * Gets the current position in the file. + * Writes data from a buffer to a file. * * @ingroup File-IO * * @param io Handle to the file. + * @param buffer Pointer to the data that should be written. + * @param size Number of bytes to write. * - * @return The current position. @c -1L if an error occurred. + * @return Number of bytes written. */ -long int io_tell(IOHANDLE io); +unsigned io_write(IOHANDLE io, const void *buffer, unsigned size); /** - * Gets the total length of the file. Resetting cursor to the beginning + * Writes a platform dependent newline to a file. * * @ingroup File-IO * * @param io Handle to the file. * - * @return The total size. @c -1L if an error occurred. + * @return `true` on success, `false` on failure. */ -long int io_length(IOHANDLE io); +bool io_write_newline(IOHANDLE io); /** * Closes a file. @@ -366,7 +393,7 @@ long int io_length(IOHANDLE io); * * @param io Handle to the file. * - * @return 0 on success. + * @return `0` on success. */ int io_close(IOHANDLE io); @@ -377,7 +404,7 @@ int io_close(IOHANDLE io); * * @param io Handle to the file. * - * @return 0 on success. + * @return `0` on success. */ int io_flush(IOHANDLE io); @@ -388,7 +415,7 @@ int io_flush(IOHANDLE io); * * @param io Handle to the file. * - * @return 0 on success. + * @return `0` on success. */ int io_sync(IOHANDLE io); @@ -399,34 +426,57 @@ int io_sync(IOHANDLE io); * * @param io Handle to the file. * - * @return nonzero on error, 0 otherwise. + * @return `0` on success, or non-`0` on error. */ int io_error(IOHANDLE io); /** * @ingroup File-IO - * @return An to the standard input. + * + * Returns a handle for the standard input. + * + * @return An @link IOHANDLE @endlink for the standard input. + * + * @remark The handle must not be closed. */ IOHANDLE io_stdin(); /** * @ingroup File-IO - * @return An to the standard output. + * + * Returns a handle for the standard output. + * + * @return An @link IOHANDLE @endlink for the standard output. + * + * @remark The handle must not be closed. */ IOHANDLE io_stdout(); /** * @ingroup File-IO - * @return An to the standard error. + * + * Returns a handle for the standard error. + * + * @return An @link IOHANDLE @endlink for the standard error. + * + * @remark The handle must not be closed. */ IOHANDLE io_stderr(); /** * @ingroup File-IO - * @return An to the current executable. + * + * Returns a handle for the current executable. + * + * @return An @link IOHANDLE @endlink for the current executable. */ IOHANDLE io_current_exe(); +/** + * Wrapper for asynchronously writing to an @link IOHANDLE @endlink. + * + * @ingroup File-IO + */ typedef struct ASYNCIO ASYNCIO; /** @@ -437,12 +487,11 @@ typedef struct ASYNCIO ASYNCIO; * @param io Handle to the file. * * @return The handle for asynchronous writing. - * */ ASYNCIO *aio_new(IOHANDLE io); /** - * Locks the ASYNCIO structure so it can't be written into by + * Locks the `ASYNCIO` structure so it can't be written into by * other threads. * * @ingroup File-IO @@ -452,7 +501,7 @@ ASYNCIO *aio_new(IOHANDLE io); void aio_lock(ASYNCIO *aio); /** - * Unlocks the ASYNCIO structure after finishing the contiguous + * Unlocks the `ASYNCIO` structure after finishing the contiguous * write. * * @ingroup File-IO @@ -478,12 +527,11 @@ void aio_write(ASYNCIO *aio, const void *buffer, unsigned size); * @ingroup File-IO * * @param aio Handle to the file. - * */ void aio_write_newline(ASYNCIO *aio); /** - * Queues a chunk of data for writing. The ASYNCIO struct must be + * Queues a chunk of data for writing. The `ASYNCIO` struct must be * locked using @link aio_lock @endlink first. * * @ingroup File-IO @@ -491,18 +539,16 @@ void aio_write_newline(ASYNCIO *aio); * @param aio Handle to the file. * @param buffer Pointer to the data that should be written. * @param size Number of bytes to write. - * */ void aio_write_unlocked(ASYNCIO *aio, const void *buffer, unsigned size); /** - * Queues a newline for writing. The ASYNCIO struct must be locked + * Queues a newline for writing. The `ASYNCIO` struct must be locked * using @link aio_lock @endlink first. * * @ingroup File-IO * * @param aio Handle to the file. - * */ void aio_write_newline_unlocked(ASYNCIO *aio); @@ -510,16 +556,15 @@ void aio_write_newline_unlocked(ASYNCIO *aio); * Checks whether errors have occurred during the asynchronous * writing. * - * Call this function regularly to see if there are errors. Call - * this function after to see if the process of writing + * Call this function regularly to see if there are errors. Call this + * function after @link aio_wait @endlink to see if the process of writing * to the file succeeded. * * @ingroup File-IO * * @param aio Handle to the file. * - * @eturn 0 if no error occurred, and nonzero on error. - * + * @return `0` on success, or non-`0` on error. */ int aio_error(ASYNCIO *aio); @@ -529,7 +574,6 @@ int aio_error(ASYNCIO *aio); * @ingroup File-IO * * @param aio Handle to the file. - * */ void aio_close(ASYNCIO *aio); @@ -539,17 +583,15 @@ void aio_close(ASYNCIO *aio); * @ingroup File-IO * * @param aio Handle to the file. - * */ void aio_wait(ASYNCIO *aio); /** - * Frees the resources associated to the asynchronous file handle. + * Frees the resources associated with the asynchronous file handle. * * @ingroup File-IO * * @param aio Handle to the file. - * */ void aio_free(ASYNCIO *aio); @@ -558,6 +600,7 @@ void aio_free(ASYNCIO *aio); * Threading related functions. * * @see Locks + * @see Semaphore */ /** @@ -567,7 +610,9 @@ void aio_free(ASYNCIO *aio); * * @param threadfunc Entry point for the new thread. * @param user Pointer to pass to the thread. - * @param name name describing the use of the thread + * @param name Name describing the use of the thread. + * + * @return Handle for the new thread. */ void *thread_init(void (*threadfunc)(void *), void *user, const char *name); @@ -581,35 +626,33 @@ void *thread_init(void (*threadfunc)(void *), void *user, const char *name); void thread_wait(void *thread); /** - * Yield the current threads execution slice. + * Yield the current thread's execution slice. * * @ingroup Threads */ void thread_yield(); /** - * Puts the thread in the detached thread, guaranteeing that + * Puts the thread in the detached state, guaranteeing that * resources of the thread will be freed immediately when the * thread terminates. * * @ingroup Threads * - * @param thread Thread to detach + * @param thread Thread to detach. */ void thread_detach(void *thread); /** - * Creates a new thread and if it succeeded detaches it. + * Creates a new thread and detaches it. * * @ingroup Threads * * @param threadfunc Entry point for the new thread. * @param user Pointer to pass to the thread. - * @param name Name describing the use of the thread - * - * @return true on success, false on failure. + * @param name Name describing the use of the thread. */ -bool thread_init_and_detach(void (*threadfunc)(void *), void *user, const char *name); +void thread_init_and_detach(void (*threadfunc)(void *), void *user, const char *name); /** * @defgroup Semaphore @@ -697,7 +740,7 @@ int64_t time_freq(); * * @return The time as a UNIX timestamp */ -int time_timestamp(); +int64_t time_timestamp(); /** * Retrieves the hours since midnight (0..23) @@ -812,9 +855,11 @@ int net_addr_comp_noport(const NETADDR *a, const NETADDR *b); * @param max_length Maximum size of the string. * @param add_port add port to string or not * + * @return true on success + * * @remark The string will always be zero terminated */ -void net_addr_str(const NETADDR *addr, char *string, int max_length, int add_port); +bool net_addr_str(const NETADDR *addr, char *string, int max_length, int add_port); /** * Turns url string into a network address struct. @@ -1228,6 +1273,32 @@ int str_format_v(char *buffer, int buffer_size, const char *format, va_list args int str_format(char *buffer, int buffer_size, const char *format, ...) GNUC_ATTRIBUTE((format(printf, 3, 4))); +#if !defined(CONF_DEBUG) +int str_format_int(char *buffer, size_t buffer_size, int value); + +template +int str_format_opt(char *buffer, int buffer_size, const char *format, Args... args) +{ + static_assert(sizeof...(args) > 0, "Use str_copy instead of str_format without format arguments"); + return str_format(buffer, buffer_size, format, args...); +} + +template<> +inline int str_format_opt(char *buffer, int buffer_size, const char *format, int val) +{ + if(strcmp(format, "%d") == 0) + { + return str_format_int(buffer, buffer_size, val); + } + else + { + return str_format(buffer, buffer_size, format, val); + } +} + +#define str_format str_format_opt +#endif + /** * Trims specific number of words at the start of a string. * @@ -1580,6 +1651,22 @@ const char *str_find_nocase(const char *haystack, const char *needle); */ const char *str_find(const char *haystack, const char *needle); +/** + * @ingroup Strings + * + * @param haystack String to search in + * @param delim String to search for + * @param offset Number of characters into the haystack + * @param start Will be set to the first delimiter on the left side of the offset (or haystack start) + * @param end Will be set to the first delimiter on the right side of the offset (or haystack end) + * + * @return `true` if both delimiters were found + * @return 'false' if a delimiter is missing (it uses haystack start and end as fallback) + * + * @remark The strings are treated as zero-terminated strings. + */ +bool str_delimiters_around_offset(const char *haystay, const char *delim, int offset, int *start, int *end); + /** * Finds the last occurrence of a character * @@ -1711,6 +1798,21 @@ void str_timestamp_format(char *buffer, int buffer_size, const char *format) void str_timestamp_ex(time_t time, char *buffer, int buffer_size, const char *format) GNUC_ATTRIBUTE((format(strftime, 4, 0))); +/** + * Parses a string into a timestamp following a specified format. + * + * @ingroup Timestamp + * + * @param string Pointer to the string to parse + * @param format The time format to use (for example FORMAT_NOSPACE below) + * @param timestamp Pointer to the timestamp result + * + * @return true on success, false if the string could not be parsed with the specified format + * + */ +bool timestamp_from_str(const char *string, const char *format, time_t *timestamp) + GNUC_ATTRIBUTE((format(strftime, 2, 0))); + #define FORMAT_TIME "%H:%M:%S" #define FORMAT_SPACE "%Y-%m-%d %H:%M:%S" #define FORMAT_NOSPACE "%Y-%m-%d_%H-%M-%S" @@ -1823,11 +1925,11 @@ int fs_makedir(const char *path); int fs_removedir(const char *path); /** - * Recursively create directories for a file. + * Recursively creates parent directories for a file or directory. * * @ingroup Filesystem * - * @param path - File for which to create directories. + * @param path File or directory for which to create parent directories. * * @return 0 on success. Negative value on failure. * @@ -2053,36 +2155,6 @@ int net_would_block(); int net_socket_read_wait(NETSOCKET sock, int time); -/* - Function: open_link - Opens a link in the browser. - - Parameters: - link - The link to open in a browser. - - Returns: - Returns 1 on success, 0 on failure. - - Remarks: - This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows. -*/ -int open_link(const char *link); - -/* - Function: open_file - Opens a file or directory with default program. - - Parameters: - path - The path to open. - - Returns: - Returns 1 on success, 0 on failure. - - Remarks: - This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows. -*/ -int open_file(const char *path); - /** * Swaps the endianness of data. Each element is swapped individually by reversing its bytes. * @@ -2105,18 +2177,12 @@ typedef struct void net_stats(NETSTATS *stats); int str_toint(const char *str); +bool str_toint(const char *str, int *out); int str_toint_base(const char *str, int base); unsigned long str_toulong_base(const char *str, int base); int64_t str_toint64_base(const char *str, int base = 10); float str_tofloat(const char *str); - -void str_from_int(int value, char *buffer, size_t buffer_size); - -template -void str_from_int(int value, char (&dst)[N]) -{ - str_from_int(value, dst, N); -} +bool str_tofloat(const char *str, float *out); /** * Determines whether a character is whitespace. @@ -2133,6 +2199,8 @@ int str_isspace(char c); char str_uppercase(char c); +bool str_isnum(char c); + int str_isallnum(const char *str); int str_isallnum_hex(const char *str); @@ -2354,6 +2422,22 @@ int str_utf8_encode(char *ptr, int chr); */ int str_utf8_check(const char *str); +/* + Function: str_utf8_copy_num + Copies a number of utf8 characters from one string to another. + + Parameters: + dst - Pointer to a buffer that shall receive the string. + src - String to be copied. + dst_size - Size of the buffer dst. + num - maximum number of utf8 characters to be copied. + + Remarks: + - The strings are treated as zero-terminated strings. + - Garantees that dst string will contain zero-termination. +*/ +void str_utf8_copy_num(char *dst, const char *src, int dst_size, int num); + /* Function: str_utf8_stats Determines the byte size and utf8 character count of a utf8 string. @@ -2457,162 +2541,247 @@ unsigned bytes_be_to_uint(const unsigned char *bytes); */ void uint_to_bytes_be(unsigned char *bytes, unsigned value); -/* - Function: pid - Returns the pid of the current process. +/** + * @defgroup Shell + * Shell, process management, OS specific functionality. + */ - Returns: - pid of the current process -*/ +/** + * Returns the ID of the current process. + * + * @ingroup Shell + * + * @return PID of the current process. + */ int pid(); -/* - Function: cmdline_fix - Fixes the command line arguments to be encoded in UTF-8 on all - systems. - - Parameters: - argc - A pointer to the argc parameter that was passed to the main function. - argv - A pointer to the argv parameter that was passed to the main function. - - Remarks: - - You need to call cmdline_free once you're no longer using the - results. -*/ +/** + * Fixes the command line arguments to be encoded in UTF-8 on all systems. + * + * @ingroup Shell + * + * @param argc A pointer to the argc parameter that was passed to the main function. + * @param argv A pointer to the argv parameter that was passed to the main function. + * + * @remark You need to call @link cmdline_free @endlink once you're no longer using the results. + */ void cmdline_fix(int *argc, const char ***argv); -/* - Function: cmdline_free - Frees memory that was allocated by cmdline_fix. - - Parameters: - argc - The argc obtained from cmdline_fix. - argv - The argv obtained from cmdline_fix. - -*/ +/** + * Frees memory that was allocated by @link cmdline_fix @endlink. + * + * @ingroup Shell + * + * @param argc The argc obtained from `cmdline_fix`. + * @param argv The argv obtained from `cmdline_fix`. + */ void cmdline_free(int argc, const char **argv); #if defined(CONF_FAMILY_WINDOWS) +/** + * A handle for a process. + * + * @ingroup Shell + */ typedef void *PROCESS; +/** + * A handle that denotes an invalid process. + * + * @ingroup Shell + */ constexpr PROCESS INVALID_PROCESS = nullptr; #else +/** + * A handle for a process. + * + * @ingroup Shell + */ typedef pid_t PROCESS; +/** + * A handle that denotes an invalid process. + * + * @ingroup Shell + */ constexpr PROCESS INVALID_PROCESS = 0; #endif +#if !defined(CONF_PLATFORM_ANDROID) +/** + * Determines the initial window state when using @link shell_execute @endlink + * to execute a process. + * + * @ingroup Shell + * + * @remark Currently only supported on Windows. + */ +enum class EShellExecuteWindowState +{ + /** + * The process window is opened in the foreground and activated. + */ + FOREGROUND, + + /** + * The process window is opened in the background without focus. + */ + BACKGROUND, +}; -/* - Function: shell_execute - Executes a given file. - - Returns: - handle/pid of the new process -*/ -PROCESS shell_execute(const char *file); - -/* - Function: kill_process - Sends kill signal to a process. - - Parameters: - process - handle/pid of the process +/** + * Executes a given file. + * + * @ingroup Shell + * + * @param file The file to execute. + * @param window_state The window state how the process window should be shown. + * + * @return Handle of the new process, or @link INVALID_PROCESS @endlink on error. + */ +PROCESS shell_execute(const char *file, EShellExecuteWindowState window_state); - Returns: - 0 - Error - 1 - Success -*/ +/** + * Sends kill signal to a process. + * + * @ingroup Shell + * + * @param process Handle of the process to kill. + * + * @return `1` on success, `0` on error. + */ int kill_process(PROCESS process); /** * Checks if a process is alive. - * + * + * @ingroup Shell + * * @param process Handle/PID of the process. - * - * @return bool Returns true if the process is currently running, false if the process is not running (dead). + * + * @return `true` if the process is currently running, + * `false` if the process is not running (dead). */ bool is_process_alive(PROCESS process); -/* - Function: generate_password - Generates a null-terminated password of length `2 * - random_length`. +/** + * Opens a link in the browser. + * + * @ingroup Shell + * + * @param link The link to open in a browser. + * + * @return `1` on success, `0` on failure. + * + * @remark The strings are treated as zero-terminated strings. + * @remark This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows. + */ +int open_link(const char *link); - Parameters: - buffer - Pointer to the start of the output buffer. - length - Length of the buffer. - random - Pointer to a randomly-initialized array of shorts. - random_length - Length of the short array. -*/ -void generate_password(char *buffer, unsigned length, const unsigned short *random, unsigned random_length); +/** + * Opens a file or directory with the default program. + * + * @ingroup Shell + * + * @param path The file or folder to open with the default program. + * + * @return `1` on success, `0` on failure. + * + * @remark The strings are treated as zero-terminated strings. + * @remark This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows. + */ +int open_file(const char *path); +#endif // !defined(CONF_PLATFORM_ANDROID) -/* - Function: secure_random_init - Initializes the secure random module. - You *MUST* check the return value of this function. +/** + * @defgroup Secure-Random + * Secure random number generation. + */ - Returns: - 0 - Initialization succeeded. - 1 - Initialization failed. -*/ -int secure_random_init(); +/** + * Generates a null-terminated password of length `2 * random_length`. + * + * @ingroup Secure-Random + * + * @param buffer Pointer to the start of the output buffer. + * @param length Length of the buffer. + * @param random Pointer to a randomly-initialized array of shorts. + * @param random_length Length of the short array. + */ +void generate_password(char *buffer, unsigned length, const unsigned short *random, unsigned random_length); -/* - Function: secure_random_uninit - Uninitializes the secure random module. +/** + * Initializes the secure random module. + * You *MUST* check the return value of this function. + * + * @ingroup Secure-Random + * + * @return `0` on success. + */ +[[nodiscard]] int secure_random_init(); - Returns: - 0 - Uninitialization succeeded. - 1 - Uninitialization failed. -*/ +/** + * Uninitializes the secure random module. + * + * @ingroup Secure-Random + * + * @return `0` on success. + */ int secure_random_uninit(); -/* - Function: secure_random_password - Fills the buffer with the specified amount of random password - characters. - - The desired password length must be greater or equal to 6, even - and smaller or equal to 128. - - Parameters: - buffer - Pointer to the start of the buffer. - length - Length of the buffer. - pw_length - Length of the desired password. -*/ +/** + * Fills the buffer with the specified amount of random password characters. + * + * @ingroup Secure-Random + * + * @param buffer Pointer to the start of the buffer. + * @param length Length of the buffer. + * @param pw_length Length of the desired password. + * + * @remark The desired password length must be greater or equal to 6, + * even and smaller or equal to 128. + */ void secure_random_password(char *buffer, unsigned length, unsigned pw_length); -/* - Function: secure_random_fill - Fills the buffer with the specified amount of random bytes. - - Parameters: - buffer - Pointer to the start of the buffer. - length - Length of the buffer. -*/ +/** + * Fills the buffer with the specified amount of random bytes. + * + * @ingroup Secure-Random + * + * @param buffer Pointer to the start of the buffer. + * @param length Length of the buffer. + */ void secure_random_fill(void *bytes, unsigned length); -/* - Function: secure_rand - Returns random int (replacement for rand()). -*/ +/** + * Returns random `int`. + * + * @ingroup Secure-Random + * + * @return Random int. + * + * @remark Can be used as a replacement for the `rand` function. + */ int secure_rand(); -/* - Function: secure_rand_below - Returns a random nonnegative integer below the given number, - with a uniform distribution. - - Parameters: - below - Upper limit (exclusive) of integers to return. -*/ +/** + * Returns a random nonnegative integer below the given number, + * with a uniform distribution. + * + * @ingroup Secure-Random + * + * @param below Upper limit (exclusive) of integers to return. + * + * @return Random nonnegative below the given number. + */ int secure_rand_below(int below); /** * Returns a human-readable version string of the operating system. * + * @ingroup Shell + * * @param version Buffer to use for the output. * @param length Length of the output buffer. * - * @return true on success, false on failure. + * @return `true` on success, `false` on failure. */ bool os_version_str(char *version, size_t length); @@ -2623,6 +2792,8 @@ bool os_version_str(char *version, size_t length); * If the preferred locale could not be determined this function * falls back to the locale `"en-US"`. * + * @ingroup Shell + * * @param locale Buffer to use for the output. * @param length Length of the output buffer. * @@ -2631,7 +2802,25 @@ bool os_version_str(char *version, size_t length); void os_locale_str(char *locale, size_t length); #if defined(CONF_EXCEPTION_HANDLING) +/** + * @defgroup Exception-Handling + * Exception handling (crash logging). + */ + +/** + * Initializes the exception handling module. + * + * @ingroup Exception-Handling + */ void init_exception_handler(); + +/** + * Sets the filename for writing the crash log. + * + * @ingroup Exception-Handling + * + * @param log_file_path Absolute path to which crash log file should be written. + */ void set_exception_handler_log_file(const char *log_file_path); #endif @@ -2648,7 +2837,9 @@ int net_socket_read_wait(NETSOCKET sock, std::chrono::nanoseconds nanoseconds); /** * Fixes the command line arguments to be encoded in UTF-8 on all systems. - * This is a RAII wrapper for cmdline_fix and cmdline_free. + * This is a RAII wrapper for @link cmdline_fix @endlink and @link cmdline_free @endlink. + * + * @ingroup Shell */ class CCmdlineFix { @@ -2670,25 +2861,29 @@ class CCmdlineFix #if defined(CONF_FAMILY_WINDOWS) /** - * Converts a utf8 encoded string to a wide character string + * Converts a UTF-8 encoded string to a wide character string * for use with the Windows API. * - * @param str The utf8 encoded string to convert. + * @ingroup Shell + * + * @param str The UTF-8 encoded string to convert. * * @return The argument as a wide character string. * * @remark The argument string must be zero-terminated. - * @remark Fails with assertion error if passed utf8 is invalid. + * @remark Fails with assertion error if passed UTF-8 is invalid. */ std::wstring windows_utf8_to_wide(const char *str); /** * Converts a wide character string obtained from the Windows API - * to a utf8 encoded string. + * to a UTF-8 encoded string. + * + * @ingroup Shell * * @param wide_str The wide character string to convert. * - * @return The argument as a utf8 encoded string, wrapped in an optional. + * @return The argument as a UTF-8 encoded string, wrapped in an optional. * The optional is empty, if the wide string contains invalid codepoints. * * @remark The argument string must be zero-terminated. @@ -2697,10 +2892,13 @@ std::optional windows_wide_to_utf8(const wchar_t *wide_str); /** * This is a RAII wrapper to initialize/uninitialize the Windows COM library, - * which may be necessary for using the open_file and open_link functions. + * which may be necessary for using the @link open_file @endlink and + * @link open_link @endlink functions. * Must be used on every thread. It's automatically used on threads created - * with thread_init. Pass true to the constructor on threads that own a - * window (i.e. pump a message queue). + * with @link thread_init @endlink. Pass `true` to the constructor on threads + * that own a window (i.e. pump a message queue). + * + * @ingroup Shell */ class CWindowsComLifecycle { @@ -2716,11 +2914,11 @@ class CWindowsComLifecycle * * @param protocol_name The name of the protocol. * @param executable The absolute path of the executable that will be associated with the protocol. - * @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated. + * @param updated Pointer to a variable that will be set to `true`, iff the shell needs to be updated. * - * @return true on success, false on failure. + * @return `true` on success, `false` on failure. * - * @remark The caller must later call shell_update, iff the shell needs to be updated. + * @remark The caller must later call @link shell_update @endlink, iff the shell needs to be updated. */ bool shell_register_protocol(const char *protocol_name, const char *executable, bool *updated); @@ -2733,11 +2931,11 @@ bool shell_register_protocol(const char *protocol_name, const char *executable, * @param description A readable description for the file extension. * @param executable_name A unique name that will used to describe the application. * @param executable The absolute path of the executable that will be associated with the file extension. - * @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated. + * @param updated Pointer to a variable that will be set to `true`, iff the shell needs to be updated. * - * @return true on success, false on failure. + * @return `true` on success, `false` on failure. * - * @remark The caller must later call shell_update, iff the shell needs to be updated. + * @remark The caller must later call @link shell_update @endlink, iff the shell needs to be updated. */ bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable, bool *updated); @@ -2748,11 +2946,11 @@ bool shell_register_extension(const char *extension, const char *description, co * * @param name Readable name of the application. * @param executable The absolute path of the executable being registered. - * @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated. + * @param updated Pointer to a variable that will be set to `true`, iff the shell needs to be updated. * - * @return true on success, false on failure. + * @return `true` on success, `false` on failure. * - * @remark The caller must later call shell_update, iff the shell needs to be updated. + * @remark The caller must later call @link shell_update @endlink, iff the shell needs to be updated. */ bool shell_register_application(const char *name, const char *executable, bool *updated); @@ -2764,11 +2962,11 @@ bool shell_register_application(const char *name, const char *executable, bool * * @param shell_class The shell class to delete. * For protocols this is the name of the protocol. * For file extensions this is the program ID associated with the file extension. - * @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated. + * @param updated Pointer to a variable that will be set to `true`, iff the shell needs to be updated. * - * @return true on success, false on failure. + * @return `true` on success, `false` on failure. * - * @remark The caller must later call shell_update, iff the shell needs to be updated. + * @remark The caller must later call @link shell_update @endlink, iff the shell needs to be updated. */ bool shell_unregister_class(const char *shell_class, bool *updated); @@ -2778,11 +2976,11 @@ bool shell_unregister_class(const char *shell_class, bool *updated); * @ingroup Shell * * @param executable The absolute path of the executable being unregistered. - * @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated. + * @param updated Pointer to a variable that will be set to `true`, iff the shell needs to be updated. * - * @return true on success, false on failure. + * @return `true` on success, `false` on failure. * - * @remark The caller must later call shell_update, iff the shell needs to be updated. + * @remark The caller must later call @link shell_update @endlink, iff the shell needs to be updated. */ bool shell_unregister_application(const char *executable, bool *updated); diff --git a/src/base/types.h b/src/base/types.h index ffa1b47ed2..b1d5af051c 100644 --- a/src/base/types.h +++ b/src/base/types.h @@ -10,6 +10,11 @@ enum class TRISTATE ALL, }; +/** + * Handle for input/output files/streams. + * + * @ingroup File-IO + */ typedef void *IOHANDLE; typedef int (*FS_LISTDIR_CALLBACK)(const char *name, int is_dir, int dir_type, void *user); @@ -45,9 +50,14 @@ enum NETTYPE_IPV4 = 1, NETTYPE_IPV6 = 2, NETTYPE_WEBSOCKET_IPV4 = 8, + /** + * 0.7 address. This is a flag in NETADDR to avoid introducing a parameter to every networking function + * to differenciate between 0.6 and 0.7 connections. + */ + NETTYPE_TW7 = 16, NETTYPE_ALL = NETTYPE_IPV4 | NETTYPE_IPV6 | NETTYPE_WEBSOCKET_IPV4, - NETTYPE_MASK = NETTYPE_ALL | NETTYPE_LINK_BROADCAST, + NETTYPE_MASK = NETTYPE_ALL | NETTYPE_LINK_BROADCAST | NETTYPE_TW7, }; /** diff --git a/src/engine/Cargo.toml b/src/engine/Cargo.toml index b6d484cd01..48eab28c4e 100644 --- a/src/engine/Cargo.toml +++ b/src/engine/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ddnet-engine" version = "0.0.1" -edition = "2018" +edition = "2021" publish = false license = "Zlib" diff --git a/src/engine/antibot.h b/src/engine/antibot.h index 0b432bb78b..22ab05fddd 100644 --- a/src/engine/antibot.h +++ b/src/engine/antibot.h @@ -11,15 +11,15 @@ class IAntibot : public IInterface virtual void RoundEnd() = 0; // Hooks - virtual void OnPlayerInit(int ClientID) = 0; - virtual void OnPlayerDestroy(int ClientID) = 0; - virtual void OnSpawn(int ClientID) = 0; - virtual void OnHammerFireReloading(int ClientID) = 0; - virtual void OnHammerFire(int ClientID) = 0; - virtual void OnHammerHit(int ClientID, int TargetID) = 0; - virtual void OnDirectInput(int ClientID) = 0; - virtual void OnCharacterTick(int ClientID) = 0; - virtual void OnHookAttach(int ClientID, bool Player) = 0; + virtual void OnPlayerInit(int ClientId) = 0; + virtual void OnPlayerDestroy(int ClientId) = 0; + virtual void OnSpawn(int ClientId) = 0; + virtual void OnHammerFireReloading(int ClientId) = 0; + virtual void OnHammerFire(int ClientId) = 0; + virtual void OnHammerHit(int ClientId, int TargetId) = 0; + virtual void OnDirectInput(int ClientId) = 0; + virtual void OnCharacterTick(int ClientId) = 0; + virtual void OnHookAttach(int ClientId, bool Player) = 0; // Commands virtual void ConsoleCommand(const char *pCommand) = 0; @@ -35,11 +35,11 @@ class IEngineAntibot : public IAntibot // Hooks virtual void OnEngineTick() = 0; - virtual void OnEngineClientJoin(int ClientID, bool Sixup) = 0; - virtual void OnEngineClientDrop(int ClientID, const char *pReason) = 0; - virtual bool OnEngineClientMessage(int ClientID, const void *pData, int Size, int Flags) = 0; - virtual bool OnEngineServerMessage(int ClientID, const void *pData, int Size, int Flags) = 0; - virtual bool OnEngineSimulateClientMessage(int *pClientID, void *pBuffer, int BufferSize, int *pOutSize, int *pFlags) = 0; + virtual void OnEngineClientJoin(int ClientId, bool Sixup) = 0; + virtual void OnEngineClientDrop(int ClientId, const char *pReason) = 0; + virtual bool OnEngineClientMessage(int ClientId, const void *pData, int Size, int Flags) = 0; + virtual bool OnEngineServerMessage(int ClientId, const void *pData, int Size, int Flags) = 0; + virtual bool OnEngineSimulateClientMessage(int *pClientId, void *pBuffer, int BufferSize, int *pOutSize, int *pFlags) = 0; virtual ~IEngineAntibot(){}; }; diff --git a/src/engine/client.h b/src/engine/client.h index d4c9f3fd43..a9599f89b4 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -8,11 +8,16 @@ #include "message.h" #include +#include + #include +#include #include #include +#include + struct SWarning; enum @@ -22,8 +27,6 @@ enum RECORDER_RACE = 2, RECORDER_REPLAYS = 3, RECORDER_MAX = 4, - - NUM_DUMMIES = 2, }; typedef bool (*CLIENTFUNC_FILTER)(const void *pData, int DataSize, void *pUser); @@ -61,11 +64,18 @@ class IClient : public IInterface { LOADING_STATE_DETAIL_INITIAL, LOADING_STATE_DETAIL_LOADING_MAP, + LOADING_STATE_DETAIL_LOADING_DEMO, LOADING_STATE_DETAIL_SENDING_READY, LOADING_STATE_DETAIL_GETTING_READY, }; - typedef std::function TMapLoadingCallbackFunc; + enum ELoadingCallbackDetail + { + LOADING_CALLBACK_DETAIL_MAP, + LOADING_CALLBACK_DETAIL_DEMO, + }; + typedef std::function TLoadingCallback; + CTranslationContext m_TranslationContext; protected: // quick access to state of the client @@ -88,7 +98,7 @@ class IClient : public IInterface float m_RenderFrameTime = 0.0001f; float m_FrameTimeAvg = 0.0001f; - TMapLoadingCallbackFunc m_MapLoadingCBFunc = nullptr; + TLoadingCallback m_LoadingCallback = nullptr; char m_aNews[3000] = ""; int m_Points = -1; @@ -99,7 +109,8 @@ class IClient : public IInterface { public: int m_Type; - int m_ID; + int m_Id; + const void *m_pData; int m_DataSize; }; @@ -128,7 +139,7 @@ class IClient : public IInterface inline int64_t StateStartTime() const { return m_StateStartTime; } void SetLoadingStateDetail(ELoadingStateDetail LoadingStateDetail) { m_LoadingStateDetail = LoadingStateDetail; } - void SetMapLoadingCBFunc(TMapLoadingCallbackFunc &&Func) { m_MapLoadingCBFunc = std::move(Func); } + void SetLoadingCallback(TLoadingCallback &&Func) { m_LoadingCallback = std::move(Func); } // tick time access inline int PrevGameTick(int Conn) const { return m_aPrevGameTick[Conn]; } @@ -153,9 +164,10 @@ class IClient : public IInterface // dummy virtual void DummyDisconnect(const char *pReason) = 0; virtual void DummyConnect() = 0; - virtual bool DummyConnected() = 0; - virtual bool DummyConnecting() = 0; - virtual bool DummyAllowed() = 0; + virtual bool DummyConnected() const = 0; + virtual bool DummyConnecting() const = 0; + virtual bool DummyConnectingDelayed() const = 0; + virtual bool DummyAllowed() const = 0; virtual void Restart() = 0; virtual void Quit() = 0; @@ -165,7 +177,7 @@ class IClient : public IInterface #endif virtual void DemoRecorder_Start(const char *pFilename, bool WithTimestamp, int Recorder, bool Verbose = false) = 0; virtual void DemoRecorder_HandleAutoStart() = 0; - virtual void DemoRecorder_Stop(int Recorder, bool RemoveFile = false) = 0; + virtual void DemoRecorder_UpdateReplayRecorder() = 0; virtual class IDemoRecorder *DemoRecorder(int Recorder) = 0; virtual void AutoScreenshot_Start() = 0; virtual void AutoStatScreenshot_Start() = 0; @@ -174,7 +186,7 @@ class IClient : public IInterface // gfx virtual void SwitchWindowScreen(int Index) = 0; - virtual void SetWindowParams(int FullscreenMode, bool IsBorderless, bool AllowResizing) = 0; + virtual void SetWindowParams(int FullscreenMode, bool IsBorderless) = 0; virtual void ToggleWindowVSync() = 0; virtual void Notify(const char *pTitle, const char *pMessage) = 0; virtual void OnWindowResize() = 0; @@ -200,9 +212,12 @@ class IClient : public IInterface virtual bool RconAuthed() const = 0; virtual bool UseTempRconCommands() const = 0; virtual void Rcon(const char *pLine) = 0; + virtual bool ReceivingRconCommands() const = 0; + virtual float GotRconCommandsPercentage() const = 0; // server info virtual void GetServerInfo(class CServerInfo *pServerInfo) const = 0; + virtual bool ServerCapAnyPlayerFlag() const = 0; virtual int GetPredictionTime() = 0; @@ -216,34 +231,41 @@ class IClient : public IInterface }; // TODO: Refactor: should redo this a bit i think, too many virtual calls - virtual int SnapNumItems(int SnapID) const = 0; - virtual const void *SnapFindItem(int SnapID, int Type, int ID) const = 0; - virtual void *SnapGetItem(int SnapID, int Index, CSnapItem *pItem) const = 0; - virtual int SnapItemSize(int SnapID, int Index) const = 0; + virtual int SnapNumItems(int SnapId) const = 0; + virtual const void *SnapFindItem(int SnapId, int Type, int Id) const = 0; + virtual CSnapItem SnapGetItem(int SnapId, int Index) const = 0; virtual void SnapSetStaticsize(int ItemType, int Size) = 0; + virtual void SnapSetStaticsize7(int ItemType, int Size) = 0; virtual int SendMsg(int Conn, CMsgPacker *pMsg, int Flags) = 0; virtual int SendMsgActive(CMsgPacker *pMsg, int Flags) = 0; template - int SendPackMsgActive(T *pMsg, int Flags) + int SendPackMsgActive(T *pMsg, int Flags, bool NoTranslate = false) { - CMsgPacker Packer(T::ms_MsgID, false); + CMsgPacker Packer(T::ms_MsgId, false, NoTranslate); if(pMsg->Pack(&Packer)) return -1; return SendMsgActive(&Packer, Flags); } + template + int SendPackMsg(int Conn, T *pMsg, int Flags, bool NoTranslate = false) + { + CMsgPacker Packer(T::ms_MsgId, false, NoTranslate); + if(pMsg->Pack(&Packer)) + return -1; + return SendMsg(Conn, &Packer, Flags); + } + // virtual const char *PlayerName() const = 0; - virtual const char *DummyName() const = 0; + virtual const char *DummyName() = 0; virtual const char *ErrorString() const = 0; virtual const char *LatestVersion() const = 0; virtual bool ConnectionProblems() const = 0; - virtual bool SoundInitFailed() const = 0; - virtual IGraphics::CTextureHandle GetDebugFont() const = 0; // TODO: remove this function //DDRace @@ -257,6 +279,9 @@ class IClient : public IInterface int Points() const { return m_Points; } int64_t ReconnectTime() const { return m_ReconnectTime; } void SetReconnectTime(int64_t ReconnectTime) { m_ReconnectTime = ReconnectTime; } + + virtual bool IsSixup() const = 0; + virtual int GetCurrentRaceTime() = 0; virtual void RaceRecord_Start(const char *pFilename) = 0; @@ -280,9 +305,29 @@ class IClient : public IInterface virtual SWarning *GetCurWarning() = 0; virtual CChecksumData *ChecksumData() = 0; - virtual bool InfoTaskRunning() = 0; virtual int UdpConnectivity(int NetType) = 0; + /** + * Opens a link in the browser. + * + * @param pLink The link to open in a browser. + * + * @return `true` on success, `false` on failure. + * + * @remark This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows. + */ + virtual bool ViewLink(const char *pLink) = 0; + /** + * Opens a file or directory with the default program. + * + * @param pFilename The file or folder to open with the default program. + * + * @return `true` on success, `false` on failure. + * + * @remark This may not be called with untrusted input or it'll result in arbitrary code execution, especially on Windows. + */ + virtual bool ViewFile(const char *pFilename) = 0; + #if defined(CONF_FAMILY_WINDOWS) virtual void ShellRegister() = 0; virtual void ShellUnregister() = 0; @@ -295,7 +340,7 @@ class IClient : public IInterface MESSAGE_BOX_TYPE_INFO, }; virtual void ShowMessageBox(const char *pTitle, const char *pMessage, EMessageBoxType Type = MESSAGE_BOX_TYPE_ERROR) = 0; - virtual void GetGPUInfoString(char (&aGPUInfo)[256]) = 0; + virtual void GetGpuInfoString(char (&aGpuInfo)[256]) = 0; }; class IGameClient : public IInterface @@ -316,7 +361,7 @@ class IGameClient : public IInterface virtual void OnUpdate() = 0; virtual void OnStateChange(int NewState, int OldState) = 0; virtual void OnConnected() = 0; - virtual void OnMessage(int MsgID, CUnpacker *pUnpacker, int Conn, bool Dummy) = 0; + virtual void OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dummy) = 0; virtual void OnPredict() = 0; virtual void OnActivateEditor() = 0; virtual void OnWindowResize() = 0; @@ -329,19 +374,28 @@ class IGameClient : public IInterface virtual const char *GetItemName(int Type) const = 0; virtual const char *Version() const = 0; virtual const char *NetVersion() const = 0; + virtual const char *NetVersion7() const = 0; virtual int DDNetVersion() const = 0; virtual const char *DDNetVersionStr() const = 0; virtual void OnDummyDisconnect() = 0; virtual void DummyResetInput() = 0; virtual void Echo(const char *pString) = 0; + virtual bool CanDisplayWarning() const = 0; - virtual bool IsDisplayingWarning() const = 0; + virtual void RenderShutdownMessage() = 0; virtual CNetObjHandler *GetNetObjHandler() = 0; + virtual protocol7::CNetObjHandler *GetNetObjHandler7() = 0; + + virtual int ClientVersion7() const = 0; + + virtual void ApplySkin7InfoFromSnapObj(const protocol7::CNetObj_De_ClientInfo *pObj, int ClientId) = 0; + virtual int OnDemoRecSnap7(class CSnapshot *pFrom, class CSnapshot *pTo, int Conn) = 0; + virtual int TranslateSnap(class CSnapshot *pSnapDstSix, class CSnapshot *pSnapSrcSeven, int Conn, bool Dummy) = 0; }; -void SnapshotRemoveExtraProjectileInfo(unsigned char *pData); +void SnapshotRemoveExtraProjectileInfo(class CSnapshot *pSnap); extern IGameClient *CreateGameClient(); #endif diff --git a/src/engine/client/backend/backend_base.cpp b/src/engine/client/backend/backend_base.cpp index 168598064f..37d3b31784 100644 --- a/src/engine/client/backend/backend_base.cpp +++ b/src/engine/client/backend/backend_base.cpp @@ -1,13 +1,7 @@ #include "backend_base.h" #include -#include -void *CCommandProcessorFragment_GLBase::Resize(const unsigned char *pData, int Width, int Height, int NewWidth, int NewHeight, int BPP) -{ - return ResizeImage((const uint8_t *)pData, Width, Height, NewWidth, NewHeight, BPP); -} - -bool CCommandProcessorFragment_GLBase::Texture2DTo3D(void *pImageBuffer, int ImageWidth, int ImageHeight, size_t PixelSize, int SplitCountWidth, int SplitCountHeight, void *pTarget3DImageData, int &Target3DImageWidth, int &Target3DImageHeight) +bool CCommandProcessorFragment_GLBase::Texture2DTo3D(uint8_t *pImageBuffer, int ImageWidth, int ImageHeight, size_t PixelSize, int SplitCountWidth, int SplitCountHeight, uint8_t *pTarget3DImageData, int &Target3DImageWidth, int &Target3DImageHeight) { Target3DImageWidth = ImageWidth / SplitCountWidth; Target3DImageHeight = ImageHeight / SplitCountHeight; @@ -23,10 +17,10 @@ bool CCommandProcessorFragment_GLBase::Texture2DTo3D(void *pImageBuffer, int Ima int DepthIndex = X + Y * SplitCountWidth; size_t TargetImageFullWidth = (size_t)Target3DImageWidth * PixelSize; - size_t TargetImageFullSize = (size_t)TargetImageFullWidth * Target3DImageHeight; + size_t TargetImageFullSize = TargetImageFullWidth * Target3DImageHeight; ptrdiff_t ImageOffset = (ptrdiff_t)(((size_t)Y * FullImageWidth * (size_t)Target3DImageHeight) + ((size_t)Y3D * FullImageWidth) + ((size_t)X * TargetImageFullWidth)); ptrdiff_t TargetImageOffset = (ptrdiff_t)(TargetImageFullSize * (size_t)DepthIndex + ((size_t)Y3D * TargetImageFullWidth)); - mem_copy(((uint8_t *)pTarget3DImageData) + TargetImageOffset, ((uint8_t *)pImageBuffer) + (ptrdiff_t)(ImageOffset), TargetImageFullWidth); + mem_copy(pTarget3DImageData + TargetImageOffset, pImageBuffer + ImageOffset, TargetImageFullWidth); } } } diff --git a/src/engine/client/backend/backend_base.h b/src/engine/client/backend/backend_base.h index 19b7eba6f2..516fb91df1 100644 --- a/src/engine/client/backend/backend_base.h +++ b/src/engine/client/backend/backend_base.h @@ -84,9 +84,7 @@ class CCommandProcessorFragment_GLBase SGfxErrorContainer m_Error; SGfxWarningContainer m_Warning; - static void *Resize(const unsigned char *pData, int Width, int Height, int NewWidth, int NewHeight, int BPP); - - static bool Texture2DTo3D(void *pImageBuffer, int ImageWidth, int ImageHeight, size_t PixelSize, int SplitCountWidth, int SplitCountHeight, void *pTarget3DImageData, int &Target3DImageWidth, int &Target3DImageHeight); + static bool Texture2DTo3D(uint8_t *pImageBuffer, int ImageWidth, int ImageHeight, size_t PixelSize, int SplitCountWidth, int SplitCountHeight, uint8_t *pTarget3DImageData, int &Target3DImageWidth, int &Target3DImageHeight); virtual bool GetPresentedImageData(uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) = 0; @@ -123,7 +121,7 @@ class CCommandProcessorFragment_GLBase char *m_pVersionString; char *m_pRendererString; - TTWGraphicsGPUList *m_pGPUList; + TTwGraphicsGpuList *m_pGpuList; }; struct SCommand_Init : public CCommandBuffer::SCommand @@ -141,7 +139,7 @@ class CCommandProcessorFragment_GLBase std::atomic *m_pStreamMemoryUsage; std::atomic *m_pStagingMemoryUsage; - TTWGraphicsGPUList *m_pGPUList; + TTwGraphicsGpuList *m_pGpuList; TGLBackendReadPresentedImageData *m_pReadPresentedImageDataFunc; diff --git a/src/engine/client/backend/opengl/backend_opengl.cpp b/src/engine/client/backend/opengl/backend_opengl.cpp index a599a99037..3ca11ff59e 100644 --- a/src/engine/client/backend/opengl/backend_opengl.cpp +++ b/src/engine/client/backend/opengl/backend_opengl.cpp @@ -5,6 +5,7 @@ #include #include +#include #if defined(BACKEND_AS_OPENGL_ES) || !defined(CONF_BACKEND_OPENGL_ES) @@ -42,13 +43,6 @@ void CCommandProcessorFragment_OpenGL::Cmd_Update_Viewport(const CCommandBuffer: glViewport(pCommand->m_X, pCommand->m_Y, pCommand->m_Width, pCommand->m_Height); } -int CCommandProcessorFragment_OpenGL::TexFormatToOpenGLFormat(int TexFormat) -{ - if(TexFormat == CCommandBuffer::TEXFORMAT_RGBA) - return GL_RGBA; - return GL_RGBA; -} - size_t CCommandProcessorFragment_OpenGL::GLFormatToPixelSize(int GLFormat) { switch(GLFormat) @@ -194,7 +188,7 @@ static void ParseVersionString(EBackendType BackendType, const char *pStr, int & bool LastWasNumber = false; while(*pStr && TotalNumbersPassed < 3) { - if(*pStr >= '0' && *pStr <= '9') + if(str_isnum(*pStr)) { aCurNumberStr[CurNumberStrLen++] = (char)*pStr; LastWasNumber = true; @@ -313,8 +307,9 @@ bool CCommandProcessorFragment_OpenGL::InitOpenGL(const SCommand_Init *pCommand) { m_IsOpenGLES = pCommand->m_RequestedBackend == BACKEND_TYPE_OPENGL_ES; - TGLBackendReadPresentedImageData &ReadPresentedImgDataFunc = *pCommand->m_pReadPresentedImageDataFunc; - ReadPresentedImgDataFunc = [this](uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) { return GetPresentedImageData(Width, Height, Format, vDstData); }; + *pCommand->m_pReadPresentedImageDataFunc = [this](uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) { + return GetPresentedImageData(Width, Height, Format, vDstData); + }; const char *pVendorString = (const char *)glGetString(GL_VENDOR); dbg_msg("opengl", "Vendor string: %s", pVendorString); @@ -325,9 +320,9 @@ bool CCommandProcessorFragment_OpenGL::InitOpenGL(const SCommand_Init *pCommand) const char *pRendererString = (const char *)glGetString(GL_RENDERER); - str_copy(pCommand->m_pVendorString, pVendorString, gs_GPUInfoStringSize); - str_copy(pCommand->m_pVersionString, pVersionString, gs_GPUInfoStringSize); - str_copy(pCommand->m_pRendererString, pRendererString, gs_GPUInfoStringSize); + str_copy(pCommand->m_pVendorString, pVendorString, gs_GpuInfoStringSize); + str_copy(pCommand->m_pVersionString, pVersionString, gs_GpuInfoStringSize); + str_copy(pCommand->m_pRendererString, pRendererString, gs_GpuInfoStringSize); // parse version string ParseVersionString(pCommand->m_RequestedBackend, pVersionString, pCommand->m_pCapabilities->m_ContextMajor, pCommand->m_pCapabilities->m_ContextMinor, pCommand->m_pCapabilities->m_ContextPatch); @@ -617,7 +612,7 @@ bool CCommandProcessorFragment_OpenGL::Cmd_Init(const SCommand_Init *pCommand) return true; } -void CCommandProcessorFragment_OpenGL::TextureUpdate(int Slot, int X, int Y, int Width, int Height, int GLFormat, void *pTexData) +void CCommandProcessorFragment_OpenGL::TextureUpdate(int Slot, int X, int Y, int Width, int Height, int GLFormat, uint8_t *pTexData) { glBindTexture(GL_TEXTURE_2D, m_vTextures[Slot].m_Tex); @@ -630,7 +625,7 @@ void CCommandProcessorFragment_OpenGL::TextureUpdate(int Slot, int X, int Y, int int ResizedW = (int)(Width * ResizeW); int ResizedH = (int)(Height * ResizeH); - void *pTmpData = Resize(static_cast(pTexData), Width, Height, ResizedW, ResizedH, GLFormatToPixelSize(GLFormat)); + uint8_t *pTmpData = ResizeImage(pTexData, Width, Height, ResizedW, ResizedH, GLFormatToPixelSize(GLFormat)); free(pTexData); pTexData = pTmpData; @@ -652,7 +647,7 @@ void CCommandProcessorFragment_OpenGL::TextureUpdate(int Slot, int X, int Y, int Y /= 2; } - void *pTmpData = Resize(static_cast(pTexData), OldWidth, OldHeight, Width, Height, GLFormatToPixelSize(GLFormat)); + uint8_t *pTmpData = ResizeImage(pTexData, OldWidth, OldHeight, Width, Height, GLFormatToPixelSize(GLFormat)); free(pTexData); pTexData = pTmpData; } @@ -661,11 +656,6 @@ void CCommandProcessorFragment_OpenGL::TextureUpdate(int Slot, int X, int Y, int free(pTexData); } -void CCommandProcessorFragment_OpenGL::Cmd_Texture_Update(const CCommandBuffer::SCommand_Texture_Update *pCommand) -{ - TextureUpdate(pCommand->m_Slot, pCommand->m_X, pCommand->m_Y, pCommand->m_Width, pCommand->m_Height, TexFormatToOpenGLFormat(pCommand->m_Format), pCommand->m_pData); -} - void CCommandProcessorFragment_OpenGL::DestroyTexture(int Slot) { m_pTextureMemoryUsage->store(m_pTextureMemoryUsage->load(std::memory_order_relaxed) - m_vTextures[Slot].m_MemSize, std::memory_order_relaxed); @@ -704,7 +694,7 @@ void CCommandProcessorFragment_OpenGL::Cmd_Texture_Destroy(const CCommandBuffer: DestroyTexture(pCommand->m_Slot); } -void CCommandProcessorFragment_OpenGL::TextureCreate(int Slot, int Width, int Height, int GLFormat, int GLStoreFormat, int Flags, void *pTexData) +void CCommandProcessorFragment_OpenGL::TextureCreate(int Slot, int Width, int Height, int GLFormat, int GLStoreFormat, int Flags, uint8_t *pTexData) { #ifndef BACKEND_GL_MODERN_API @@ -727,7 +717,7 @@ void CCommandProcessorFragment_OpenGL::TextureCreate(int Slot, int Width, int He int PowerOfTwoHeight = HighestBit(Height); if(Width != PowerOfTwoWidth || Height != PowerOfTwoHeight) { - void *pTmpData = Resize(static_cast(pTexData), Width, Height, PowerOfTwoWidth, PowerOfTwoHeight, GLFormatToPixelSize(GLFormat)); + uint8_t *pTmpData = ResizeImage(pTexData, Width, Height, PowerOfTwoWidth, PowerOfTwoHeight, GLFormatToPixelSize(GLFormat)); free(pTexData); pTexData = pTmpData; @@ -759,7 +749,7 @@ void CCommandProcessorFragment_OpenGL::TextureCreate(int Slot, int Width, int He if(NeedsResize) { - void *pTmpData = Resize(static_cast(pTexData), OldWidth, OldHeight, Width, Height, GLFormatToPixelSize(GLFormat)); + uint8_t *pTmpData = ResizeImage(pTexData, OldWidth, OldHeight, Width, Height, GLFormatToPixelSize(GLFormat)); free(pTexData); pTexData = pTmpData; } @@ -866,9 +856,7 @@ void CCommandProcessorFragment_OpenGL::TextureCreate(int Slot, int Width, int He glBindSampler(0, 0); } - uint8_t *p3DImageData = NULL; - - p3DImageData = (uint8_t *)malloc((size_t)Width * Height * PixelSize); + uint8_t *p3DImageData = static_cast(malloc((size_t)Width * Height * PixelSize)); int Image3DWidth, Image3DHeight; int ConvertWidth = Width; @@ -879,7 +867,7 @@ void CCommandProcessorFragment_OpenGL::TextureCreate(int Slot, int Width, int He dbg_msg("gfx", "3D/2D array texture was resized"); int NewWidth = maximum(HighestBit(ConvertWidth), 16); int NewHeight = maximum(HighestBit(ConvertHeight), 16); - uint8_t *pNewTexData = (uint8_t *)Resize((const uint8_t *)pTexData, ConvertWidth, ConvertHeight, NewWidth, NewHeight, GLFormatToPixelSize(GLFormat)); + uint8_t *pNewTexData = ResizeImage(pTexData, ConvertWidth, ConvertHeight, NewWidth, NewHeight, GLFormatToPixelSize(GLFormat)); ConvertWidth = NewWidth; ConvertHeight = NewHeight; @@ -888,7 +876,7 @@ void CCommandProcessorFragment_OpenGL::TextureCreate(int Slot, int Width, int He pTexData = pNewTexData; } - if((Texture2DTo3D(pTexData, ConvertWidth, ConvertHeight, PixelSize, 16, 16, p3DImageData, Image3DWidth, Image3DHeight))) + if(Texture2DTo3D(pTexData, ConvertWidth, ConvertHeight, PixelSize, 16, 16, p3DImageData, Image3DWidth, Image3DHeight)) { glTexImage3D(Target, 0, GLStoreFormat, Image3DWidth, Image3DHeight, 256, 0, GLFormat, GL_UNSIGNED_BYTE, p3DImageData); } @@ -916,7 +904,7 @@ void CCommandProcessorFragment_OpenGL::TextureCreate(int Slot, int Width, int He void CCommandProcessorFragment_OpenGL::Cmd_Texture_Create(const CCommandBuffer::SCommand_Texture_Create *pCommand) { - TextureCreate(pCommand->m_Slot, pCommand->m_Width, pCommand->m_Height, TexFormatToOpenGLFormat(pCommand->m_Format), TexFormatToOpenGLFormat(pCommand->m_StoreFormat), pCommand->m_Flags, pCommand->m_pData); + TextureCreate(pCommand->m_Slot, pCommand->m_Width, pCommand->m_Height, GL_RGBA, GL_RGBA, pCommand->m_Flags, pCommand->m_pData); } void CCommandProcessorFragment_OpenGL::Cmd_TextTexture_Update(const CCommandBuffer::SCommand_TextTexture_Update *pCommand) @@ -983,10 +971,27 @@ void CCommandProcessorFragment_OpenGL::Cmd_Render(const CCommandBuffer::SCommand #endif } -void CCommandProcessorFragment_OpenGL::Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand) +void CCommandProcessorFragment_OpenGL::Cmd_ReadPixel(const CCommandBuffer::SCommand_TrySwapAndReadPixel *pCommand) { - *pCommand->m_pSwapped = false; + // get size of viewport + GLint aViewport[4] = {0, 0, 0, 0}; + glGetIntegerv(GL_VIEWPORT, aViewport); + const int h = aViewport[3]; + // fetch the pixel + uint8_t aPixelData[3]; + GLint Alignment; + glGetIntegerv(GL_PACK_ALIGNMENT, &Alignment); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(pCommand->m_Position.x, h - 1 - pCommand->m_Position.y, 1, 1, GL_RGB, GL_UNSIGNED_BYTE, aPixelData); + glPixelStorei(GL_PACK_ALIGNMENT, Alignment); + + // fill in the information + *pCommand->m_pColor = ColorRGBA(aPixelData[0] / 255.0f, aPixelData[1] / 255.0f, aPixelData[2] / 255.0f, 1.0f); +} + +void CCommandProcessorFragment_OpenGL::Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand) +{ // fetch image data GLint aViewport[4] = {0, 0, 0, 0}; glGetIntegerv(GL_VIEWPORT, aViewport); @@ -1047,9 +1052,6 @@ ERunCommandReturnTypes CCommandProcessorFragment_OpenGL::RunCommand(const CComma case CCommandBuffer::CMD_TEXTURE_DESTROY: Cmd_Texture_Destroy(static_cast(pBaseCommand)); break; - case CCommandBuffer::CMD_TEXTURE_UPDATE: - Cmd_Texture_Update(static_cast(pBaseCommand)); - break; case CCommandBuffer::CMD_TEXT_TEXTURES_CREATE: Cmd_TextTextures_Create(static_cast(pBaseCommand)); break; @@ -1068,6 +1070,9 @@ ERunCommandReturnTypes CCommandProcessorFragment_OpenGL::RunCommand(const CComma case CCommandBuffer::CMD_RENDER_TEX3D: Cmd_RenderTex3D(static_cast(pBaseCommand)); break; + case CCommandBuffer::CMD_TRY_SWAP_AND_READ_PIXEL: + Cmd_ReadPixel(static_cast(pBaseCommand)); + break; case CCommandBuffer::CMD_TRY_SWAP_AND_SCREENSHOT: Cmd_Screenshot(static_cast(pBaseCommand)); break; @@ -1643,7 +1648,7 @@ bool CCommandProcessorFragment_OpenGL2::Cmd_Init(const SCommand_Init *pCommand) m_pTileProgram->AddShader(&VertexShader); m_pTileProgram->AddShader(&FragmentShader); - glBindAttribLocation(m_pTileProgram->GetProgramID(), 0, "inVertex"); + glBindAttribLocation(m_pTileProgram->GetProgramId(), 0, "inVertex"); m_pTileProgram->LinkProgram(); @@ -1670,8 +1675,8 @@ bool CCommandProcessorFragment_OpenGL2::Cmd_Init(const SCommand_Init *pCommand) m_pTileProgramTextured->AddShader(&VertexShader); m_pTileProgramTextured->AddShader(&FragmentShader); - glBindAttribLocation(m_pTileProgram->GetProgramID(), 0, "inVertex"); - glBindAttribLocation(m_pTileProgram->GetProgramID(), 1, "inVertexTexCoord"); + glBindAttribLocation(m_pTileProgram->GetProgramId(), 0, "inVertex"); + glBindAttribLocation(m_pTileProgram->GetProgramId(), 1, "inVertexTexCoord"); m_pTileProgramTextured->LinkProgram(); @@ -1696,7 +1701,7 @@ bool CCommandProcessorFragment_OpenGL2::Cmd_Init(const SCommand_Init *pCommand) m_pBorderTileProgram->AddShader(&VertexShader); m_pBorderTileProgram->AddShader(&FragmentShader); - glBindAttribLocation(m_pBorderTileProgram->GetProgramID(), 0, "inVertex"); + glBindAttribLocation(m_pBorderTileProgram->GetProgramId(), 0, "inVertex"); m_pBorderTileProgram->LinkProgram(); @@ -1725,8 +1730,8 @@ bool CCommandProcessorFragment_OpenGL2::Cmd_Init(const SCommand_Init *pCommand) m_pBorderTileProgramTextured->AddShader(&VertexShader); m_pBorderTileProgramTextured->AddShader(&FragmentShader); - glBindAttribLocation(m_pBorderTileProgramTextured->GetProgramID(), 0, "inVertex"); - glBindAttribLocation(m_pBorderTileProgramTextured->GetProgramID(), 1, "inVertexTexCoord"); + glBindAttribLocation(m_pBorderTileProgramTextured->GetProgramId(), 0, "inVertex"); + glBindAttribLocation(m_pBorderTileProgramTextured->GetProgramId(), 1, "inVertexTexCoord"); m_pBorderTileProgramTextured->LinkProgram(); @@ -1843,17 +1848,17 @@ void CCommandProcessorFragment_OpenGL2::Cmd_CreateBufferObject(const CCommandBuf } } - GLuint VertBufferID = 0; + GLuint VertBufferId = 0; - glGenBuffers(1, &VertBufferID); - glBindBuffer(GL_ARRAY_BUFFER, VertBufferID); + glGenBuffers(1, &VertBufferId); + glBindBuffer(GL_ARRAY_BUFFER, VertBufferId); glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)(pCommand->m_DataSize), pUploadData, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); SBufferObject &BufferObject = m_vBufferObjectIndices[Index]; - BufferObject.m_BufferObjectID = VertBufferID; + BufferObject.m_BufferObjectId = VertBufferId; BufferObject.m_DataSize = pCommand->m_DataSize; - BufferObject.m_pData = malloc(pCommand->m_DataSize); + BufferObject.m_pData = static_cast(malloc(pCommand->m_DataSize)); if(pUploadData) mem_copy(BufferObject.m_pData, pUploadData, pCommand->m_DataSize); @@ -1867,13 +1872,13 @@ void CCommandProcessorFragment_OpenGL2::Cmd_RecreateBufferObject(const CCommandB int Index = pCommand->m_BufferIndex; SBufferObject &BufferObject = m_vBufferObjectIndices[Index]; - glBindBuffer(GL_ARRAY_BUFFER, BufferObject.m_BufferObjectID); + glBindBuffer(GL_ARRAY_BUFFER, BufferObject.m_BufferObjectId); glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)(pCommand->m_DataSize), pUploadData, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0); BufferObject.m_DataSize = pCommand->m_DataSize; free(BufferObject.m_pData); - BufferObject.m_pData = malloc(pCommand->m_DataSize); + BufferObject.m_pData = static_cast(malloc(pCommand->m_DataSize)); if(pUploadData) mem_copy(BufferObject.m_pData, pUploadData, pCommand->m_DataSize); @@ -1887,12 +1892,12 @@ void CCommandProcessorFragment_OpenGL2::Cmd_UpdateBufferObject(const CCommandBuf int Index = pCommand->m_BufferIndex; SBufferObject &BufferObject = m_vBufferObjectIndices[Index]; - glBindBuffer(GL_ARRAY_BUFFER, BufferObject.m_BufferObjectID); + glBindBuffer(GL_ARRAY_BUFFER, BufferObject.m_BufferObjectId); glBufferSubData(GL_ARRAY_BUFFER, (GLintptr)(pCommand->m_pOffset), (GLsizeiptr)(pCommand->m_DataSize), pUploadData); glBindBuffer(GL_ARRAY_BUFFER, 0); if(pUploadData) - mem_copy(((uint8_t *)BufferObject.m_pData) + (ptrdiff_t)pCommand->m_pOffset, pUploadData, pCommand->m_DataSize); + mem_copy(BufferObject.m_pData + (ptrdiff_t)pCommand->m_pOffset, pUploadData, pCommand->m_DataSize); if(pCommand->m_DeletePointer) free(pUploadData); @@ -1906,10 +1911,10 @@ void CCommandProcessorFragment_OpenGL2::Cmd_CopyBufferObject(const CCommandBuffe SBufferObject &ReadBufferObject = m_vBufferObjectIndices[ReadIndex]; SBufferObject &WriteBufferObject = m_vBufferObjectIndices[WriteIndex]; - mem_copy(((uint8_t *)WriteBufferObject.m_pData) + (ptrdiff_t)pCommand->m_WriteOffset, ((uint8_t *)ReadBufferObject.m_pData) + (ptrdiff_t)pCommand->m_ReadOffset, pCommand->m_CopySize); + mem_copy(WriteBufferObject.m_pData + (ptrdiff_t)pCommand->m_WriteOffset, ReadBufferObject.m_pData + (ptrdiff_t)pCommand->m_ReadOffset, pCommand->m_CopySize); - glBindBuffer(GL_ARRAY_BUFFER, WriteBufferObject.m_BufferObjectID); - glBufferSubData(GL_ARRAY_BUFFER, (GLintptr)(pCommand->m_WriteOffset), (GLsizeiptr)(pCommand->m_CopySize), ((uint8_t *)WriteBufferObject.m_pData) + (ptrdiff_t)pCommand->m_WriteOffset); + glBindBuffer(GL_ARRAY_BUFFER, WriteBufferObject.m_BufferObjectId); + glBufferSubData(GL_ARRAY_BUFFER, (GLintptr)(pCommand->m_WriteOffset), (GLsizeiptr)(pCommand->m_CopySize), WriteBufferObject.m_pData + (ptrdiff_t)pCommand->m_WriteOffset); glBindBuffer(GL_ARRAY_BUFFER, 0); } @@ -1918,7 +1923,7 @@ void CCommandProcessorFragment_OpenGL2::Cmd_DeleteBufferObject(const CCommandBuf int Index = pCommand->m_BufferIndex; SBufferObject &BufferObject = m_vBufferObjectIndices[Index]; - glDeleteBuffers(1, &BufferObject.m_BufferObjectID); + glDeleteBuffers(1, &BufferObject.m_BufferObjectId); free(BufferObject.m_pData); BufferObject.m_pData = NULL; @@ -1971,13 +1976,13 @@ void CCommandProcessorFragment_OpenGL2::Cmd_DeleteBufferContainer(const CCommand if(pCommand->m_DestroyAllBO) { - int VertBufferID = BufferContainer.m_ContainerInfo.m_VertBufferBindingIndex; - if(VertBufferID != -1) + int VertBufferId = BufferContainer.m_ContainerInfo.m_VertBufferBindingIndex; + if(VertBufferId != -1) { - glDeleteBuffers(1, &m_vBufferObjectIndices[VertBufferID].m_BufferObjectID); + glDeleteBuffers(1, &m_vBufferObjectIndices[VertBufferId].m_BufferObjectId); - free(m_vBufferObjectIndices[VertBufferID].m_pData); - m_vBufferObjectIndices[VertBufferID].m_pData = NULL; + free(m_vBufferObjectIndices[VertBufferId].m_pData); + m_vBufferObjectIndices[VertBufferId].m_pData = NULL; } } @@ -2014,7 +2019,7 @@ void CCommandProcessorFragment_OpenGL2::Cmd_RenderBorderTile(const CCommandBuffe SBufferObject &BufferObject = m_vBufferObjectIndices[(size_t)BufferContainer.m_ContainerInfo.m_VertBufferBindingIndex]; - glBindBuffer(GL_ARRAY_BUFFER, BufferObject.m_BufferObjectID); + glBindBuffer(GL_ARRAY_BUFFER, BufferObject.m_BufferObjectId); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, false, BufferContainer.m_ContainerInfo.m_Stride, BufferContainer.m_ContainerInfo.m_vAttributes[0].m_pOffset); @@ -2066,7 +2071,7 @@ void CCommandProcessorFragment_OpenGL2::Cmd_RenderTileLayer(const CCommandBuffer SBufferObject &BufferObject = m_vBufferObjectIndices[(size_t)BufferContainer.m_ContainerInfo.m_VertBufferBindingIndex]; - glBindBuffer(GL_ARRAY_BUFFER, BufferObject.m_BufferObjectID); + glBindBuffer(GL_ARRAY_BUFFER, BufferObject.m_BufferObjectId); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, false, BufferContainer.m_ContainerInfo.m_Stride, BufferContainer.m_ContainerInfo.m_vAttributes[0].m_pOffset); diff --git a/src/engine/client/backend/opengl/backend_opengl.h b/src/engine/client/backend/opengl/backend_opengl.h index 2d339c0fdb..f0d833cc48 100644 --- a/src/engine/client/backend/opengl/backend_opengl.h +++ b/src/engine/client/backend/opengl/backend_opengl.h @@ -82,15 +82,13 @@ class CCommandProcessorFragment_OpenGL : public CCommandProcessorFragment_GLBase bool GetPresentedImageData(uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) override; - static int TexFormatToOpenGLFormat(int TexFormat); static size_t GLFormatToPixelSize(int GLFormat); - void TextureUpdate(int Slot, int X, int Y, int Width, int Height, int GLFormat, void *pTexData); - void TextureCreate(int Slot, int Width, int Height, int GLFormat, int GLStoreFormat, int Flags, void *pTexData); + void TextureUpdate(int Slot, int X, int Y, int Width, int Height, int GLFormat, uint8_t *pTexData); + void TextureCreate(int Slot, int Width, int Height, int GLFormat, int GLStoreFormat, int Flags, uint8_t *pTexData); virtual bool Cmd_Init(const SCommand_Init *pCommand); virtual void Cmd_Shutdown(const SCommand_Shutdown *pCommand) {} - virtual void Cmd_Texture_Update(const CCommandBuffer::SCommand_Texture_Update *pCommand); virtual void Cmd_Texture_Destroy(const CCommandBuffer::SCommand_Texture_Destroy *pCommand); virtual void Cmd_Texture_Create(const CCommandBuffer::SCommand_Texture_Create *pCommand); virtual void Cmd_TextTexture_Update(const CCommandBuffer::SCommand_TextTexture_Update *pCommand); @@ -99,6 +97,7 @@ class CCommandProcessorFragment_OpenGL : public CCommandProcessorFragment_GLBase virtual void Cmd_Clear(const CCommandBuffer::SCommand_Clear *pCommand); virtual void Cmd_Render(const CCommandBuffer::SCommand_Render *pCommand); virtual void Cmd_RenderTex3D(const CCommandBuffer::SCommand_RenderTex3D *pCommand) { dbg_assert(false, "Call of unsupported Cmd_RenderTex3D"); } + virtual void Cmd_ReadPixel(const CCommandBuffer::SCommand_TrySwapAndReadPixel *pCommand); virtual void Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand); virtual void Cmd_Update_Viewport(const CCommandBuffer::SCommand_Update_Viewport *pCommand); @@ -143,14 +142,14 @@ class CCommandProcessorFragment_OpenGL2 : public CCommandProcessorFragment_OpenG struct SBufferObject { - SBufferObject(TWGLuint BufferObjectID) : - m_BufferObjectID(BufferObjectID) + SBufferObject(TWGLuint BufferObjectId) : + m_BufferObjectId(BufferObjectId) { m_pData = NULL; m_DataSize = 0; } - TWGLuint m_BufferObjectID; - void *m_pData; + TWGLuint m_BufferObjectId; + uint8_t *m_pData; size_t m_DataSize; }; diff --git a/src/engine/client/backend/opengl/backend_opengl3.cpp b/src/engine/client/backend/opengl/backend_opengl3.cpp index 315dd887d7..95dbf3d366 100644 --- a/src/engine/client/backend/opengl/backend_opengl3.cpp +++ b/src/engine/client/backend/opengl/backend_opengl3.cpp @@ -30,19 +30,12 @@ static constexpr GLenum BUFFER_INIT_VERTEX_TARGET = GL_COPY_WRITE_BUFFER; #endif // ------------ CCommandProcessorFragment_OpenGL3_3 -int CCommandProcessorFragment_OpenGL3_3::TexFormatToNewOpenGLFormat(int TexFormat) -{ - if(TexFormat == CCommandBuffer::TEXFORMAT_RGBA) - return GL_RGBA; - return GL_RGBA; -} - void CCommandProcessorFragment_OpenGL3_3::UseProgram(CGLSLTWProgram *pProgram) { - if(m_LastProgramID != pProgram->GetProgramID()) + if(m_LastProgramId != pProgram->GetProgramId()) { pProgram->UseProgram(); - m_LastProgramID = pProgram->GetProgramID(); + m_LastProgramId = pProgram->GetProgramId(); } } @@ -114,7 +107,7 @@ bool CCommandProcessorFragment_OpenGL3_3::Cmd_Init(const SCommand_Init *pCommand m_pPrimitiveExProgramRotationless = new CGLSLPrimitiveExProgram; m_pPrimitiveExProgramTexturedRotationless = new CGLSLPrimitiveExProgram; m_pSpriteProgramMultiple = new CGLSLSpriteMultipleProgram; - m_LastProgramID = 0; + m_LastProgramId = 0; CGLSLCompiler ShaderCompiler(g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch, m_IsOpenGLES, m_OpenGLTextureLodBIAS / 1000.0f); @@ -366,15 +359,15 @@ bool CCommandProcessorFragment_OpenGL3_3::Cmd_Init(const SCommand_Init *pCommand m_LastStreamBuffer = 0; - glGenBuffers(MAX_STREAM_BUFFER_COUNT, m_aPrimitiveDrawBufferID); - glGenVertexArrays(MAX_STREAM_BUFFER_COUNT, m_aPrimitiveDrawVertexID); - glGenBuffers(1, &m_PrimitiveDrawBufferIDTex3D); - glGenVertexArrays(1, &m_PrimitiveDrawVertexIDTex3D); + glGenBuffers(MAX_STREAM_BUFFER_COUNT, m_aPrimitiveDrawBufferId); + glGenVertexArrays(MAX_STREAM_BUFFER_COUNT, m_aPrimitiveDrawVertexId); + glGenBuffers(1, &m_PrimitiveDrawBufferIdTex3D); + glGenVertexArrays(1, &m_PrimitiveDrawVertexIdTex3D); for(int i = 0; i < MAX_STREAM_BUFFER_COUNT; ++i) { - glBindBuffer(GL_ARRAY_BUFFER, m_aPrimitiveDrawBufferID[i]); - glBindVertexArray(m_aPrimitiveDrawVertexID[i]); + glBindBuffer(GL_ARRAY_BUFFER, m_aPrimitiveDrawBufferId[i]); + glBindVertexArray(m_aPrimitiveDrawVertexId[i]); glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); glEnableVertexAttribArray(2); @@ -386,8 +379,8 @@ bool CCommandProcessorFragment_OpenGL3_3::Cmd_Init(const SCommand_Init *pCommand m_aLastIndexBufferBound[i] = 0; } - glBindBuffer(GL_ARRAY_BUFFER, m_PrimitiveDrawBufferIDTex3D); - glBindVertexArray(m_PrimitiveDrawVertexIDTex3D); + glBindBuffer(GL_ARRAY_BUFFER, m_PrimitiveDrawBufferIdTex3D); + glBindVertexArray(m_PrimitiveDrawVertexIdTex3D); glEnableVertexAttribArray(0); glEnableVertexAttribArray(1); glEnableVertexAttribArray(2); @@ -400,8 +393,8 @@ bool CCommandProcessorFragment_OpenGL3_3::Cmd_Init(const SCommand_Init *pCommand glGetIntegerv(GL_MAX_TEXTURE_SIZE, &m_MaxTexSize); glBindVertexArray(0); - glGenBuffers(1, &m_QuadDrawIndexBufferID); - glBindBuffer(BUFFER_INIT_INDEX_TARGET, m_QuadDrawIndexBufferID); + glGenBuffers(1, &m_QuadDrawIndexBufferId); + glBindBuffer(BUFFER_INIT_INDEX_TARGET, m_QuadDrawIndexBufferId); unsigned int aIndices[CCommandBuffer::MAX_VERTICES / 4 * 6]; int Primq = 0; @@ -469,11 +462,11 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_Shutdown(const SCommand_Shutdown * delete m_pSpriteProgramMultiple; glBindVertexArray(0); - glDeleteBuffers(MAX_STREAM_BUFFER_COUNT, m_aPrimitiveDrawBufferID); - glDeleteBuffers(1, &m_QuadDrawIndexBufferID); - glDeleteVertexArrays(MAX_STREAM_BUFFER_COUNT, m_aPrimitiveDrawVertexID); - glDeleteBuffers(1, &m_PrimitiveDrawBufferIDTex3D); - glDeleteVertexArrays(1, &m_PrimitiveDrawVertexIDTex3D); + glDeleteBuffers(MAX_STREAM_BUFFER_COUNT, m_aPrimitiveDrawBufferId); + glDeleteBuffers(1, &m_QuadDrawIndexBufferId); + glDeleteVertexArrays(MAX_STREAM_BUFFER_COUNT, m_aPrimitiveDrawVertexId); + glDeleteBuffers(1, &m_PrimitiveDrawBufferIdTex3D); + glDeleteVertexArrays(1, &m_PrimitiveDrawVertexIdTex3D); for(int i = 0; i < (int)m_vTextures.size(); ++i) { @@ -488,7 +481,7 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_Shutdown(const SCommand_Shutdown * m_vBufferContainers.clear(); } -void CCommandProcessorFragment_OpenGL3_3::TextureUpdate(int Slot, int X, int Y, int Width, int Height, int GLFormat, void *pTexData) +void CCommandProcessorFragment_OpenGL3_3::TextureUpdate(int Slot, int X, int Y, int Width, int Height, int GLFormat, uint8_t *pTexData) { glBindTexture(GL_TEXTURE_2D, m_vTextures[Slot].m_Tex); @@ -503,7 +496,7 @@ void CCommandProcessorFragment_OpenGL3_3::TextureUpdate(int Slot, int X, int Y, Y /= 2; } - void *pTmpData = Resize(static_cast(pTexData), Width, Height, Width, Height, GLFormatToPixelSize(GLFormat)); + uint8_t *pTmpData = ResizeImage(pTexData, Width, Height, Width, Height, GLFormatToPixelSize(GLFormat)); free(pTexData); pTexData = pTmpData; } @@ -512,11 +505,6 @@ void CCommandProcessorFragment_OpenGL3_3::TextureUpdate(int Slot, int X, int Y, free(pTexData); } -void CCommandProcessorFragment_OpenGL3_3::Cmd_Texture_Update(const CCommandBuffer::SCommand_Texture_Update *pCommand) -{ - TextureUpdate(pCommand->m_Slot, pCommand->m_X, pCommand->m_Y, pCommand->m_Width, pCommand->m_Height, TexFormatToOpenGLFormat(pCommand->m_Format), pCommand->m_pData); -} - void CCommandProcessorFragment_OpenGL3_3::Cmd_Texture_Destroy(const CCommandBuffer::SCommand_Texture_Destroy *pCommand) { int Slot = 0; @@ -525,7 +513,7 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_Texture_Destroy(const CCommandBuff DestroyTexture(pCommand->m_Slot); } -void CCommandProcessorFragment_OpenGL3_3::TextureCreate(int Slot, int Width, int Height, int GLFormat, int GLStoreFormat, int Flags, void *pTexData) +void CCommandProcessorFragment_OpenGL3_3::TextureCreate(int Slot, int Width, int Height, int GLFormat, int GLStoreFormat, int Flags, uint8_t *pTexData) { while(Slot >= (int)m_vTextures.size()) m_vTextures.resize(m_vTextures.size() * 2); @@ -543,7 +531,7 @@ void CCommandProcessorFragment_OpenGL3_3::TextureCreate(int Slot, int Width, int ++RescaleCount; } while(Width > m_MaxTexSize || Height > m_MaxTexSize); - void *pTmpData = Resize(static_cast(pTexData), Width, Height, Width, Height, GLFormatToPixelSize(GLFormat)); + uint8_t *pTmpData = ResizeImage(pTexData, Width, Height, Width, Height, GLFormatToPixelSize(GLFormat)); free(pTexData); pTexData = pTmpData; } @@ -618,9 +606,7 @@ void CCommandProcessorFragment_OpenGL3_3::TextureCreate(int Slot, int Width, int glSamplerParameterf(m_vTextures[Slot].m_Sampler2DArray, GL_TEXTURE_LOD_BIAS, ((GLfloat)m_OpenGLTextureLodBIAS / 1000.0f)); #endif - uint8_t *p3DImageData = NULL; - - p3DImageData = (uint8_t *)malloc((size_t)Width * Height * PixelSize); + uint8_t *p3DImageData = static_cast(malloc((size_t)Width * Height * PixelSize)); int Image3DWidth, Image3DHeight; int ConvertWidth = Width; @@ -631,7 +617,7 @@ void CCommandProcessorFragment_OpenGL3_3::TextureCreate(int Slot, int Width, int dbg_msg("gfx", "3D/2D array texture was resized"); int NewWidth = maximum(HighestBit(ConvertWidth), 16); int NewHeight = maximum(HighestBit(ConvertHeight), 16); - uint8_t *pNewTexData = (uint8_t *)Resize((const uint8_t *)pTexData, ConvertWidth, ConvertHeight, NewWidth, NewHeight, GLFormatToPixelSize(GLFormat)); + uint8_t *pNewTexData = ResizeImage(pTexData, ConvertWidth, ConvertHeight, NewWidth, NewHeight, GLFormatToPixelSize(GLFormat)); ConvertWidth = NewWidth; ConvertHeight = NewHeight; @@ -640,7 +626,7 @@ void CCommandProcessorFragment_OpenGL3_3::TextureCreate(int Slot, int Width, int pTexData = pNewTexData; } - if((Texture2DTo3D(pTexData, ConvertWidth, ConvertHeight, PixelSize, 16, 16, p3DImageData, Image3DWidth, Image3DHeight))) + if(Texture2DTo3D(pTexData, ConvertWidth, ConvertHeight, PixelSize, 16, 16, p3DImageData, Image3DWidth, Image3DHeight)) { glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GLStoreFormat, Image3DWidth, Image3DHeight, 256, 0, GLFormat, GL_UNSIGNED_BYTE, p3DImageData); glGenerateMipmap(GL_TEXTURE_2D_ARRAY); @@ -668,7 +654,7 @@ void CCommandProcessorFragment_OpenGL3_3::TextureCreate(int Slot, int Width, int void CCommandProcessorFragment_OpenGL3_3::Cmd_Texture_Create(const CCommandBuffer::SCommand_Texture_Create *pCommand) { - TextureCreate(pCommand->m_Slot, pCommand->m_Width, pCommand->m_Height, TexFormatToOpenGLFormat(pCommand->m_Format), TexFormatToOpenGLFormat(pCommand->m_StoreFormat), pCommand->m_Flags, pCommand->m_pData); + TextureCreate(pCommand->m_Slot, pCommand->m_Width, pCommand->m_Height, GL_RGBA, GL_RGBA, pCommand->m_Flags, pCommand->m_pData); } void CCommandProcessorFragment_OpenGL3_3::Cmd_TextTexture_Update(const CCommandBuffer::SCommand_TextTexture_Update *pCommand) @@ -727,9 +713,9 @@ void CCommandProcessorFragment_OpenGL3_3::UploadStreamBufferData(unsigned int Pr }; if(AsTex3D) - glBindBuffer(GL_ARRAY_BUFFER, m_PrimitiveDrawBufferIDTex3D); + glBindBuffer(GL_ARRAY_BUFFER, m_PrimitiveDrawBufferIdTex3D); else - glBindBuffer(GL_ARRAY_BUFFER, m_aPrimitiveDrawBufferID[m_LastStreamBuffer]); + glBindBuffer(GL_ARRAY_BUFFER, m_aPrimitiveDrawBufferId[m_LastStreamBuffer]); glBufferData(GL_ARRAY_BUFFER, VertSize * Count, pVertices, GL_STREAM_DRAW); } @@ -744,7 +730,7 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_Render(const CCommandBuffer::SComm UploadStreamBufferData(pCommand->m_PrimType, pCommand->m_pVertices, sizeof(CCommandBuffer::SVertex), pCommand->m_PrimCount); - glBindVertexArray(m_aPrimitiveDrawVertexID[m_LastStreamBuffer]); + glBindVertexArray(m_aPrimitiveDrawVertexId[m_LastStreamBuffer]); switch(pCommand->m_PrimType) { @@ -756,10 +742,10 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_Render(const CCommandBuffer::SComm glDrawArrays(GL_TRIANGLES, 0, pCommand->m_PrimCount * 3); break; case CCommandBuffer::PRIMTYPE_QUADS: - if(m_aLastIndexBufferBound[m_LastStreamBuffer] != m_QuadDrawIndexBufferID) + if(m_aLastIndexBufferBound[m_LastStreamBuffer] != m_QuadDrawIndexBufferId) { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferID); - m_aLastIndexBufferBound[m_LastStreamBuffer] = m_QuadDrawIndexBufferID; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferId); + m_aLastIndexBufferBound[m_LastStreamBuffer] = m_QuadDrawIndexBufferId; } glDrawElements(GL_TRIANGLES, pCommand->m_PrimCount * 6, GL_UNSIGNED_INT, 0); break; @@ -780,7 +766,7 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderTex3D(const CCommandBuffer:: UploadStreamBufferData(pCommand->m_PrimType, pCommand->m_pVertices, sizeof(CCommandBuffer::SVertexTex3DStream), pCommand->m_PrimCount, true); - glBindVertexArray(m_PrimitiveDrawVertexIDTex3D); + glBindVertexArray(m_PrimitiveDrawVertexIdTex3D); switch(pCommand->m_PrimType) { @@ -789,7 +775,7 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderTex3D(const CCommandBuffer:: glDrawArrays(GL_LINES, 0, pCommand->m_PrimCount * 2); break; case CCommandBuffer::PRIMTYPE_QUADS: - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferID); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferId); glDrawElements(GL_TRIANGLES, pCommand->m_PrimCount * 6, GL_UNSIGNED_INT, 0); break; default: @@ -800,16 +786,16 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderTex3D(const CCommandBuffer:: void CCommandProcessorFragment_OpenGL3_3::DestroyBufferContainer(int Index, bool DeleteBOs) { SBufferContainer &BufferContainer = m_vBufferContainers[Index]; - if(BufferContainer.m_VertArrayID != 0) - glDeleteVertexArrays(1, &BufferContainer.m_VertArrayID); + if(BufferContainer.m_VertArrayId != 0) + glDeleteVertexArrays(1, &BufferContainer.m_VertArrayId); // all buffer objects can deleted automatically, so the program doesn't need to deal with them (e.g. causing crashes because of driver bugs) if(DeleteBOs) { - int VertBufferID = BufferContainer.m_ContainerInfo.m_VertBufferBindingIndex; - if(VertBufferID != -1) + int VertBufferId = BufferContainer.m_ContainerInfo.m_VertBufferBindingIndex; + if(VertBufferId != -1) { - glDeleteBuffers(1, &m_vBufferObjectIndices[VertBufferID]); + glDeleteBuffers(1, &m_vBufferObjectIndices[VertBufferId]); } } @@ -835,10 +821,10 @@ void CCommandProcessorFragment_OpenGL3_3::AppendIndices(unsigned int NewIndicesC Primq += 4; } - glBindBuffer(GL_COPY_READ_BUFFER, m_QuadDrawIndexBufferID); - GLuint NewIndexBufferID; - glGenBuffers(1, &NewIndexBufferID); - glBindBuffer(BUFFER_INIT_INDEX_TARGET, NewIndexBufferID); + glBindBuffer(GL_COPY_READ_BUFFER, m_QuadDrawIndexBufferId); + GLuint NewIndexBufferId; + glGenBuffers(1, &NewIndexBufferId); + glBindBuffer(BUFFER_INIT_INDEX_TARGET, NewIndexBufferId); GLsizeiptr size = sizeof(unsigned int); glBufferData(BUFFER_INIT_INDEX_TARGET, (GLsizeiptr)NewIndicesCount * size, NULL, GL_STATIC_DRAW); glCopyBufferSubData(GL_COPY_READ_BUFFER, BUFFER_INIT_INDEX_TARGET, 0, 0, (GLsizeiptr)m_CurrentIndicesInBuffer * size); @@ -846,8 +832,8 @@ void CCommandProcessorFragment_OpenGL3_3::AppendIndices(unsigned int NewIndicesC glBindBuffer(BUFFER_INIT_INDEX_TARGET, 0); glBindBuffer(GL_COPY_READ_BUFFER, 0); - glDeleteBuffers(1, &m_QuadDrawIndexBufferID); - m_QuadDrawIndexBufferID = NewIndexBufferID; + glDeleteBuffers(1, &m_QuadDrawIndexBufferId); + m_QuadDrawIndexBufferId = NewIndexBufferId; for(unsigned int &i : m_aLastIndexBufferBound) i = 0; @@ -873,13 +859,13 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_CreateBufferObject(const CCommandB } } - GLuint VertBufferID = 0; + GLuint VertBufferId = 0; - glGenBuffers(1, &VertBufferID); - glBindBuffer(BUFFER_INIT_VERTEX_TARGET, VertBufferID); + glGenBuffers(1, &VertBufferId); + glBindBuffer(BUFFER_INIT_VERTEX_TARGET, VertBufferId); glBufferData(BUFFER_INIT_VERTEX_TARGET, (GLsizeiptr)(pCommand->m_DataSize), pUploadData, GL_STATIC_DRAW); - m_vBufferObjectIndices[Index] = VertBufferID; + m_vBufferObjectIndices[Index] = VertBufferId; if(pCommand->m_DeletePointer) free(pUploadData); @@ -942,8 +928,8 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_CreateBufferContainer(const CComma } SBufferContainer &BufferContainer = m_vBufferContainers[Index]; - glGenVertexArrays(1, &BufferContainer.m_VertArrayID); - glBindVertexArray(BufferContainer.m_VertArrayID); + glGenVertexArrays(1, &BufferContainer.m_VertArrayId); + glBindVertexArray(BufferContainer.m_VertArrayId); BufferContainer.m_LastIndexBufferBound = 0; @@ -971,7 +957,7 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_UpdateBufferContainer(const CComma { SBufferContainer &BufferContainer = m_vBufferContainers[pCommand->m_BufferContainerIndex]; - glBindVertexArray(BufferContainer.m_VertArrayID); + glBindVertexArray(BufferContainer.m_VertArrayId); // disable all old attributes for(size_t i = 0; i < BufferContainer.m_ContainerInfo.m_vAttributes.size(); ++i) @@ -1017,7 +1003,7 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderBorderTile(const CCommandBuf return; SBufferContainer &BufferContainer = m_vBufferContainers[Index]; - if(BufferContainer.m_VertArrayID == 0) + if(BufferContainer.m_VertArrayId == 0) return; CGLSLTileProgram *pProgram = NULL; @@ -1033,11 +1019,11 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderBorderTile(const CCommandBuf pProgram->SetUniformVec2(pProgram->m_LocOffset, 1, (float *)&pCommand->m_Offset); pProgram->SetUniformVec2(pProgram->m_LocScale, 1, (float *)&pCommand->m_Scale); - glBindVertexArray(BufferContainer.m_VertArrayID); - if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferID) + glBindVertexArray(BufferContainer.m_VertArrayId); + if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferId) { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferID); - BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferID; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferId); + BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferId; } glDrawElements(GL_TRIANGLES, pCommand->m_DrawNum * 6, GL_UNSIGNED_INT, pCommand->m_pIndicesOffset); } @@ -1050,7 +1036,7 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderTileLayer(const CCommandBuff return; SBufferContainer &BufferContainer = m_vBufferContainers[Index]; - if(BufferContainer.m_VertArrayID == 0) + if(BufferContainer.m_VertArrayId == 0) return; if(pCommand->m_IndicesDrawNum == 0) @@ -1071,11 +1057,11 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderTileLayer(const CCommandBuff SetState(pCommand->m_State, pProgram, true); pProgram->SetUniformVec4(pProgram->m_LocColor, 1, (float *)&pCommand->m_Color); - glBindVertexArray(BufferContainer.m_VertArrayID); - if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferID) + glBindVertexArray(BufferContainer.m_VertArrayId); + if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferId) { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferID); - BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferID; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferId); + BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferId; } for(int i = 0; i < pCommand->m_IndicesDrawNum; ++i) { @@ -1091,7 +1077,7 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderQuadLayer(const CCommandBuff return; SBufferContainer &BufferContainer = m_vBufferContainers[Index]; - if(BufferContainer.m_VertArrayID == 0) + if(BufferContainer.m_VertArrayId == 0) return; if(pCommand->m_QuadNum == 0) @@ -1110,11 +1096,11 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderQuadLayer(const CCommandBuff UseProgram(pProgram); SetState(pCommand->m_State, pProgram); - glBindVertexArray(BufferContainer.m_VertArrayID); - if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferID) + glBindVertexArray(BufferContainer.m_VertArrayId); + if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferId) { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferID); - BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferID; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferId); + BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferId; } int QuadsLeft = pCommand->m_QuadNum; @@ -1209,14 +1195,14 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderText(const CCommandBuffer::S return; SBufferContainer &BufferContainer = m_vBufferContainers[Index]; - if(BufferContainer.m_VertArrayID == 0) + if(BufferContainer.m_VertArrayId == 0) return; - glBindVertexArray(BufferContainer.m_VertArrayID); - if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferID) + glBindVertexArray(BufferContainer.m_VertArrayId); + if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferId) { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferID); - BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferID; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferId); + BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferId; } RenderText(pCommand->m_State, pCommand->m_DrawNum, pCommand->m_TextTextureIndex, pCommand->m_TextOutlineTextureIndex, pCommand->m_TextureSize, pCommand->m_TextColor, pCommand->m_TextOutlineColor); @@ -1235,14 +1221,14 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderQuadContainer(const CCommand return; SBufferContainer &BufferContainer = m_vBufferContainers[Index]; - if(BufferContainer.m_VertArrayID == 0) + if(BufferContainer.m_VertArrayId == 0) return; - glBindVertexArray(BufferContainer.m_VertArrayID); - if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferID) + glBindVertexArray(BufferContainer.m_VertArrayId); + if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferId) { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferID); - BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferID; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferId); + BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferId; } CGLSLTWProgram *pProgram = m_pPrimitiveProgram; @@ -1267,14 +1253,14 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderQuadContainerEx(const CComma return; SBufferContainer &BufferContainer = m_vBufferContainers[Index]; - if(BufferContainer.m_VertArrayID == 0) + if(BufferContainer.m_VertArrayId == 0) return; - glBindVertexArray(BufferContainer.m_VertArrayID); - if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferID) + glBindVertexArray(BufferContainer.m_VertArrayId); + if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferId) { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferID); - BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferID; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferId); + BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferId; } CGLSLPrimitiveExProgram *pProgram = m_pPrimitiveExProgramRotationless; @@ -1328,14 +1314,14 @@ void CCommandProcessorFragment_OpenGL3_3::Cmd_RenderQuadContainerAsSpriteMultipl return; SBufferContainer &BufferContainer = m_vBufferContainers[Index]; - if(BufferContainer.m_VertArrayID == 0) + if(BufferContainer.m_VertArrayId == 0) return; - glBindVertexArray(BufferContainer.m_VertArrayID); - if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferID) + glBindVertexArray(BufferContainer.m_VertArrayId); + if(BufferContainer.m_LastIndexBufferBound != m_QuadDrawIndexBufferId) { - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferID); - BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferID; + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_QuadDrawIndexBufferId); + BufferContainer.m_LastIndexBufferBound = m_QuadDrawIndexBufferId; } UseProgram(m_pSpriteProgramMultiple); diff --git a/src/engine/client/backend/opengl/backend_opengl3.h b/src/engine/client/backend/opengl/backend_opengl3.h index 5b3b883605..7b68d813d0 100644 --- a/src/engine/client/backend/opengl/backend_opengl3.h +++ b/src/engine/client/backend/opengl/backend_opengl3.h @@ -37,18 +37,18 @@ class CCommandProcessorFragment_OpenGL3_3 : public CCommandProcessorFragment_Ope CGLSLPrimitiveExProgram *m_pPrimitiveExProgramTexturedRotationless; CGLSLSpriteMultipleProgram *m_pSpriteProgramMultiple; - TWGLuint m_LastProgramID; + TWGLuint m_LastProgramId; - TWGLuint m_aPrimitiveDrawVertexID[MAX_STREAM_BUFFER_COUNT]; - TWGLuint m_PrimitiveDrawVertexIDTex3D; - TWGLuint m_aPrimitiveDrawBufferID[MAX_STREAM_BUFFER_COUNT]; - TWGLuint m_PrimitiveDrawBufferIDTex3D; + TWGLuint m_aPrimitiveDrawVertexId[MAX_STREAM_BUFFER_COUNT]; + TWGLuint m_PrimitiveDrawVertexIdTex3D; + TWGLuint m_aPrimitiveDrawBufferId[MAX_STREAM_BUFFER_COUNT]; + TWGLuint m_PrimitiveDrawBufferIdTex3D; TWGLuint m_aLastIndexBufferBound[MAX_STREAM_BUFFER_COUNT]; int m_LastStreamBuffer; - TWGLuint m_QuadDrawIndexBufferID; + TWGLuint m_QuadDrawIndexBufferId; unsigned int m_CurrentIndicesInBuffer; void DestroyBufferContainer(int Index, bool DeleteBOs = true); @@ -58,8 +58,8 @@ class CCommandProcessorFragment_OpenGL3_3 : public CCommandProcessorFragment_Ope struct SBufferContainer { SBufferContainer() : - m_VertArrayID(0), m_LastIndexBufferBound(0) {} - TWGLuint m_VertArrayID; + m_VertArrayId(0), m_LastIndexBufferBound(0) {} + TWGLuint m_VertArrayId; TWGLuint m_LastIndexBufferBound; SBufferContainerInfo m_ContainerInfo; }; @@ -71,19 +71,17 @@ class CCommandProcessorFragment_OpenGL3_3 : public CCommandProcessorFragment_Ope void InitPrimExProgram(CGLSLPrimitiveExProgram *pProgram, class CGLSLCompiler *pCompiler, class IStorage *pStorage, bool Textured, bool Rotationless); - static int TexFormatToNewOpenGLFormat(int TexFormat); bool IsNewApi() override { return true; } void UseProgram(CGLSLTWProgram *pProgram); void UploadStreamBufferData(unsigned int PrimitiveType, const void *pVertices, size_t VertSize, unsigned int PrimitiveCount, bool AsTex3D = false); void RenderText(const CCommandBuffer::SState &State, int DrawNum, int TextTextureIndex, int TextOutlineTextureIndex, int TextureSize, const ColorRGBA &TextColor, const ColorRGBA &TextOutlineColor); - void TextureUpdate(int Slot, int X, int Y, int Width, int Height, int GLFormat, void *pTexData); - void TextureCreate(int Slot, int Width, int Height, int GLFormat, int GLStoreFormat, int Flags, void *pTexData); + void TextureUpdate(int Slot, int X, int Y, int Width, int Height, int GLFormat, uint8_t *pTexData); + void TextureCreate(int Slot, int Width, int Height, int GLFormat, int GLStoreFormat, int Flags, uint8_t *pTexData); bool Cmd_Init(const SCommand_Init *pCommand) override; void Cmd_Shutdown(const SCommand_Shutdown *pCommand) override; - void Cmd_Texture_Update(const CCommandBuffer::SCommand_Texture_Update *pCommand) override; void Cmd_Texture_Destroy(const CCommandBuffer::SCommand_Texture_Destroy *pCommand) override; void Cmd_Texture_Create(const CCommandBuffer::SCommand_Texture_Create *pCommand) override; void Cmd_TextTexture_Update(const CCommandBuffer::SCommand_TextTexture_Update *pCommand) override; diff --git a/src/engine/client/backend/opengl/opengl_sl.cpp b/src/engine/client/backend/opengl/opengl_sl.cpp index 046e4168d7..a8b16c6af5 100644 --- a/src/engine/client/backend/opengl/opengl_sl.cpp +++ b/src/engine/client/backend/opengl/opengl_sl.cpp @@ -23,10 +23,10 @@ bool CGLSL::LoadShader(CGLSLCompiler *pCompiler, IStorage *pStorage, const char { if(m_IsLoaded) return true; - IOHANDLE f = pStorage->OpenFile(pFile, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL); + CLineReader LineReader; std::vector vLines; - if(f) + if(LineReader.OpenFile(pStorage->OpenFile(pFile, IOFLAG_READ, IStorage::TYPE_ALL))) { EBackendType BackendType = pCompiler->m_IsOpenGLES ? BACKEND_TYPE_OPENGL_ES : BACKEND_TYPE_OPENGL; bool IsNewOpenGL = (BackendType == BACKEND_TYPE_OPENGL ? (pCompiler->m_OpenGLVersionMajor >= 4 || (pCompiler->m_OpenGLVersionMajor == 3 && pCompiler->m_OpenGLVersionMinor == 3)) : pCompiler->m_OpenGLVersionMajor >= 3); @@ -81,17 +81,13 @@ bool CGLSL::LoadShader(CGLSLCompiler *pCompiler, IStorage *pStorage, const char vLines.emplace_back("#extension GL_EXT_texture_array : enable\r\n"); } - CLineReader LineReader; - LineReader.Init(f); - char *pReadLine = NULL; - while((pReadLine = LineReader.Get())) + while(const char *pReadLine = LineReader.Get()) { std::string Line; pCompiler->ParseLine(Line, pReadLine, Type == GL_FRAGMENT_SHADER ? GLSL_SHADER_COMPILER_TYPE_FRAGMENT : GLSL_SHADER_COMPILER_TYPE_VERTEX); Line.append("\r\n"); vLines.push_back(Line); } - io_close(f); const char **ShaderCode = new const char *[vLines.size()]; @@ -126,7 +122,7 @@ bool CGLSL::LoadShader(CGLSLCompiler *pCompiler, IStorage *pStorage, const char m_Type = Type; m_IsLoaded = true; - m_ShaderID = shader; + m_ShaderId = shader; return true; } @@ -139,7 +135,7 @@ void CGLSL::DeleteShader() if(!IsLoaded()) return; m_IsLoaded = false; - glDeleteShader(m_ShaderID); + glDeleteShader(m_ShaderId); } bool CGLSL::IsLoaded() const @@ -147,9 +143,9 @@ bool CGLSL::IsLoaded() const return m_IsLoaded; } -TWGLuint CGLSL::GetShaderID() const +TWGLuint CGLSL::GetShaderId() const { - return m_ShaderID; + return m_ShaderId; } CGLSL::CGLSL() diff --git a/src/engine/client/backend/opengl/opengl_sl.h b/src/engine/client/backend/opengl/opengl_sl.h index 3c2a640023..79024af8ee 100644 --- a/src/engine/client/backend/opengl/opengl_sl.h +++ b/src/engine/client/backend/opengl/opengl_sl.h @@ -21,13 +21,13 @@ class CGLSL void DeleteShader(); bool IsLoaded() const; - TWGLuint GetShaderID() const; + TWGLuint GetShaderId() const; CGLSL(); virtual ~CGLSL(); private: - TWGLuint m_ShaderID; + TWGLuint m_ShaderId; int m_Type; bool m_IsLoaded; }; diff --git a/src/engine/client/backend/opengl/opengl_sl_program.cpp b/src/engine/client/backend/opengl/opengl_sl_program.cpp index 2ba94325b1..0ad2e2ed37 100644 --- a/src/engine/client/backend/opengl/opengl_sl_program.cpp +++ b/src/engine/client/backend/opengl/opengl_sl_program.cpp @@ -14,7 +14,7 @@ void CGLSLProgram::CreateProgram() { - m_ProgramID = glCreateProgram(); + m_ProgramId = glCreateProgram(); } void CGLSLProgram::DeleteProgram() @@ -22,14 +22,14 @@ void CGLSLProgram::DeleteProgram() if(!m_IsLinked) return; m_IsLinked = false; - glDeleteProgram(m_ProgramID); + glDeleteProgram(m_ProgramId); } bool CGLSLProgram::AddShader(CGLSL *pShader) const { if(pShader->IsLoaded()) { - glAttachShader(m_ProgramID, pShader->GetShaderID()); + glAttachShader(m_ProgramId, pShader->GetShaderId()); return true; } return false; @@ -39,27 +39,27 @@ void CGLSLProgram::DetachShader(CGLSL *pShader) const { if(pShader->IsLoaded()) { - DetachShaderByID(pShader->GetShaderID()); + DetachShaderById(pShader->GetShaderId()); } } -void CGLSLProgram::DetachShaderByID(TWGLuint ShaderID) const +void CGLSLProgram::DetachShaderById(TWGLuint ShaderId) const { - glDetachShader(m_ProgramID, ShaderID); + glDetachShader(m_ProgramId, ShaderId); } void CGLSLProgram::LinkProgram() { - glLinkProgram(m_ProgramID); + glLinkProgram(m_ProgramId); int LinkStatus; - glGetProgramiv(m_ProgramID, GL_LINK_STATUS, &LinkStatus); + glGetProgramiv(m_ProgramId, GL_LINK_STATUS, &LinkStatus); m_IsLinked = LinkStatus == GL_TRUE; if(!m_IsLinked) { char aInfoLog[1024]; char aFinalMessage[1536]; - int iLogLength; - glGetProgramInfoLog(m_ProgramID, 1024, &iLogLength, aInfoLog); + int LogLength; + glGetProgramInfoLog(m_ProgramId, 1024, &LogLength, aInfoLog); str_format(aFinalMessage, sizeof(aFinalMessage), "Error! Shader program wasn't linked! The linker returned:\n\n%s", aInfoLog); dbg_msg("glslprogram", "%s", aFinalMessage); } @@ -74,13 +74,13 @@ void CGLSLProgram::DetachAllShaders() const GLsizei ReturnedCount = 0; while(true) { - glGetAttachedShaders(m_ProgramID, 100, &ReturnedCount, aShaders); + glGetAttachedShaders(m_ProgramId, 100, &ReturnedCount, aShaders); if(ReturnedCount > 0) { for(GLsizei i = 0; i < ReturnedCount; ++i) { - DetachShaderByID(aShaders[i]); + DetachShaderById(aShaders[i]); } } @@ -121,18 +121,18 @@ void CGLSLProgram::SetUniform(int Loc, const bool Value) int CGLSLProgram::GetUniformLoc(const char *pName) const { - return glGetUniformLocation(m_ProgramID, pName); + return glGetUniformLocation(m_ProgramId, pName); } void CGLSLProgram::UseProgram() const { if(m_IsLinked) - glUseProgram(m_ProgramID); + glUseProgram(m_ProgramId); } -TWGLuint CGLSLProgram::GetProgramID() const +TWGLuint CGLSLProgram::GetProgramId() const { - return m_ProgramID; + return m_ProgramId; } CGLSLProgram::CGLSLProgram() diff --git a/src/engine/client/backend/opengl/opengl_sl_program.h b/src/engine/client/backend/opengl/opengl_sl_program.h index f278731c86..404c7a6f08 100644 --- a/src/engine/client/backend/opengl/opengl_sl_program.h +++ b/src/engine/client/backend/opengl/opengl_sl_program.h @@ -26,10 +26,10 @@ class CGLSLProgram void LinkProgram(); void UseProgram() const; - TWGLuint GetProgramID() const; + TWGLuint GetProgramId() const; void DetachShader(CGLSL *pShader) const; - void DetachShaderByID(TWGLuint ShaderID) const; + void DetachShaderById(TWGLuint ShaderId) const; void DetachAllShaders() const; //Support various types @@ -47,7 +47,7 @@ class CGLSLProgram virtual ~CGLSLProgram(); protected: - TWGLuint m_ProgramID; + TWGLuint m_ProgramId; bool m_IsLinked; }; diff --git a/src/engine/client/backend/vulkan/backend_vulkan.cpp b/src/engine/client/backend/vulkan/backend_vulkan.cpp index 84a8b48d86..f49025d86e 100644 --- a/src/engine/client/backend/vulkan/backend_vulkan.cpp +++ b/src/engine/client/backend/vulkan/backend_vulkan.cpp @@ -15,26 +15,23 @@ #include #include -#include -#include -#include -#include - #include - +#include +#include #include +#include #include #include +#include #include -#include - -#include #include +#include +#include +#include #include - -#include - #include +#include +#include #include #include @@ -116,10 +113,10 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase * STRUCT DEFINITIONS ************************/ - static constexpr size_t s_StagingBufferCacheID = 0; - static constexpr size_t s_StagingBufferImageCacheID = 1; - static constexpr size_t s_VertexBufferCacheID = 2; - static constexpr size_t s_ImageBufferCacheID = 3; + static constexpr size_t s_StagingBufferCacheId = 0; + static constexpr size_t s_StagingBufferImageCacheId = 1; + static constexpr size_t s_VertexBufferCacheId = 2; + static constexpr size_t s_ImageBufferCacheId = 3; struct SDeviceMemoryBlock { @@ -164,6 +161,20 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase size_t m_OffsetToAlign; SMemoryHeapElement *m_pElementInHeap; [[nodiscard]] bool operator>(const SMemoryHeapQueueElement &Other) const { return m_AllocationSize > Other.m_AllocationSize; } + struct SMemoryHeapQueueElementFind + { + // respects alignment requirements + constexpr bool operator()(const SMemoryHeapQueueElement &Val, const std::pair &Other) const + { + auto AllocSize = Other.first; + auto AllocAlignment = Other.second; + size_t ExtraSizeAlign = Val.m_OffsetInHeap % AllocAlignment; + if(ExtraSizeAlign != 0) + ExtraSizeAlign = AllocAlignment - ExtraSizeAlign; + size_t RealAllocSize = AllocSize + ExtraSizeAlign; + return Val.m_AllocationSize < RealAllocSize; + } + }; }; typedef std::multiset> TMemoryHeapQueue; @@ -206,24 +217,32 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } else { - // calculate the alignment - size_t ExtraSizeAlign = m_Elements.begin()->m_OffsetInHeap % AllocAlignment; - if(ExtraSizeAlign != 0) - ExtraSizeAlign = AllocAlignment - ExtraSizeAlign; - size_t RealAllocSize = AllocSize + ExtraSizeAlign; - // check if there is enough space in this instance - if(m_Elements.begin()->m_AllocationSize < RealAllocSize) + if(SMemoryHeapQueueElement::SMemoryHeapQueueElementFind{}(*m_Elements.begin(), std::make_pair(AllocSize, AllocAlignment))) { return false; } else { - auto TopEl = *m_Elements.begin(); + // see SMemoryHeapQueueElement::operator> + SMemoryHeapQueueElement FindAllocSize; + FindAllocSize.m_AllocationSize = AllocSize; + // find upper bound for a allocation size + auto Upper = m_Elements.upper_bound(FindAllocSize); + // then find the first entry that respects alignment, this is a linear search! + auto FoundEl = std::lower_bound(std::make_reverse_iterator(Upper), m_Elements.rend(), std::make_pair(AllocSize, AllocAlignment), SMemoryHeapQueueElement::SMemoryHeapQueueElementFind{}); + + auto TopEl = *FoundEl; m_Elements.erase(TopEl.m_pElementInHeap->m_InQueue); TopEl.m_pElementInHeap->m_InUse = true; + // calculate the real alloc size + alignment offset + size_t ExtraSizeAlign = TopEl.m_OffsetInHeap % AllocAlignment; + if(ExtraSizeAlign != 0) + ExtraSizeAlign = AllocAlignment - ExtraSizeAlign; + size_t RealAllocSize = AllocSize + ExtraSizeAlign; + // the heap element gets children TopEl.m_pElementInHeap->m_pLeft = std::make_unique(); TopEl.m_pElementInHeap->m_pLeft->m_AllocationSize = RealAllocSize; @@ -306,7 +325,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } }; - template + template struct SMemoryBlock { SMemoryHeap::SMemoryHeapQueueElement m_HeapData; @@ -323,13 +342,13 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase SMemoryHeap *m_pHeap; }; - template - struct SMemoryImageBlock : public SMemoryBlock + template + struct SMemoryImageBlock : public SMemoryBlock { uint32_t m_ImageMemoryBits; }; - template + template struct SMemoryBlockCache { struct SMemoryCacheType @@ -345,7 +364,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase std::vector m_vpMemoryHeaps; }; SMemoryCacheType m_MemoryCaches; - std::vector>> m_vvFrameDelayedCachedBufferCleanup; + std::vector>> m_vvFrameDelayedCachedBufferCleanup; bool m_CanShrink = false; @@ -392,7 +411,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_vvFrameDelayedCachedBufferCleanup[ImgIndex].clear(); } - void FreeMemBlock(SMemoryBlock &Block, size_t ImgIndex) + void FreeMemBlock(SMemoryBlock &Block, size_t ImgIndex) { m_vvFrameDelayedCachedBufferCleanup[ImgIndex].push_back(Block); } @@ -436,12 +455,12 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase struct CTexture { VkImage m_Img = VK_NULL_HANDLE; - SMemoryImageBlock m_ImgMem; + SMemoryImageBlock m_ImgMem; VkImageView m_ImgView = VK_NULL_HANDLE; VkSampler m_aSamplers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE}; VkImage m_Img3D = VK_NULL_HANDLE; - SMemoryImageBlock m_Img3DMem; + SMemoryImageBlock m_Img3DMem; VkImageView m_Img3DView = VK_NULL_HANDLE; VkSampler m_Sampler3D = VK_NULL_HANDLE; @@ -458,7 +477,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase struct SBufferObject { - SMemoryBlock m_Mem; + SMemoryBlock m_Mem; }; struct SBufferObjectFrame @@ -483,9 +502,9 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase size_t m_OffsetInBuffer = 0; size_t m_Size; size_t m_UsedSize; - void *m_pMappedBufferData; + uint8_t *m_pMappedBufferData; - SFrameBuffers(VkBuffer Buffer, SDeviceMemoryBlock BufferMem, size_t OffsetInBuffer, size_t Size, size_t UsedSize, void *pMappedBufferData) : + SFrameBuffers(VkBuffer Buffer, SDeviceMemoryBlock BufferMem, size_t OffsetInBuffer, size_t Size, size_t UsedSize, uint8_t *pMappedBufferData) : m_Buffer(Buffer), m_BufferMem(BufferMem), m_OffsetInBuffer(OffsetInBuffer), m_Size(Size), m_UsedSize(UsedSize), m_pMappedBufferData(pMappedBufferData) { } @@ -495,7 +514,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase { std::array m_aUniformSets; - SFrameUniformBuffers(VkBuffer Buffer, SDeviceMemoryBlock BufferMem, size_t OffsetInBuffer, size_t Size, size_t UsedSize, void *pMappedBufferData) : + SFrameUniformBuffers(VkBuffer Buffer, SDeviceMemoryBlock BufferMem, size_t OffsetInBuffer, size_t Size, size_t UsedSize, uint8_t *pMappedBufferData) : SFrameBuffers(Buffer, BufferMem, OffsetInBuffer, Size, UsedSize, pMappedBufferData) {} }; @@ -846,7 +865,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase struct SSwapChainMultiSampleImage { VkImage m_Image = VK_NULL_HANDLE; - SMemoryImageBlock m_ImgMem; + SMemoryImageBlock m_ImgMem; VkImageView m_ImgView = VK_NULL_HANDLE; }; @@ -856,10 +875,10 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase std::unordered_map m_ShaderFiles; - SMemoryBlockCache m_StagingBufferCache; - SMemoryBlockCache m_StagingBufferCacheImage; - SMemoryBlockCache m_VertexBufferCache; - std::map> m_ImageBufferCaches; + SMemoryBlockCache m_StagingBufferCache; + SMemoryBlockCache m_StagingBufferCacheImage; + SMemoryBlockCache m_VertexBufferCache; + std::map> m_ImageBufferCaches; std::vector m_vNonFlushedStagingBufferRange; @@ -870,7 +889,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase std::atomic *m_pStreamMemoryUsage; std::atomic *m_pStagingMemoryUsage; - TTWGraphicsGPUList *m_pGPUList; + TTwGraphicsGpuList *m_pGpuList; int m_GlobalTextureLodBIAS; uint32_t m_MultiSamplingCount = 1; @@ -904,6 +923,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase uint32_t m_MinUniformAlign; + std::vector m_vReadPixelHelper; std::vector m_vScreenshotHelper; SDeviceMemoryBlock m_GetPresentedImgDataHelperMem; @@ -966,8 +986,6 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase std::vector m_vWaitSemaphores; std::vector m_vSigSemaphores; - std::vector m_vMemorySemaphores; - std::vector m_vFrameFences; std::vector m_vImagesFences; @@ -1033,7 +1051,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase std::vector> m_vStreamedVertexBuffers; std::vector> m_vStreamedUniformBuffers; - uint32_t m_CurFrames = 0; + uint32_t m_CurFrameSyncObject = 0; uint32_t m_CurImageIndex = 0; uint32_t m_CanvasWidth; @@ -1245,7 +1263,6 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase { m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXTURE_CREATE)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_Texture_Create(static_cast(pBaseCommand)); }}; m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXTURE_DESTROY)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_Texture_Destroy(static_cast(pBaseCommand)); }}; - m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXTURE_UPDATE)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_Texture_Update(static_cast(pBaseCommand)); }}; m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXT_TEXTURES_CREATE)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_TextTextures_Create(static_cast(pBaseCommand)); }}; m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXT_TEXTURES_DESTROY)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_TextTextures_Destroy(static_cast(pBaseCommand)); }}; m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TEXT_TEXTURE_UPDATE)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_TextTexture_Update(static_cast(pBaseCommand)); }}; @@ -1278,6 +1295,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_VSYNC)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_VSync(static_cast(pBaseCommand)); }}; m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_MULTISAMPLING)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_MultiSampling(static_cast(pBaseCommand)); }}; + m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TRY_SWAP_AND_READ_PIXEL)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_ReadPixel(static_cast(pBaseCommand)); }}; m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_TRY_SWAP_AND_SCREENSHOT)] = {false, [](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) {}, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_Screenshot(static_cast(pBaseCommand)); }}; m_aCommandCallbacks[CommandBufferCMDOff(CCommandBuffer::CMD_UPDATE_VIEWPORT)] = {false, [this](SRenderCommandExecuteBuffer &ExecBuffer, const CCommandBuffer::SCommand *pBaseCommand) { Cmd_Update_Viewport_FillExecuteBuffer(ExecBuffer, static_cast(pBaseCommand)); }, [this](const CCommandBuffer::SCommand *pBaseCommand, SRenderCommandExecuteBuffer &ExecBuffer) { return Cmd_Update_Viewport(static_cast(pBaseCommand)); }}; @@ -1378,15 +1396,29 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } } - [[nodiscard]] bool GetPresentedImageDataImpl(uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData, bool FlipImgData, bool ResetAlpha) + [[nodiscard]] bool GetPresentedImageDataImpl(uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData, bool ResetAlpha, std::optional PixelOffset) { bool IsB8G8R8A8 = m_VKSurfFormat.format == VK_FORMAT_B8G8R8A8_UNORM; bool UsesRGBALikeFormat = m_VKSurfFormat.format == VK_FORMAT_R8G8B8A8_UNORM || IsB8G8R8A8; if(UsesRGBALikeFormat && m_LastPresentedSwapChainImageIndex != std::numeric_limits::max()) { auto Viewport = m_VKSwapImgAndViewportExtent.GetPresentedImageViewport(); - Width = Viewport.width; - Height = Viewport.height; + VkOffset3D SrcOffset; + if(PixelOffset.has_value()) + { + SrcOffset.x = PixelOffset.value().x; + SrcOffset.y = PixelOffset.value().y; + Width = 1; + Height = 1; + } + else + { + SrcOffset.x = 0; + SrcOffset.y = 0; + Width = Viewport.width; + Height = Viewport.height; + } + SrcOffset.z = 0; Format = CImageInfo::FORMAT_RGBA; const size_t ImageTotalSize = (size_t)Width * Height * CImageInfo::PixelSize(Format); @@ -1400,17 +1432,6 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return false; VkCommandBuffer &CommandBuffer = *pCommandBuffer; - VkBufferImageCopy Region{}; - Region.bufferOffset = 0; - Region.bufferRowLength = 0; - Region.bufferImageHeight = 0; - Region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - Region.imageSubresource.mipLevel = 0; - Region.imageSubresource.baseArrayLayer = 0; - Region.imageSubresource.layerCount = 1; - Region.imageOffset = {0, 0, 0}; - Region.imageExtent = {Viewport.width, Viewport.height, 1}; - auto &SwapImg = m_vSwapChainImages[m_LastPresentedSwapChainImageIndex]; if(!ImageBarrier(m_GetPresentedImgDataHelperImage, 0, 1, 0, 1, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)) @@ -1425,10 +1446,12 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase BlitSize.x = Width; BlitSize.y = Height; BlitSize.z = 1; + VkImageBlit ImageBlitRegion{}; ImageBlitRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ImageBlitRegion.srcSubresource.layerCount = 1; - ImageBlitRegion.srcOffsets[1] = BlitSize; + ImageBlitRegion.srcOffsets[0] = SrcOffset; + ImageBlitRegion.srcOffsets[1] = {SrcOffset.x + BlitSize.x, SrcOffset.y + BlitSize.y, SrcOffset.z + BlitSize.z}; ImageBlitRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ImageBlitRegion.dstSubresource.layerCount = 1; ImageBlitRegion.dstOffsets[1] = BlitSize; @@ -1447,6 +1470,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase VkImageCopy ImageCopyRegion{}; ImageCopyRegion.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ImageCopyRegion.srcSubresource.layerCount = 1; + ImageCopyRegion.srcOffset = SrcOffset; ImageCopyRegion.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; ImageCopyRegion.dstSubresource.layerCount = 1; ImageCopyRegion.extent.width = Width; @@ -1469,7 +1493,6 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase VkSubmitInfo SubmitInfo{}; SubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - SubmitInfo.commandBufferCount = 1; SubmitInfo.pCommandBuffers = &CommandBuffer; @@ -1485,8 +1508,9 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase vkInvalidateMappedMemoryRanges(m_VKDevice, 1, &MemRange); size_t RealFullImageSize = maximum(ImageTotalSize, (size_t)(Height * m_GetPresentedImgDataHelperMappedLayoutPitch)); - if(vDstData.size() < RealFullImageSize + (Width * 4)) - vDstData.resize(RealFullImageSize + (Width * 4)); // extra space for flipping + size_t ExtraRowSize = Width * 4; + if(vDstData.size() < RealFullImageSize + ExtraRowSize) + vDstData.resize(RealFullImageSize + ExtraRowSize); mem_copy(vDstData.data(), pResImageData, RealFullImageSize); @@ -1497,7 +1521,8 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase { size_t OffsetImagePacked = (Y * Width * 4); size_t OffsetImageUnpacked = (Y * m_GetPresentedImgDataHelperMappedLayoutPitch); - mem_copy(vDstData.data() + OffsetImagePacked, vDstData.data() + OffsetImageUnpacked, Width * 4); + mem_copy(vDstData.data() + RealFullImageSize, vDstData.data() + OffsetImageUnpacked, Width * 4); + mem_copy(vDstData.data() + OffsetImagePacked, vDstData.data() + RealFullImageSize, Width * 4); } } @@ -1518,17 +1543,6 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } } - if(FlipImgData) - { - uint8_t *pTempRow = vDstData.data() + Width * Height * 4; - for(uint32_t Y = 0; Y < Height / 2; ++Y) - { - mem_copy(pTempRow, vDstData.data() + Y * Width * 4, Width * 4); - mem_copy(vDstData.data() + Y * Width * 4, vDstData.data() + ((Height - Y) - 1) * Width * 4, Width * 4); - mem_copy(vDstData.data() + ((Height - Y) - 1) * Width * 4, pTempRow, Width * 4); - } - } - return true; } else @@ -1547,7 +1561,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase [[nodiscard]] bool GetPresentedImageData(uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) override { - return GetPresentedImageDataImpl(Width, Height, Format, vDstData, false, false); + return GetPresentedImageDataImpl(Width, Height, Format, vDstData, false, {}); } /************************ @@ -1585,10 +1599,10 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return CreateBuffer(RequiredSize, MemUsage, BufferUsage, BufferProperties, Buffer, BufferMemory); } - template - [[nodiscard]] bool GetBufferBlockImpl(SMemoryBlock &RetBlock, SMemoryBlockCache &MemoryCache, VkBufferUsageFlags BufferUsage, VkMemoryPropertyFlags BufferProperties, const void *pBufferData, VkDeviceSize RequiredSize, VkDeviceSize TargetAlignment) + [[nodiscard]] bool GetBufferBlockImpl(SMemoryBlock &RetBlock, SMemoryBlockCache &MemoryCache, VkBufferUsageFlags BufferUsage, VkMemoryPropertyFlags BufferProperties, const void *pBufferData, VkDeviceSize RequiredSize, VkDeviceSize TargetAlignment) { bool Res = true; @@ -1596,7 +1610,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase bool FoundAllocation = false; SMemoryHeap::SMemoryHeapQueueElement AllocatedMem; SDeviceMemoryBlock TmpBufferMemory; - typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pCacheHeap = nullptr; + typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pCacheHeap = nullptr; auto &Heaps = MemoryCache.m_MemoryCaches.m_vpMemoryHeaps; for(size_t i = 0; i < Heaps.size(); ++i) { @@ -1611,7 +1625,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } if(!FoundAllocation) { - typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pNewHeap = new typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap(); + typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pNewHeap = new typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap(); VkBuffer TmpBuffer; if(!GetBufferImpl(MemoryBlockSize * BlockCount, RequiresMapping ? MEMORY_BLOCK_USAGE_STAGING : MEMORY_BLOCK_USAGE_BUFFER, TmpBuffer, TmpBufferMemory, BufferUsage, BufferProperties)) @@ -1697,18 +1711,18 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return Res; } - [[nodiscard]] bool GetStagingBuffer(SMemoryBlock &ResBlock, const void *pBufferData, VkDeviceSize RequiredSize) + [[nodiscard]] bool GetStagingBuffer(SMemoryBlock &ResBlock, const void *pBufferData, VkDeviceSize RequiredSize) { - return GetBufferBlockImpl(ResBlock, m_StagingBufferCache, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, pBufferData, RequiredSize, maximum(m_NonCoherentMemAlignment, 16)); + return GetBufferBlockImpl(ResBlock, m_StagingBufferCache, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, pBufferData, RequiredSize, maximum(m_NonCoherentMemAlignment, 16)); } - [[nodiscard]] bool GetStagingBufferImage(SMemoryBlock &ResBlock, const void *pBufferData, VkDeviceSize RequiredSize) + [[nodiscard]] bool GetStagingBufferImage(SMemoryBlock &ResBlock, const void *pBufferData, VkDeviceSize RequiredSize) { - return GetBufferBlockImpl(ResBlock, m_StagingBufferCacheImage, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, pBufferData, RequiredSize, maximum(m_OptimalImageCopyMemAlignment, maximum(m_NonCoherentMemAlignment, 16))); + return GetBufferBlockImpl(ResBlock, m_StagingBufferCacheImage, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT, pBufferData, RequiredSize, maximum(m_OptimalImageCopyMemAlignment, maximum(m_NonCoherentMemAlignment, 16))); } - template - void PrepareStagingMemRange(SMemoryBlock &Block) + template + void PrepareStagingMemRange(SMemoryBlock &Block) { VkMappedMemoryRange UploadRange{}; UploadRange.sType = VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; @@ -1727,7 +1741,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_vNonFlushedStagingBufferRange.push_back(UploadRange); } - void UploadAndFreeStagingMemBlock(SMemoryBlock &Block) + void UploadAndFreeStagingMemBlock(SMemoryBlock &Block) { PrepareStagingMemRange(Block); if(!Block.m_IsCached) @@ -1740,7 +1754,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } } - void UploadAndFreeStagingImageMemBlock(SMemoryBlock &Block) + void UploadAndFreeStagingImageMemBlock(SMemoryBlock &Block) { PrepareStagingMemRange(Block); if(!Block.m_IsCached) @@ -1753,12 +1767,12 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } } - [[nodiscard]] bool GetVertexBuffer(SMemoryBlock &ResBlock, VkDeviceSize RequiredSize) + [[nodiscard]] bool GetVertexBuffer(SMemoryBlock &ResBlock, VkDeviceSize RequiredSize) { - return GetBufferBlockImpl(ResBlock, m_VertexBufferCache, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, nullptr, RequiredSize, 16); + return GetBufferBlockImpl(ResBlock, m_VertexBufferCache, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, nullptr, RequiredSize, 16); } - void FreeVertexMemBlock(SMemoryBlock &Block) + void FreeVertexMemBlock(SMemoryBlock &Block) { if(!Block.m_IsCached) { @@ -1810,15 +1824,15 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return true; } - template - [[nodiscard]] bool GetImageMemoryBlockImpl(SMemoryImageBlock &RetBlock, SMemoryBlockCache &MemoryCache, VkMemoryPropertyFlags BufferProperties, VkDeviceSize RequiredSize, VkDeviceSize RequiredAlignment, uint32_t RequiredMemoryTypeBits) + [[nodiscard]] bool GetImageMemoryBlockImpl(SMemoryImageBlock &RetBlock, SMemoryBlockCache &MemoryCache, VkMemoryPropertyFlags BufferProperties, VkDeviceSize RequiredSize, VkDeviceSize RequiredAlignment, uint32_t RequiredMemoryTypeBits) { auto &&CreateCacheBlock = [&]() -> bool { bool FoundAllocation = false; SMemoryHeap::SMemoryHeapQueueElement AllocatedMem; SDeviceMemoryBlock TmpBufferMemory; - typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pCacheHeap = nullptr; + typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pCacheHeap = nullptr; for(size_t i = 0; i < MemoryCache.m_MemoryCaches.m_vpMemoryHeaps.size(); ++i) { auto *pHeap = MemoryCache.m_MemoryCaches.m_vpMemoryHeaps[i]; @@ -1832,7 +1846,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } if(!FoundAllocation) { - typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pNewHeap = new typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap(); + typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap *pNewHeap = new typename SMemoryBlockCache::SMemoryCacheType::SMemoryCacheHeap(); if(!GetImageMemoryImpl(MemoryBlockSize * BlockCount, RequiredMemoryTypeBits, TmpBufferMemory, BufferProperties)) { @@ -1892,7 +1906,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return true; } - [[nodiscard]] bool GetImageMemory(SMemoryImageBlock &RetBlock, VkDeviceSize RequiredSize, VkDeviceSize RequiredAlignment, uint32_t RequiredMemoryTypeBits) + [[nodiscard]] bool GetImageMemory(SMemoryImageBlock &RetBlock, VkDeviceSize RequiredSize, VkDeviceSize RequiredAlignment, uint32_t RequiredMemoryTypeBits) { auto it = m_ImageBufferCaches.find(RequiredMemoryTypeBits); if(it == m_ImageBufferCaches.end()) @@ -1901,10 +1915,10 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase it->second.Init(m_SwapChainImageCount); } - return GetImageMemoryBlockImpl(RetBlock, it->second, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, RequiredSize, RequiredAlignment, RequiredMemoryTypeBits); + return GetImageMemoryBlockImpl(RetBlock, it->second, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, RequiredSize, RequiredAlignment, RequiredMemoryTypeBits); } - void FreeImageMemBlock(SMemoryImageBlock &Block) + void FreeImageMemBlock(SMemoryImageBlock &Block) { if(!Block.m_IsCached) { @@ -1971,7 +1985,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(IsVerbose()) { - VerboseDeallocatedMemory(BufferMem.m_Size, (size_t)ImageIndex, BufferMem.m_UsageType); + VerboseDeallocatedMemory(BufferMem.m_Size, ImageIndex, BufferMem.m_UsageType); } BufferMem.m_Mem = VK_NULL_HANDLE; @@ -2068,7 +2082,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_pStagingMemoryUsage->store(m_pStagingMemoryUsage->load(std::memory_order_relaxed) - FreeedMemory, std::memory_order_relaxed); if(IsVerbose()) { - dbg_msg("vulkan", "deallocated chunks of memory with size: %" PRIzu " from all frames (staging buffer)", (size_t)FreeedMemory); + dbg_msg("vulkan", "deallocated chunks of memory with size: %" PRIzu " from all frames (staging buffer)", FreeedMemory); } } FreeedMemory = 0; @@ -2078,7 +2092,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_pBufferMemoryUsage->store(m_pBufferMemoryUsage->load(std::memory_order_relaxed) - FreeedMemory, std::memory_order_relaxed); if(IsVerbose()) { - dbg_msg("vulkan", "deallocated chunks of memory with size: %" PRIzu " from all frames (buffer)", (size_t)FreeedMemory); + dbg_msg("vulkan", "deallocated chunks of memory with size: %" PRIzu " from all frames (buffer)", FreeedMemory); } } FreeedMemory = 0; @@ -2089,7 +2103,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_pTextureMemoryUsage->store(m_pTextureMemoryUsage->load(std::memory_order_relaxed) - FreeedMemory, std::memory_order_relaxed); if(IsVerbose()) { - dbg_msg("vulkan", "deallocated chunks of memory with size: %" PRIzu " from all frames (texture)", (size_t)FreeedMemory); + dbg_msg("vulkan", "deallocated chunks of memory with size: %" PRIzu " from all frames (texture)", FreeedMemory); } } } @@ -2260,7 +2274,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return false; } - VkSemaphore WaitSemaphore = m_vWaitSemaphores[m_CurFrames]; + VkSemaphore WaitSemaphore = m_vWaitSemaphores[m_CurFrameSyncObject]; VkSubmitInfo SubmitInfo{}; SubmitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; @@ -2289,13 +2303,13 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase SubmitInfo.pWaitSemaphores = aWaitSemaphores.data(); SubmitInfo.pWaitDstStageMask = aWaitStages.data(); - std::array aSignalSemaphores = {m_vSigSemaphores[m_CurFrames]}; + std::array aSignalSemaphores = {m_vSigSemaphores[m_CurFrameSyncObject]}; SubmitInfo.signalSemaphoreCount = aSignalSemaphores.size(); SubmitInfo.pSignalSemaphores = aSignalSemaphores.data(); - vkResetFences(m_VKDevice, 1, &m_vFrameFences[m_CurFrames]); + vkResetFences(m_VKDevice, 1, &m_vFrameFences[m_CurFrameSyncObject]); - VkResult QueueSubmitRes = vkQueueSubmit(m_VKGraphicsQueue, 1, &SubmitInfo, m_vFrameFences[m_CurFrames]); + VkResult QueueSubmitRes = vkQueueSubmit(m_VKGraphicsQueue, 1, &SubmitInfo, m_vFrameFences[m_CurFrameSyncObject]); if(QueueSubmitRes != VK_SUCCESS) { const char *pCritErrorMsg = CheckVulkanCriticalError(QueueSubmitRes); @@ -2306,7 +2320,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } } - std::swap(m_vWaitSemaphores[m_CurFrames], m_vSigSemaphores[m_CurFrames]); + std::swap(m_vWaitSemaphores[m_CurFrameSyncObject], m_vSigSemaphores[m_CurFrameSyncObject]); VkPresentInfoKHR PresentInfo{}; PresentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; @@ -2333,7 +2347,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } } - m_CurFrames = (m_CurFrames + 1) % m_SwapChainImageCount; + m_CurFrameSyncObject = (m_CurFrameSyncObject + 1) % m_vWaitSemaphores.size(); return true; } @@ -2349,7 +2363,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase RecreateSwapChain(); } - auto AcqResult = vkAcquireNextImageKHR(m_VKDevice, m_VKSwapChain, std::numeric_limits::max(), m_vSigSemaphores[m_CurFrames], VK_NULL_HANDLE, &m_CurImageIndex); + auto AcqResult = vkAcquireNextImageKHR(m_VKDevice, m_VKSwapChain, std::numeric_limits::max(), m_vSigSemaphores[m_CurFrameSyncObject], VK_NULL_HANDLE, &m_CurImageIndex); if(AcqResult != VK_SUCCESS) { if(AcqResult == VK_ERROR_OUT_OF_DATE_KHR || m_RecreateSwapChain) @@ -2380,13 +2394,13 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } } } - std::swap(m_vWaitSemaphores[m_CurFrames], m_vSigSemaphores[m_CurFrames]); + std::swap(m_vWaitSemaphores[m_CurFrameSyncObject], m_vSigSemaphores[m_CurFrameSyncObject]); if(m_vImagesFences[m_CurImageIndex] != VK_NULL_HANDLE) { vkWaitForFences(m_VKDevice, 1, &m_vImagesFences[m_CurImageIndex], VK_TRUE, std::numeric_limits::max()); } - m_vImagesFences[m_CurImageIndex] = m_vFrameFences[m_CurFrames]; + m_vImagesFences[m_CurImageIndex] = m_vFrameFences[m_CurFrameSyncObject]; // next frame m_CurFrame++; @@ -2513,10 +2527,10 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return 4; } - [[nodiscard]] bool UpdateTexture(size_t TextureSlot, VkFormat Format, void *&pData, int64_t XOff, int64_t YOff, size_t Width, size_t Height) + [[nodiscard]] bool UpdateTexture(size_t TextureSlot, VkFormat Format, uint8_t *&pData, int64_t XOff, int64_t YOff, size_t Width, size_t Height) { const size_t ImageSize = Width * Height * VulkanFormatToPixelSize(Format); - SMemoryBlock StagingBuffer; + SMemoryBlock StagingBuffer; if(!GetStagingBufferImage(StagingBuffer, pData, ImageSize)) return false; @@ -2533,7 +2547,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase YOff /= 2; } - void *pTmpData = Resize((const uint8_t *)pData, Width, Height, Width, Height, VulkanFormatToPixelSize(Format)); + uint8_t *pTmpData = ResizeImage(pData, Width, Height, Width, Height, VulkanFormatToPixelSize(Format)); free(pData); pData = pTmpData; } @@ -2566,7 +2580,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase VkFormat Format, VkFormat StoreFormat, int Flags, - void *&pData) + uint8_t *&pData) { size_t ImageIndex = (size_t)Slot; const size_t PixelSize = VulkanFormatToPixelSize(Format); @@ -2587,7 +2601,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase ++RescaleCount; } while((size_t)Width > m_MaxTextureSize || (size_t)Height > m_MaxTextureSize); - void *pTmpData = Resize((const uint8_t *)(pData), Width, Height, Width, Height, PixelSize); + uint8_t *pTmpData = ResizeImage(pData, Width, Height, Width, Height, PixelSize); free(pData); pData = pTmpData; } @@ -2642,7 +2656,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase dbg_msg("vulkan", "3D/2D array texture was resized"); int NewWidth = maximum(HighestBit(ConvertWidth), 16); int NewHeight = maximum(HighestBit(ConvertHeight), 16); - uint8_t *pNewTexData = (uint8_t *)Resize((const uint8_t *)pData, ConvertWidth, ConvertHeight, NewWidth, NewHeight, PixelSize); + uint8_t *pNewTexData = ResizeImage(pData, ConvertWidth, ConvertHeight, NewWidth, NewHeight, PixelSize); ConvertWidth = NewWidth; ConvertHeight = NewHeight; @@ -2652,7 +2666,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } bool Needs3DTexDel = false; - void *p3DTexData = malloc((size_t)PixelSize * ConvertWidth * ConvertHeight); + uint8_t *p3DTexData = static_cast(malloc((size_t)PixelSize * ConvertWidth * ConvertHeight)); if(!Texture2DTo3D(pData, ConvertWidth, ConvertHeight, PixelSize, 16, 16, p3DTexData, Image3DWidth, Image3DHeight)) { free(p3DTexData); @@ -2689,13 +2703,6 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return true; } - VkFormat TextureFormatToVulkanFormat(int TexFormat) - { - if(TexFormat == CCommandBuffer::TEXFORMAT_RGBA) - return VK_FORMAT_R8G8B8A8_UNORM; - return VK_FORMAT_R8G8B8A8_UNORM; - } - [[nodiscard]] bool BuildMipmaps(VkImage Image, VkFormat ImageFormat, size_t Width, size_t Height, size_t Depth, size_t MipMapLevelCount) { VkCommandBuffer *pMemCommandBuffer; @@ -2778,11 +2785,11 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return true; } - [[nodiscard]] bool CreateTextureImage(size_t ImageIndex, VkImage &NewImage, SMemoryImageBlock &NewImgMem, const void *pData, VkFormat Format, size_t Width, size_t Height, size_t Depth, size_t PixelSize, size_t MipMapLevelCount) + [[nodiscard]] bool CreateTextureImage(size_t ImageIndex, VkImage &NewImage, SMemoryImageBlock &NewImgMem, const uint8_t *pData, VkFormat Format, size_t Width, size_t Height, size_t Depth, size_t PixelSize, size_t MipMapLevelCount) { - int ImageSize = Width * Height * Depth * PixelSize; + VkDeviceSize ImageSize = Width * Height * Depth * PixelSize; - SMemoryBlock StagingBuffer; + SMemoryBlock StagingBuffer; if(!GetStagingBufferImage(StagingBuffer, pData, ImageSize)) return false; @@ -2888,7 +2895,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return ImageView; } - [[nodiscard]] bool CreateImage(uint32_t Width, uint32_t Height, uint32_t Depth, size_t MipMapLevelCount, VkFormat Format, VkImageTiling Tiling, VkImage &Image, SMemoryImageBlock &ImageMemory, VkImageUsageFlags ImageUsage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT) + [[nodiscard]] bool CreateImage(uint32_t Width, uint32_t Height, uint32_t Depth, size_t MipMapLevelCount, VkFormat Format, VkImageTiling Tiling, VkImage &Image, SMemoryImageBlock &ImageMemory, VkImageUsageFlags ImageUsage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT) { VkImageCreateInfo ImageInfo{}; ImageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; @@ -3073,11 +3080,11 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase size_t BufferOffset = 0; if(!IsOneFrameBuffer) { - SMemoryBlock StagingBuffer; + SMemoryBlock StagingBuffer; if(!GetStagingBuffer(StagingBuffer, pUploadData, BufferDataSize)) return false; - SMemoryBlock Mem; + SMemoryBlock Mem; if(!GetVertexBuffer(Mem, BufferDataSize)) return false; @@ -3279,7 +3286,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase else { Scissor.offset = {0, 0}; - Scissor.extent = {(uint32_t)ScissorViewport.width, (uint32_t)ScissorViewport.height}; + Scissor.extent = {ScissorViewport.width, ScissorViewport.height}; } // if there is a dynamic viewport make sure the scissor data is scaled down to that @@ -3395,7 +3402,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, VertexPushConstantSize, &VertexPushConstants); vkCmdPushConstants(CommandBuffer, PipeLayout, VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(SUniformTileGPosBorder) + sizeof(SUniformTileGVertColorAlign), FragPushConstantSize, &FragPushConstants); - size_t DrawCount = (size_t)IndicesDrawNum; + size_t DrawCount = IndicesDrawNum; vkCmdBindIndexBuffer(CommandBuffer, ExecBuffer.m_IndexBuffer, 0, VK_INDEX_TYPE_UINT32); for(size_t i = 0; i < DrawCount; ++i) { @@ -3567,6 +3574,22 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return true; } + bool IsGpuDenied(uint32_t Vendor, uint32_t DriverVersion, uint32_t ApiMajor, uint32_t ApiMinor, uint32_t ApiPatch) + { +#ifdef CONF_FAMILY_WINDOWS + // AMD + if(0x1002 == Vendor) + { + auto Major = (DriverVersion >> 22); + auto Minor = (DriverVersion >> 12) & 0x3ff; + auto Patch = DriverVersion & 0xfff; + + return Major == 2 && Minor == 0 && Patch > 116 && Patch < 220 && ((ApiMajor <= 1 && ApiMinor < 3) || (ApiMajor <= 1 && ApiMinor == 3 && ApiPatch < 206)); + } +#endif + return false; + } + [[nodiscard]] bool CreateVulkanInstance(const std::vector &vVKLayers, const std::vector &vVKExtensions, bool TryDebugExtensions) { std::vector vLayersCStr; @@ -3638,25 +3661,25 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return true; } - STWGraphicGPU::ETWGraphicsGPUType VKGPUTypeToGraphicsGPUType(VkPhysicalDeviceType VKGPUType) + STWGraphicGpu::ETWGraphicsGpuType VKGPUTypeToGraphicsGpuType(VkPhysicalDeviceType VKGPUType) { if(VKGPUType == VkPhysicalDeviceType::VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) - return STWGraphicGPU::ETWGraphicsGPUType::GRAPHICS_GPU_TYPE_DISCRETE; + return STWGraphicGpu::ETWGraphicsGpuType::GRAPHICS_GPU_TYPE_DISCRETE; else if(VKGPUType == VkPhysicalDeviceType::VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU) - return STWGraphicGPU::ETWGraphicsGPUType::GRAPHICS_GPU_TYPE_INTEGRATED; + return STWGraphicGpu::ETWGraphicsGpuType::GRAPHICS_GPU_TYPE_INTEGRATED; else if(VKGPUType == VkPhysicalDeviceType::VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU) - return STWGraphicGPU::ETWGraphicsGPUType::GRAPHICS_GPU_TYPE_VIRTUAL; + return STWGraphicGpu::ETWGraphicsGpuType::GRAPHICS_GPU_TYPE_VIRTUAL; else if(VKGPUType == VkPhysicalDeviceType::VK_PHYSICAL_DEVICE_TYPE_CPU) - return STWGraphicGPU::ETWGraphicsGPUType::GRAPHICS_GPU_TYPE_CPU; + return STWGraphicGpu::ETWGraphicsGpuType::GRAPHICS_GPU_TYPE_CPU; - return STWGraphicGPU::ETWGraphicsGPUType::GRAPHICS_GPU_TYPE_CPU; + return STWGraphicGpu::ETWGraphicsGpuType::GRAPHICS_GPU_TYPE_CPU; } // from: https://github.com/SaschaWillems/vulkan.gpuinfo.org/blob/5c3986798afc39d736b825bf8a5fbf92b8d9ed49/includes/functions.php#L364 - const char *GetDriverVerson(char (&aBuff)[256], uint32_t DriverVersion, uint32_t VendorID) + const char *GetDriverVerson(char (&aBuff)[256], uint32_t DriverVersion, uint32_t VendorId) { // NVIDIA - if(VendorID == 4318) + if(VendorId == 4318) { str_format(aBuff, std::size(aBuff), "%d.%d.%d.%d", (DriverVersion >> 22) & 0x3ff, @@ -3666,7 +3689,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } #ifdef CONF_FAMILY_WINDOWS // windows only - else if(VendorID == 0x8086) + else if(VendorId == 0x8086) { str_format(aBuff, std::size(aBuff), "%d.%d", @@ -3687,7 +3710,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return aBuff; } - [[nodiscard]] bool SelectGPU(char *pRendererName, char *pVendorName, char *pVersionName) + [[nodiscard]] bool SelectGpu(char *pRendererName, char *pVendorName, char *pVersionName) { uint32_t DevicesCount = 0; auto Res = vkEnumeratePhysicalDevices(m_VKInstance, &DevicesCount, nullptr); @@ -3721,59 +3744,72 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase size_t Index = 0; std::vector vDevicePropList(vDeviceList.size()); - m_pGPUList->m_vGPUs.reserve(vDeviceList.size()); + m_pGpuList->m_vGpus.reserve(vDeviceList.size()); size_t FoundDeviceIndex = 0; - size_t FoundGPUType = STWGraphicGPU::ETWGraphicsGPUType::GRAPHICS_GPU_TYPE_INVALID; - STWGraphicGPU::ETWGraphicsGPUType AutoGPUType = STWGraphicGPU::ETWGraphicsGPUType::GRAPHICS_GPU_TYPE_INVALID; + STWGraphicGpu::ETWGraphicsGpuType AutoGpuType = STWGraphicGpu::ETWGraphicsGpuType::GRAPHICS_GPU_TYPE_INVALID; - bool IsAutoGPU = str_comp(g_Config.m_GfxGPUName, "auto") == 0; + bool IsAutoGpu = str_comp(g_Config.m_GfxGpuName, "auto") == 0; + bool UserSelectedGpuChosen = false; for(auto &CurDevice : vDeviceList) { vkGetPhysicalDeviceProperties(CurDevice, &(vDevicePropList[Index])); auto &DeviceProp = vDevicePropList[Index]; - STWGraphicGPU::ETWGraphicsGPUType GPUType = VKGPUTypeToGraphicsGPUType(DeviceProp.deviceType); - - STWGraphicGPU::STWGraphicGPUItem NewGPU; - str_copy(NewGPU.m_aName, DeviceProp.deviceName); - NewGPU.m_GPUType = GPUType; - m_pGPUList->m_vGPUs.push_back(NewGPU); - - Index++; + STWGraphicGpu::ETWGraphicsGpuType GPUType = VKGPUTypeToGraphicsGpuType(DeviceProp.deviceType); - int DevAPIMajor = (int)VK_API_VERSION_MAJOR(DeviceProp.apiVersion); - int DevAPIMinor = (int)VK_API_VERSION_MINOR(DeviceProp.apiVersion); + int DevApiMajor = (int)VK_API_VERSION_MAJOR(DeviceProp.apiVersion); + int DevApiMinor = (int)VK_API_VERSION_MINOR(DeviceProp.apiVersion); + int DevApiPatch = (int)VK_API_VERSION_PATCH(DeviceProp.apiVersion); - if(GPUType < AutoGPUType && (DevAPIMajor > gs_BackendVulkanMajor || (DevAPIMajor == gs_BackendVulkanMajor && DevAPIMinor >= gs_BackendVulkanMinor))) + auto IsDenied = CCommandProcessorFragment_Vulkan::IsGpuDenied(DeviceProp.vendorID, DeviceProp.driverVersion, DevApiMajor, DevApiMinor, DevApiPatch); + if((DevApiMajor > gs_BackendVulkanMajor || (DevApiMajor == gs_BackendVulkanMajor && DevApiMinor >= gs_BackendVulkanMinor)) && !IsDenied) { - str_copy(m_pGPUList->m_AutoGPU.m_aName, DeviceProp.deviceName); - m_pGPUList->m_AutoGPU.m_GPUType = GPUType; + STWGraphicGpu::STWGraphicGpuItem NewGpu; + str_copy(NewGpu.m_aName, DeviceProp.deviceName); + NewGpu.m_GpuType = GPUType; + m_pGpuList->m_vGpus.push_back(NewGpu); - AutoGPUType = GPUType; - } + // We always decide what the 'auto' GPU would be, even if user is forcing a GPU by name in config + // Reminder: A worse GPU enumeration has a higher value than a better GPU enumeration, thus the '>' + if(AutoGpuType > STWGraphicGpu::ETWGraphicsGpuType::GRAPHICS_GPU_TYPE_INTEGRATED) + { + str_copy(m_pGpuList->m_AutoGpu.m_aName, DeviceProp.deviceName); + m_pGpuList->m_AutoGpu.m_GpuType = GPUType; - if(((IsAutoGPU && (FoundGPUType > STWGraphicGPU::ETWGraphicsGPUType::GRAPHICS_GPU_TYPE_INTEGRATED && GPUType < FoundGPUType)) || str_comp(DeviceProp.deviceName, g_Config.m_GfxGPUName) == 0) && (DevAPIMajor > gs_BackendVulkanMajor || (DevAPIMajor == gs_BackendVulkanMajor && DevAPIMinor >= gs_BackendVulkanMinor))) - { - FoundDeviceIndex = Index; - FoundGPUType = GPUType; + AutoGpuType = GPUType; + + if(IsAutoGpu) + FoundDeviceIndex = Index; + } + // We only select the first GPU that matches, because it comes first in the enumeration array, it's preferred by the system + // Reminder: We can't break the cycle here if the name matches because we need to choose the best GPU for 'auto' mode + if(!IsAutoGpu && !UserSelectedGpuChosen && str_comp(DeviceProp.deviceName, g_Config.m_GfxGpuName) == 0) + { + FoundDeviceIndex = Index; + UserSelectedGpuChosen = true; + } } + Index++; } - if(FoundDeviceIndex == 0) - FoundDeviceIndex = 1; + if(m_pGpuList->m_vGpus.empty()) + { + dbg_msg("vulkan", "no devices with required vulkan version found."); + return false; + } { - auto &DeviceProp = vDevicePropList[FoundDeviceIndex - 1]; + auto &DeviceProp = vDevicePropList[FoundDeviceIndex]; - int DevAPIMajor = (int)VK_API_VERSION_MAJOR(DeviceProp.apiVersion); - int DevAPIMinor = (int)VK_API_VERSION_MINOR(DeviceProp.apiVersion); - int DevAPIPatch = (int)VK_API_VERSION_PATCH(DeviceProp.apiVersion); + int DevApiMajor = (int)VK_API_VERSION_MAJOR(DeviceProp.apiVersion); + int DevApiMinor = (int)VK_API_VERSION_MINOR(DeviceProp.apiVersion); + int DevApiPatch = (int)VK_API_VERSION_PATCH(DeviceProp.apiVersion); - str_copy(pRendererName, DeviceProp.deviceName, gs_GPUInfoStringSize); + str_copy(pRendererName, DeviceProp.deviceName, gs_GpuInfoStringSize); const char *pVendorNameStr = NULL; switch(DeviceProp.vendorID) { @@ -3808,8 +3844,8 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } char aBuff[256]; - str_copy(pVendorName, pVendorNameStr, gs_GPUInfoStringSize); - str_format(pVersionName, gs_GPUInfoStringSize, "Vulkan %d.%d.%d (driver: %s)", DevAPIMajor, DevAPIMinor, DevAPIPatch, GetDriverVerson(aBuff, DeviceProp.driverVersion, DeviceProp.vendorID)); + str_copy(pVendorName, pVendorNameStr, gs_GpuInfoStringSize); + str_format(pVersionName, gs_GpuInfoStringSize, "Vulkan %d.%d.%d (driver: %s)", DevApiMajor, DevApiMinor, DevApiPatch, GetDriverVerson(aBuff, DeviceProp.driverVersion, DeviceProp.vendorID)); // get important device limits m_NonCoherentMemAlignment = DeviceProp.limits.nonCoherentAtomSize; @@ -3827,7 +3863,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } } - VkPhysicalDevice CurDevice = vDeviceList[FoundDeviceIndex - 1]; + VkPhysicalDevice CurDevice = vDeviceList[FoundDeviceIndex]; uint32_t FamQueueCount = 0; vkGetPhysicalDeviceQueueFamilyProperties(CurDevice, &FamQueueCount, nullptr); @@ -5318,12 +5354,12 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase [[nodiscard]] bool CreateSyncObjects() { - m_vWaitSemaphores.resize(m_SwapChainImageCount); - m_vSigSemaphores.resize(m_SwapChainImageCount); + // Create one more sync object than there are frames in flight + auto SyncObjectCount = m_SwapChainImageCount + 1; + m_vWaitSemaphores.resize(SyncObjectCount); + m_vSigSemaphores.resize(SyncObjectCount); - m_vMemorySemaphores.resize(m_SwapChainImageCount); - - m_vFrameFences.resize(m_SwapChainImageCount); + m_vFrameFences.resize(SyncObjectCount); m_vImagesFences.resize(m_SwapChainImageCount, VK_NULL_HANDLE); VkSemaphoreCreateInfo CreateSemaphoreInfo{}; @@ -5333,11 +5369,10 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase FenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; FenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - for(size_t i = 0; i < m_SwapChainImageCount; i++) + for(size_t i = 0; i < SyncObjectCount; i++) { if(vkCreateSemaphore(m_VKDevice, &CreateSemaphoreInfo, nullptr, &m_vWaitSemaphores[i]) != VK_SUCCESS || vkCreateSemaphore(m_VKDevice, &CreateSemaphoreInfo, nullptr, &m_vSigSemaphores[i]) != VK_SUCCESS || - vkCreateSemaphore(m_VKDevice, &CreateSemaphoreInfo, nullptr, &m_vMemorySemaphores[i]) != VK_SUCCESS || vkCreateFence(m_VKDevice, &FenceInfo, nullptr, &m_vFrameFences[i]) != VK_SUCCESS) { SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating swap chain sync objects(fences, semaphores) failed."); @@ -5350,21 +5385,20 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase void DestroySyncObjects() { - for(size_t i = 0; i < m_SwapChainImageCount; i++) + for(size_t i = 0; i < m_vWaitSemaphores.size(); i++) { vkDestroySemaphore(m_VKDevice, m_vWaitSemaphores[i], nullptr); vkDestroySemaphore(m_VKDevice, m_vSigSemaphores[i], nullptr); - vkDestroySemaphore(m_VKDevice, m_vMemorySemaphores[i], nullptr); vkDestroyFence(m_VKDevice, m_vFrameFences[i], nullptr); } m_vWaitSemaphores.clear(); m_vSigSemaphores.clear(); - m_vMemorySemaphores.clear(); - m_vFrameFences.clear(); m_vImagesFences.clear(); + + m_CurFrameSyncObject = 0; } void DestroyBufferOfFrame(size_t ImageIndex, SFrameBuffers &Buffer) @@ -5418,7 +5452,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } template - void CleanupVulkan() + void CleanupVulkan(size_t SwapchainCount) { if(IsLastCleanup) { @@ -5456,7 +5490,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_vStreamedVertexBuffers.clear(); m_vStreamedUniformBuffers.clear(); - for(size_t i = 0; i < m_SwapChainImageCount; ++i) + for(size_t i = 0; i < SwapchainCount; ++i) { ClearFrameData(i); } @@ -5465,11 +5499,11 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_vvFrameDelayedTextureCleanup.clear(); m_vvFrameDelayedTextTexturesCleanup.clear(); - m_StagingBufferCache.DestroyFrameData(m_SwapChainImageCount); - m_StagingBufferCacheImage.DestroyFrameData(m_SwapChainImageCount); - m_VertexBufferCache.DestroyFrameData(m_SwapChainImageCount); + m_StagingBufferCache.DestroyFrameData(SwapchainCount); + m_StagingBufferCacheImage.DestroyFrameData(SwapchainCount); + m_VertexBufferCache.DestroyFrameData(SwapchainCount); for(auto &ImageBufferCache : m_ImageBufferCaches) - ImageBufferCache.second.DestroyFrameData(m_SwapChainImageCount); + ImageBufferCache.second.DestroyFrameData(SwapchainCount); if(IsLastCleanup) { @@ -5547,7 +5581,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(OldSwapChainImageCount != m_SwapChainImageCount) { - CleanupVulkan(); + CleanupVulkan(OldSwapChainImageCount); InitVulkan(); } @@ -5591,7 +5625,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } } - if(!SelectGPU(pRendererString, pVendorString, pVersionString)) + if(!SelectGpu(pRendererString, pVendorString, pVersionString)) return -1; if(!CreateLogicalDevice(vVKLayers)) @@ -6101,20 +6135,20 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase template int InitVulkan() { - if(!CreateDescriptorSetLayouts()) - return -1; + if(IsFirstInitialization) + { + if(!CreateDescriptorSetLayouts()) + return -1; - if(!CreateTextDescriptorSetLayout()) - return -1; + if(!CreateTextDescriptorSetLayout()) + return -1; - if(!CreateSpriteMultiUniformDescriptorSetLayout()) - return -1; + if(!CreateSpriteMultiUniformDescriptorSetLayout()) + return -1; - if(!CreateQuadUniformDescriptorSetLayout()) - return -1; + if(!CreateQuadUniformDescriptorSetLayout()) + return -1; - if(IsFirstInitialization) - { VkSwapchainKHR OldSwapChain = VK_NULL_HANDLE; if(InitVulkanSwapChain(OldSwapChain) != 0) return -1; @@ -6288,7 +6322,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase BufferMem = BufferOfFrame.m_BufferMem; Offset = BufferOfFrame.m_UsedSize; BufferOfFrame.m_UsedSize += DataSize; - pMem = (uint8_t *)BufferOfFrame.m_pMappedBufferData; + pMem = BufferOfFrame.m_pMappedBufferData; pBufferMem = &BufferOfFrame; break; } @@ -6321,7 +6355,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase BufferMem = StreamBufferMemory; pBufferMem = &NewStreamBuffer; - pMem = (uint8_t *)NewStreamBuffer.m_pMappedBufferData; + pMem = NewStreamBuffer.m_pMappedBufferData; Offset = NewStreamBuffer.m_OffsetInBuffer; NewStreamBuffer.m_UsedSize += DataSize; @@ -6329,7 +6363,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } { - mem_copy(pMem + Offset, pData, (size_t)DataSize); + mem_copy(pMem + Offset, pData, DataSize); } NewBuffer = Buffer; @@ -6378,7 +6412,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase { VkDeviceSize BufferDataSize = DataSize; - SMemoryBlock StagingBuffer; + SMemoryBlock StagingBuffer; if(!GetStagingBuffer(StagingBuffer, pData, DataSize)) return false; @@ -6545,8 +6579,9 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_MultiSamplingCount = (g_Config.m_GfxFsaaSamples & 0xFFFFFFFE); // ignore the uneven bit, only even multi sampling works - TGLBackendReadPresentedImageData &ReadPresentedImgDataFunc = *pCommand->m_pReadPresentedImageDataFunc; - ReadPresentedImgDataFunc = [this](uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) { return GetPresentedImageData(Width, Height, Format, vDstData); }; + *pCommand->m_pReadPresentedImageDataFunc = [this](uint32_t &Width, uint32_t &Height, CImageInfo::EImageFormat &Format, std::vector &vDstData) { + return GetPresentedImageData(Width, Height, Format, vDstData); + }; m_pWindow = pCommand->m_pWindow; @@ -6610,21 +6645,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase DestroyIndexBuffer(m_IndexBuffer, m_IndexBufferMemory); DestroyIndexBuffer(m_RenderIndexBuffer, m_RenderIndexBufferMemory); - CleanupVulkan(); - - return true; - } - - [[nodiscard]] bool Cmd_Texture_Update(const CCommandBuffer::SCommand_Texture_Update *pCommand) - { - size_t IndexTex = pCommand->m_Slot; - - void *pData = pCommand->m_pData; - - if(!UpdateTexture(IndexTex, VK_FORMAT_B8G8R8A8_UNORM, pData, pCommand->m_X, pCommand->m_Y, pCommand->m_Width, pCommand->m_Height)) - return false; - - free(pData); + CleanupVulkan(m_SwapChainImageCount); return true; } @@ -6646,12 +6667,10 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase int Slot = pCommand->m_Slot; int Width = pCommand->m_Width; int Height = pCommand->m_Height; - int Format = pCommand->m_Format; - int StoreFormat = pCommand->m_StoreFormat; int Flags = pCommand->m_Flags; - void *pData = pCommand->m_pData; + uint8_t *pData = pCommand->m_pData; - if(!CreateTextureCMD(Slot, Width, Height, TextureFormatToVulkanFormat(Format), TextureFormatToVulkanFormat(StoreFormat), Flags, pData)) + if(!CreateTextureCMD(Slot, Width, Height, VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM, Flags, pData)) return false; free(pData); @@ -6666,8 +6685,8 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase int Width = pCommand->m_Width; int Height = pCommand->m_Height; - void *pTmpData = pCommand->m_pTextData; - void *pTmpData2 = pCommand->m_pTextOutlineData; + uint8_t *pTmpData = pCommand->m_pTextData; + uint8_t *pTmpData2 = pCommand->m_pTextOutlineData; if(!CreateTextureCMD(Slot, Width, Height, VK_FORMAT_R8_UNORM, VK_FORMAT_R8_UNORM, CCommandBuffer::TEXFLAG_NOMIPMAPS, pTmpData)) return false; @@ -6701,8 +6720,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase [[nodiscard]] bool Cmd_TextTexture_Update(const CCommandBuffer::SCommand_TextTexture_Update *pCommand) { size_t IndexTex = pCommand->m_Slot; - - void *pData = pCommand->m_pData; + uint8_t *pData = pCommand->m_pData; if(!UpdateTexture(IndexTex, VK_FORMAT_R8_UNORM, pData, pCommand->m_X, pCommand->m_Y, pCommand->m_Width, pCommand->m_Height)) return false; @@ -6770,19 +6788,40 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return RenderStandard(ExecBuffer, pCommand->m_State, pCommand->m_PrimType, pCommand->m_pVertices, pCommand->m_PrimCount); } + [[nodiscard]] bool Cmd_ReadPixel(const CCommandBuffer::SCommand_TrySwapAndReadPixel *pCommand) + { + if(!*pCommand->m_pSwapped && !NextFrame()) + return false; + *pCommand->m_pSwapped = true; + + uint32_t Width; + uint32_t Height; + CImageInfo::EImageFormat Format; + if(GetPresentedImageDataImpl(Width, Height, Format, m_vReadPixelHelper, false, pCommand->m_Position)) + { + *pCommand->m_pColor = ColorRGBA(m_vReadPixelHelper[0] / 255.0f, m_vReadPixelHelper[1] / 255.0f, m_vReadPixelHelper[2] / 255.0f, 1.0f); + } + else + { + *pCommand->m_pColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + + return true; + } + [[nodiscard]] bool Cmd_Screenshot(const CCommandBuffer::SCommand_TrySwapAndScreenshot *pCommand) { - if(!NextFrame()) + if(!*pCommand->m_pSwapped && !NextFrame()) return false; *pCommand->m_pSwapped = true; uint32_t Width; uint32_t Height; CImageInfo::EImageFormat Format; - if(GetPresentedImageDataImpl(Width, Height, Format, m_vScreenshotHelper, false, true)) + if(GetPresentedImageDataImpl(Width, Height, Format, m_vScreenshotHelper, true, {})) { - size_t ImgSize = (size_t)Width * (size_t)Height * (size_t)4; - pCommand->m_pImage->m_pData = malloc(ImgSize); + const size_t ImgSize = (size_t)Width * (size_t)Height * CImageInfo::PixelSize(Format); + pCommand->m_pImage->m_pData = static_cast(malloc(ImgSize)); mem_copy(pCommand->m_pImage->m_pData, m_vScreenshotHelper.data(), ImgSize); } else @@ -6908,7 +6947,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase void *pUploadData = pCommand->m_pUploadData; VkDeviceSize DataSize = (VkDeviceSize)pCommand->m_DataSize; - SMemoryBlock StagingBuffer; + SMemoryBlock StagingBuffer; if(!GetStagingBuffer(StagingBuffer, pUploadData, DataSize)) return false; @@ -7453,7 +7492,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase [[nodiscard]] bool Cmd_WindowCreateNtf(const CCommandBuffer::SCommand_WindowCreateNtf *pCommand) { log_debug("vulkan", "creating new surface."); - m_pWindow = SDL_GetWindowFromID(pCommand->m_WindowID); + m_pWindow = SDL_GetWindowFromID(pCommand->m_WindowId); if(m_RenderingPaused) { #ifdef CONF_PLATFORM_ANDROID @@ -7490,7 +7529,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase [[nodiscard]] bool Cmd_PreInit(const CCommandProcessorFragment_GLBase::SCommand_PreInit *pCommand) { - m_pGPUList = pCommand->m_pGPUList; + m_pGpuList = pCommand->m_pGpuList; if(InitVulkanSDL(pCommand->m_pWindow, pCommand->m_Width, pCommand->m_Height, pCommand->m_pRendererString, pCommand->m_pVendorString, pCommand->m_pVersionString) != 0) { m_VKInstance = VK_NULL_HANDLE; diff --git a/src/engine/client/backend_sdl.cpp b/src/engine/client/backend_sdl.cpp index ab5c402d95..cb42a1cfcf 100644 --- a/src/engine/client/backend_sdl.cpp +++ b/src/engine/client/backend_sdl.cpp @@ -226,7 +226,7 @@ void CCommandProcessorFragment_SDL::Cmd_VSync(const CCommandBuffer::SCommand_VSy void CCommandProcessorFragment_SDL::Cmd_WindowCreateNtf(const CCommandBuffer::SCommand_WindowCreateNtf *pCommand) { - m_pWindow = SDL_GetWindowFromID(pCommand->m_WindowID); + m_pWindow = SDL_GetWindowFromID(pCommand->m_WindowId); // Android destroys windows when they are not visible, so we get the new one and work with that // The graphic context does not need to be recreated, just unbound see @see SCommand_WindowDestroyNtf #ifdef CONF_PLATFORM_ANDROID @@ -423,7 +423,7 @@ const SGfxErrorContainer &CCommandProcessor_SDL_GL::GetError() const void CCommandProcessor_SDL_GL::ErroneousCleanup() { - return m_pGLBackend->ErroneousCleanup(); + m_pGLBackend->ErroneousCleanup(); } const SGfxWarningContainer &CCommandProcessor_SDL_GL::GetWarning() const @@ -443,7 +443,9 @@ static bool BackendInitGlew(EBackendType BackendType, int &GlewMajor, int &GlewM #ifdef CONF_GLEW_HAS_CONTEXT_INIT if(GLEW_OK != glewContextInit()) #else - if(GLEW_OK != glewInit()) + GLenum InitResult = glewInit(); + const char *pVideoDriver = SDL_GetCurrentVideoDriver(); + if(GLEW_OK != InitResult && pVideoDriver && !str_comp(pVideoDriver, "wayland") && GLEW_ERROR_NO_GLX_DISPLAY != InitResult) #endif return false; @@ -900,18 +902,17 @@ static void DisplayToVideoMode(CVideoMode *pVMode, SDL_DisplayMode *pMode, int H pVMode->m_Format = pMode->format; } -void CGraphicsBackend_SDL_GL::GetVideoModes(CVideoMode *pModes, int MaxModes, int *pNumModes, int HiDPIScale, int MaxWindowWidth, int MaxWindowHeight, int ScreenID) +void CGraphicsBackend_SDL_GL::GetVideoModes(CVideoMode *pModes, int MaxModes, int *pNumModes, int HiDPIScale, int MaxWindowWidth, int MaxWindowHeight, int ScreenId) { SDL_DisplayMode DesktopMode; - int maxModes = SDL_GetNumDisplayModes(ScreenID); - int numModes = 0; + int MaxModesAvailable = SDL_GetNumDisplayModes(ScreenId); // Only collect fullscreen modes when requested, that makes sure in windowed mode no refresh rates are shown that aren't supported without // fullscreen anyway(except fullscreen desktop) bool IsFullscreenDestkop = m_pWindow != NULL && (((SDL_GetWindowFlags(m_pWindow) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN_DESKTOP) || g_Config.m_GfxFullscreen == 3); bool CollectFullscreenModes = m_pWindow == NULL || ((SDL_GetWindowFlags(m_pWindow) & SDL_WINDOW_FULLSCREEN) != 0 && !IsFullscreenDestkop); - if(SDL_GetDesktopDisplayMode(ScreenID, &DesktopMode) < 0) + if(SDL_GetDesktopDisplayMode(ScreenId, &DesktopMode) < 0) { dbg_msg("gfx", "unable to get display mode: %s", SDL_GetError()); } @@ -919,65 +920,66 @@ void CGraphicsBackend_SDL_GL::GetVideoModes(CVideoMode *pModes, int MaxModes, in constexpr int ModeCount = 256; SDL_DisplayMode aModes[ModeCount]; int NumModes = 0; - for(int i = 0; i < maxModes && NumModes < ModeCount; i++) + for(int i = 0; i < MaxModesAvailable && NumModes < ModeCount; i++) { - SDL_DisplayMode mode; - if(SDL_GetDisplayMode(ScreenID, i, &mode) < 0) + SDL_DisplayMode Mode; + if(SDL_GetDisplayMode(ScreenId, i, &Mode) < 0) { dbg_msg("gfx", "unable to get display mode: %s", SDL_GetError()); continue; } - aModes[NumModes] = mode; + aModes[NumModes] = Mode; ++NumModes; } - auto &&ModeInsert = [&](SDL_DisplayMode &mode) { - if(numModes < MaxModes) + int NumModesInserted = 0; + auto &&ModeInsert = [&](SDL_DisplayMode &Mode) { + if(NumModesInserted < MaxModes) { // if last mode was equal, ignore this one --- in fullscreen this can really only happen if the screen // supports different color modes // in non fullscren these are the modes that show different refresh rate, but are basically the same - if(numModes > 0 && pModes[numModes - 1].m_WindowWidth == mode.w && pModes[numModes - 1].m_WindowHeight == mode.h && (pModes[numModes - 1].m_RefreshRate == mode.refresh_rate || (mode.refresh_rate != DesktopMode.refresh_rate && !CollectFullscreenModes))) + if(NumModesInserted > 0 && pModes[NumModesInserted - 1].m_WindowWidth == Mode.w && pModes[NumModesInserted - 1].m_WindowHeight == Mode.h && (pModes[NumModesInserted - 1].m_RefreshRate == Mode.refresh_rate || (Mode.refresh_rate != DesktopMode.refresh_rate && !CollectFullscreenModes))) return; - DisplayToVideoMode(&pModes[numModes], &mode, HiDPIScale, !CollectFullscreenModes ? DesktopMode.refresh_rate : mode.refresh_rate); - numModes++; + DisplayToVideoMode(&pModes[NumModesInserted], &Mode, HiDPIScale, !CollectFullscreenModes ? DesktopMode.refresh_rate : Mode.refresh_rate); + NumModesInserted++; } }; for(int i = 0; i < NumModes; i++) { - SDL_DisplayMode &mode = aModes[i]; + SDL_DisplayMode &Mode = aModes[i]; - if(mode.w > MaxWindowWidth || mode.h > MaxWindowHeight) + if(Mode.w > MaxWindowWidth || Mode.h > MaxWindowHeight) continue; - ModeInsert(mode); + ModeInsert(Mode); if(IsFullscreenDestkop) break; - if(numModes >= MaxModes) + if(NumModesInserted >= MaxModes) break; } - *pNumModes = numModes; + *pNumModes = NumModesInserted; } -void CGraphicsBackend_SDL_GL::GetCurrentVideoMode(CVideoMode &CurMode, int HiDPIScale, int MaxWindowWidth, int MaxWindowHeight, int ScreenID) +void CGraphicsBackend_SDL_GL::GetCurrentVideoMode(CVideoMode &CurMode, int HiDPIScale, int MaxWindowWidth, int MaxWindowHeight, int ScreenId) { - SDL_DisplayMode DPMode; + SDL_DisplayMode DpMode; // if "real" fullscreen, obtain the video mode for that if((SDL_GetWindowFlags(m_pWindow) & SDL_WINDOW_FULLSCREEN_DESKTOP) == SDL_WINDOW_FULLSCREEN) { - if(SDL_GetCurrentDisplayMode(ScreenID, &DPMode)) + if(SDL_GetCurrentDisplayMode(ScreenId, &DpMode)) { dbg_msg("gfx", "unable to get display mode: %s", SDL_GetError()); } } else { - if(SDL_GetDesktopDisplayMode(ScreenID, &DPMode) < 0) + if(SDL_GetDesktopDisplayMode(ScreenId, &DpMode) < 0) { dbg_msg("gfx", "unable to get display mode: %s", SDL_GetError()); } @@ -986,11 +988,11 @@ void CGraphicsBackend_SDL_GL::GetCurrentVideoMode(CVideoMode &CurMode, int HiDPI int Width = 0; int Height = 0; SDL_GL_GetDrawableSize(m_pWindow, &Width, &Height); - DPMode.w = Width; - DPMode.h = Height; + DpMode.w = Width; + DpMode.h = Height; } } - DisplayToVideoMode(&CurMode, &DPMode, HiDPIScale, DPMode.refresh_rate); + DisplayToVideoMode(&CurMode, &DpMode, HiDPIScale, DpMode.refresh_rate); } CGraphicsBackend_SDL_GL::CGraphicsBackend_SDL_GL(TTranslateFunc &&TranslateFunc) : @@ -1019,6 +1021,13 @@ int CGraphicsBackend_SDL_GL::Init(const char *pName, int *pScreen, int *pWidth, SDL_GetVersion(&Linked); dbg_msg("sdl", "SDL version %d.%d.%d (compiled = %d.%d.%d)", Linked.major, Linked.minor, Linked.patch, Compiled.major, Compiled.minor, Compiled.patch); + +#if CONF_PLATFORM_LINUX && SDL_VERSION_ATLEAST(2, 0, 22) + // needed to workaround SDL from forcing exclusively X11 if linking against the GLX flavour of GLEW instead of the EGL one + // w/o this on Wayland systems (no XWayland support) SDL's Video subsystem will fail to load (starting from SDL2.30+) + if(Linked.major == 2 && Linked.minor >= 30) + SDL_SetHint(SDL_HINT_VIDEODRIVER, "x11,wayland"); +#endif } if(!SDL_WasInit(SDL_INIT_VIDEO)) @@ -1275,7 +1284,7 @@ int CGraphicsBackend_SDL_GL::Init(const char *pName, int *pScreen, int *pWidth, CmdPre.m_pVendorString = m_aVendorString; CmdPre.m_pVersionString = m_aVersionString; CmdPre.m_pRendererString = m_aRendererString; - CmdPre.m_pGPUList = &m_GPUList; + CmdPre.m_pGpuList = &m_GpuList; CmdBuffer.AddCommandUnsafe(CmdPre); RunBufferSingleThreadedUnsafe(&CmdBuffer); CmdBuffer.Reset(); @@ -1299,7 +1308,7 @@ int CGraphicsBackend_SDL_GL::Init(const char *pName, int *pScreen, int *pWidth, CmdGL.m_pBufferMemoryUsage = &m_BufferMemoryUsage; CmdGL.m_pStreamMemoryUsage = &m_StreamMemoryUsage; CmdGL.m_pStagingMemoryUsage = &m_StagingMemoryUsage; - CmdGL.m_pGPUList = &m_GPUList; + CmdGL.m_pGpuList = &m_GpuList; CmdGL.m_pReadPresentedImageDataFunc = &m_ReadPresentedImageDataFunc; CmdGL.m_pStorage = pStorage; CmdGL.m_pCapabilities = &m_Capabilites; @@ -1444,9 +1453,9 @@ uint64_t CGraphicsBackend_SDL_GL::StagingMemoryUsage() const return m_StagingMemoryUsage; } -const TTWGraphicsGPUList &CGraphicsBackend_SDL_GL::GetGPUs() const +const TTwGraphicsGpuList &CGraphicsBackend_SDL_GL::GetGpus() const { - return m_GPUList; + return m_GpuList; } void CGraphicsBackend_SDL_GL::Minimize() @@ -1459,7 +1468,7 @@ void CGraphicsBackend_SDL_GL::Maximize() // TODO: SDL } -void CGraphicsBackend_SDL_GL::SetWindowParams(int FullscreenMode, bool IsBorderless, bool AllowResizing) +void CGraphicsBackend_SDL_GL::SetWindowParams(int FullscreenMode, bool IsBorderless) { if(FullscreenMode > 0) { @@ -1488,14 +1497,14 @@ void CGraphicsBackend_SDL_GL::SetWindowParams(int FullscreenMode, bool IsBorderl SDL_SetWindowFullscreen(m_pWindow, 0); SDL_SetWindowBordered(m_pWindow, SDL_TRUE); SDL_SetWindowResizable(m_pWindow, SDL_FALSE); - SDL_DisplayMode DPMode; - if(SDL_GetDesktopDisplayMode(g_Config.m_GfxScreen, &DPMode) < 0) + SDL_DisplayMode DpMode; + if(SDL_GetDesktopDisplayMode(g_Config.m_GfxScreen, &DpMode) < 0) { dbg_msg("gfx", "unable to get display mode: %s", SDL_GetError()); } else { - ResizeWindow(DPMode.w, DPMode.h, DPMode.refresh_rate); + ResizeWindow(DpMode.w, DpMode.h, DpMode.refresh_rate); SDL_SetWindowPosition(m_pWindow, SDL_WINDOWPOS_CENTERED_DISPLAY(g_Config.m_GfxScreen), SDL_WINDOWPOS_CENTERED_DISPLAY(g_Config.m_GfxScreen)); } } @@ -1520,6 +1529,8 @@ bool CGraphicsBackend_SDL_GL::SetWindowScreen(int Index) { return false; } + // Todo SDL: remove this when fixed (changing screen when in fullscreen is bugged) + SDL_SetWindowBordered(m_pWindow, SDL_TRUE); //fixing primary monitor goes black when switch screen (borderless OpenGL) SDL_SetWindowPosition(m_pWindow, SDL_WINDOWPOS_CENTERED_DISPLAY(Index), @@ -1537,6 +1548,7 @@ bool CGraphicsBackend_SDL_GL::UpdateDisplayMode(int Index) return false; } + g_Config.m_GfxScreen = Index; g_Config.m_GfxDesktopWidth = DisplayMode.w; g_Config.m_GfxDesktopHeight = DisplayMode.h; @@ -1574,7 +1586,7 @@ bool CGraphicsBackend_SDL_GL::ResizeWindow(int w, int h, int RefreshRate) { #ifdef CONF_FAMILY_WINDOWS // in windows make the window windowed mode first, this prevents strange window glitches (other games probably do something similar) - SetWindowParams(0, true, true); + SetWindowParams(0, true); #endif SDL_DisplayMode SetMode = {}; SDL_DisplayMode ClosestMode = {}; @@ -1586,7 +1598,7 @@ bool CGraphicsBackend_SDL_GL::ResizeWindow(int w, int h, int RefreshRate) #ifdef CONF_FAMILY_WINDOWS // now change it back to fullscreen, this will restore the above set state, bcs SDL saves fullscreen modes apart from other video modes (as of SDL 2.0.16) // see implementation of SDL_SetWindowDisplayMode - SetWindowParams(1, false, true); + SetWindowParams(1, false); #endif return true; } @@ -1618,13 +1630,13 @@ void CGraphicsBackend_SDL_GL::NotifyWindow() #endif } -void CGraphicsBackend_SDL_GL::WindowDestroyNtf(uint32_t WindowID) +void CGraphicsBackend_SDL_GL::WindowDestroyNtf(uint32_t WindowId) { } -void CGraphicsBackend_SDL_GL::WindowCreateNtf(uint32_t WindowID) +void CGraphicsBackend_SDL_GL::WindowCreateNtf(uint32_t WindowId) { - m_pWindow = SDL_GetWindowFromID(WindowID); + m_pWindow = SDL_GetWindowFromID(WindowId); } TGLBackendReadPresentedImageData &CGraphicsBackend_SDL_GL::GetReadPresentedImageDataFuncUnsafe() diff --git a/src/engine/client/backend_sdl.h b/src/engine/client/backend_sdl.h index 952ba052c9..de04f7e593 100644 --- a/src/engine/client/backend_sdl.h +++ b/src/engine/client/backend_sdl.h @@ -198,7 +198,7 @@ class CCommandProcessor_SDL_GL : public CGraphicsBackend_Threaded::ICommandProce void HandleWarning(); }; -static constexpr size_t gs_GPUInfoStringSize = 256; +static constexpr size_t gs_GpuInfoStringSize = 256; // graphics backend implemented with SDL and the graphics library @see EBackendType class CGraphicsBackend_SDL_GL : public CGraphicsBackend_Threaded @@ -211,7 +211,7 @@ class CGraphicsBackend_SDL_GL : public CGraphicsBackend_Threaded std::atomic m_StreamMemoryUsage{0}; std::atomic m_StagingMemoryUsage{0}; - TTWGraphicsGPUList m_GPUList; + TTwGraphicsGpuList m_GpuList; TGLBackendReadPresentedImageData m_ReadPresentedImageDataFunc; @@ -219,9 +219,9 @@ class CGraphicsBackend_SDL_GL : public CGraphicsBackend_Threaded SBackendCapabilites m_Capabilites; - char m_aVendorString[gs_GPUInfoStringSize] = {}; - char m_aVersionString[gs_GPUInfoStringSize] = {}; - char m_aRendererString[gs_GPUInfoStringSize] = {}; + char m_aVendorString[gs_GpuInfoStringSize] = {}; + char m_aVersionString[gs_GpuInfoStringSize] = {}; + char m_aRendererString[gs_GpuInfoStringSize] = {}; EBackendType m_BackendType = BACKEND_TYPE_AUTO; @@ -240,17 +240,17 @@ class CGraphicsBackend_SDL_GL : public CGraphicsBackend_Threaded uint64_t StreamedMemoryUsage() const override; uint64_t StagingMemoryUsage() const override; - const TTWGraphicsGPUList &GetGPUs() const override; + const TTwGraphicsGpuList &GetGpus() const override; int GetNumScreens() const override { return m_NumScreens; } const char *GetScreenName(int Screen) const override; - void GetVideoModes(CVideoMode *pModes, int MaxModes, int *pNumModes, int HiDPIScale, int MaxWindowWidth, int MaxWindowHeight, int ScreenID) override; - void GetCurrentVideoMode(CVideoMode &CurMode, int HiDPIScale, int MaxWindowWidth, int MaxWindowHeight, int ScreenID) override; + void GetVideoModes(CVideoMode *pModes, int MaxModes, int *pNumModes, int HiDPIScale, int MaxWindowWidth, int MaxWindowHeight, int ScreenId) override; + void GetCurrentVideoMode(CVideoMode &CurMode, int HiDPIScale, int MaxWindowWidth, int MaxWindowHeight, int ScreenId) override; void Minimize() override; void Maximize() override; - void SetWindowParams(int FullscreenMode, bool IsBorderless, bool AllowResizing) override; + void SetWindowParams(int FullscreenMode, bool IsBorderless) override; bool SetWindowScreen(int Index) override; bool UpdateDisplayMode(int Index) override; int GetWindowScreen() override; @@ -261,8 +261,8 @@ class CGraphicsBackend_SDL_GL : public CGraphicsBackend_Threaded void GetViewportSize(int &w, int &h) override; void NotifyWindow() override; - void WindowDestroyNtf(uint32_t WindowID) override; - void WindowCreateNtf(uint32_t WindowID) override; + void WindowDestroyNtf(uint32_t WindowId) override; + void WindowCreateNtf(uint32_t WindowId) override; bool GetDriverVersion(EGraphicsDriverAgeType DriverAgeType, int &Major, int &Minor, int &Patch, const char *&pName, EBackendType BackendType) override; bool IsConfigModernAPI() override { return IsModernAPI(m_BackendType); } diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index ef40081005..731227b151 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -1,10 +1,9 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ -#define _WIN32_WINNT 0x0501 - #include #include +#include #include #include #include @@ -38,12 +37,18 @@ #include #include #include +#include #include #include #include #include #include +#include +#include + +#include + #include #include @@ -57,13 +62,17 @@ #include "video.h" #endif +#if defined(CONF_PLATFORM_ANDROID) +#include +#endif + #include "SDL.h" #ifdef main #undef main #endif #include -#include +#include #include #include #include @@ -75,13 +84,15 @@ static const ColorRGBA gs_ClientNetworkPrintColor{0.7f, 1, 0.7f, 1.0f}; static const ColorRGBA gs_ClientNetworkErrPrintColor{1.0f, 0.25f, 0.25f, 1.0f}; CClient::CClient() : - m_DemoPlayer(&m_SnapshotDelta, true, [&]() { UpdateDemoIntraTimers(); }) + m_DemoPlayer(&m_SnapshotDelta, true, [&]() { UpdateDemoIntraTimers(); }), + m_InputtimeMarginGraph(128), + m_GametimeMarginGraph(128), + m_FpsGraph(4096) { m_StateStartTime = time_get(); for(auto &DemoRecorder : m_aDemoRecorder) DemoRecorder = CDemoRecorder(&m_SnapshotDelta); m_LastRenderTime = time_get(); - IStorage::FormatTmpPath(m_aDDNetInfoTmp, sizeof(m_aDDNetInfoTmp), DDNET_INFO_FILE); mem_zero(m_aInputs, sizeof(m_aInputs)); mem_zero(m_aapSnapshots, sizeof(m_aapSnapshots)); for(auto &SnapshotStorage : m_aSnapshotStorage) @@ -92,20 +103,64 @@ CClient::CClient() : for(auto &GameTime : m_aGameTime) GameTime.Init(0); m_PredictedTime.Init(0); + + m_Sixup = false; } // ----- send functions ----- -static inline bool RepackMsg(const CMsgPacker *pMsg, CPacker &Packer) +static inline bool RepackMsg(const CMsgPacker *pMsg, CPacker &Packer, bool Sixup) { + int MsgId = pMsg->m_MsgId; Packer.Reset(); - if(pMsg->m_MsgID < OFFSET_UUID) + + if(Sixup && !pMsg->m_NoTranslate) + { + if(pMsg->m_System) + { + if(MsgId >= OFFSET_UUID) + ; + else if(MsgId == NETMSG_INFO || MsgId == NETMSG_REQUEST_MAP_DATA) + ; + else if(MsgId == NETMSG_READY) + MsgId = protocol7::NETMSG_READY; + else if(MsgId == NETMSG_RCON_CMD) + MsgId = protocol7::NETMSG_RCON_CMD; + else if(MsgId == NETMSG_ENTERGAME) + MsgId = protocol7::NETMSG_ENTERGAME; + else if(MsgId == NETMSG_INPUT) + MsgId = protocol7::NETMSG_INPUT; + else if(MsgId == NETMSG_RCON_AUTH) + MsgId = protocol7::NETMSG_RCON_AUTH; + else if(MsgId == NETMSGTYPE_CL_SETTEAM) + MsgId = protocol7::NETMSGTYPE_CL_SETTEAM; + else if(MsgId == NETMSGTYPE_CL_VOTE) + MsgId = protocol7::NETMSGTYPE_CL_VOTE; + else if(MsgId == NETMSG_PING) + MsgId = protocol7::NETMSG_PING; + else + { + dbg_msg("net", "0.7 DROP send sys %d", MsgId); + return true; + } + } + else + { + if(MsgId >= 0 && MsgId < OFFSET_UUID) + MsgId = Msg_SixToSeven(MsgId); + + if(MsgId < 0) + return true; + } + } + + if(pMsg->m_MsgId < OFFSET_UUID) { - Packer.AddInt((pMsg->m_MsgID << 1) | (pMsg->m_System ? 1 : 0)); + Packer.AddInt((MsgId << 1) | (pMsg->m_System ? 1 : 0)); } else { Packer.AddInt(pMsg->m_System ? 1 : 0); // NETMSG_EX, NETMSGTYPE_EX - g_UuidManager.PackUuid(pMsg->m_MsgID, &Packer); + g_UuidManager.PackUuid(pMsg->m_MsgId, &Packer); } Packer.AddRaw(pMsg->Data(), pMsg->Size()); @@ -121,11 +176,11 @@ int CClient::SendMsg(int Conn, CMsgPacker *pMsg, int Flags) // repack message (inefficient) CPacker Pack; - if(RepackMsg(pMsg, Pack)) + if(RepackMsg(pMsg, Pack, IsSixup())) return 0; mem_zero(&Packet, sizeof(CNetChunk)); - Packet.m_ClientID = 0; + Packet.m_ClientId = 0; Packet.m_pData = Pack.Data(); Packet.m_DataSize = Pack.Size(); @@ -154,30 +209,24 @@ int CClient::SendMsgActive(CMsgPacker *pMsg, int Flags) return SendMsg(g_Config.m_ClDummy, pMsg, Flags); } -void CClient::SendStA(bool Dummy) -{ - CMsgPacker Msg(NETMSG_IAMSTA, true); - Msg.AddInt(GAME_STA_VERSION); - Msg.AddString( - "StA " STA_VERSION - " (Teeworlds " GAME_VERSION - ", DDNet " GAME_VERSION - ", built on " STA_BUILD_DATE - ")", - 0); - Msg.AddInt(Dummy); - SendMsg(Dummy, &Msg, MSGFLAG_VITAL); -} - void CClient::SendInfo(int Conn) { - SendStA(false); CMsgPacker MsgVer(NETMSG_CLIENTVER, true); - MsgVer.AddRaw(&m_ConnectionID, sizeof(m_ConnectionID)); + MsgVer.AddRaw(&m_ConnectionId, sizeof(m_ConnectionId)); MsgVer.AddInt(GameClient()->DDNetVersion()); MsgVer.AddString(GameClient()->DDNetVersionStr()); SendMsg(Conn, &MsgVer, MSGFLAG_VITAL); + if(IsSixup()) + { + CMsgPacker Msg(NETMSG_INFO, true); + Msg.AddString(GAME_NETVERSION7, 128); + Msg.AddString(Config()->m_Password); + Msg.AddInt(GameClient()->ClientVersion7()); + SendMsg(Conn, &Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH); + return; + } + CMsgPacker Msg(NETMSG_INFO, true); Msg.AddString(GameClient()->NetVersion()); Msg.AddString(m_aPassword); @@ -198,15 +247,19 @@ void CClient::SendReady(int Conn) void CClient::SendMapRequest() { - if(m_MapdownloadFileTemp) + dbg_assert(!m_MapdownloadFileTemp, "Map download already in progress"); + m_MapdownloadFileTemp = Storage()->OpenFile(m_aMapdownloadFilenameTemp, IOFLAG_WRITE, IStorage::TYPE_SAVE); + if(IsSixup()) { - io_close(m_MapdownloadFileTemp); - Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE); + CMsgPacker MsgP(protocol7::NETMSG_REQUEST_MAP_DATA, true, true); + SendMsg(CONN_MAIN, &MsgP, MSGFLAG_VITAL | MSGFLAG_FLUSH); + } + else + { + CMsgPacker Msg(NETMSG_REQUEST_MAP_DATA, true); + Msg.AddInt(m_MapdownloadChunk); + SendMsg(CONN_MAIN, &Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH); } - m_MapdownloadFileTemp = Storage()->OpenFile(m_aMapdownloadFilenameTemp, IOFLAG_WRITE, IStorage::TYPE_SAVE); - CMsgPacker Msg(NETMSG_REQUEST_MAP_DATA, true); - Msg.AddInt(m_MapdownloadChunk); - SendMsg(CONN_MAIN, &Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH); } void CClient::RconAuth(const char *pName, const char *pPassword) @@ -219,6 +272,14 @@ void CClient::RconAuth(const char *pName, const char *pPassword) if(pPassword != m_aRconPassword) str_copy(m_aRconPassword, pPassword); + if(IsSixup()) + { + CMsgPacker Msg7(protocol7::NETMSG_RCON_AUTH, true, true); + Msg7.AddString(pPassword); + SendMsgActive(&Msg7, MSGFLAG_VITAL); + return; + } + CMsgPacker Msg(NETMSG_RCON_AUTH, true); Msg.AddString(pName); Msg.AddString(pPassword); @@ -233,22 +294,19 @@ void CClient::Rcon(const char *pCmd) SendMsgActive(&Msg, MSGFLAG_VITAL); } -bool CClient::ConnectionProblems() const +float CClient::GotRconCommandsPercentage() const { - return m_aNetClient[g_Config.m_ClDummy].GotProblems(MaxLatencyTicks() * time_freq() / GameTickSpeed()) != 0; + if(m_ExpectedRconCommands < 1) + return -1.0f; + if(m_GotRconCommands > m_ExpectedRconCommands) + return -1.0f; + + return (float)m_GotRconCommands / (float)m_ExpectedRconCommands; } -void CClient::DirectInput(int *pInput, int Size) +bool CClient::ConnectionProblems() const { - CMsgPacker Msg(NETMSG_INPUT, true); - Msg.AddInt(m_aAckGameTick[g_Config.m_ClDummy]); - Msg.AddInt(m_aPredTick[g_Config.m_ClDummy]); - Msg.AddInt(Size); - - for(int i = 0; i < Size / 4; i++) - Msg.AddInt(pInput[i]); - - SendMsgActive(&Msg, 0); + return m_aNetClient[g_Config.m_ClDummy].GotProblems(MaxLatencyTicks() * time_freq() / GameTickSpeed()) != 0; } void CClient::SendInput() @@ -262,7 +320,7 @@ void CClient::SendInput() // fetch input for(int Dummy = 0; Dummy < NUM_DUMMIES; Dummy++) { - if(!m_DummyConnected && Dummy != 0) + if(!DummyConnected() && Dummy != 0) { break; } @@ -284,7 +342,18 @@ void CClient::SendInput() // pack it for(int k = 0; k < Size / 4; k++) - Msg.AddInt(m_aInputs[i][m_aCurrentInput[i]].m_aData[k]); + { + static const int FlagsOffset = offsetof(CNetObj_PlayerInput, m_PlayerFlags) / sizeof(int); + if(k == FlagsOffset && IsSixup()) + { + int PlayerFlags = m_aInputs[i][m_aCurrentInput[i]].m_aData[k]; + Msg.AddInt(PlayerFlags_SixToSeven(PlayerFlags)); + } + else + { + Msg.AddInt(m_aInputs[i][m_aCurrentInput[i]].m_aData[k]); + } + } m_aCurrentInput[i]++; m_aCurrentInput[i] %= 200; @@ -311,7 +380,7 @@ int *CClient::GetInput(int Tick, int IsDummy) const const int d = IsDummy ^ g_Config.m_ClDummy; for(int i = 0; i < 200; i++) { - if(m_aInputs[d][i].m_Tick <= Tick && (Best == -1 || m_aInputs[d][Best].m_Tick < m_aInputs[d][i].m_Tick)) + if(m_aInputs[d][i].m_Tick != -1 && m_aInputs[d][i].m_Tick <= Tick && (Best == -1 || m_aInputs[d][Best].m_Tick < m_aInputs[d][i].m_Tick)) Best = i; } @@ -321,44 +390,44 @@ int *CClient::GetInput(int Tick, int IsDummy) const } // ------ state handling ----- -void CClient::SetState(EClientState s) +void CClient::SetState(EClientState State) { if(m_State == IClient::STATE_QUITTING || m_State == IClient::STATE_RESTARTING) return; + if(m_State == State) + return; - int Old = m_State; if(g_Config.m_Debug) { - char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "state change. last=%d current=%d", m_State, s); + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "state change. last=%d current=%d", m_State, State); m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", aBuf); } - m_State = s; - if(Old != s) - { - m_StateStartTime = time_get(); - GameClient()->OnStateChange(m_State, Old); - if(s == IClient::STATE_OFFLINE && m_ReconnectTime == 0) - { - if(g_Config.m_ClReconnectFull > 0 && (str_find_nocase(ErrorString(), "full") || str_find_nocase(ErrorString(), "reserved"))) - m_ReconnectTime = time_get() + time_freq() * g_Config.m_ClReconnectFull; - else if(g_Config.m_ClReconnectTimeout > 0 && (str_find_nocase(ErrorString(), "Timeout") || str_find_nocase(ErrorString(), "Too weak connection"))) - m_ReconnectTime = time_get() + time_freq() * g_Config.m_ClReconnectTimeout; - } + const EClientState OldState = m_State; + m_State = State; - if(s == IClient::STATE_ONLINE) - { - const bool AnnounceAddr = m_ServerBrowser.IsRegistered(ServerAddress()); - Discord()->Start(); - Discord()->SetGameInfo(ServerAddress(), m_aCurrentMap, AnnounceAddr, "Playing", "greenline", ""); - Steam()->SetGameInfo(ServerAddress(), m_aCurrentMap, AnnounceAddr); - } - else if(Old == IClient::STATE_ONLINE) - { - Discord()->ClearGameInfo(); - Steam()->ClearGameInfo(); - } + m_StateStartTime = time_get(); + GameClient()->OnStateChange(m_State, OldState); + + if(State == IClient::STATE_OFFLINE && m_ReconnectTime == 0) + { + if(g_Config.m_ClReconnectFull > 0 && (str_find_nocase(ErrorString(), "full") || str_find_nocase(ErrorString(), "reserved"))) + m_ReconnectTime = time_get() + time_freq() * g_Config.m_ClReconnectFull; + else if(g_Config.m_ClReconnectTimeout > 0 && (str_find_nocase(ErrorString(), "Timeout") || str_find_nocase(ErrorString(), "Too weak connection"))) + m_ReconnectTime = time_get() + time_freq() * g_Config.m_ClReconnectTimeout; + } + + if(State == IClient::STATE_ONLINE) + { + const bool AnnounceAddr = m_ServerBrowser.IsRegistered(ServerAddress()); + Discord()->SetGameInfo(ServerAddress(), m_aCurrentMap, AnnounceAddr); + Steam()->SetGameInfo(ServerAddress(), m_aCurrentMap, AnnounceAddr); + } + else if(OldState == IClient::STATE_ONLINE) + { + Discord()->ClearGameInfo(); + Steam()->ClearGameInfo(); } } @@ -373,22 +442,32 @@ void CClient::OnEnterGame(bool Dummy) m_aCurrentInput[Dummy] = 0; // reset snapshots - m_aapSnapshots[Dummy][SNAP_CURRENT] = 0; - m_aapSnapshots[Dummy][SNAP_PREV] = 0; + m_aapSnapshots[Dummy][SNAP_CURRENT] = nullptr; + m_aapSnapshots[Dummy][SNAP_PREV] = nullptr; m_aSnapshotStorage[Dummy].PurgeAll(); - // Also make gameclient aware that snapshots have been purged - GameClient()->InvalidateSnapshot(); m_aReceivedSnapshots[Dummy] = 0; m_aSnapshotParts[Dummy] = 0; - m_aPredTick[Dummy] = 0; + m_aSnapshotIncomingDataSize[Dummy] = 0; + m_SnapCrcErrors = 0; + // Also make gameclient aware that snapshots have been purged + GameClient()->InvalidateSnapshot(); + + // reset times m_aAckGameTick[Dummy] = -1; m_aCurrentRecvTick[Dummy] = 0; - m_aCurGameTick[Dummy] = 0; m_aPrevGameTick[Dummy] = 0; + m_aCurGameTick[Dummy] = 0; + m_aGameIntraTick[Dummy] = 0.0f; + m_aGameTickTime[Dummy] = 0.0f; + m_aGameIntraTickSincePrev[Dummy] = 0.0f; + m_aPredTick[Dummy] = 0; + m_aPredIntraTick[Dummy] = 0.0f; + m_aGameTime[Dummy].Init(0); + m_PredictedTime.Init(0); - if(Dummy == 0) + if(!Dummy) { - m_LastDummyConnectTime = 0; + m_LastDummyConnectTime = 0.0f; } GameClient()->OnEnterGame(); @@ -455,28 +534,31 @@ void CClient::GenerateTimeoutCodes(const NETADDR *pAddrs, int NumAddrs) void CClient::Connect(const char *pAddress, const char *pPassword) { + // Disconnect will not change the state if we are already quitting/restarting + if(m_State == IClient::STATE_QUITTING || m_State == IClient::STATE_RESTARTING) + return; Disconnect(); + dbg_assert(m_State == IClient::STATE_OFFLINE, "Disconnect must ensure that client is offline"); - m_ConnectionID = RandomUuid(); if(pAddress != m_aConnectAddressStr) str_copy(m_aConnectAddressStr, pAddress); char aMsg[512]; - str_format(aMsg, sizeof(aMsg), "Joining... '%s'", m_aConnectAddressStr); + str_format(aMsg, sizeof(aMsg), "connecting to '%s'", m_aConnectAddressStr); m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aMsg, gs_ClientNetworkPrintColor); - ServerInfoRequest(); - int NumConnectAddrs = 0; NETADDR aConnectAddrs[MAX_SERVER_ADDRESSES]; mem_zero(aConnectAddrs, sizeof(aConnectAddrs)); const char *pNextAddr = pAddress; char aBuffer[128]; + bool OnlySixup = true; while((pNextAddr = str_next_token(pNextAddr, ",", aBuffer, sizeof(aBuffer)))) { NETADDR NextAddr; char aHost[128]; int url = net_addr_from_url(&NextAddr, aBuffer, aHost, sizeof(aHost)); + bool Sixup = NextAddr.type & NETTYPE_TW7; if(url > 0) str_copy(aHost, aBuffer); @@ -495,6 +577,10 @@ void CClient::Connect(const char *pAddress, const char *pPassword) NextAddr.port = 8303; } char aNextAddr[NETADDR_MAXSTRSIZE]; + if(Sixup) + NextAddr.type |= NETTYPE_TW7; + else + OnlySixup = false; net_addr_str(&NextAddr, aNextAddr, sizeof(aNextAddr), true); log_debug("client", "resolved connect address '%s' to %s", aBuffer, aNextAddr); aConnectAddrs[NumConnectAddrs] = NextAddr; @@ -503,11 +589,18 @@ void CClient::Connect(const char *pAddress, const char *pPassword) if(NumConnectAddrs == 0) { - log_error("client", "could not find any connect address, defaulting to localhost for whatever reason..."); - net_host_lookup("localhost", &aConnectAddrs[0], m_aNetClient[CONN_MAIN].NetType()); - NumConnectAddrs = 1; + log_error("client", "could not find any connect address"); + char aWarning[256]; + str_format(aWarning, sizeof(aWarning), Localize("Could not resolve connect address '%s'. See local console for details."), m_aConnectAddressStr); + SWarning Warning(Localize("Connect address error"), aWarning); + Warning.m_AutoHide = false; + AddWarning(Warning); + return; } + m_ConnectionId = RandomUuid(); + ServerInfoRequest(); + if(m_SendPassword) { str_copy(m_aPassword, g_Config.m_Password); @@ -519,20 +612,18 @@ void CClient::Connect(const char *pAddress, const char *pPassword) str_copy(m_aPassword, pPassword); m_CanReceiveServerCapabilities = true; - // Deregister Rcon commands from last connected server, might not have called - // DisconnectWithReason if the server was shut down - m_aRconAuthed[0] = 0; - m_UseTempRconCommands = 0; - m_pConsole->DeregisterTempAll(); - m_aNetClient[CONN_MAIN].Connect(aConnectAddrs, NumConnectAddrs); + m_Sixup = OnlySixup; + if(m_Sixup) + { + m_aNetClient[CONN_MAIN].Connect7(aConnectAddrs, NumConnectAddrs); + } + else + m_aNetClient[CONN_MAIN].Connect(aConnectAddrs, NumConnectAddrs); + m_aNetClient[CONN_MAIN].RefreshStun(); SetState(IClient::STATE_CONNECTING); - for(int i = 0; i < RECORDER_MAX; i++) - if(m_aDemoRecorder[i].IsRecording()) - DemoRecorder_Stop(i); - m_InputtimeMarginGraph.Init(-150.0f, 150.0f); m_GametimeMarginGraph.Init(-150.0f, 150.0f); @@ -551,16 +642,21 @@ void CClient::DisconnectWithReason(const char *pReason) m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf, gs_ClientNetworkPrintColor); // stop demo playback and recorder + // make sure to remove replay tmp demo m_DemoPlayer.Stop(); - for(int i = 0; i < RECORDER_MAX; i++) - DemoRecorder_Stop(i); + for(int Recorder = 0; Recorder < RECORDER_MAX; Recorder++) + { + DemoRecorder(Recorder)->Stop(Recorder == RECORDER_REPLAYS ? IDemoRecorder::EStopMode::REMOVE_FILE : IDemoRecorder::EStopMode::KEEP_FILE); + } - // m_aRconAuthed[0] = 0; mem_zero(m_aRconUsername, sizeof(m_aRconUsername)); mem_zero(m_aRconPassword, sizeof(m_aRconPassword)); + m_MapDetailsPresent = false; m_ServerSentCapabilities = false; m_UseTempRconCommands = 0; + m_ExpectedRconCommands = -1; + m_GotRconCommands = 0; m_pConsole->DeregisterTempAll(); m_aNetClient[CONN_MAIN].Disconnect(pReason); SetState(IClient::STATE_OFFLINE); @@ -572,22 +668,7 @@ void CClient::DisconnectWithReason(const char *pReason) m_CurrentServerCurrentPingTime = -1; m_CurrentServerNextPingTime = -1; - // disable all downloads - m_MapdownloadChunk = 0; - if(m_pMapdownloadTask) - m_pMapdownloadTask->Abort(); - if(m_MapdownloadFileTemp) - { - io_close(m_MapdownloadFileTemp); - Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE); - } - m_MapdownloadFileTemp = 0; - m_MapdownloadSha256Present = false; - m_MapdownloadSha256 = SHA256_ZEROED; - m_MapdownloadCrc = 0; - m_MapdownloadTotalsize = -1; - m_MapdownloadAmount = 0; - m_MapDetailsPresent = false; + ResetMapDownload(true); // clear the current server info mem_zero(&m_CurrentServerInfo, sizeof(m_CurrentServerInfo)); @@ -596,60 +677,77 @@ void CClient::DisconnectWithReason(const char *pReason) m_aapSnapshots[0][SNAP_CURRENT] = 0; m_aapSnapshots[0][SNAP_PREV] = 0; m_aReceivedSnapshots[0] = 0; + m_LastDummy = false; + + // 0.7 + m_TranslationContext.Reset(); + m_Sixup = false; } void CClient::Disconnect() { - m_ButtonRender = false; if(m_State != IClient::STATE_OFFLINE) - DisconnectWithReason(0); - - // make sure to remove replay tmp demo - if(g_Config.m_ClReplays) { - DemoRecorder_Stop(RECORDER_REPLAYS, true); + DisconnectWithReason(nullptr); } } -bool CClient::DummyConnected() +bool CClient::DummyConnected() const { return m_DummyConnected; } -bool CClient::DummyConnecting() +bool CClient::DummyConnecting() const { - return !m_DummyConnected && m_LastDummyConnectTime > 0 && m_LastDummyConnectTime + GameTickSpeed() * 5 > GameTick(g_Config.m_ClDummy); + return m_DummyConnecting; } -void CClient::DummyConnect() +bool CClient::DummyConnectingDelayed() const { - if(m_LastDummyConnectTime > 0 && m_LastDummyConnectTime + GameTickSpeed() * 5 > GameTick(g_Config.m_ClDummy)) - return; + return !DummyConnected() && !DummyConnecting() && m_LastDummyConnectTime > 0.0f && m_LastDummyConnectTime + 5.0f > GlobalTime(); +} +void CClient::DummyConnect() +{ if(m_aNetClient[CONN_MAIN].State() != NETSTATE_ONLINE) + { + log_info("client", "Not online."); return; + } - if(m_DummyConnected || !DummyAllowed()) + if(!DummyAllowed()) + { + log_info("client", "Dummy is not allowed on this server."); return; + } + if(DummyConnected() || DummyConnecting()) + { + log_info("client", "Dummy is already connected/connecting."); + return; + } + if(DummyConnectingDelayed()) + { + log_info("client", "Wait before connecting dummy again."); + return; + } - m_LastDummyConnectTime = GameTick(g_Config.m_ClDummy); - + m_LastDummyConnectTime = GlobalTime(); m_aRconAuthed[1] = 0; - m_DummySendConnInfo = true; g_Config.m_ClDummyCopyMoves = 0; g_Config.m_ClDummyHammer = 0; + m_DummyConnecting = true; // connect to the server - m_aNetClient[CONN_DUMMY].Connect(m_aNetClient[CONN_MAIN].ServerAddress(), 1); + if(IsSixup()) + m_aNetClient[CONN_DUMMY].Connect7(m_aNetClient[CONN_MAIN].ServerAddress(), 1); + else + m_aNetClient[CONN_DUMMY].Connect(m_aNetClient[CONN_MAIN].ServerAddress(), 1); } void CClient::DummyDisconnect(const char *pReason) { - if(!m_DummyConnected) - return; - m_aNetClient[CONN_DUMMY].Disconnect(pReason); g_Config.m_ClDummy = 0; @@ -663,10 +761,13 @@ void CClient::DummyDisconnect(const char *pReason) m_aapSnapshots[1][SNAP_PREV] = 0; m_aReceivedSnapshots[1] = 0; m_DummyConnected = false; + m_DummyConnecting = false; + m_DummyReconnectOnReload = false; + m_DummyDeactivateOnReconnect = false; GameClient()->OnDummyDisconnect(); } -bool CClient::DummyAllowed() +bool CClient::DummyAllowed() const { return m_ServerCapabilities.m_AllowDummy; } @@ -681,9 +782,6 @@ int CClient::GetCurrentRaceTime() void CClient::GetServerInfo(CServerInfo *pServerInfo) const { mem_copy(pServerInfo, &m_CurrentServerInfo, sizeof(m_CurrentServerInfo)); - - if(m_DemoPlayer.IsPlaying() && g_Config.m_ClDemoAssumeRace) - str_copy(pServerInfo->m_aGameType, "DDraceNetwork"); } void CClient::ServerInfoRequest() @@ -699,37 +797,33 @@ void CClient::LoadDebugFont() // --- -void *CClient::SnapGetItem(int SnapID, int Index, CSnapItem *pItem) const +IClient::CSnapItem CClient::SnapGetItem(int SnapId, int Index) const { - dbg_assert(SnapID >= 0 && SnapID < NUM_SNAPSHOT_TYPES, "invalid SnapID"); - const CSnapshot *pSnapshot = m_aapSnapshots[g_Config.m_ClDummy][SnapID]->m_pAltSnap; + dbg_assert(SnapId >= 0 && SnapId < NUM_SNAPSHOT_TYPES, "invalid SnapId"); + const CSnapshot *pSnapshot = m_aapSnapshots[g_Config.m_ClDummy][SnapId]->m_pAltSnap; const CSnapshotItem *pSnapshotItem = pSnapshot->GetItem(Index); - pItem->m_DataSize = pSnapshot->GetItemSize(Index); - pItem->m_Type = pSnapshot->GetItemType(Index); - pItem->m_ID = pSnapshotItem->ID(); - return (void *)pSnapshotItem->Data(); + CSnapItem Item; + Item.m_Type = pSnapshot->GetItemType(Index); + Item.m_Id = pSnapshotItem->Id(); + Item.m_pData = pSnapshotItem->Data(); + Item.m_DataSize = pSnapshot->GetItemSize(Index); + return Item; } -int CClient::SnapItemSize(int SnapID, int Index) const +const void *CClient::SnapFindItem(int SnapId, int Type, int Id) const { - dbg_assert(SnapID >= 0 && SnapID < NUM_SNAPSHOT_TYPES, "invalid SnapID"); - return m_aapSnapshots[g_Config.m_ClDummy][SnapID]->m_pAltSnap->GetItemSize(Index); -} - -const void *CClient::SnapFindItem(int SnapID, int Type, int ID) const -{ - if(!m_aapSnapshots[g_Config.m_ClDummy][SnapID]) + if(!m_aapSnapshots[g_Config.m_ClDummy][SnapId]) return nullptr; - return m_aapSnapshots[g_Config.m_ClDummy][SnapID]->m_pAltSnap->FindItem(Type, ID); + return m_aapSnapshots[g_Config.m_ClDummy][SnapId]->m_pAltSnap->FindItem(Type, Id); } -int CClient::SnapNumItems(int SnapID) const +int CClient::SnapNumItems(int SnapId) const { - dbg_assert(SnapID >= 0 && SnapID < NUM_SNAPSHOT_TYPES, "invalid SnapID"); - if(!m_aapSnapshots[g_Config.m_ClDummy][SnapID]) + dbg_assert(SnapId >= 0 && SnapId < NUM_SNAPSHOT_TYPES, "invalid SnapId"); + if(!m_aapSnapshots[g_Config.m_ClDummy][SnapId]) return 0; - return m_aapSnapshots[g_Config.m_ClDummy][SnapID]->m_pAltSnap->NumItems(); + return m_aapSnapshots[g_Config.m_ClDummy][SnapId]->m_pAltSnap->NumItems(); } void CClient::SnapSetStaticsize(int ItemType, int Size) @@ -737,6 +831,11 @@ void CClient::SnapSetStaticsize(int ItemType, int Size) m_SnapshotDelta.SetStaticsize(ItemType, Size); } +void CClient::SnapSetStaticsize7(int ItemType, int Size) +{ + m_SnapshotDelta.SetStaticsize7(ItemType, Size); +} + void CClient::DebugRender() { if(!g_Config.m_Debug) @@ -765,7 +864,7 @@ void CClient::DebugRender() total = 42 */ s_FrameTimeAvg = s_FrameTimeAvg * 0.9f + m_RenderFrameTime * 0.1f; - str_format(aBuffer, sizeof(aBuffer), "ticks: %8d %8d gfx mem(tex/buff/stream/staging): (%" PRIu64 "k/%" PRIu64 "k/%" PRIu64 "k/%" PRIu64 "k) fps: %3d", + str_format(aBuffer, sizeof(aBuffer), "ticks: %8d %8d gfx mem(tex/buff/stream/staging): (%" PRIu64 " KiB/%" PRIu64 " KiB/%" PRIu64 " KiB/%" PRIu64 " KiB) fps: %3d", m_aCurGameTick[g_Config.m_ClDummy], m_aPredTick[g_Config.m_ClDummy], (Graphics()->TextureMemoryUsage() / 1024), (Graphics()->BufferMemoryUsage() / 1024), @@ -786,7 +885,7 @@ void CClient::DebugRender() SendPackets++; if(!RecvPackets) RecvPackets++; - str_format(aBuffer, sizeof(aBuffer), "send: %3" PRIu64 " %5" PRIu64 "+%4" PRIu64 "=%5" PRIu64 " (%3" PRIu64 " kbps) avg: %5" PRIu64 "\nrecv: %3" PRIu64 " %5" PRIu64 "+%4" PRIu64 "=%5" PRIu64 " (%3" PRIu64 " kbps) avg: %5" PRIu64, + str_format(aBuffer, sizeof(aBuffer), "send: %3" PRIu64 " %5" PRIu64 "+%4" PRIu64 "=%5" PRIu64 " (%3" PRIu64 " Kibit/s) avg: %5" PRIu64 "\nrecv: %3" PRIu64 " %5" PRIu64 "+%4" PRIu64 "=%5" PRIu64 " (%3" PRIu64 " Kibit/s) avg: %5" PRIu64, SendPackets, SendBytes, SendPackets * 42, SendTotal, (SendTotal * 8) / 1024, SendBytes / SendPackets, RecvPackets, RecvBytes, RecvPackets * 42, RecvTotal, (RecvTotal * 8) / 1024, RecvBytes / RecvPackets); Graphics()->QuadsText(2, 14, 16, aBuffer); @@ -843,11 +942,11 @@ void CClient::DebugRender() float sp = Graphics()->ScreenWidth() / 100.0f; float x = Graphics()->ScreenWidth() - w - sp; - m_FpsGraph.Scale(); + m_FpsGraph.Scale(time_freq()); m_FpsGraph.Render(Graphics(), TextRender(), x, sp * 5, w, h, "FPS"); - m_InputtimeMarginGraph.Scale(); + m_InputtimeMarginGraph.Scale(5 * time_freq()); m_InputtimeMarginGraph.Render(Graphics(), TextRender(), x, sp * 6 + h, w, h, "Prediction Margin"); - m_GametimeMarginGraph.Scale(); + m_GametimeMarginGraph.Scale(5 * time_freq()); m_GametimeMarginGraph.Render(Graphics(), TextRender(), x, sp * 7 + h * 2, w, h, "Gametime Margin"); } } @@ -875,7 +974,7 @@ const char *CClient::PlayerName() const return "nameless tee"; } -const char *CClient::DummyName() const +const char *CClient::DummyName() { if(g_Config.m_ClDummyName[0]) { @@ -892,9 +991,8 @@ const char *CClient::DummyName() const } if(pBase) { - static char aDummyNameBuf[16]; - str_format(aDummyNameBuf, sizeof(aDummyNameBuf), "[D] %s", pBase); - return aDummyNameBuf; + str_format(m_aAutomaticDummyName, sizeof(m_aAutomaticDummyName), "[D] %s", pBase); + return m_aAutomaticDummyName; } return "brainless tee"; } @@ -933,9 +1031,8 @@ const char *CClient::LoadMap(const char *pName, const char *pFilename, SHA256_DI SetState(IClient::STATE_LOADING); SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_LOADING_MAP); - - if((bool)m_MapLoadingCBFunc) - m_MapLoadingCBFunc(); + if((bool)m_LoadingCallback) + m_LoadingCallback(IClient::LOADING_CALLBACK_DETAIL_MAP); if(!m_pMap->Load(pFilename)) { @@ -965,13 +1062,14 @@ const char *CClient::LoadMap(const char *pName, const char *pFilename, SHA256_DI } // stop demo recording if we loaded a new map - for(int i = 0; i < RECORDER_MAX; i++) - DemoRecorder_Stop(i, i == RECORDER_REPLAYS); + for(int Recorder = 0; Recorder < RECORDER_MAX; Recorder++) + { + DemoRecorder(Recorder)->Stop(Recorder == RECORDER_REPLAYS ? IDemoRecorder::EStopMode::REMOVE_FILE : IDemoRecorder::EStopMode::KEEP_FILE); + } char aBuf[256]; str_format(aBuf, sizeof(aBuf), "loaded map '%s'", pFilename); m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf); - m_aReceivedSnapshots[g_Config.m_ClDummy] = 0; str_copy(m_aCurrentMap, pName); str_copy(m_aCurrentMapPath, pFilename); @@ -1016,8 +1114,6 @@ const char *CClient::LoadMapSearch(const char *pMapName, SHA256_DIGEST *pWantedS } str_format(aBuf, sizeof(aBuf), "loading map, map=%s wanted %scrc=%08x", pMapName, aWanted, WantedCrc); m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf); - SetState(IClient::STATE_LOADING); - SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_LOADING_MAP); // try the normal maps folder str_format(aBuf, sizeof(aBuf), "maps/%s.map", pMapName); @@ -1280,7 +1376,7 @@ void CClient::ProcessServerInfo(int RawType, NETADDR *pFrom, const void *pData, #undef GET_INT } -static CServerCapabilities GetServerCapabilities(int Version, int Flags) +static CServerCapabilities GetServerCapabilities(int Version, int Flags, bool Sixup) { CServerCapabilities Result; bool DDNet = false; @@ -1289,7 +1385,7 @@ static CServerCapabilities GetServerCapabilities(int Version, int Flags) DDNet = Flags & SERVERCAPFLAG_DDNET; } Result.m_ChatTimeoutCode = DDNet; - Result.m_AnyPlayerFlag = DDNet; + Result.m_AnyPlayerFlag = !Sixup; Result.m_PingEx = false; Result.m_AllowDummy = true; Result.m_SyncWeaponInput = false; @@ -1327,7 +1423,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) bool Sys; CUuid Uuid; - int Result = UnpackMessageID(&Msg, &Sys, &Uuid, &Unpacker, &Packer); + int Result = UnpackMessageId(&Msg, &Sys, &Uuid, &Unpacker, &Packer); if(Result == UNPACKMESSAGE_ERROR) { return; @@ -1337,6 +1433,20 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) SendMsg(Conn, &Packer, MSGFLAG_VITAL); } + // allocates the memory for the translated data + CPacker Packer6; + if(IsSixup()) + { + bool IsExMsg = false; + int Success = !TranslateSysMsg(&Msg, Sys, &Unpacker, &Packer6, pPacket, &IsExMsg); + if(Msg < 0) + return; + if(Success && !IsExMsg) + { + Unpacker.Reset(Packer6.Data(), Packer6.Size()); + } + } + if(Sys) { // system message @@ -1376,7 +1486,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) { return; } - m_ServerCapabilities = GetServerCapabilities(Version, Flags); + m_ServerCapabilities = GetServerCapabilities(Version, Flags, IsSixup()); m_CanReceiveServerCapabilities = false; m_ServerSentCapabilities = true; } @@ -1384,7 +1494,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) { if(m_CanReceiveServerCapabilities) { - m_ServerCapabilities = GetServerCapabilities(0, 0); + m_ServerCapabilities = GetServerCapabilities(0, 0, IsSixup()); m_CanReceiveServerCapabilities = false; } bool MapDetailsWerePresent = m_MapDetailsPresent; @@ -1393,24 +1503,32 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) const char *pMap = Unpacker.GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES); int MapCrc = Unpacker.GetInt(); int MapSize = Unpacker.GetInt(); - if(Unpacker.Error() || MapSize < 0) + if(Unpacker.Error()) { return; } + if(MapSize < 0 || MapSize > 1024 * 1024 * 1024) // 1 GiB + { + DisconnectWithReason("invalid map size"); + return; + } for(int i = 0; pMap[i]; i++) // protect the player from nasty map names { if(pMap[i] == '/' || pMap[i] == '\\') { + DisconnectWithReason("strange character in map name"); return; } } - if(m_DummyConnected) + if(m_DummyConnected && !m_DummyReconnectOnReload) { DummyDisconnect(0); } + ResetMapDownload(true); + SHA256_DIGEST *pMapSha256 = nullptr; const char *pMapUrl = nullptr; if(MapDetailsWerePresent && str_comp(m_aMapDetailsName, pMap) == 0 && m_MapDetailsCrc == MapCrc) @@ -1427,12 +1545,6 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) } else { - if(m_MapdownloadFileTemp) - { - io_close(m_MapdownloadFileTemp); - Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE); - } - // start map download FormatMapDownloadFilename(pMap, pMapSha256, MapCrc, false, m_aMapdownloadFilename, sizeof(m_aMapdownloadFilename)); FormatMapDownloadFilename(pMap, pMapSha256, MapCrc, true, m_aMapdownloadFilenameTemp, sizeof(m_aMapdownloadFilenameTemp)); @@ -1441,16 +1553,11 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) str_format(aBuf, sizeof(aBuf), "starting to download map to '%s'", m_aMapdownloadFilenameTemp); m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", aBuf); - m_MapdownloadChunk = 0; str_copy(m_aMapdownloadName, pMap); - m_MapdownloadSha256Present = (bool)pMapSha256; m_MapdownloadSha256 = pMapSha256 ? *pMapSha256 : SHA256_ZEROED; m_MapdownloadCrc = MapCrc; m_MapdownloadTotalsize = MapSize; - m_MapdownloadAmount = 0; - - ResetMapDownload(); if(pMapSha256) { @@ -1462,9 +1569,9 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) m_pMapdownloadTask = HttpGetFile(pMapUrl ? pMapUrl : aUrl, Storage(), m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE); m_pMapdownloadTask->Timeout(CTimeout{g_Config.m_ClMapDownloadConnectTimeoutMs, 0, g_Config.m_ClMapDownloadLowSpeedLimit, g_Config.m_ClMapDownloadLowSpeedTime}); - m_pMapdownloadTask->MaxResponseSize(1024 * 1024 * 1024); // 1 GiB + m_pMapdownloadTask->MaxResponseSize(MapSize); m_pMapdownloadTask->ExpectSha256(*pMapSha256); - Engine()->AddJob(m_pMapdownloadTask); + Http()->Run(m_pMapdownloadTask); } else { @@ -1474,12 +1581,31 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) } else if(Conn == CONN_MAIN && Msg == NETMSG_MAP_DATA) { - int Last = Unpacker.GetInt(); - int MapCRC = Unpacker.GetInt(); - int Chunk = Unpacker.GetInt(); - int Size = Unpacker.GetInt(); + if(!m_MapdownloadFileTemp) + { + return; + } + int Last = -1; + int MapCRC = -1; + int Chunk = -1; + int Size = -1; + + if(IsSixup()) + { + MapCRC = m_MapdownloadCrc; + Chunk = m_MapdownloadChunk; + Size = minimum(m_TranslationContext.m_MapDownloadChunkSize, m_TranslationContext.m_MapdownloadTotalsize - m_MapdownloadAmount); + } + else + { + Last = Unpacker.GetInt(); + MapCRC = Unpacker.GetInt(); + Chunk = Unpacker.GetInt(); + Size = Unpacker.GetInt(); + } + const unsigned char *pData = Unpacker.GetRaw(Size); - if(Unpacker.Error() || Size <= 0 || MapCRC != m_MapdownloadCrc || Chunk != m_MapdownloadChunk || !m_MapdownloadFileTemp) + if(Unpacker.Error() || Size <= 0 || MapCRC != m_MapdownloadCrc || Chunk != m_MapdownloadChunk) { return; } @@ -1488,6 +1614,9 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) m_MapdownloadAmount += Size; + if(IsSixup()) + Last = m_MapdownloadAmount == m_TranslationContext.m_MapdownloadTotalsize; + if(Last) { if(m_MapdownloadFileTemp) @@ -1502,9 +1631,17 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) // request new chunk m_MapdownloadChunk++; - CMsgPacker MsgP(NETMSG_REQUEST_MAP_DATA, true); - MsgP.AddInt(m_MapdownloadChunk); - SendMsg(CONN_MAIN, &MsgP, MSGFLAG_VITAL | MSGFLAG_FLUSH); + if(IsSixup() && (m_MapdownloadChunk % m_TranslationContext.m_MapDownloadChunksPerRequest == 0)) + { + CMsgPacker MsgP(protocol7::NETMSG_REQUEST_MAP_DATA, true, true); + SendMsg(CONN_MAIN, &MsgP, MSGFLAG_VITAL | MSGFLAG_FLUSH); + } + else + { + CMsgPacker MsgP(NETMSG_REQUEST_MAP_DATA, true); + MsgP.AddInt(m_MapdownloadChunk); + SendMsg(CONN_MAIN, &MsgP, MSGFLAG_VITAL | MSGFLAG_FLUSH); + } if(g_Config.m_Debug) { @@ -1514,42 +1651,61 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) } } } + else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_MAP_RELOAD) + { + if(m_DummyConnected) + { + m_DummyReconnectOnReload = true; + m_DummyDeactivateOnReconnect = g_Config.m_ClDummy == 0; + g_Config.m_ClDummy = 0; + } + else + m_DummyDeactivateOnReconnect = false; + } else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_CON_READY) { GameClient()->OnConnected(); + if(m_DummyReconnectOnReload) + { + m_DummySendConnInfo = true; + m_DummyReconnectOnReload = false; + } } else if(Conn == CONN_DUMMY && Msg == NETMSG_CON_READY) { m_DummyConnected = true; + m_DummyConnecting = false; g_Config.m_ClDummy = 1; Rcon("crashmeplx"); - if(m_aRconAuthed[0]) + if(m_aRconAuthed[0] && !m_aRconAuthed[1]) RconAuth(m_aRconUsername, m_aRconPassword); } else if(Msg == NETMSG_PING) { CMsgPacker MsgP(NETMSG_PING_REPLY, true); - SendMsg(Conn, &MsgP, MSGFLAG_FLUSH); + int Vital = (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 ? MSGFLAG_VITAL : 0; + SendMsg(Conn, &MsgP, MSGFLAG_FLUSH | Vital); } else if(Msg == NETMSG_PINGEX) { - CUuid *pID = (CUuid *)Unpacker.GetRaw(sizeof(*pID)); + CUuid *pId = (CUuid *)Unpacker.GetRaw(sizeof(*pId)); if(Unpacker.Error()) { return; } CMsgPacker MsgP(NETMSG_PONGEX, true); - MsgP.AddRaw(pID, sizeof(*pID)); - SendMsg(Conn, &MsgP, MSGFLAG_FLUSH); + MsgP.AddRaw(pId, sizeof(*pId)); + int Vital = (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 ? MSGFLAG_VITAL : 0; + SendMsg(Conn, &MsgP, MSGFLAG_FLUSH | Vital); } else if(Conn == CONN_MAIN && Msg == NETMSG_PONGEX) { - CUuid *pID = (CUuid *)Unpacker.GetRaw(sizeof(*pID)); + CUuid *pId = (CUuid *)Unpacker.GetRaw(sizeof(*pId)); if(Unpacker.Error()) { return; } - if(m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && *pID == m_CurrentServerPingUuid) + if(m_ServerCapabilities.m_PingEx && m_CurrentServerCurrentPingTime >= 0 && *pId == m_CurrentServerPingUuid) { int LatencyMs = (time_get() - m_CurrentServerCurrentPingTime) * 1000 / time_freq(); m_ServerBrowser.SetCurrentServerPing(ServerAddress(), LatencyMs); @@ -1583,11 +1739,10 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) { return; } - char aAddr[128]; - char aIP[64]; + char aAddr[NETADDR_MAXSTRSIZE]; NETADDR ServerAddr = ServerAddress(); - net_addr_str(&ServerAddr, aIP, sizeof(aIP), 0); - str_format(aAddr, sizeof(aAddr), "%s:%d", aIP, RedirectPort); + ServerAddr.port = RedirectPort; + net_addr_str(&ServerAddr, aAddr, sizeof(aAddr), true); Connect(aAddr); } else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_ADD) @@ -1599,6 +1754,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) { m_pConsole->RegisterTemp(pName, pParams, CFGFLAG_SERVER, pHelp); } + m_GotRconCommands++; } else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_REM) { @@ -1626,6 +1782,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) if(Old != 0 && m_UseTempRconCommands == 0) { m_pConsole->DeregisterTempAll(); + m_ExpectedRconCommands = -1; } } } @@ -1671,12 +1828,6 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) } else if(Msg == NETMSG_SNAP || Msg == NETMSG_SNAPSINGLE || Msg == NETMSG_SNAPEMPTY) { - // only allow packets from the server we actually want - if(net_addr_comp(&pPacket->m_Address, &ServerAddress())) - { - return; - } - // we are not allowed to process snapshot yet if(State() < IClient::STATE_LOADING) { @@ -1774,7 +1925,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) } // unpack delta - const int SnapSize = m_SnapshotDelta.UnpackDelta(pDeltaShot, pTmpBuffer3, pDeltaData, DeltaSize); + const int SnapSize = m_SnapshotDelta.UnpackDelta(pDeltaShot, pTmpBuffer3, pDeltaData, DeltaSize, IsSixup()); if(SnapSize < 0) { dbg_msg("client", "delta unpack failed. error=%d", SnapSize); @@ -1788,13 +1939,8 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) if(Msg != NETMSG_SNAPEMPTY && pTmpBuffer3->Crc() != Crc) { - if(g_Config.m_Debug) - { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "snapshot crc error #%d - tick=%d wantedcrc=%d gotcrc=%d compressed_size=%d delta_tick=%d", - m_SnapCrcErrors, GameTick, Crc, pTmpBuffer3->Crc(), m_aSnapshotIncomingDataSize[Conn], DeltaTick); - m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "client", aBuf); - } + log_error("client", "snapshot crc error #%d - tick=%d wantedcrc=%d gotcrc=%d compressed_size=%d delta_tick=%d", + m_SnapCrcErrors, GameTick, Crc, pTmpBuffer3->Crc(), m_aSnapshotIncomingDataSize[Conn], DeltaTick); m_SnapCrcErrors++; if(m_SnapCrcErrors > 10) @@ -1821,9 +1967,22 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) m_aSnapshotStorage[Conn].PurgeUntil(PurgeTick); // create a verified and unpacked snapshot + int AltSnapSize = -1; unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE]; CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer; - const int AltSnapSize = UnpackAndValidateSnapshot(pTmpBuffer3, pAltSnapBuffer); + + if(IsSixup()) + { + unsigned char aTmpTransSnapBuffer[CSnapshot::MAX_SIZE]; + CSnapshot *pTmpTransSnapBuffer = (CSnapshot *)aTmpTransSnapBuffer; + mem_copy(pTmpTransSnapBuffer, pTmpBuffer3, CSnapshot::MAX_SIZE); + AltSnapSize = GameClient()->TranslateSnap(pAltSnapBuffer, pTmpTransSnapBuffer, Conn, Dummy); + } + else + { + AltSnapSize = UnpackAndValidateSnapshot(pTmpBuffer3, pAltSnapBuffer); + } + if(AltSnapSize < 0) { dbg_msg("client", "unpack snapshot and validate failed. error=%d", AltSnapSize); @@ -1836,17 +1995,30 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) if(!Dummy) { // for antiping: if the projectile netobjects from the server contains extra data, this is removed and the original content restored before recording demo - unsigned char aExtraInfoRemoved[CSnapshot::MAX_SIZE]; - mem_copy(aExtraInfoRemoved, pTmpBuffer3, SnapSize); - SnapshotRemoveExtraProjectileInfo(aExtraInfoRemoved); + SnapshotRemoveExtraProjectileInfo(pTmpBuffer3); - // add snapshot to demo - for(auto &DemoRecorder : m_aDemoRecorder) + unsigned char aSnapSeven[CSnapshot::MAX_SIZE]; + CSnapshot *pSnapSeven = (CSnapshot *)aSnapSeven; + int DemoSnapSize = SnapSize; + if(IsSixup()) { - if(DemoRecorder.IsRecording()) + DemoSnapSize = GameClient()->OnDemoRecSnap7(pTmpBuffer3, pSnapSeven, Conn); + if(DemoSnapSize < 0) { - // write snapshot - DemoRecorder.RecordSnapshot(GameTick, aExtraInfoRemoved, SnapSize); + dbg_msg("sixup", "demo snapshot failed. error=%d", DemoSnapSize); + } + } + + if(DemoSnapSize >= 0) + { + // add snapshot to demo + for(auto &DemoRecorder : m_aDemoRecorder) + { + if(DemoRecorder.IsRecording()) + { + // write snapshot + DemoRecorder.RecordSnapshot(GameTick, IsSixup() ? pSnapSeven : pTmpBuffer3, DemoSnapSize); + } } } } @@ -1867,6 +2039,8 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) m_aGameTime[Conn].Init((GameTick - 1) * time_freq() / GameTickSpeed()); m_aapSnapshots[Conn][SNAP_PREV] = m_aSnapshotStorage[Conn].m_pFirst; m_aapSnapshots[Conn][SNAP_CURRENT] = m_aSnapshotStorage[Conn].m_pLast; + m_aPrevGameTick[Conn] = m_aapSnapshots[Conn][SNAP_PREV]->m_Tick; + m_aCurGameTick[Conn] = m_aapSnapshots[Conn][SNAP_CURRENT]->m_Tick; if(!Dummy) { m_LocalStartTime = time_get(); @@ -1895,8 +2069,6 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) { if(m_ServerCapabilities.m_ChatTimeoutCode) { - CNetMsg_Cl_Say MsgP; - MsgP.m_Team = 0; char aBuf[128]; char aBufMsg[256]; if(!g_Config.m_ClRunOnJoin[0] && !g_Config.m_ClDummyDefaultEyes && !g_Config.m_ClPlayerDefaultEyes) @@ -1940,10 +2112,23 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) str_append(aBufMsg, aBuf); } } - MsgP.m_pMessage = aBufMsg; - CMsgPacker PackerTimeout(&MsgP); - MsgP.Pack(&PackerTimeout); - SendMsg(Conn, &PackerTimeout, MSGFLAG_VITAL); + if(IsSixup()) + { + protocol7::CNetMsg_Cl_Say Msg7; + Msg7.m_Mode = protocol7::CHAT_ALL; + Msg7.m_Target = -1; + Msg7.m_pMessage = aBufMsg; + SendPackMsg(Conn, &Msg7, MSGFLAG_VITAL, true); + } + else + { + CNetMsg_Cl_Say MsgP; + MsgP.m_Team = 0; + MsgP.m_pMessage = aBufMsg; + CMsgPacker PackerTimeout(&MsgP); + MsgP.Pack(&PackerTimeout); + SendMsg(Conn, &PackerTimeout, MSGFLAG_VITAL); + } } m_aCodeRunAfterJoin[Conn] = true; } @@ -1961,6 +2146,19 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) GameClient()->OnRconType(UsernameReq); } } + else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_GROUP_START) + { + int ExpectedRconCommands = Unpacker.GetInt(); + if(Unpacker.Error()) + return; + + m_ExpectedRconCommands = ExpectedRconCommands; + m_GotRconCommands = 0; + } + else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_GROUP_END) + { + m_ExpectedRconCommands = -1; + } } else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0) { @@ -2005,7 +2203,7 @@ int CClient::UnpackAndValidateSnapshot(CSnapshot *pFrom, CSnapshot *pTo) } const int ItemSize = pNetObjHandler->GetUnpackedObjSize(ItemType); - void *pObj = Builder.NewItem(pFromItem->Type(), pFromItem->ID(), ItemSize); + void *pObj = Builder.NewItem(pFromItem->Type(), pFromItem->Id(), ItemSize); if(!pObj) return -4; @@ -2015,24 +2213,44 @@ int CClient::UnpackAndValidateSnapshot(CSnapshot *pFrom, CSnapshot *pTo) return Builder.Finish(pTo); } -void CClient::ResetMapDownload() +void CClient::ResetMapDownload(bool ResetActive) { if(m_pMapdownloadTask) { m_pMapdownloadTask->Abort(); - m_pMapdownloadTask = NULL; + m_pMapdownloadTask = nullptr; + } + + if(m_MapdownloadFileTemp) + { + io_close(m_MapdownloadFileTemp); + m_MapdownloadFileTemp = 0; + } + + if(Storage()->FileExists(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE)) + { + Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE); + } + + if(ResetActive) + { + m_MapdownloadChunk = 0; + m_MapdownloadSha256Present = false; + m_MapdownloadSha256 = SHA256_ZEROED; + m_MapdownloadCrc = 0; + m_MapdownloadTotalsize = -1; + m_MapdownloadAmount = 0; + m_aMapdownloadFilename[0] = '\0'; + m_aMapdownloadFilenameTemp[0] = '\0'; + m_aMapdownloadName[0] = '\0'; } - m_MapdownloadFileTemp = 0; - m_MapdownloadAmount = 0; } void CClient::FinishMapDownload() { m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "download complete, loading map"); - int Prev = m_MapdownloadTotalsize; - m_MapdownloadTotalsize = -1; - SHA256_DIGEST *pSha256 = m_MapdownloadSha256Present ? &m_MapdownloadSha256 : 0; + SHA256_DIGEST *pSha256 = m_MapdownloadSha256Present ? &m_MapdownloadSha256 : nullptr; bool FileSuccess = true; if(Storage()->FileExists(m_aMapdownloadFilename, IStorage::TYPE_SAVE)) @@ -2040,41 +2258,31 @@ void CClient::FinishMapDownload() FileSuccess &= Storage()->RenameFile(m_aMapdownloadFilenameTemp, m_aMapdownloadFilename, IStorage::TYPE_SAVE); if(!FileSuccess) { - ResetMapDownload(); char aError[128 + IO_MAX_PATH_LENGTH]; str_format(aError, sizeof(aError), Localize("Could not save downloaded map. Try manually deleting this file: %s"), m_aMapdownloadFilename); DisconnectWithReason(aError); return; } - // load map const char *pError = LoadMap(m_aMapdownloadName, m_aMapdownloadFilename, pSha256, m_MapdownloadCrc); if(!pError) { - ResetMapDownload(); + ResetMapDownload(true); m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "loading done"); SendReady(CONN_MAIN); } else if(m_pMapdownloadTask) // fallback { - ResetMapDownload(); - m_MapdownloadTotalsize = Prev; + ResetMapDownload(false); SendMapRequest(); } else { - if(m_MapdownloadFileTemp) - { - io_close(m_MapdownloadFileTemp); - m_MapdownloadFileTemp = 0; - Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE); - } - ResetMapDownload(); DisconnectWithReason(pError); } } -void CClient::ResetDDNetInfo() +void CClient::ResetDDNetInfoTask() { if(m_pDDNetInfoTask) { @@ -2083,56 +2291,49 @@ void CClient::ResetDDNetInfo() } } -bool CClient::IsDDNetInfoChanged() +void CClient::FinishDDNetInfo() { - IOHANDLE OldFile = m_pStorage->OpenFile(DDNET_INFO_FILE, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_SAVE); - - if(!OldFile) - return true; - - IOHANDLE NewFile = m_pStorage->OpenFile(m_aDDNetInfoTmp, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_SAVE); - - if(NewFile) + if(m_ServerBrowser.DDNetInfoSha256() == m_pDDNetInfoTask->ResultSha256()) { - char aOldData[4096]; - char aNewData[4096]; - unsigned OldBytes; - unsigned NewBytes; - - do - { - OldBytes = io_read(OldFile, aOldData, sizeof(aOldData)); - NewBytes = io_read(NewFile, aNewData, sizeof(aNewData)); - - if(OldBytes != NewBytes || mem_comp(aOldData, aNewData, OldBytes) != 0) - { - io_close(NewFile); - io_close(OldFile); - return true; - } - } while(OldBytes > 0); + log_debug("client/info", "DDNet info already up-to-date"); + return; + } - io_close(NewFile); + char aTempFilename[IO_MAX_PATH_LENGTH]; + IStorage::FormatTmpPath(aTempFilename, sizeof(aTempFilename), DDNET_INFO_FILE); + IOHANDLE File = Storage()->OpenFile(aTempFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE); + if(!File) + { + log_error("client/info", "Failed to open temporary DDNet info '%s' for writing", aTempFilename); + return; } - io_close(OldFile); - return false; -} + unsigned char *pResult; + size_t ResultLength; + m_pDDNetInfoTask->Result(&pResult, &ResultLength); + bool Error = io_write(File, pResult, ResultLength) != ResultLength; + Error |= io_close(File) != 0; + if(Error) + { + log_error("client/info", "Error writing temporary DDNet info to file '%s'", aTempFilename); + return; + } -void CClient::FinishDDNetInfo() -{ - ResetDDNetInfo(); - if(IsDDNetInfoChanged()) + if(Storage()->FileExists(DDNET_INFO_FILE, IStorage::TYPE_SAVE) && !Storage()->RemoveFile(DDNET_INFO_FILE, IStorage::TYPE_SAVE)) { - m_pStorage->RenameFile(m_aDDNetInfoTmp, DDNET_INFO_FILE, IStorage::TYPE_SAVE); - LoadDDNetInfo(); - if(m_ServerBrowser.GetCurrentType() == IServerBrowser::TYPE_INTERNET || m_ServerBrowser.GetCurrentType() == IServerBrowser::TYPE_FAVORITES) - m_ServerBrowser.Refresh(m_ServerBrowser.GetCurrentType()); + log_error("client/info", "Failed to remove old DDNet info '%s'", DDNET_INFO_FILE); + Storage()->RemoveFile(aTempFilename, IStorage::TYPE_SAVE); + return; } - else + if(!Storage()->RenameFile(aTempFilename, DDNET_INFO_FILE, IStorage::TYPE_SAVE)) { - m_pStorage->RemoveFile(m_aDDNetInfoTmp, IStorage::TYPE_SAVE); + log_error("client/info", "Failed to rename temporary DDNet info '%s' to '%s'", aTempFilename, DDNET_INFO_FILE); + Storage()->RemoveFile(aTempFilename, IStorage::TYPE_SAVE); + return; } + + log_debug("client/info", "Loading new DDNet info"); + LoadDDNetInfo(); } typedef std::tuple TVersion; @@ -2212,7 +2413,7 @@ void CClient::LoadDDNetInfo() NETADDR Addr; if(!net_addr_from_str(&Addr, StunServersIpv6[0])) { - m_aNetClient->FeedStunServer(Addr); + m_aNetClient[CONN_MAIN].FeedStunServer(Addr); } } const json_value &StunServersIpv4 = DDNetInfo["stun-servers-ipv4"]; @@ -2221,7 +2422,7 @@ void CClient::LoadDDNetInfo() NETADDR Addr; if(!net_addr_from_str(&Addr, StunServersIpv4[0])) { - m_aNetClient->FeedStunServer(Addr); + m_aNetClient[CONN_MAIN].FeedStunServer(Addr); } } const json_value &ConnectingIp = DDNetInfo["connecting-ip"]; @@ -2261,25 +2462,33 @@ void CClient::PumpNetwork() if(State() != IClient::STATE_DEMOPLAYBACK) { - // check for errors - if(State() != IClient::STATE_OFFLINE && State() < IClient::STATE_QUITTING && m_aNetClient[CONN_MAIN].State() == NETSTATE_OFFLINE) - { - Disconnect(); - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "offline error='%s'", m_aNetClient[CONN_MAIN].ErrorString()); - m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf, gs_ClientNetworkErrPrintColor); - } - - if(State() != IClient::STATE_OFFLINE && State() < IClient::STATE_QUITTING && m_DummyConnected && - m_aNetClient[CONN_DUMMY].State() == NETSTATE_OFFLINE) + // check for errors of main and dummy + if(State() != IClient::STATE_OFFLINE && State() < IClient::STATE_QUITTING) { - DummyDisconnect(0); - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "offline dummy error='%s'", m_aNetClient[CONN_DUMMY].ErrorString()); - m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf, gs_ClientNetworkErrPrintColor); + if(m_aNetClient[CONN_MAIN].State() == NETSTATE_OFFLINE) + { + // This will also disconnect the dummy, so the branch below is an `else if` + Disconnect(); + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "offline error='%s'", m_aNetClient[CONN_MAIN].ErrorString()); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf, gs_ClientNetworkErrPrintColor); + } + else if((DummyConnecting() || DummyConnected()) && m_aNetClient[CONN_DUMMY].State() == NETSTATE_OFFLINE) + { + const bool WasConnecting = DummyConnecting(); + DummyDisconnect(nullptr); + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "offline dummy error='%s'", m_aNetClient[CONN_DUMMY].ErrorString()); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", aBuf, gs_ClientNetworkErrPrintColor); + if(WasConnecting) + { + str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Could not connect dummy"), m_aNetClient[CONN_DUMMY].ErrorString()); + GameClient()->Echo(aBuf); + } + } } - // + // check if main was connected if(State() == IClient::STATE_CONNECTING && m_aNetClient[CONN_MAIN].State() == NETSTATE_ONLINE) { // we switched to online @@ -2292,20 +2501,20 @@ void CClient::PumpNetwork() // process packets CNetChunk Packet; - for(int i = 0; i < NUM_CONNS; i++) + SECURITY_TOKEN ResponseToken; + for(int Conn = 0; Conn < NUM_CONNS; Conn++) { - while(m_aNetClient[i].Recv(&Packet)) + while(m_aNetClient[Conn].Recv(&Packet, &ResponseToken, IsSixup())) { - if(Packet.m_ClientID == -1) + if(Packet.m_ClientId == -1) { ProcessConnlessPacket(&Packet); continue; } - if(i > 1) + if(Conn == CONN_MAIN || Conn == CONN_DUMMY) { - continue; + ProcessServerPacket(&Packet, Conn, g_Config.m_ClDummy ^ Conn); } - ProcessServerPacket(&Packet, i, g_Config.m_ClDummy ^ i); } } } @@ -2314,23 +2523,37 @@ void CClient::OnDemoPlayerSnapshot(void *pData, int Size) { // update ticks, they could have changed const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info(); - m_aCurGameTick[g_Config.m_ClDummy] = pInfo->m_Info.m_CurrentTick; - m_aPrevGameTick[g_Config.m_ClDummy] = pInfo->m_PreviousTick; + m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick; + m_aPrevGameTick[0] = pInfo->m_PreviousTick; // create a verified and unpacked snapshot unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE]; CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer; - const int AltSnapSize = UnpackAndValidateSnapshot((CSnapshot *)pData, pAltSnapBuffer); - if(AltSnapSize < 0) + int AltSnapSize; + + if(IsSixup()) { - dbg_msg("client", "unpack snapshot and validate failed. error=%d", AltSnapSize); - return; + AltSnapSize = GameClient()->TranslateSnap(pAltSnapBuffer, (CSnapshot *)pData, CONN_MAIN, false); + if(AltSnapSize < 0) + { + dbg_msg("sixup", "failed to translate snapshot. error=%d", AltSnapSize); + return; + } + } + else + { + AltSnapSize = UnpackAndValidateSnapshot((CSnapshot *)pData, pAltSnapBuffer); + if(AltSnapSize < 0) + { + dbg_msg("client", "unpack snapshot and validate failed. error=%d", AltSnapSize); + return; + } } // handle snapshots after validation - std::swap(m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV], m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]); - mem_copy(m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pSnap, pData, Size); - mem_copy(m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pAltSnap, pAltSnapBuffer, AltSnapSize); + std::swap(m_aapSnapshots[0][SNAP_PREV], m_aapSnapshots[0][SNAP_CURRENT]); + mem_copy(m_aapSnapshots[0][SNAP_CURRENT]->m_pSnap, pData, Size); + mem_copy(m_aapSnapshots[0][SNAP_CURRENT]->m_pAltSnap, pAltSnapBuffer, AltSnapSize); GameClient()->OnNewSnapshot(); } @@ -2346,7 +2569,7 @@ void CClient::OnDemoPlayerMessage(void *pData, int Size) bool Sys; CUuid Uuid; - int Result = UnpackMessageID(&Msg, &Sys, &Uuid, &Unpacker, &Packer); + int Result = UnpackMessageId(&Msg, &Sys, &Uuid, &Unpacker, &Packer); if(Result == UNPACKMESSAGE_ERROR) { return; @@ -2360,43 +2583,44 @@ void CClient::UpdateDemoIntraTimers() { // update timers const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info(); - m_aCurGameTick[g_Config.m_ClDummy] = pInfo->m_Info.m_CurrentTick; - m_aPrevGameTick[g_Config.m_ClDummy] = pInfo->m_PreviousTick; - m_aGameIntraTick[g_Config.m_ClDummy] = pInfo->m_IntraTick; - m_aGameTickTime[g_Config.m_ClDummy] = pInfo->m_TickTime; - m_aGameIntraTickSincePrev[g_Config.m_ClDummy] = pInfo->m_IntraTickSincePrev; + m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick; + m_aPrevGameTick[0] = pInfo->m_PreviousTick; + m_aGameIntraTick[0] = pInfo->m_IntraTick; + m_aGameTickTime[0] = pInfo->m_TickTime; + m_aGameIntraTickSincePrev[0] = pInfo->m_IntraTickSincePrev; }; void CClient::Update() { + PumpNetwork(); + if(State() == IClient::STATE_DEMOPLAYBACK) { -#if defined(CONF_VIDEORECORDER) - if(m_DemoPlayer.IsPlaying() && IVideo::Current()) + if(m_DemoPlayer.IsPlaying()) { - IVideo::Current()->NextVideoFrame(); - IVideo::Current()->NextAudioFrameTimeline([this](short *pFinalOut, unsigned Frames) { - Sound()->Mix(pFinalOut, Frames); - }); - } - else if(m_ButtonRender) - Disconnect(); +#if defined(CONF_VIDEORECORDER) + if(IVideo::Current()) + { + IVideo::Current()->NextVideoFrame(); + IVideo::Current()->NextAudioFrameTimeline([this](short *pFinalOut, unsigned Frames) { + Sound()->Mix(pFinalOut, Frames); + }); + } #endif - m_DemoPlayer.Update(); + m_DemoPlayer.Update(); - if(m_DemoPlayer.IsPlaying()) - { // update timers const CDemoPlayer::CPlaybackInfo *pInfo = m_DemoPlayer.Info(); - m_aCurGameTick[g_Config.m_ClDummy] = pInfo->m_Info.m_CurrentTick; - m_aPrevGameTick[g_Config.m_ClDummy] = pInfo->m_PreviousTick; - m_aGameIntraTick[g_Config.m_ClDummy] = pInfo->m_IntraTick; - m_aGameTickTime[g_Config.m_ClDummy] = pInfo->m_TickTime; + m_aCurGameTick[0] = pInfo->m_Info.m_CurrentTick; + m_aPrevGameTick[0] = pInfo->m_PreviousTick; + m_aGameIntraTick[0] = pInfo->m_IntraTick; + m_aGameTickTime[0] = pInfo->m_TickTime; } else { - // disconnect on error + // Disconnect when demo playback stopped, either due to playback error + // or because the end of the demo was reached when rendering it. DisconnectWithReason(m_DemoPlayer.ErrorMessage()); if(m_DemoPlayer.ErrorMessage()[0] != '\0') { @@ -2415,43 +2639,35 @@ void CClient::Update() GameClient()->OnDummySwap(); } - if(m_aReceivedSnapshots[!g_Config.m_ClDummy] >= 3) + if(m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]) { // switch dummy snapshot int64_t Now = m_aGameTime[!g_Config.m_ClDummy].Get(time_get()); while(true) { - CSnapshotStorage::CHolder *pCur = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]; - int64_t TickStart = (pCur->m_Tick) * time_freq() / GameTickSpeed(); + if(!m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext) + break; + int64_t TickStart = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed(); + if(TickStart >= Now) + break; - if(TickStart < Now) - { - CSnapshotStorage::CHolder *pNext = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext; - if(pNext) - { - m_aapSnapshots[!g_Config.m_ClDummy][SNAP_PREV] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]; - m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT] = pNext; + m_aapSnapshots[!g_Config.m_ClDummy][SNAP_PREV] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]; + m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext; - // set ticks - m_aCurGameTick[!g_Config.m_ClDummy] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick; - m_aPrevGameTick[!g_Config.m_ClDummy] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_PREV]->m_Tick; - } - else - break; - } - else - break; + // set ticks + m_aCurGameTick[!g_Config.m_ClDummy] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick; + m_aPrevGameTick[!g_Config.m_ClDummy] = m_aapSnapshots[!g_Config.m_ClDummy][SNAP_PREV]->m_Tick; } } - if(m_aReceivedSnapshots[g_Config.m_ClDummy] >= 3) + if(m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]) { // switch snapshot bool Repredict = false; int64_t Now = m_aGameTime[g_Config.m_ClDummy].Get(time_get()); int64_t PredNow = m_PredictedTime.Get(time_get()); - if(m_LastDummy != (bool)g_Config.m_ClDummy && m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT] && m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]) + if(m_LastDummy != (bool)g_Config.m_ClDummy && m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]) { // Load snapshot for m_ClDummy GameClient()->OnNewSnapshot(); @@ -2460,35 +2676,24 @@ void CClient::Update() while(true) { - CSnapshotStorage::CHolder *pCur = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]; - int64_t TickStart = (pCur->m_Tick) * time_freq() / GameTickSpeed(); + if(!m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext) + break; + int64_t TickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed(); + if(TickStart >= Now) + break; - if(TickStart < Now) - { - CSnapshotStorage::CHolder *pNext = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext; - if(pNext) - { - m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]; - m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT] = pNext; + m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]; + m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_pNext; - // set ticks - m_aCurGameTick[g_Config.m_ClDummy] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick; - m_aPrevGameTick[g_Config.m_ClDummy] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick; + // set ticks + m_aCurGameTick[g_Config.m_ClDummy] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick; + m_aPrevGameTick[g_Config.m_ClDummy] = m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick; - if(m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT] && m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]) - { - GameClient()->OnNewSnapshot(); - Repredict = true; - } - } - else - break; - } - else - break; + GameClient()->OnNewSnapshot(); + Repredict = true; } - if(m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT] && m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]) + if(m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]) { int64_t CurTickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_CURRENT]->m_Tick * time_freq() / GameTickSpeed(); int64_t PrevTickStart = m_aapSnapshots[g_Config.m_ClDummy][SNAP_PREV]->m_Tick * time_freq() / GameTickSpeed(); @@ -2527,8 +2732,7 @@ void CClient::Update() } // fetch server info if we don't have it - if(State() >= IClient::STATE_LOADING && - m_CurrentServerInfoRequestTime >= 0 && + if(m_CurrentServerInfoRequestTime >= 0 && time_get() > m_CurrentServerInfoRequestTime) { m_ServerBrowser.RequestCurrentServer(ServerAddress()); @@ -2536,8 +2740,7 @@ void CClient::Update() } // periodically ping server - if(State() == IClient::STATE_ONLINE && - m_CurrentServerNextPingTime >= 0 && + if(m_CurrentServerNextPingTime >= 0 && time_get() > m_CurrentServerNextPingTime) { int64_t NowPing = time_get(); @@ -2563,6 +2766,16 @@ void CClient::Update() } } + if(m_DummyDeactivateOnReconnect && g_Config.m_ClDummy == 1) + { + m_DummyDeactivateOnReconnect = false; + g_Config.m_ClDummy = 0; + } + else if(!m_DummyConnected && m_DummyDeactivateOnReconnect) + { + m_DummyDeactivateOnReconnect = false; + } + m_LastDummy = (bool)g_Config.m_ClDummy; } @@ -2570,57 +2783,51 @@ void CClient::Update() #ifdef CONF_DEBUG if(g_Config.m_DbgStress) { - static int64_t ActionTaken = 0; + static int64_t s_ActionTaken = 0; int64_t Now = time_get(); if(State() == IClient::STATE_OFFLINE) { - if(Now > ActionTaken + time_freq() * 2) + if(Now > s_ActionTaken + time_freq() * 2) { m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "stress", "reconnecting!"); Connect(g_Config.m_DbgStressServer); - ActionTaken = Now; + s_ActionTaken = Now; } } else { - if(Now > ActionTaken + time_freq() * (10 + g_Config.m_DbgStress)) + if(Now > s_ActionTaken + time_freq() * (10 + g_Config.m_DbgStress)) { m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "stress", "disconnecting!"); Disconnect(); - ActionTaken = Now; + s_ActionTaken = Now; } } } #endif - // pump the network - PumpNetwork(); - if(m_pMapdownloadTask) { - if(m_pMapdownloadTask->State() == HTTP_DONE) + if(m_pMapdownloadTask->State() == EHttpState::DONE) FinishMapDownload(); - else if(m_pMapdownloadTask->State() == HTTP_ERROR || m_pMapdownloadTask->State() == HTTP_ABORTED) + else if(m_pMapdownloadTask->State() == EHttpState::ERROR || m_pMapdownloadTask->State() == EHttpState::ABORTED) { dbg_msg("webdl", "http failed, falling back to gameserver"); - ResetMapDownload(); + ResetMapDownload(false); SendMapRequest(); } } if(m_pDDNetInfoTask) { - if(m_pDDNetInfoTask->State() == HTTP_DONE) - FinishDDNetInfo(); - else if(m_pDDNetInfoTask->State() == HTTP_ERROR) + if(m_pDDNetInfoTask->State() == EHttpState::DONE) { - Storage()->RemoveFile(m_aDDNetInfoTmp, IStorage::TYPE_SAVE); - ResetDDNetInfo(); + FinishDDNetInfo(); + ResetDDNetInfoTask(); } - else if(m_pDDNetInfoTask->State() == HTTP_ABORTED) + else if(m_pDDNetInfoTask->State() == EHttpState::ERROR || m_pDDNetInfoTask->State() == EHttpState::ABORTED) { - Storage()->RemoveFile(m_aDDNetInfoTmp, IStorage::TYPE_SAVE); - m_pDDNetInfoTask = NULL; + ResetDDNetInfoTask(); } } @@ -2629,14 +2836,23 @@ void CClient::Update() if(!m_EditJobs.empty()) { std::shared_ptr pJob = m_EditJobs.front(); - if(pJob->Status() == IJob::STATE_DONE) + if(pJob->State() == IJob::STATE_DONE) { char aBuf[IO_MAX_PATH_LENGTH + 64]; - str_format(aBuf, sizeof(aBuf), "Successfully saved the replay to %s!", pJob->Destination()); - m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", aBuf); + if(pJob->Success()) + { + str_format(aBuf, sizeof(aBuf), "Successfully saved the replay to '%s'!", pJob->Destination()); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", aBuf); - GameClient()->Echo(Localize("Successfully saved the replay!")); + GameClient()->Echo(Localize("Successfully saved the replay!")); + } + else + { + str_format(aBuf, sizeof(aBuf), "Failed saving the replay to '%s'...", pJob->Destination()); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", aBuf); + GameClient()->Echo(Localize("Failed saving the replay!")); + } m_EditJobs.pop_front(); } } @@ -2681,6 +2897,7 @@ void CClient::RegisterInterfaces() #endif Kernel()->RegisterInterface(static_cast(&m_Friends), false); Kernel()->ReregisterInterface(static_cast(&m_Foes)); + Kernel()->RegisterInterface(static_cast(&m_Http), false); } void CClient::InitInterfaces() @@ -2703,14 +2920,12 @@ void CClient::InitInterfaces() m_pNotifications = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); - m_DemoEditor.Init(m_pGameClient->NetVersion(), &m_SnapshotDelta, m_pConsole, m_pStorage); + m_DemoEditor.Init(&m_SnapshotDelta, m_pConsole, m_pStorage); m_ServerBrowser.SetBaseInfo(&m_aNetClient[CONN_CONTACT], m_pGameClient->NetVersion()); - HttpInit(m_pStorage); - #if defined(CONF_AUTOUPDATE) - m_Updater.Init(); + m_Updater.Init(&m_Http); #endif m_pConfigManager->RegisterCallback(IFavorites::ConfigSaveCallback, m_pFavorites); @@ -2744,13 +2959,31 @@ void CClient::Run() g_UuidManager.DebugDump(); } +#ifndef CONF_WEBASM + char aNetworkError[256]; + if(!InitNetworkClient(aNetworkError, sizeof(aNetworkError))) + { + log_error("client", "%s", aNetworkError); + ShowMessageBox("Network Error", aNetworkError); + return; + } +#endif + + if(!m_Http.Init(std::chrono::seconds{1})) + { + const char *pErrorMessage = "Failed to initialize the HTTP client."; + log_error("client", "%s", pErrorMessage); + ShowMessageBox("HTTP Error", pErrorMessage); + return; + } + // init graphics m_pGraphics = CreateEngineGraphicsThreaded(); Kernel()->RegisterInterface(m_pGraphics); // IEngineGraphics Kernel()->RegisterInterface(static_cast(m_pGraphics), false); if(m_pGraphics->Init() != 0) { - dbg_msg("client", "couldn't init graphics"); + log_error("client", "couldn't init graphics"); ShowMessageBox("Graphics Error", "The graphics could not be initialized."); return; } @@ -2760,23 +2993,13 @@ void CClient::Run() Graphics()->Swap(); // init sound, allowed to fail - m_SoundInitFailed = Sound()->Init() != 0; + const bool SoundInitFailed = Sound()->Init() != 0; #if defined(CONF_VIDEORECORDER) // init video recorder aka ffmpeg CVideo::Init(); #endif -#ifndef CONF_WEBASM - char aNetworkError[256]; - if(!InitNetworkClient(aNetworkError, sizeof(aNetworkError))) - { - dbg_msg("client", "%s", aNetworkError); - ShowMessageBox("Network Error", aNetworkError); - return; - } -#endif - // init text render m_pTextRender = Kernel()->RequestInterface(); m_pTextRender->Init(); @@ -2830,6 +3053,13 @@ void CClient::Run() else RequestDDNetInfo(); + if(SoundInitFailed) + { + SWarning Warning(Localize("Sound error"), Localize("The audio device couldn't be initialised.")); + Warning.m_AutoHide = false; + m_vWarnings.emplace_back(Warning); + } + bool LastD = false; bool LastE = false; bool LastG = false; @@ -2854,7 +3084,7 @@ void CClient::Run() { const char *pError = DemoPlayer_Play(m_aCmdPlayDemo, IStorage::TYPE_ALL_OR_ABSOLUTE); if(pError) - dbg_msg("demo_player", "playing passed demo file '%s' failed: %s", m_aCmdPlayDemo, pError); + log_error("demo_player", "playing passed demo file '%s' failed: %s", m_aCmdPlayDemo, pError); m_aCmdPlayDemo[0] = 0; } @@ -2865,12 +3095,12 @@ void CClient::Run() if(Result) g_Config.m_ClEditor = true; else - dbg_msg("editor", "editing passed map file '%s' failed", m_aCmdEditMap); + log_error("editor", "editing passed map file '%s' failed", m_aCmdEditMap); m_aCmdEditMap[0] = 0; } - // progress on dummy connect if security token handshake skipped/passed - if(m_DummySendConnInfo && !m_aNetClient[CONN_DUMMY].SecurityTokenUnknown()) + // progress on dummy connect when the connection is online + if(m_DummySendConnInfo && m_aNetClient[CONN_DUMMY].State() == NETSTATE_ONLINE) { m_DummySendConnInfo = false; SendInfo(CONN_DUMMY); @@ -3006,29 +3236,11 @@ void CClient::Run() AutoStatScreenshot_Cleanup(); AutoCSV_Cleanup(); - // check conditions - if(State() == IClient::STATE_QUITTING || State() == IClient::STATE_RESTARTING) - { - static bool s_SavedConfig = false; - if(!s_SavedConfig) - { - // write down the config and quit - if(!m_pConfigManager->Save()) - m_vWarnings.emplace_back(Localize("Saving ddnet-settings.cfg failed")); - s_SavedConfig = true; - } - - if(m_pStorage->FileExists(m_aDDNetInfoTmp, IStorage::TYPE_SAVE)) - { - m_pStorage->RemoveFile(m_aDDNetInfoTmp, IStorage::TYPE_SAVE); - } - - if(m_vWarnings.empty() && !GameClient()->IsDisplayingWarning()) - break; - } - m_Fifo.Update(); + if(State() == IClient::STATE_QUITTING || State() == IClient::STATE_RESTARTING) + break; + // beNice auto Now = time_get_nanoseconds(); decltype(Now) SleepTimeInNanoSeconds{0}; @@ -3076,17 +3288,28 @@ void CClient::Run() m_GlobalTime = (time_get() - m_GlobalStartTime) / (float)time_freq(); } + GameClient()->RenderShutdownMessage(); + Disconnect(); + + if(!m_pConfigManager->Save()) + { + char aError[128]; + str_format(aError, sizeof(aError), Localize("Saving settings to '%s' failed"), CONFIG_FILE); + m_vQuittingWarnings.emplace_back(Localize("Error saving settings"), aError); + } + m_Fifo.Shutdown(); + m_Http.Shutdown(); + Engine()->ShutdownJobs(); + GameClient()->RenderShutdownMessage(); GameClient()->OnShutdown(); - Disconnect(); + delete m_pEditor; - // close socket + // close sockets for(unsigned int i = 0; i < std::size(m_aNetClient); i++) m_aNetClient[i].Close(); - delete m_pEditor; - // shutdown text render while graphics are still available m_pTextRender->Shutdown(); } @@ -3123,7 +3346,7 @@ bool CClient::InitNetworkClient(char *pError, size_t ErrorSize) if(g_Config.m_Bindaddr[0]) str_format(pError, ErrorSize, "Could not open the network client, try changing or unsetting the bindaddr '%s'.", g_Config.m_Bindaddr); else - str_format(pError, ErrorSize, "Could not open the network client."); + str_copy(pError, "Could not open the network client.", ErrorSize); return false; } } @@ -3167,7 +3390,7 @@ void CClient::Con_DummyConnect(IConsole::IResult *pResult, void *pUserData) void CClient::Con_DummyDisconnect(IConsole::IResult *pResult, void *pUserData) { CClient *pSelf = (CClient *)pUserData; - pSelf->DummyDisconnect(0); + pSelf->DummyDisconnect(nullptr); } void CClient::Con_DummyResetInput(IConsole::IResult *pResult, void *pUserData) @@ -3279,57 +3502,73 @@ void CClient::Con_Screenshot(IConsole::IResult *pResult, void *pUserData) void CClient::Con_StartVideo(IConsole::IResult *pResult, void *pUserData) { - CClient *pSelf = (CClient *)pUserData; + CClient *pSelf = static_cast(pUserData); - if(pSelf->State() != IClient::STATE_DEMOPLAYBACK) + if(pResult->NumArguments()) { - pSelf->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "videorecorder", "Can not start videorecorder outside of demoplayer."); - return; + pSelf->StartVideo(pResult->GetString(0), false); } - - if(!IVideo::Current()) + else { - // wait for idle, so there is no data race - pSelf->Graphics()->WaitForIdle(); - // pause the sound device while creating the video instance - pSelf->Sound()->PauseAudioDevice(); - new CVideo((CGraphics_Threaded *)pSelf->m_pGraphics, pSelf->Sound(), pSelf->Storage(), pSelf->Graphics()->ScreenWidth(), pSelf->Graphics()->ScreenHeight(), ""); - pSelf->Sound()->UnpauseAudioDevice(); - IVideo::Current()->Start(); - bool paused = pSelf->m_DemoPlayer.Info()->m_Info.m_Paused; - if(paused) - IVideo::Current()->Pause(true); + pSelf->StartVideo("video", true); } - else - pSelf->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "videorecorder", "Videorecorder already running."); } -void CClient::StartVideo(IConsole::IResult *pResult, void *pUserData, const char *pVideoName) +void CClient::StartVideo(const char *pFilename, bool WithTimestamp) { - CClient *pSelf = (CClient *)pUserData; + if(State() != IClient::STATE_DEMOPLAYBACK) + { + log_error("videorecorder", "Video can only be recorded in demo player."); + return; + } - if(pSelf->State() != IClient::STATE_DEMOPLAYBACK) - pSelf->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "videorecorder", "Can not start videorecorder outside of demoplayer."); + if(IVideo::Current()) + { + log_error("videorecorder", "Already recording."); + return; + } - pSelf->m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "demo_render", pVideoName); - if(!IVideo::Current()) + char aFilename[IO_MAX_PATH_LENGTH]; + if(WithTimestamp) { - // wait for idle, so there is no data race - pSelf->Graphics()->WaitForIdle(); - // pause the sound device while creating the video instance - pSelf->Sound()->PauseAudioDevice(); - new CVideo((CGraphics_Threaded *)pSelf->m_pGraphics, pSelf->Sound(), pSelf->Storage(), pSelf->Graphics()->ScreenWidth(), pSelf->Graphics()->ScreenHeight(), pVideoName); - pSelf->Sound()->UnpauseAudioDevice(); - IVideo::Current()->Start(); + char aTimestamp[20]; + str_timestamp(aTimestamp, sizeof(aTimestamp)); + str_format(aFilename, sizeof(aFilename), "videos/%s_%s.mp4", pFilename, aTimestamp); } else - pSelf->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "videorecorder", "Videorecorder already running."); + { + str_format(aFilename, sizeof(aFilename), "videos/%s.mp4", pFilename); + } + + // wait for idle, so there is no data race + Graphics()->WaitForIdle(); + // pause the sound device while creating the video instance + Sound()->PauseAudioDevice(); + new CVideo(Graphics(), Sound(), Storage(), Graphics()->ScreenWidth(), Graphics()->ScreenHeight(), aFilename); + Sound()->UnpauseAudioDevice(); + if(!IVideo::Current()->Start()) + { + log_error("videorecorder", "Failed to start recording to '%s'", aFilename); + m_DemoPlayer.Stop("Failed to start video recording. See local console for details."); + return; + } + if(m_DemoPlayer.Info()->m_Info.m_Paused) + { + IVideo::Current()->Pause(true); + } + log_info("videorecorder", "Recording to '%s'", aFilename); } void CClient::Con_StopVideo(IConsole::IResult *pResult, void *pUserData) { - if(IVideo::Current()) - IVideo::Current()->Stop(); + if(!IVideo::Current()) + { + log_error("videorecorder", "Not recording."); + return; + } + + IVideo::Current()->Stop(); + log_info("videorecorder", "Stopped recording."); } #endif @@ -3486,26 +3725,40 @@ void CClient::SaveReplay(const int Length, const char *pFilename) } if(!DemoRecorder(RECORDER_REPLAYS)->IsRecording()) + { m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", "ERROR: demorecorder isn't recording. Try to rejoin to fix that."); + } else if(DemoRecorder(RECORDER_REPLAYS)->Length() < 1) + { m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", "ERROR: demorecorder isn't recording for at least 1 second."); + } else { - // First we stop the recorder to slice correctly the demo after - DemoRecorder_Stop(RECORDER_REPLAYS); - - char aDate[64]; - str_timestamp(aDate, sizeof(aDate)); - char aFilename[IO_MAX_PATH_LENGTH]; - if(str_comp(pFilename, "") == 0) - str_format(aFilename, sizeof(aFilename), "demos/replays/%s_%s (replay).demo", m_aCurrentMap, aDate); + if(pFilename[0] == '\0') + { + char aTimestamp[20]; + str_timestamp(aTimestamp, sizeof(aTimestamp)); + str_format(aFilename, sizeof(aFilename), "demos/replays/%s_%s_(replay).demo", m_aCurrentMap, aTimestamp); + } else + { str_format(aFilename, sizeof(aFilename), "demos/replays/%s.demo", pFilename); + IOHANDLE Handle = m_pStorage->OpenFile(aFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE); + if(!Handle) + { + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "replay", "ERROR: invalid filename. Try a different one!"); + return; + } + io_close(Handle); + m_pStorage->RemoveFile(aFilename, IStorage::TYPE_SAVE); + } - char *pSrc = m_aDemoRecorder[RECORDER_REPLAYS].GetCurrentFilename(); + // Stop the recorder to correctly slice the demo after + DemoRecorder(RECORDER_REPLAYS)->Stop(IDemoRecorder::EStopMode::KEEP_FILE); // Slice the demo to get only the last cl_replay_length seconds + const char *pSrc = m_aDemoRecorder[RECORDER_REPLAYS].CurrentFilename(); const int EndTick = GameTick(g_Config.m_ClDummy); const int StartTick = EndTick - Length * GameTickSpeed(); @@ -3517,7 +3770,7 @@ void CClient::SaveReplay(const int Length, const char *pFilename) m_EditJobs.push_back(pDemoEditTask); // And we restart the recorder - DemoRecorder_StartReplayRecorder(); + DemoRecorder_UpdateReplayRecorder(); } } @@ -3538,10 +3791,20 @@ const char *CClient::DemoPlayer_Play(const char *pFilename, int StorageType) Disconnect(); m_aNetClient[CONN_MAIN].ResetErrorString(); + SetState(IClient::STATE_LOADING); + SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_LOADING_DEMO); + if((bool)m_LoadingCallback) + m_LoadingCallback(IClient::LOADING_CALLBACK_DETAIL_DEMO); + // try to start playback m_DemoPlayer.SetListener(this); if(m_DemoPlayer.Load(Storage(), m_pConsole, pFilename, StorageType)) + { + DisconnectWithReason(m_DemoPlayer.ErrorMessage()); return m_DemoPlayer.ErrorMessage(); + } + + m_Sixup = m_DemoPlayer.IsSixup(); // load map const CMapInfo *pMapInfo = m_DemoPlayer.GetMapInfo(); @@ -3565,7 +3828,8 @@ const char *CClient::DemoPlayer_Play(const char *pFilename, int StorageType) } } - // setup current info + // setup current server info + mem_zero(&m_CurrentServerInfo, sizeof(m_CurrentServerInfo)); str_copy(m_CurrentServerInfo.m_aMap, pMapInfo->m_aName); m_CurrentServerInfo.m_MapCrc = pMapInfo->m_Crc; m_CurrentServerInfo.m_MapSize = pMapInfo->m_Size; @@ -3577,12 +3841,12 @@ const char *CClient::DemoPlayer_Play(const char *pFilename, int StorageType) for(int SnapshotType = 0; SnapshotType < NUM_SNAPSHOT_TYPES; SnapshotType++) { - m_aapSnapshots[g_Config.m_ClDummy][SnapshotType] = &m_aDemorecSnapshotHolders[SnapshotType]; - m_aapSnapshots[g_Config.m_ClDummy][SnapshotType]->m_pSnap = (CSnapshot *)&m_aaaDemorecSnapshotData[SnapshotType][0]; - m_aapSnapshots[g_Config.m_ClDummy][SnapshotType]->m_pAltSnap = (CSnapshot *)&m_aaaDemorecSnapshotData[SnapshotType][1]; - m_aapSnapshots[g_Config.m_ClDummy][SnapshotType]->m_SnapSize = 0; - m_aapSnapshots[g_Config.m_ClDummy][SnapshotType]->m_AltSnapSize = 0; - m_aapSnapshots[g_Config.m_ClDummy][SnapshotType]->m_Tick = -1; + m_aapSnapshots[0][SnapshotType] = &m_aDemorecSnapshotHolders[SnapshotType]; + m_aapSnapshots[0][SnapshotType]->m_pSnap = (CSnapshot *)&m_aaaDemorecSnapshotData[SnapshotType][0]; + m_aapSnapshots[0][SnapshotType]->m_pAltSnap = (CSnapshot *)&m_aaaDemorecSnapshotData[SnapshotType][1]; + m_aapSnapshots[0][SnapshotType]->m_SnapSize = 0; + m_aapSnapshots[0][SnapshotType]->m_AltSnapSize = 0; + m_aapSnapshots[0][SnapshotType]->m_Tick = -1; } // enter demo playback state @@ -3600,10 +3864,8 @@ const char *CClient::DemoPlayer_Render(const char *pFilename, int StorageType, c const char *pError = DemoPlayer_Play(pFilename, StorageType); if(pError) return pError; - m_ButtonRender = true; - this->CClient::StartVideo(NULL, this, pVideoName); - m_DemoPlayer.Play(); + StartVideo(pVideoName, false); m_DemoPlayer.SetSpeedIndex(SpeedIndex); if(StartPaused) { @@ -3646,21 +3908,38 @@ void CClient::DemoRecorder_Start(const char *pFilename, bool WithTimestamp, int if(State() != IClient::STATE_ONLINE) { if(Verbose) + { m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demorec/record", "client is not online"); + } } else { char aFilename[IO_MAX_PATH_LENGTH]; if(WithTimestamp) { - char aDate[20]; - str_timestamp(aDate, sizeof(aDate)); - str_format(aFilename, sizeof(aFilename), "demos/%s_%s.demo", pFilename, aDate); + char aTimestamp[20]; + str_timestamp(aTimestamp, sizeof(aTimestamp)); + str_format(aFilename, sizeof(aFilename), "demos/%s_%s.demo", pFilename, aTimestamp); } else + { str_format(aFilename, sizeof(aFilename), "demos/%s.demo", pFilename); + } - m_aDemoRecorder[Recorder].Start(Storage(), m_pConsole, aFilename, GameClient()->NetVersion(), m_aCurrentMap, m_pMap->Sha256(), m_pMap->Crc(), "client", m_pMap->MapSize(), 0, m_pMap->File()); + m_aDemoRecorder[Recorder].Start( + Storage(), + m_pConsole, + aFilename, + IsSixup() ? GameClient()->NetVersion7() : GameClient()->NetVersion(), + m_aCurrentMap, + m_pMap->Sha256(), + m_pMap->Crc(), + "client", + m_pMap->MapSize(), + 0, + m_pMap->File(), + nullptr, + nullptr); } } @@ -3668,10 +3947,12 @@ void CClient::DemoRecorder_HandleAutoStart() { if(g_Config.m_ClAutoDemoRecord) { - DemoRecorder_Stop(RECORDER_AUTO); - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "auto/%s", m_aCurrentMap); - DemoRecorder_Start(aBuf, true, RECORDER_AUTO); + DemoRecorder(RECORDER_AUTO)->Stop(IDemoRecorder::EStopMode::KEEP_FILE); + + char aFilename[IO_MAX_PATH_LENGTH]; + str_format(aFilename, sizeof(aFilename), "auto/%s", m_aCurrentMap); + DemoRecorder_Start(aFilename, true, RECORDER_AUTO); + if(g_Config.m_ClAutoDemoMax) { // clean up auto recorded demos @@ -3679,34 +3960,22 @@ void CClient::DemoRecorder_HandleAutoStart() AutoDemos.Init(Storage(), "demos/auto", "" /* empty for wild card */, ".demo", g_Config.m_ClAutoDemoMax); } } - if(!DemoRecorder(RECORDER_REPLAYS)->IsRecording()) - { - DemoRecorder_StartReplayRecorder(); - } + + DemoRecorder_UpdateReplayRecorder(); } -void CClient::DemoRecorder_StartReplayRecorder() +void CClient::DemoRecorder_UpdateReplayRecorder() { - if(g_Config.m_ClReplays) + if(!g_Config.m_ClReplays && DemoRecorder(RECORDER_REPLAYS)->IsRecording()) { - DemoRecorder_Stop(RECORDER_REPLAYS); - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "replays/replay_tmp-%s", m_aCurrentMap); - DemoRecorder_Start(aBuf, true, RECORDER_REPLAYS); + DemoRecorder(RECORDER_REPLAYS)->Stop(IDemoRecorder::EStopMode::REMOVE_FILE); } -} -void CClient::DemoRecorder_Stop(int Recorder, bool RemoveFile) -{ - m_aDemoRecorder[Recorder].Stop(); - if(RemoveFile) + if(g_Config.m_ClReplays && !DemoRecorder(RECORDER_REPLAYS)->IsRecording()) { - const char *pFilename = m_aDemoRecorder[Recorder].GetCurrentFilename(); - if(pFilename[0] != '\0') - { - Storage()->RemoveFile(pFilename, IStorage::TYPE_SAVE); - m_aDemoRecorder[Recorder].ClearCurrentFilename(); - } + char aFilename[IO_MAX_PATH_LENGTH]; + str_format(aFilename, sizeof(aFilename), "replays/replay_tmp_%s", m_aCurrentMap); + DemoRecorder_Start(aFilename, true, RECORDER_REPLAYS); } } @@ -3739,7 +4008,7 @@ void CClient::Con_Record(IConsole::IResult *pResult, void *pUserData) void CClient::Con_StopRecord(IConsole::IResult *pResult, void *pUserData) { CClient *pSelf = (CClient *)pUserData; - pSelf->DemoRecorder_Stop(RECORDER_MANUAL); + pSelf->DemoRecorder(RECORDER_MANUAL)->Stop(IDemoRecorder::EStopMode::KEEP_FILE); } void CClient::Con_AddDemoMarker(IConsole::IResult *pResult, void *pUserData) @@ -3769,6 +4038,7 @@ void CClient::UpdateAndSwap() Input()->Update(); Graphics()->Swap(); Graphics()->Clear(0, 0, 0); + m_GlobalTime = (time_get() - m_GlobalStartTime) / (float)time_freq(); } void CClient::ServerBrowserUpdate() @@ -3816,7 +4086,7 @@ int CClient::HandleChecksum(int Conn, CUuid Uuid, CUnpacker *pUnpacker) { return 1; } - if(Start < 0 || Length < 0 || Start > INT_MAX - Length) + if(Start < 0 || Length < 0 || Start > std::numeric_limits::max() - Length) { return 2; } @@ -3908,19 +4178,29 @@ int CClient::HandleChecksum(int Conn, CUuid Uuid, CUnpacker *pUnpacker) void CClient::SwitchWindowScreen(int Index) { - // Todo SDL: remove this when fixed (changing screen when in fullscreen is bugged) - if(g_Config.m_GfxFullscreen) - { - SetWindowParams(0, g_Config.m_GfxBorderless, g_Config.m_GfxFullscreen != 3); - if(Graphics()->SetWindowScreen(Index)) - g_Config.m_GfxScreen = Index; - SetWindowParams(g_Config.m_GfxFullscreen, g_Config.m_GfxBorderless, g_Config.m_GfxFullscreen != 3); - } - else + //Tested on windows 11 64 bit (gtx 1660 super, intel UHD 630 opengl 1.2.0, 3.3.0 and vulkan 1.1.0) + int IsFullscreen = g_Config.m_GfxFullscreen; + int IsBorderless = g_Config.m_GfxBorderless; + + if(!Graphics()->SetWindowScreen(Index)) { - if(Graphics()->SetWindowScreen(Index)) - g_Config.m_GfxScreen = Index; + return; } + + SetWindowParams(3, false); // prevent DDNet to get stretch on monitors + + CVideoMode CurMode; + Graphics()->GetCurrentVideoMode(CurMode, Index); + + const int Depth = CurMode.m_Red + CurMode.m_Green + CurMode.m_Blue > 16 ? 24 : 16; + g_Config.m_GfxColorDepth = Depth; + g_Config.m_GfxScreenWidth = CurMode.m_WindowWidth; + g_Config.m_GfxScreenHeight = CurMode.m_WindowHeight; + g_Config.m_GfxScreenRefreshRate = CurMode.m_RefreshRate; + + Graphics()->ResizeToScreen(); + + SetWindowParams(IsFullscreen, IsBorderless); } void CClient::ConchainWindowScreen(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) @@ -3935,11 +4215,11 @@ void CClient::ConchainWindowScreen(IConsole::IResult *pResult, void *pUserData, pfnCallback(pResult, pCallbackUserData); } -void CClient::SetWindowParams(int FullscreenMode, bool IsBorderless, bool AllowResizing) +void CClient::SetWindowParams(int FullscreenMode, bool IsBorderless) { g_Config.m_GfxFullscreen = clamp(FullscreenMode, 0, 3); g_Config.m_GfxBorderless = (int)IsBorderless; - Graphics()->SetWindowParams(FullscreenMode, IsBorderless, AllowResizing); + Graphics()->SetWindowParams(FullscreenMode, IsBorderless); } void CClient::ConchainFullscreen(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) @@ -3948,7 +4228,7 @@ void CClient::ConchainFullscreen(IConsole::IResult *pResult, void *pUserData, IC if(pSelf->Graphics() && pResult->NumArguments()) { if(g_Config.m_GfxFullscreen != pResult->GetInteger(0)) - pSelf->SetWindowParams(pResult->GetInteger(0), g_Config.m_GfxBorderless, pResult->GetInteger(0) != 3); + pSelf->SetWindowParams(pResult->GetInteger(0), g_Config.m_GfxBorderless); } else pfnCallback(pResult, pCallbackUserData); @@ -3960,7 +4240,7 @@ void CClient::ConchainWindowBordered(IConsole::IResult *pResult, void *pUserData if(pSelf->Graphics() && pResult->NumArguments()) { if(!g_Config.m_GfxFullscreen && (g_Config.m_GfxBorderless != pResult->GetInteger(0))) - pSelf->SetWindowParams(g_Config.m_GfxFullscreen, !g_Config.m_GfxBorderless, g_Config.m_GfxFullscreen != 3); + pSelf->SetWindowParams(g_Config.m_GfxFullscreen, !g_Config.m_GfxBorderless); } else pfnCallback(pResult, pCallbackUserData); @@ -4007,7 +4287,7 @@ void CClient::ConchainWindowResize(IConsole::IResult *pResult, void *pUserData, pfnCallback(pResult, pCallbackUserData); if(pSelf->Graphics() && pResult->NumArguments()) { - pSelf->Graphics()->Resize(g_Config.m_GfxScreenWidth, g_Config.m_GfxScreenHeight, g_Config.m_GfxScreenRefreshRate); + pSelf->Graphics()->ResizeToScreen(); } } @@ -4033,17 +4313,7 @@ void CClient::ConchainReplays(IConsole::IResult *pResult, void *pUserData, ICons pfnCallback(pResult, pCallbackUserData); if(pResult->NumArguments()) { - int Status = pResult->GetInteger(0); - if(Status == 0) - { - // stop recording and remove the tmp demo file - pSelf->DemoRecorder_Stop(RECORDER_REPLAYS, true); - } - else - { - // start recording - pSelf->DemoRecorder_HandleAutoStart(); - } + pSelf->DemoRecorder_UpdateReplayRecorder(); } } @@ -4070,16 +4340,6 @@ void CClient::ConchainStdoutOutputLevel(IConsole::IResult *pResult, void *pUserD void CClient::RegisterCommands() { m_pConsole = Kernel()->RequestInterface(); - // register server dummy commands for tab completion - m_pConsole->Register("kick", "i[id] ?r[reason]", CFGFLAG_SERVER, 0, 0, "Kick player with specified id for any reason"); - m_pConsole->Register("ban", "s[ip|id] ?i[minutes] r[reason]", CFGFLAG_SERVER, 0, 0, "Ban player with ip/id for x minutes for any reason"); - m_pConsole->Register("unban", "r[ip]", CFGFLAG_SERVER, 0, 0, "Unban ip"); - m_pConsole->Register("bans", "?i[page]", CFGFLAG_SERVER, 0, 0, "Show banlist (page 0 by default, 20 entries per page)"); - m_pConsole->Register("status", "?r[name]", CFGFLAG_SERVER, 0, 0, "List players containing name or all players"); - m_pConsole->Register("shutdown", "", CFGFLAG_SERVER, 0, 0, "Shut down"); - m_pConsole->Register("record", "r[file]", CFGFLAG_SERVER, 0, 0, "Record to a file"); - m_pConsole->Register("stoprecord", "", CFGFLAG_SERVER, 0, 0, "Stop recording"); - m_pConsole->Register("reload", "", CFGFLAG_SERVER, 0, 0, "Reload the map"); m_pConsole->Register("dummy_connect", "", CFGFLAG_CLIENT, Con_DummyConnect, this, "Connect dummy"); m_pConsole->Register("dummy_disconnect", "", CFGFLAG_CLIENT, Con_DummyDisconnect, this, "Disconnect dummy"); @@ -4095,25 +4355,25 @@ void CClient::RegisterCommands() m_pConsole->Register("screenshot", "", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Screenshot, this, "Take a screenshot"); #if defined(CONF_VIDEORECORDER) - m_pConsole->Register("start_video", "", CFGFLAG_CLIENT, Con_StartVideo, this, "Start recording a video"); + m_pConsole->Register("start_video", "?r[file]", CFGFLAG_CLIENT, Con_StartVideo, this, "Start recording a video"); m_pConsole->Register("stop_video", "", CFGFLAG_CLIENT, Con_StopVideo, this, "Stop recording a video"); #endif m_pConsole->Register("rcon", "r[rcon-command]", CFGFLAG_CLIENT, Con_Rcon, this, "Send specified command to rcon"); m_pConsole->Register("rcon_auth", "r[password]", CFGFLAG_CLIENT, Con_RconAuth, this, "Authenticate to rcon"); m_pConsole->Register("rcon_login", "s[username] r[password]", CFGFLAG_CLIENT, Con_RconLogin, this, "Authenticate to rcon with a username"); - m_pConsole->Register("play", "r[file]", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Play, this, "Play the file specified"); - m_pConsole->Register("record", "?r[file]", CFGFLAG_CLIENT, Con_Record, this, "Record to the file"); - m_pConsole->Register("stoprecord", "", CFGFLAG_CLIENT, Con_StopRecord, this, "Stop recording"); + m_pConsole->Register("play", "r[file]", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_Play, this, "Play back a demo"); + m_pConsole->Register("record", "?r[file]", CFGFLAG_CLIENT, Con_Record, this, "Start recording a demo"); + m_pConsole->Register("stoprecord", "", CFGFLAG_CLIENT, Con_StopRecord, this, "Stop recording a demo"); m_pConsole->Register("add_demomarker", "", CFGFLAG_CLIENT, Con_AddDemoMarker, this, "Add demo timeline marker"); m_pConsole->Register("begin_favorite_group", "", CFGFLAG_CLIENT, Con_BeginFavoriteGroup, this, "Use this before `add_favorite` to group favorites. End with `end_favorite_group`"); m_pConsole->Register("end_favorite_group", "", CFGFLAG_CLIENT, Con_EndFavoriteGroup, this, "Use this after `add_favorite` to group favorites. Start with `begin_favorite_group`"); m_pConsole->Register("add_favorite", "s[host|ip] ?s['allow_ping']", CFGFLAG_CLIENT, Con_AddFavorite, this, "Add a server as a favorite"); m_pConsole->Register("remove_favorite", "r[host|ip]", CFGFLAG_CLIENT, Con_RemoveFavorite, this, "Remove a server from favorites"); - m_pConsole->Register("demo_slice_start", "", CFGFLAG_CLIENT, Con_DemoSliceBegin, this, "Mark the beginning of a cut"); - m_pConsole->Register("demo_slice_end", "", CFGFLAG_CLIENT, Con_DemoSliceEnd, this, "Mark the end of a cut"); - m_pConsole->Register("demo_play", "", CFGFLAG_CLIENT, Con_DemoPlay, this, "Play demo"); - m_pConsole->Register("demo_speed", "i[speed]", CFGFLAG_CLIENT, Con_DemoSpeed, this, "Set demo speed"); + m_pConsole->Register("demo_slice_start", "", CFGFLAG_CLIENT, Con_DemoSliceBegin, this, "Mark the beginning of a demo cut"); + m_pConsole->Register("demo_slice_end", "", CFGFLAG_CLIENT, Con_DemoSliceEnd, this, "Mark the end of a demo cut"); + m_pConsole->Register("demo_play", "", CFGFLAG_CLIENT, Con_DemoPlay, this, "Play/pause the current demo"); + m_pConsole->Register("demo_speed", "i[speed]", CFGFLAG_CLIENT, Con_DemoSpeed, this, "Set current demo speed"); m_pConsole->Register("save_replay", "?i[length] ?r[filename]", CFGFLAG_CLIENT, Con_SaveReplay, this, "Save a replay of the last defined amount of seconds"); m_pConsole->Register("benchmark_quit", "i[seconds] r[file]", CFGFLAG_CLIENT | CFGFLAG_STORE, Con_BenchmarkQuit, this, "Benchmark frame times for number of seconds to file, then quit"); @@ -4127,8 +4387,20 @@ void CClient::RegisterCommands() // used for server browser update m_pConsole->Chain("br_filter_string", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_exclude_string", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_full", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_empty", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_spectators", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_friends", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_country", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_country_index", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_pw", ConchainServerBrowserUpdate, this); m_pConsole->Chain("br_filter_gametype", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_gametype_strict", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_connecting_players", ConchainServerBrowserUpdate, this); m_pConsole->Chain("br_filter_serveraddress", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_unfinished_map", ConchainServerBrowserUpdate, this); + m_pConsole->Chain("br_filter_login", ConchainServerBrowserUpdate, this); m_pConsole->Chain("add_favorite", ConchainServerBrowserUpdate, this); m_pConsole->Chain("remove_favorite", ConchainServerBrowserUpdate, this); m_pConsole->Chain("end_favorite_group", ConchainServerBrowserUpdate, this); @@ -4143,12 +4415,6 @@ void CClient::RegisterCommands() m_pConsole->Chain("loglevel", ConchainLoglevel, this); m_pConsole->Chain("stdout_output_level", ConchainStdoutOutputLevel, this); - - // DDRace - -#define CONSOLE_COMMAND(name, params, flags, callback, userdata, help) m_pConsole->Register(name, params, flags, 0, 0, help); -#include -#undef CONSOLE_COMMAND } static CClient *CreateClient() @@ -4215,6 +4481,26 @@ static bool SaveUnknownCommandCallback(const char *pCommand, void *pUser) return true; } +static Uint32 GetSdlMessageBoxFlags(IClient::EMessageBoxType Type) +{ + switch(Type) + { + case IClient::MESSAGE_BOX_TYPE_ERROR: + return SDL_MESSAGEBOX_ERROR; + case IClient::MESSAGE_BOX_TYPE_WARNING: + return SDL_MESSAGEBOX_WARNING; + case IClient::MESSAGE_BOX_TYPE_INFO: + return SDL_MESSAGEBOX_INFORMATION; + } + dbg_assert(false, "Type invalid"); + return 0; +} + +static void ShowMessageBox(const char *pTitle, const char *pMessage, IClient::EMessageBoxType Type = IClient::MESSAGE_BOX_TYPE_ERROR) +{ + SDL_ShowSimpleMessageBox(GetSdlMessageBoxFlags(Type), pTitle, pMessage, nullptr); +} + /* Server Time Client Mirror Time @@ -4230,35 +4516,30 @@ static bool SaveUnknownCommandCallback(const char *pCommand, void *pUser) #if defined(CONF_PLATFORM_MACOS) extern "C" int TWMain(int argc, const char **argv) #elif defined(CONF_PLATFORM_ANDROID) +static int gs_AndroidStarted = false; extern "C" __attribute__((visibility("default"))) int SDL_main(int argc, char *argv[]); -extern "C" void InitAndroid(); - int SDL_main(int argc, char *argv2[]) #else int main(int argc, const char **argv) #endif { + const int64_t MainStart = time_get(); + #if defined(CONF_PLATFORM_ANDROID) const char **argv = const_cast(argv2); + // Android might not unload the library from memory, causing globals like gs_AndroidStarted + // not to be initialized correctly when starting the app again. + if(gs_AndroidStarted) + { + ::ShowMessageBox("Android Error", "The app was started, but not closed properly, this causes bugs. Please restart or manually close this task."); + std::exit(0); + } + gs_AndroidStarted = true; #elif defined(CONF_FAMILY_WINDOWS) CWindowsComLifecycle WindowsComLifecycle(true); #endif CCmdlineFix CmdlineFix(&argc, &argv); - bool Silent = false; - - for(int i = 1; i < argc; i++) - { - if(str_comp("-s", argv[i]) == 0 || str_comp("--silent", argv[i]) == 0) - { - Silent = true; - } - } - -#if defined(CONF_PLATFORM_ANDROID) - InitAndroid(); -#endif - #if defined(CONF_EXCEPTION_HANDLING) init_exception_handler(); #endif @@ -4268,6 +4549,14 @@ int main(int argc, const char **argv) #if defined(CONF_PLATFORM_ANDROID) pStdoutLogger = std::shared_ptr(log_logger_android()); #else + bool Silent = false; + for(int i = 1; i < argc; i++) + { + if(str_comp("-s", argv[i]) == 0 || str_comp("--silent", argv[i]) == 0) + { + Silent = true; + } + } if(!Silent) { pStdoutLogger = std::shared_ptr(log_logger_stdout()); @@ -4285,6 +4574,17 @@ int main(int argc, const char **argv) vpLoggers.push_back(pFutureAssertionLogger); log_set_global_logger(log_logger_collection(std::move(vpLoggers)).release()); +#if defined(CONF_PLATFORM_ANDROID) + // Initialize Android after logger is available + const char *pAndroidInitError = InitAndroid(); + if(pAndroidInitError != nullptr) + { + log_error("android", "%s", pAndroidInitError); + ::ShowMessageBox("Android Error", pAndroidInitError); + std::exit(0); + } +#endif + std::stack> CleanerFunctions; std::function PerformCleanup = [&CleanerFunctions]() mutable { while(!CleanerFunctions.empty()) @@ -4295,7 +4595,17 @@ int main(int argc, const char **argv) }; std::function PerformFinalCleanup = []() { #ifdef CONF_PLATFORM_ANDROID - // properly close this native thread, so globals are destructed + // Forcefully terminate the entire process, to ensure that static variables + // will be initialized correctly when the app is started again after quitting. + // Returning from the main function is not enough, as this only results in the + // native thread terminating, but the Java thread will continue. Java does not + // support unloading libraries once they have been loaded, so all static + // variables will not have their expected initial values anymore when the app + // is started again after quitting. The variable gs_AndroidStarted above is + // used to check that static variables have been initialized properly. + // TODO: This is not the correct way to close an activity on Android, as it + // ignores the activity lifecycle entirely, which may cause issues if + // we ever used any global resources like the camera. std::exit(0); #endif }; @@ -4320,6 +4630,8 @@ int main(int argc, const char **argv) pKernel->RegisterInterface(pClient, false); pClient->RegisterInterfaces(); CleanerFunctions.emplace([pKernel, pClient]() { + // Ensure that the assert handler doesn't use the client/graphics after they've been destroyed + dbg_assert_set_handler(nullptr); pKernel->Shutdown(); delete pKernel; delete pClient; @@ -4332,8 +4644,8 @@ int main(int argc, const char **argv) char aVersionStr[128]; if(!os_version_str(aVersionStr, sizeof(aVersionStr))) str_copy(aVersionStr, "unknown"); - char aGPUInfo[256]; - pClient->GetGPUInfoString(aGPUInfo); + char aGpuInfo[256]; + pClient->GetGpuInfoString(aGpuInfo); char aMessage[768]; str_format(aMessage, sizeof(aMessage), "An assertion error occurred. Please write down or take a screenshot of the following information and report this error.\n" @@ -4344,7 +4656,7 @@ int main(int argc, const char **argv) "OS version: %s\n\n" "%s", // GPU info pMsg, CONF_PLATFORM_STRING, GAME_RELEASE_VERSION, GIT_SHORTREV_HASH != nullptr ? GIT_SHORTREV_HASH : "", aVersionStr, - aGPUInfo); + aGpuInfo); pClient->ShowMessageBox("Assertion Error", aMessage); // Client will crash due to assertion, don't call PerformAllCleanup in this inconsistent state }); @@ -4357,7 +4669,7 @@ int main(int argc, const char **argv) delete pEngine; }); - IStorage *pStorage = CreateStorage(IStorage::STORAGETYPE_CLIENT, argc, (const char **)argv); + IStorage *pStorage = CreateStorage(IStorage::STORAGETYPE_CLIENT, argc, argv); pKernel->RegisterInterface(pStorage); pFutureAssertionLogger->Set(CreateAssertionLogger(pStorage, GAME_NAME)); @@ -4375,7 +4687,7 @@ int main(int argc, const char **argv) if(RandInitFailed) { const char *pError = "Failed to initialize the secure RNG."; - dbg_msg("secure", "%s", pError); + log_error("secure", "%s", pError); pClient->ShowMessageBox("Secure RNG Error", pError); PerformAllCleanup(); return -1; @@ -4436,7 +4748,7 @@ int main(int argc, const char **argv) if(!pConsole->ExecuteFile(CONFIG_FILE)) { const char *pError = "Failed to load config from '" CONFIG_FILE "'."; - dbg_msg("client", "%s", pError); + log_error("client", "%s", pError); pClient->ShowMessageBox("Config File Error", pError); PerformAllCleanup(); return -1; @@ -4467,7 +4779,7 @@ int main(int argc, const char **argv) // parse the command line arguments pConsole->SetUnknownCommandCallback(UnknownArgumentCallback, pClient); - pConsole->ParseArguments(argc - 1, (const char **)&argv[1]); + pConsole->ParseArguments(argc - 1, &argv[1]); pConsole->SetUnknownCommandCallback(IConsole::EmptyUnknownCommandCallback, nullptr); if(pSteam->GetConnectAddress()) @@ -4476,9 +4788,9 @@ int main(int argc, const char **argv) pSteam->ClearConnectAddress(); } - const int Mode = g_Config.m_Logappend ? IOFLAG_APPEND : IOFLAG_WRITE; if(g_Config.m_Logfile[0]) { + const int Mode = g_Config.m_Logappend ? IOFLAG_APPEND : IOFLAG_WRITE; IOHANDLE Logfile = pStorage->OpenFile(g_Config.m_Logfile, Mode, IStorage::TYPE_SAVE_OR_ABSOLUTE); if(Logfile) { @@ -4486,15 +4798,29 @@ int main(int argc, const char **argv) } else { - dbg_msg("client", "failed to open '%s' for logging", g_Config.m_Logfile); + log_error("client", "failed to open '%s' for logging", g_Config.m_Logfile); + pFutureFileLogger->Set(log_logger_noop()); } } + else + { + pFutureFileLogger->Set(log_logger_noop()); + } // Register protocol and file extensions #if defined(CONF_FAMILY_WINDOWS) pClient->ShellRegister(); #endif + // Do not automatically translate touch events to mouse events and vice versa. + SDL_SetHint("SDL_TOUCH_MOUSE_EVENTS", "0"); + SDL_SetHint("SDL_MOUSE_TOUCH_EVENTS", "0"); + + // Support longer IME composition strings (enables SDL_TEXTEDITING_EXT). +#if SDL_VERSION_ATLEAST(2, 0, 22) + SDL_SetHint(SDL_HINT_IME_SUPPORT_EXTENDED_TEXT, "1"); +#endif + #if defined(CONF_PLATFORM_MACOS) // Hints will not be set if there is an existing override hint or environment variable that takes precedence. // So this respects cli environment overrides. @@ -4507,33 +4833,54 @@ int main(int argc, const char **argv) SDL_SetHint("SDL_IME_SHOW_UI", "1"); #endif +#if defined(CONF_PLATFORM_ANDROID) + // Trap the Android back button so it can be handled in our code reliably + // instead of letting the system handle it. + SDL_SetHint("SDL_ANDROID_TRAP_BACK_BUTTON", "1"); + // Force landscape screen orientation. + SDL_SetHint("SDL_IOS_ORIENTATIONS", "LandscapeLeft LandscapeRight"); +#endif + // init SDL if(SDL_Init(0) < 0) { char aError[256]; str_format(aError, sizeof(aError), "Unable to initialize SDL base: %s", SDL_GetError()); - dbg_msg("client", "%s", aError); + log_error("client", "%s", aError); pClient->ShowMessageBox("SDL Error", aError); PerformAllCleanup(); return -1; } // run the client - dbg_msg("client", "starting..."); + log_trace("client", "initialization finished after %.2fms, starting...", (time_get() - MainStart) * 1000.0f / (float)time_freq()); pClient->Run(); const bool Restarting = pClient->State() == CClient::STATE_RESTARTING; +#if !defined(CONF_PLATFORM_ANDROID) char aRestartBinaryPath[IO_MAX_PATH_LENGTH]; if(Restarting) { pStorage->GetBinaryPath(PLAT_CLIENT_EXEC, aRestartBinaryPath, sizeof(aRestartBinaryPath)); } +#endif + + std::vector vQuittingWarnings = pClient->QuittingWarnings(); PerformCleanup(); + for(const SWarning &Warning : vQuittingWarnings) + { + ::ShowMessageBox(Warning.m_aWarningTitle, Warning.m_aWarningMsg); + } + if(Restarting) { - shell_execute(aRestartBinaryPath); +#if defined(CONF_PLATFORM_ANDROID) + RestartAndroidApp(); +#else + shell_execute(aRestartBinaryPath, EShellExecuteWindowState::FOREGROUND); +#endif } PerformFinalCleanup(); @@ -4568,13 +4915,28 @@ void CClient::RaceRecord_Start(const char *pFilename) if(State() != IClient::STATE_ONLINE) m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demorec/record", "client is not online"); else - m_aDemoRecorder[RECORDER_RACE].Start(Storage(), m_pConsole, pFilename, GameClient()->NetVersion(), m_aCurrentMap, m_pMap->Sha256(), m_pMap->Crc(), "client", m_pMap->MapSize(), 0, m_pMap->File()); + m_aDemoRecorder[RECORDER_RACE].Start( + Storage(), + m_pConsole, + pFilename, + IsSixup() ? GameClient()->NetVersion7() : GameClient()->NetVersion(), + m_aCurrentMap, + m_pMap->Sha256(), + m_pMap->Crc(), + "client", + m_pMap->MapSize(), + 0, + m_pMap->File(), + nullptr, + nullptr); } void CClient::RaceRecord_Stop() { if(m_aDemoRecorder[RECORDER_RACE].IsRecording()) - m_aDemoRecorder[RECORDER_RACE].Stop(); + { + m_aDemoRecorder[RECORDER_RACE].Stop(IDemoRecorder::EStopMode::KEEP_FILE); + } } bool CClient::RaceRecord_IsRecording() @@ -4584,6 +4946,9 @@ bool CClient::RaceRecord_IsRecording() void CClient::RequestDDNetInfo() { + if(m_pDDNetInfoTask && !m_pDDNetInfoTask->Done()) + return; + char aUrl[256]; str_copy(aUrl, DDNET_INFO_URL); @@ -4596,10 +4961,10 @@ void CClient::RequestDDNetInfo() } // Use ipv4 so we can know the ingame ip addresses of players before they join game servers - m_pDDNetInfoTask = HttpGetFile(aUrl, Storage(), m_aDDNetInfoTmp, IStorage::TYPE_SAVE); + m_pDDNetInfoTask = HttpGet(aUrl); m_pDDNetInfoTask->Timeout(CTimeout{10000, 0, 500, 10}); m_pDDNetInfoTask->IpResolve(IPRESOLVE::V4); - Engine()->AddJob(m_pDDNetInfoTask); + Http()->Run(m_pDDNetInfoTask); } int CClient::GetPredictionTime() @@ -4694,6 +5059,51 @@ int CClient::UdpConnectivity(int NetType) return Connectivity; } +bool CClient::ViewLink(const char *pLink) +{ +#if defined(CONF_PLATFORM_ANDROID) + if(SDL_OpenURL(pLink) == 0) + { + return true; + } + log_error("client", "Failed to open link '%s' (%s)", pLink, SDL_GetError()); + return false; +#else + if(open_link(pLink)) + { + return true; + } + log_error("client", "Failed to open link '%s'", pLink); + return false; +#endif +} + +bool CClient::ViewFile(const char *pFilename) +{ +#if defined(CONF_PLATFORM_MACOS) + return ViewLink(pFilename); +#else + // Create a file link so the path can contain forward and + // backward slashes. But the file link must be absolute. + char aWorkingDir[IO_MAX_PATH_LENGTH]; + if(fs_is_relative_path(pFilename)) + { + if(!fs_getcwd(aWorkingDir, sizeof(aWorkingDir))) + { + log_error("client", "Failed to open file '%s' (failed to get working directory)", pFilename); + return false; + } + str_append(aWorkingDir, "/"); + } + else + aWorkingDir[0] = '\0'; + + char aFileLink[IO_MAX_PATH_LENGTH]; + str_format(aFileLink, sizeof(aFileLink), "file://%s%s", aWorkingDir, pFilename); + return ViewLink(aFileLink); +#endif +} + #if defined(CONF_FAMILY_WINDOWS) void CClient::ShellRegister() { @@ -4701,19 +5111,19 @@ void CClient::ShellRegister() Storage()->GetBinaryPathAbsolute(PLAT_CLIENT_EXEC, aFullPath, sizeof(aFullPath)); if(!aFullPath[0]) { - dbg_msg("client", "Failed to register protocol and file extensions: could not determine absolute path"); + log_error("client", "Failed to register protocol and file extensions: could not determine absolute path"); return; } bool Updated = false; if(!shell_register_protocol("ddnet", aFullPath, &Updated)) - dbg_msg("client", "Failed to register ddnet protocol"); + log_error("client", "Failed to register ddnet protocol"); if(!shell_register_extension(".map", "Map File", GAME_NAME, aFullPath, &Updated)) - dbg_msg("client", "Failed to register .map file extension"); + log_error("client", "Failed to register .map file extension"); if(!shell_register_extension(".demo", "Demo File", GAME_NAME, aFullPath, &Updated)) - dbg_msg("client", "Failed to register .demo file extension"); + log_error("client", "Failed to register .demo file extension"); if(!shell_register_application(GAME_NAME, aFullPath, &Updated)) - dbg_msg("client", "Failed to register application"); + log_error("client", "Failed to register application"); if(Updated) shell_update(); } @@ -4724,54 +5134,39 @@ void CClient::ShellUnregister() Storage()->GetBinaryPathAbsolute(PLAT_CLIENT_EXEC, aFullPath, sizeof(aFullPath)); if(!aFullPath[0]) { - dbg_msg("client", "Failed to unregister protocol and file extensions: could not determine absolute path"); + log_error("client", "Failed to unregister protocol and file extensions: could not determine absolute path"); return; } bool Updated = false; if(!shell_unregister_class("ddnet", &Updated)) - dbg_msg("client", "Failed to unregister ddnet protocol"); + log_error("client", "Failed to unregister ddnet protocol"); if(!shell_unregister_class(GAME_NAME ".map", &Updated)) - dbg_msg("client", "Failed to unregister .map file extension"); + log_error("client", "Failed to unregister .map file extension"); if(!shell_unregister_class(GAME_NAME ".demo", &Updated)) - dbg_msg("client", "Failed to unregister .demo file extension"); + log_error("client", "Failed to unregister .demo file extension"); if(!shell_unregister_application(aFullPath, &Updated)) - dbg_msg("client", "Failed to unregister application"); + log_error("client", "Failed to unregister application"); if(Updated) shell_update(); } #endif -static Uint32 GetSdlMessageBoxFlags(IClient::EMessageBoxType Type) -{ - switch(Type) - { - case IClient::MESSAGE_BOX_TYPE_ERROR: - return SDL_MESSAGEBOX_ERROR; - case IClient::MESSAGE_BOX_TYPE_WARNING: - return SDL_MESSAGEBOX_WARNING; - case IClient::MESSAGE_BOX_TYPE_INFO: - return SDL_MESSAGEBOX_INFORMATION; - } - dbg_assert(false, "Type invalid"); - return 0; -} - void CClient::ShowMessageBox(const char *pTitle, const char *pMessage, EMessageBoxType Type) { if(m_pGraphics == nullptr || !m_pGraphics->ShowMessageBox(GetSdlMessageBoxFlags(Type), pTitle, pMessage)) - SDL_ShowSimpleMessageBox(GetSdlMessageBoxFlags(Type), pTitle, pMessage, nullptr); + ::ShowMessageBox(pTitle, pMessage, Type); } -void CClient::GetGPUInfoString(char (&aGPUInfo)[256]) +void CClient::GetGpuInfoString(char (&aGpuInfo)[256]) { if(m_pGraphics != nullptr && m_pGraphics->IsBackendInitialized()) { - str_format(aGPUInfo, std::size(aGPUInfo), "GPU: %s - %s - %s", m_pGraphics->GetVendorString(), m_pGraphics->GetRendererString(), m_pGraphics->GetVersionString()); + str_format(aGpuInfo, std::size(aGpuInfo), "GPU: %s - %s - %s", m_pGraphics->GetVendorString(), m_pGraphics->GetRendererString(), m_pGraphics->GetVersionString()); } else { - str_copy(aGPUInfo, "Graphics backend was not yet initialized."); + str_copy(aGpuInfo, "Graphics backend was not yet initialized."); } } diff --git a/src/engine/client/client.h b/src/engine/client/client.h index 7317ff52df..6113c4acf6 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include "graph.h" @@ -76,6 +77,7 @@ class CClient : public IClient, public CDemoPlayer::IListener IStorage *m_pStorage = nullptr; IEngineTextRender *m_pTextRender = nullptr; IUpdater *m_pUpdater = nullptr; + CHttp m_Http; CNetClient m_aNetClient[NUM_CONNS]; CDemoPlayer m_DemoPlayer; @@ -90,7 +92,8 @@ class CClient : public IClient, public CDemoPlayer::IListener char m_aConnectAddressStr[MAX_SERVER_ADDRESSES * NETADDR_MAXSTRSIZE] = ""; - CUuid m_ConnectionID = UUID_ZEROED; + CUuid m_ConnectionId = UUID_ZEROED; + bool m_Sixup; bool m_HaveGlobalTcpAddr = false; NETADDR m_GlobalTcpAddr = NETADDR_ZEROED; @@ -108,7 +111,6 @@ class CClient : public IClient, public CDemoPlayer::IListener bool m_AutoStatScreenshotRecycle = false; bool m_AutoCSVRecycle = false; bool m_EditorActive = false; - bool m_SoundInitFailed = false; int m_aAckGameTick[NUM_DUMMIES] = {-1, -1}; int m_aCurrentRecvTick[NUM_DUMMIES] = {0, 0}; @@ -116,9 +118,10 @@ class CClient : public IClient, public CDemoPlayer::IListener char m_aRconUsername[32] = ""; char m_aRconPassword[sizeof(g_Config.m_SvRconPassword)] = ""; int m_UseTempRconCommands = 0; + int m_ExpectedRconCommands = -1; + int m_GotRconCommands = 0; char m_aPassword[sizeof(g_Config.m_Password)] = ""; bool m_SendPassword = false; - bool m_ButtonRender = false; // version-checking char m_aVersionStr[10] = "0"; @@ -157,7 +160,6 @@ class CClient : public IClient, public CDemoPlayer::IListener SHA256_DIGEST m_MapDetailsSha256 = SHA256_ZEROED; char m_aMapDetailsUrl[256] = ""; - char m_aDDNetInfoTmp[64]; std::shared_ptr m_pDDNetInfoTask = nullptr; // time @@ -177,8 +179,11 @@ class CClient : public IClient, public CDemoPlayer::IListener int m_aCurrentInput[NUM_DUMMIES] = {0, 0}; bool m_LastDummy = false; bool m_DummySendConnInfo = false; + bool m_DummyConnecting = false; bool m_DummyConnected = false; - int m_LastDummyConnectTime = 0; + float m_LastDummyConnectTime = 0.0f; + bool m_DummyReconnectOnReload = false; + bool m_DummyDeactivateOnReconnect = false; // graphs CGraph m_InputtimeMarginGraph; @@ -205,6 +210,8 @@ class CClient : public IClient, public CDemoPlayer::IListener bool m_ServerSentCapabilities = false; CServerCapabilities m_ServerCapabilities; + bool ServerCapAnyPlayerFlag() const override { return m_ServerCapabilities.m_AnyPlayerFlag; } + CServerInfo m_CurrentServerInfo; int64_t m_CurrentServerInfoRequestTime = -1; // >= 0 should request, == -1 got info @@ -229,6 +236,7 @@ class CClient : public IClient, public CDemoPlayer::IListener } m_VersionInfo; std::vector m_vWarnings; + std::vector m_vQuittingWarnings; CFifo m_Fifo; @@ -252,6 +260,9 @@ class CClient : public IClient, public CDemoPlayer::IListener std::shared_ptr m_pFileLogger = nullptr; std::shared_ptr m_pStdoutLogger = nullptr; + // For DummyName function + char m_aAutomaticDummyName[MAX_NAME_LENGTH]; + public: IConfigManager *ConfigManager() { return m_pConfigManager; } CConfig *Config() { return m_pConfig; } @@ -266,6 +277,7 @@ class CClient : public IClient, public CDemoPlayer::IListener IStorage *Storage() { return m_pStorage; } IEngineTextRender *TextRender() { return m_pTextRender; } IUpdater *Updater() { return m_pUpdater; } + IHttp *Http() { return &m_Http; } CClient(); @@ -273,7 +285,7 @@ class CClient : public IClient, public CDemoPlayer::IListener int SendMsg(int Conn, CMsgPacker *pMsg, int Flags) override; // Send via the currently active client (main/dummy) int SendMsgActive(CMsgPacker *pMsg, int Flags) override; - void SendStA(bool Dummy); + void SendInfo(int Conn); void SendEnterGame(int Conn); void SendReady(int Conn); @@ -283,14 +295,13 @@ class CClient : public IClient, public CDemoPlayer::IListener bool UseTempRconCommands() const override { return m_UseTempRconCommands != 0; } void RconAuth(const char *pName, const char *pPassword) override; void Rcon(const char *pCmd) override; + bool ReceivingRconCommands() const override { return m_ExpectedRconCommands > 0; } + float GotRconCommandsPercentage() const override; bool ConnectionProblems() const override; - bool SoundInitFailed() const override { return m_SoundInitFailed; } - IGraphics::CTextureHandle GetDebugFont() const override { return m_DebugFont; } - void DirectInput(int *pInput, int Size); void SendInput(); // TODO: OPT: do this a lot smarter! @@ -299,7 +310,7 @@ class CClient : public IClient, public CDemoPlayer::IListener const char *LatestVersion() const override; // ------ state handling ----- - void SetState(EClientState s); + void SetState(EClientState State); // called when the map is loaded and we should init for a new round void OnEnterGame(bool Dummy); @@ -311,9 +322,10 @@ class CClient : public IClient, public CDemoPlayer::IListener void DummyDisconnect(const char *pReason) override; void DummyConnect() override; - bool DummyConnected() override; - bool DummyConnecting() override; - bool DummyAllowed() override; + bool DummyConnected() const override; + bool DummyConnecting() const override; + bool DummyConnectingDelayed() const override; + bool DummyAllowed() const override; void GetServerInfo(CServerInfo *pServerInfo) const override; void ServerInfoRequest(); @@ -323,11 +335,11 @@ class CClient : public IClient, public CDemoPlayer::IListener // --- int GetPredictionTime() override; - void *SnapGetItem(int SnapID, int Index, CSnapItem *pItem) const override; - int SnapItemSize(int SnapID, int Index) const override; - const void *SnapFindItem(int SnapID, int Type, int ID) const override; - int SnapNumItems(int SnapID) const override; + CSnapItem SnapGetItem(int SnapId, int Index) const override; + const void *SnapFindItem(int SnapId, int Type, int Id) const override; + int SnapNumItems(int SnapId) const override; void SnapSetStaticsize(int ItemType, int Size) override; + void SnapSetStaticsize7(int ItemType, int Size) override; void Render(); void DebugRender(); @@ -336,27 +348,30 @@ class CClient : public IClient, public CDemoPlayer::IListener void Quit() override; const char *PlayerName() const override; - const char *DummyName() const override; + const char *DummyName() override; const char *ErrorString() const override; const char *LoadMap(const char *pName, const char *pFilename, SHA256_DIGEST *pWantedSha256, unsigned WantedCrc); const char *LoadMapSearch(const char *pMapName, SHA256_DIGEST *pWantedSha256, int WantedCrc); + int TranslateSysMsg(int *pMsgId, bool System, CUnpacker *pUnpacker, CPacker *pPacker, CNetChunk *pPacket, bool *pIsExMsg); + void ProcessConnlessPacket(CNetChunk *pPacket); void ProcessServerInfo(int Type, NETADDR *pFrom, const void *pData, int DataSize); void ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy); int UnpackAndValidateSnapshot(CSnapshot *pFrom, CSnapshot *pTo); - void ResetMapDownload(); + void ResetMapDownload(bool ResetActive); void FinishMapDownload(); void RequestDDNetInfo() override; - void ResetDDNetInfo(); - bool IsDDNetInfoChanged(); + void ResetDDNetInfoTask(); void FinishDDNetInfo(); void LoadDDNetInfo(); + bool IsSixup() const override { return m_Sixup; } + const NETADDR &ServerAddress() const override { return *m_aNetClient[CONN_MAIN].ServerAddress(); } int ConnectNetTypes() const override; const char *ConnectAddressString() const override { return m_aConnectAddressStr; } @@ -395,7 +410,7 @@ class CClient : public IClient, public CDemoPlayer::IListener static void Con_Screenshot(IConsole::IResult *pResult, void *pUserData); #if defined(CONF_VIDEORECORDER) - static void StartVideo(IConsole::IResult *pResult, void *pUserData, const char *pVideoName); + void StartVideo(const char *pFilename, bool WithTimestamp); static void Con_StartVideo(IConsole::IResult *pResult, void *pUserData); static void Con_StopVideo(IConsole::IResult *pResult, void *pUserData); const char *DemoPlayer_Render(const char *pFilename, int StorageType, const char *pVideoName, int SpeedIndex, bool StartPaused = false) override; @@ -435,8 +450,7 @@ class CClient : public IClient, public CDemoPlayer::IListener const char *DemoPlayer_Play(const char *pFilename, int StorageType) override; void DemoRecorder_Start(const char *pFilename, bool WithTimestamp, int Recorder, bool Verbose = false) override; void DemoRecorder_HandleAutoStart() override; - void DemoRecorder_StartReplayRecorder(); - void DemoRecorder_Stop(int Recorder, bool RemoveFile = false) override; + void DemoRecorder_UpdateReplayRecorder() override; void DemoRecorder_AddDemoMarker(int Recorder); IDemoRecorder *DemoRecorder(int Recorder) override; @@ -460,7 +474,7 @@ class CClient : public IClient, public CDemoPlayer::IListener // gfx void SwitchWindowScreen(int Index) override; - void SetWindowParams(int FullscreenMode, bool IsBorderless, bool AllowResizing) override; + void SetWindowParams(int FullscreenMode, bool IsBorderless) override; void ToggleWindowVSync() override; void Notify(const char *pTitle, const char *pMessage) override; void OnWindowResize() override; @@ -497,18 +511,21 @@ class CClient : public IClient, public CDemoPlayer::IListener void AddWarning(const SWarning &Warning) override; SWarning *GetCurWarning() override; + std::vector &&QuittingWarnings() { return std::move(m_vQuittingWarnings); } CChecksumData *ChecksumData() override { return &m_Checksum.m_Data; } - bool InfoTaskRunning() override { return m_pDDNetInfoTask != nullptr; } int UdpConnectivity(int NetType) override; + bool ViewLink(const char *pLink) override; + bool ViewFile(const char *pFilename) override; + #if defined(CONF_FAMILY_WINDOWS) void ShellRegister() override; void ShellUnregister() override; #endif void ShowMessageBox(const char *pTitle, const char *pMessage, EMessageBoxType Type = MESSAGE_BOX_TYPE_ERROR) override; - void GetGPUInfoString(char (&aGPUInfo)[256]) override; + void GetGpuInfoString(char (&aGpuInfo)[256]) override; void SetLoggers(std::shared_ptr &&pFileLogger, std::shared_ptr &&pStdoutLogger); }; diff --git a/src/engine/client/demoedit.cpp b/src/engine/client/demoedit.cpp index 2872fa31f4..410331e8a1 100644 --- a/src/engine/client/demoedit.cpp +++ b/src/engine/client/demoedit.cpp @@ -14,13 +14,14 @@ CDemoEdit::CDemoEdit(const char *pNetVersion, class CSnapshotDelta *pSnapshotDel m_EndTick = EndTick; // Init the demoeditor - m_DemoEditor.Init(pNetVersion, &m_SnapshotDelta, NULL, pStorage); + m_DemoEditor.Init(&m_SnapshotDelta, NULL, pStorage); } void CDemoEdit::Run() { // Slice the current demo - m_DemoEditor.Slice(m_aDemo, m_aDst, m_StartTick, m_EndTick, NULL, 0); - // We remove the temporary demo file - m_pStorage->RemoveFile(m_aDemo, IStorage::TYPE_SAVE); + m_Success = m_DemoEditor.Slice(m_aDemo, m_aDst, m_StartTick, m_EndTick, NULL, 0); + // We remove the temporary demo file if slicing is successful + if(m_Success) + m_pStorage->RemoveFile(m_aDemo, IStorage::TYPE_SAVE); } diff --git a/src/engine/client/demoedit.h b/src/engine/client/demoedit.h index a186fc249b..d6b2640253 100644 --- a/src/engine/client/demoedit.h +++ b/src/engine/client/demoedit.h @@ -18,10 +18,12 @@ class CDemoEdit : public IJob char m_aDst[256]; int m_StartTick; int m_EndTick; + bool m_Success; public: CDemoEdit(const char *pNetVersion, CSnapshotDelta *pSnapshotDelta, IStorage *pStorage, const char *pDemo, const char *pDst, int StartTick, int EndTick); void Run() override; char *Destination() { return m_aDst; } + bool Success() { return m_Success; } }; #endif diff --git a/src/engine/client/discord.cpp b/src/engine/client/discord.cpp index a65c38d258..cef226d85a 100644 --- a/src/engine/client/discord.cpp +++ b/src/engine/client/discord.cpp @@ -1,9 +1,7 @@ #include #include -// Hack for universal binary builds on macOS: Ignore arm64 until Discord -// releases a native arm64 SDK for macOS. -#include "game/client/component.h" -#if defined(CONF_DISCORD) && !(defined(CONF_ARCH_ARM64) && defined(CONF_PLATFORM_MACOS)) + +#if defined(CONF_DISCORD) #include typedef enum EDiscordResult DISCORD_API (*FDiscordCreate)(DiscordVersion, struct DiscordCreateParams *, struct IDiscordCore **); @@ -32,8 +30,6 @@ class CDiscord : public IDiscord IDiscordActivityEvents m_ActivityEvents; IDiscordActivityManager *m_pActivityManager; - int m_StartTime; - public: bool Init(FDiscordCreate pfnDiscordCreate) { @@ -44,7 +40,7 @@ class CDiscord : public IDiscord DiscordCreateParams Params; DiscordCreateParamsSetDefault(&Params); - Params.client_id = 1089816029913436171; // DDNet + Params.client_id = 752165779117441075; // DDNet Params.flags = EDiscordCreateFlags::DiscordCreateFlags_NoRequireDiscord; Params.event_data = this; Params.activity_events = &m_ActivityEvents; @@ -56,112 +52,33 @@ class CDiscord : public IDiscord } m_pActivityManager = m_pCore->get_activity_manager(m_pCore); - m_StartTime = time_timestamp(); + ClearGameInfo(); return false; } void Update() override { m_pCore->run_callbacks(m_pCore); } - void Start() override - { - m_StartTime = time_timestamp(); - } - void ClearGameInfo() override { - m_pActivityManager->clear_activity(m_pActivityManager, 0, 0); + DiscordActivity Activity; + mem_zero(&Activity, sizeof(DiscordActivity)); + str_copy(Activity.assets.large_image, "ddnet_logo", sizeof(Activity.assets.large_image)); + str_copy(Activity.assets.large_text, "DDNet logo", sizeof(Activity.assets.large_text)); + Activity.timestamps.start = time_timestamp(); + str_copy(Activity.details, "Offline", sizeof(Activity.details)); + m_pActivityManager->update_activity(m_pActivityManager, &Activity, 0, 0); } - - void SetGameInfo(const NETADDR &ServerAddr, const char *pMapName, bool AnnounceAddr, const char *pText, const char *pImage, const char *pPlayerName) override + void SetGameInfo(const NETADDR &ServerAddr, const char *pMapName, bool AnnounceAddr) override { DiscordActivity Activity; - mem_zero(&Activity, sizeof(DiscordActivity)); - /* - if(strcmp(pMapName, "Stronghold") == 0) - { - str_copy(Activity.assets.large_image, "stronghold1",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Multeasymap") == 0) - { - str_copy(Activity.assets.large_image, "multeasy",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Multeasymap") == 0) - { - str_copy(Activity.assets.large_image, "multeasy",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Copy Love Box 2s") == 0) - { - str_copy(Activity.assets.large_image, "CLB2s",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Copy Love Box") == 0) - { - str_copy(Activity.assets.large_image, "CLB",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Tutorial") == 0) - { - str_copy(Activity.assets.large_image, "tutorial",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Simple Down") == 0) - { - str_copy(Activity.assets.large_image, "simpledown",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Baby Aim 1.0") == 0) - { - str_copy(Activity.assets.large_image, "babyaim1",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Baby Aim 2.0") == 0) - { - str_copy(Activity.assets.large_image, "babyaim2",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Baby Aim 3.0") == 0) - { - str_copy(Activity.assets.large_image, "babyaim3",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Back in Time 3") == 0) - { - str_copy(Activity.assets.large_image, "bit3",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Back in Time 3") == 0) - { - str_copy(Activity.assets.large_image, "bit3",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Grandma") == 0) - { - str_copy(Activity.assets.large_image, "grandma",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Luxis") == 0) - { - str_copy(Activity.assets.large_image, "luxis",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Stronghold 2") == 0) - { - str_copy(Activity.assets.large_image, "stronghold2",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Stronghold 3") == 0) - { - str_copy(Activity.assets.large_image, "stronghold3",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Stronghold 4[Final]") == 0) - { - str_copy(Activity.assets.large_image, "strongholdF",sizeof(Activity.assets.large_image)); - } - if(strcmp(pMapName, "Grandma") == 0) - { - str_copy(Activity.assets.large_image, "grandma",sizeof(Activity.assets.large_image)); - } - fuck off */ - str_copy(Activity.assets.large_text, "StA-Client", sizeof(Activity.assets.large_text)); - str_copy(Activity.assets.small_image, pImage, sizeof(Activity.assets.large_image)); - str_copy(Activity.assets.small_text, pText, sizeof(Activity.assets.large_text)); - Activity.timestamps.start = m_StartTime; - str_copy(Activity.assets.large_image, "150e4e96-9c83-4309-b692-b212c08e1934",sizeof(Activity.assets.large_image)); - str_copy(Activity.state, pPlayerName); + str_copy(Activity.assets.large_image, "ddnet_logo", sizeof(Activity.assets.large_image)); + str_copy(Activity.assets.large_text, "DDNet logo", sizeof(Activity.assets.large_text)); + Activity.timestamps.start = time_timestamp(); + str_copy(Activity.details, "Online", sizeof(Activity.details)); str_copy(Activity.state, pMapName, sizeof(Activity.state)); m_pActivityManager->update_activity(m_pActivityManager, &Activity, 0, 0); - - } }; @@ -189,10 +106,9 @@ IDiscord *CreateDiscordImpl() class CDiscordStub : public IDiscord { - void Start() override {} void Update() override {} void ClearGameInfo() override {} - void SetGameInfo(const NETADDR &ServerAddr, const char *pMapName, bool AnnounceAddr, const char *pText, const char *pImage, const char *pPlayerName) override {} + void SetGameInfo(const NETADDR &ServerAddr, const char *pMapName, bool AnnounceAddr) override {} }; IDiscord *CreateDiscord() @@ -203,4 +119,4 @@ IDiscord *CreateDiscord() return pDiscord; } return new CDiscordStub(); -} \ No newline at end of file +} diff --git a/src/engine/client/enums.h b/src/engine/client/enums.h new file mode 100644 index 0000000000..45a475fa12 --- /dev/null +++ b/src/engine/client/enums.h @@ -0,0 +1,11 @@ +/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ +/* If you are missing that file, acquire a complete release at teeworlds.com. */ +#ifndef ENGINE_CLIENT_ENUMS_H +#define ENGINE_CLIENT_ENUMS_H + +enum +{ + NUM_DUMMIES = 2, +}; + +#endif diff --git a/src/engine/client/graph.cpp b/src/engine/client/graph.cpp index 95f62ed8e5..59ab25e684 100644 --- a/src/engine/client/graph.cpp +++ b/src/engine/client/graph.cpp @@ -6,13 +6,15 @@ #include "graph.h" +CGraph::CGraph(int MaxEntries) : + m_Entries(MaxEntries * (sizeof(SEntry) + 2 * CRingBufferBase::ITEM_SIZE), CRingBufferBase::FLAG_RECYCLE) +{ +} + void CGraph::Init(float Min, float Max) { SetMin(Min); SetMax(Max); - m_Index = 0; - for(auto &Entry : m_aEntries) - Entry.m_Initialized = false; } void CGraph::SetMin(float Min) @@ -25,34 +27,103 @@ void CGraph::SetMax(float Max) m_MaxRange = m_Max = Max; } -void CGraph::Scale() +void CGraph::Scale(int64_t WantedTotalTime) { + // Scale X axis for wanted total time + if(m_Entries.First() != nullptr) + { + const int64_t EndTime = m_Entries.Last()->m_Time; + bool ScaleTotalTime = false; + m_pFirstScaled = nullptr; + + if(m_Entries.First()->m_Time >= EndTime - WantedTotalTime) + { + m_pFirstScaled = m_Entries.First(); + } + else + { + m_pFirstScaled = m_Entries.Last(); + while(m_pFirstScaled) + { + SEntry *pPrev = m_Entries.Prev(m_pFirstScaled); + if(pPrev == nullptr) + break; + if(pPrev->m_Time < EndTime - WantedTotalTime) + { + // Scale based on actual total time instead of based on wanted total time, + // to avoid flickering last segment due to rounding errors. + ScaleTotalTime = true; + break; + } + m_pFirstScaled = pPrev; + } + } + + m_RenderedTotalTime = ScaleTotalTime ? (EndTime - m_pFirstScaled->m_Time) : WantedTotalTime; + + // Ensure that color is applied to first line segment + if(m_pFirstScaled) + { + m_pFirstScaled->m_ApplyColor = true; + SEntry *pNext = m_Entries.Next(m_pFirstScaled); + if(pNext != nullptr) + { + pNext->m_ApplyColor = true; + } + } + } + else + { + m_pFirstScaled = nullptr; + m_RenderedTotalTime = 0; + } + + // Scale Y axis m_Min = m_MinRange; m_Max = m_MaxRange; - for(auto &Entry : m_aEntries) + for(SEntry *pEntry = m_pFirstScaled; pEntry != nullptr; pEntry = m_Entries.Next(pEntry)) { - if(Entry.m_Value > m_Max) - m_Max = Entry.m_Value; - else if(Entry.m_Value < m_Min) - m_Min = Entry.m_Value; + if(pEntry->m_Value > m_Max) + m_Max = pEntry->m_Value; + else if(pEntry->m_Value < m_Min) + m_Min = pEntry->m_Value; } } void CGraph::Add(float Value, ColorRGBA Color) { - InsertAt(m_Index, Value, Color); - m_Index = (m_Index + 1) % MAX_VALUES; + InsertAt(time_get(), Value, Color); } -void CGraph::InsertAt(size_t Index, float Value, ColorRGBA Color) +void CGraph::InsertAt(int64_t Time, float Value, ColorRGBA Color) { - dbg_assert(Index < MAX_VALUES, "Index out of bounds"); - m_aEntries[Index].m_Initialized = true; - m_aEntries[Index].m_Value = Value; - m_aEntries[Index].m_Color = Color; + SEntry *pEntry = m_Entries.Allocate(sizeof(SEntry)); + pEntry->m_Time = Time; + pEntry->m_Value = Value; + pEntry->m_Color = Color; + + // Determine whether the line (pPrev, pEntry) has different + // vertex colors than the line (pPrevPrev, pPrev). + SEntry *pPrev = m_Entries.Prev(pEntry); + if(pPrev == nullptr) + { + pEntry->m_ApplyColor = true; + } + else + { + SEntry *pPrevPrev = m_Entries.Prev(pPrev); + if(pPrevPrev == nullptr) + { + pEntry->m_ApplyColor = true; + } + else + { + pEntry->m_ApplyColor = Color != pPrev->m_Color || pPrev->m_Color != pPrevPrev->m_Color; + } + } } -void CGraph::Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, float y, float w, float h, const char *pDescription) const +void CGraph::Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, float y, float w, float h, const char *pDescription) { pGraphics->TextureClear(); @@ -66,28 +137,61 @@ void CGraph::Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, flo pGraphics->SetColor(0.95f, 0.95f, 0.95f, 1.0f); IGraphics::CLineItem LineItem(x, y + h / 2, x + w, y + h / 2); pGraphics->LinesDraw(&LineItem, 1); + pGraphics->SetColor(0.5f, 0.5f, 0.5f, 0.75f); IGraphics::CLineItem aLineItems[2] = { IGraphics::CLineItem(x, y + (h * 3) / 4, x + w, y + (h * 3) / 4), IGraphics::CLineItem(x, y + h / 4, x + w, y + h / 4)}; pGraphics->LinesDraw(aLineItems, std::size(aLineItems)); - for(int i = 1; i < MAX_VALUES; i++) + + if(m_pFirstScaled != nullptr) { - const auto &Entry0 = m_aEntries[(m_Index + i - 1) % MAX_VALUES]; - const auto &Entry1 = m_aEntries[(m_Index + i) % MAX_VALUES]; - if(!Entry0.m_Initialized || !Entry1.m_Initialized) - continue; - float a0 = (i - 1) / (float)(MAX_VALUES - 1); - float a1 = i / (float)(MAX_VALUES - 1); - float v0 = (Entry0.m_Value - m_Min) / (m_Max - m_Min); - float v1 = (Entry1.m_Value - m_Min) / (m_Max - m_Min); - - IGraphics::CColorVertex aColorVertices[2] = { - IGraphics::CColorVertex(0, Entry0.m_Color.r, Entry0.m_Color.g, Entry0.m_Color.b, Entry0.m_Color.a), - IGraphics::CColorVertex(1, Entry1.m_Color.r, Entry1.m_Color.g, Entry1.m_Color.b, Entry1.m_Color.a)}; - pGraphics->SetColorVertex(aColorVertices, std::size(aColorVertices)); - IGraphics::CLineItem LineItem2(x + a0 * w, y + h - v0 * h, x + a1 * w, y + h - v1 * h); - pGraphics->LinesDraw(&LineItem2, 1); + IGraphics::CLineItem aValueLineItems[128]; + size_t NumValueLineItems = 0; + + const int64_t StartTime = m_pFirstScaled->m_Time; + + SEntry *pEntry0 = m_pFirstScaled; + int a0 = round_to_int((pEntry0->m_Time - StartTime) * w / m_RenderedTotalTime); + int v0 = round_to_int((pEntry0->m_Value - m_Min) * h / (m_Max - m_Min)); + while(pEntry0 != nullptr) + { + SEntry *pEntry1 = m_Entries.Next(pEntry0); + if(pEntry1 == nullptr) + break; + + const int a1 = round_to_int((pEntry1->m_Time - StartTime) * w / m_RenderedTotalTime); + const int v1 = round_to_int((pEntry1->m_Value - m_Min) * h / (m_Max - m_Min)); + + if(pEntry1->m_ApplyColor) + { + if(NumValueLineItems) + { + pGraphics->LinesDraw(aValueLineItems, NumValueLineItems); + NumValueLineItems = 0; + } + + IGraphics::CColorVertex aColorVertices[2] = { + IGraphics::CColorVertex(0, pEntry0->m_Color.r, pEntry0->m_Color.g, pEntry0->m_Color.b, pEntry0->m_Color.a), + IGraphics::CColorVertex(1, pEntry1->m_Color.r, pEntry1->m_Color.g, pEntry1->m_Color.b, pEntry1->m_Color.a)}; + pGraphics->SetColorVertex(aColorVertices, std::size(aColorVertices)); + } + if(NumValueLineItems == std::size(aValueLineItems)) + { + pGraphics->LinesDraw(aValueLineItems, NumValueLineItems); + NumValueLineItems = 0; + } + aValueLineItems[NumValueLineItems] = IGraphics::CLineItem(x + a0, y + h - v0, x + a1, y + h - v1); + ++NumValueLineItems; + + pEntry0 = pEntry1; + a0 = a1; + v0 = v1; + } + if(NumValueLineItems) + { + pGraphics->LinesDraw(aValueLineItems, NumValueLineItems); + } } pGraphics->LinesEnd(); diff --git a/src/engine/client/graph.h b/src/engine/client/graph.h index 296c6ff9aa..090d506faf 100644 --- a/src/engine/client/graph.h +++ b/src/engine/client/graph.h @@ -6,6 +6,8 @@ #include +#include + #include class IGraphics; @@ -13,33 +15,31 @@ class ITextRender; class CGraph { -public: - enum - { - MAX_VALUES = 128, - }; - private: struct SEntry { - bool m_Initialized; + int64_t m_Time; float m_Value; ColorRGBA m_Color; + bool m_ApplyColor; }; + SEntry *m_pFirstScaled = nullptr; + int64_t m_RenderedTotalTime = 0; float m_Min, m_Max; float m_MinRange, m_MaxRange; - SEntry m_aEntries[MAX_VALUES]; - size_t m_Index; + CDynamicRingBuffer m_Entries; public: + CGraph(int MaxEntries); + void Init(float Min, float Max); void SetMin(float Min); void SetMax(float Max); - void Scale(); + void Scale(int64_t WantedTotalTime); void Add(float Value, ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.75f)); - void InsertAt(size_t Index, float Value, ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.75f)); - void Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, float y, float w, float h, const char *pDescription) const; + void InsertAt(int64_t Time, float Value, ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.75f)); + void Render(IGraphics *pGraphics, ITextRender *pTextRender, float x, float y, float w, float h, const char *pDescription); }; #endif diff --git a/src/engine/client/graphics_threaded.cpp b/src/engine/client/graphics_threaded.cpp index ab22ddeebd..8dc82c34ae 100644 --- a/src/engine/client/graphics_threaded.cpp +++ b/src/engine/client/graphics_threaded.cpp @@ -208,9 +208,9 @@ uint64_t CGraphics_Threaded::StagingMemoryUsage() const return m_pBackend->StagingMemoryUsage(); } -const TTWGraphicsGPUList &CGraphics_Threaded::GetGPUs() const +const TTwGraphicsGpuList &CGraphics_Threaded::GetGpus() const { - return m_pBackend->GetGPUs(); + return m_pBackend->GetGpus(); } void CGraphics_Threaded::MapScreen(float TopLeftX, float TopLeftY, float BottomRightX, float BottomRightY) @@ -288,106 +288,41 @@ void CGraphics_Threaded::FreeTextureIndex(CTextureHandle *pIndex) pIndex->Invalidate(); } -int CGraphics_Threaded::UnloadTexture(CTextureHandle *pIndex) +void CGraphics_Threaded::UnloadTexture(CTextureHandle *pIndex) { - if(pIndex->Id() == m_NullTexture.Id()) - return 0; - - if(!pIndex->IsValid()) - return 0; + if(pIndex->IsNullTexture() || !pIndex->IsValid()) + return; CCommandBuffer::SCommand_Texture_Destroy Cmd; Cmd.m_Slot = pIndex->Id(); AddCmd(Cmd); FreeTextureIndex(pIndex); - return 0; -} - -static bool ConvertToRGBA(uint8_t *pDest, const uint8_t *pSrc, size_t SrcWidth, size_t SrcHeight, CImageInfo::EImageFormat SrcFormat) -{ - if(SrcFormat == CImageInfo::FORMAT_RGBA) - { - mem_copy(pDest, pSrc, SrcWidth * SrcHeight * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA)); - return true; - } - else - { - const size_t SrcChannelCount = CImageInfo::PixelSize(SrcFormat); - const size_t DstChannelCount = CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA); - for(size_t Y = 0; Y < SrcHeight; ++Y) - { - for(size_t X = 0; X < SrcWidth; ++X) - { - size_t ImgOffsetSrc = (Y * SrcWidth * SrcChannelCount) + (X * SrcChannelCount); - size_t ImgOffsetDest = (Y * SrcWidth * DstChannelCount) + (X * DstChannelCount); - size_t CopySize = SrcChannelCount; - if(SrcFormat == CImageInfo::FORMAT_RGB) - { - mem_copy(&pDest[ImgOffsetDest], &pSrc[ImgOffsetSrc], CopySize); - pDest[ImgOffsetDest + 3] = 255; - } - else if(SrcFormat == CImageInfo::FORMAT_SINGLE_COMPONENT) - { - pDest[ImgOffsetDest + 0] = 255; - pDest[ImgOffsetDest + 1] = 255; - pDest[ImgOffsetDest + 2] = 255; - pDest[ImgOffsetDest + 3] = pSrc[ImgOffsetSrc]; - } - } - } - return false; - } } -int CGraphics_Threaded::LoadTextureRawSub(CTextureHandle TextureID, int x, int y, size_t Width, size_t Height, CImageInfo::EImageFormat Format, const void *pData) +IGraphics::CTextureHandle CGraphics_Threaded::LoadSpriteTexture(const CImageInfo &FromImageInfo, const CDataSprite *pSprite) { - dbg_assert(TextureID.IsValid(), "Invalid texture handle used with LoadTextureRawSub."); + int ImageGridX = FromImageInfo.m_Width / pSprite->m_pSet->m_Gridx; + int ImageGridY = FromImageInfo.m_Height / pSprite->m_pSet->m_Gridy; + int x = pSprite->m_X * ImageGridX; + int y = pSprite->m_Y * ImageGridY; + int w = pSprite->m_W * ImageGridX; + int h = pSprite->m_H * ImageGridY; - CCommandBuffer::SCommand_Texture_Update Cmd; - Cmd.m_Slot = TextureID.Id(); - Cmd.m_X = x; - Cmd.m_Y = y; - Cmd.m_Width = Width; - Cmd.m_Height = Height; - Cmd.m_Format = CCommandBuffer::TEXFORMAT_RGBA; - - // calculate memory usage - const size_t MemSize = Width * Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA); - - // copy texture data - void *pTmpData = malloc(MemSize); - ConvertToRGBA((uint8_t *)pTmpData, (const uint8_t *)pData, Width, Height, Format); - Cmd.m_pData = pTmpData; - AddCmd(Cmd); - - return 0; -} - -IGraphics::CTextureHandle CGraphics_Threaded::LoadSpriteTextureImpl(CImageInfo &FromImageInfo, int x, int y, size_t w, size_t h) -{ - const size_t PixelSize = FromImageInfo.PixelSize(); - m_vSpriteHelper.resize(w * h * PixelSize); - CopyTextureFromTextureBufferSub(m_vSpriteHelper.data(), w, h, (uint8_t *)FromImageInfo.m_pData, FromImageInfo.m_Width, FromImageInfo.m_Height, PixelSize, x, y, w, h); - return LoadTextureRaw(w, h, FromImageInfo.m_Format, m_vSpriteHelper.data(), 0); + CImageInfo SpriteInfo; + SpriteInfo.m_Width = w; + SpriteInfo.m_Height = h; + SpriteInfo.m_Format = FromImageInfo.m_Format; + SpriteInfo.m_pData = static_cast(malloc(SpriteInfo.DataSize())); + SpriteInfo.CopyRectFrom(FromImageInfo, x, y, w, h, 0, 0); + return LoadTextureRawMove(SpriteInfo, 0, pSprite->m_pName); } -IGraphics::CTextureHandle CGraphics_Threaded::LoadSpriteTexture(CImageInfo &FromImageInfo, CDataSprite *pSprite) +bool CGraphics_Threaded::IsImageSubFullyTransparent(const CImageInfo &FromImageInfo, int x, int y, int w, int h) { - int imggx = FromImageInfo.m_Width / pSprite->m_pSet->m_Gridx; - int imggy = FromImageInfo.m_Height / pSprite->m_pSet->m_Gridy; - int x = pSprite->m_X * imggx; - int y = pSprite->m_Y * imggy; - int w = pSprite->m_W * imggx; - int h = pSprite->m_H * imggy; - return LoadSpriteTextureImpl(FromImageInfo, x, y, w, h); -} - -bool CGraphics_Threaded::IsImageSubFullyTransparent(CImageInfo &FromImageInfo, int x, int y, int w, int h) -{ - if(FromImageInfo.m_Format == CImageInfo::FORMAT_SINGLE_COMPONENT || FromImageInfo.m_Format == CImageInfo::FORMAT_RGBA) + if(FromImageInfo.m_Format == CImageInfo::FORMAT_R || FromImageInfo.m_Format == CImageInfo::FORMAT_RA || FromImageInfo.m_Format == CImageInfo::FORMAT_RGBA) { - uint8_t *pImgData = (uint8_t *)FromImageInfo.m_pData; + const uint8_t *pImgData = FromImageInfo.m_pData; const size_t PixelSize = FromImageInfo.PixelSize(); for(int iy = 0; iy < h; ++iy) { @@ -404,19 +339,18 @@ bool CGraphics_Threaded::IsImageSubFullyTransparent(CImageInfo &FromImageInfo, i return false; } -bool CGraphics_Threaded::IsSpriteTextureFullyTransparent(CImageInfo &FromImageInfo, CDataSprite *pSprite) +bool CGraphics_Threaded::IsSpriteTextureFullyTransparent(const CImageInfo &FromImageInfo, const CDataSprite *pSprite) { - int imggx = FromImageInfo.m_Width / pSprite->m_pSet->m_Gridx; - int imggy = FromImageInfo.m_Height / pSprite->m_pSet->m_Gridy; - int x = pSprite->m_X * imggx; - int y = pSprite->m_Y * imggy; - int w = pSprite->m_W * imggx; - int h = pSprite->m_H * imggy; - + int ImageGridX = FromImageInfo.m_Width / pSprite->m_pSet->m_Gridx; + int ImageGridY = FromImageInfo.m_Height / pSprite->m_pSet->m_Gridy; + int x = pSprite->m_X * ImageGridX; + int y = pSprite->m_Y * ImageGridY; + int w = pSprite->m_W * ImageGridX; + int h = pSprite->m_H * ImageGridY; return IsImageSubFullyTransparent(FromImageInfo, x, y, w, h); } -IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRaw(size_t Width, size_t Height, CImageInfo::EImageFormat Format, const void *pData, int Flags, const char *pTexName) +static void LoadTextureAddWarning(size_t Width, size_t Height, int Flags, const char *pTexName, std::vector &vWarnings) { if((Flags & IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE) != 0 || (Flags & IGraphics::TEXLOAD_TO_3D_TEXTURE) != 0) { @@ -424,33 +358,21 @@ IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRaw(size_t Width, size_ { SWarning NewWarning; char aText[128]; - aText[0] = '\0'; - if(pTexName) - { - str_format(aText, sizeof(aText), "\"%s\"", pTexName); - } + str_format(aText, sizeof(aText), "\"%s\"", pTexName ? pTexName : "(no name)"); str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg), Localize("The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs."), aText, 16, 16); - - m_vWarnings.emplace_back(NewWarning); + vWarnings.emplace_back(NewWarning); } } +} - if(Width == 0 || Height == 0) - return IGraphics::CTextureHandle(); - - IGraphics::CTextureHandle TextureHandle = FindFreeTextureIndex(); - +static CCommandBuffer::SCommand_Texture_Create LoadTextureCreateCommand(int TextureId, size_t Width, size_t Height, int Flags) +{ CCommandBuffer::SCommand_Texture_Create Cmd; - Cmd.m_Slot = TextureHandle.Id(); + Cmd.m_Slot = TextureId; Cmd.m_Width = Width; Cmd.m_Height = Height; - Cmd.m_Format = CCommandBuffer::TEXFORMAT_RGBA; - Cmd.m_StoreFormat = CCommandBuffer::TEXFORMAT_RGBA; - // flags Cmd.m_Flags = 0; - if(Flags & IGraphics::TEXLOAD_NOMIPMAPS) - Cmd.m_Flags |= CCommandBuffer::TEXFLAG_NOMIPMAPS; if((Flags & IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE) != 0) Cmd.m_Flags |= CCommandBuffer::TEXFLAG_TO_2D_ARRAY_TEXTURE; if((Flags & IGraphics::TEXLOAD_TO_3D_TEXTURE) != 0) @@ -458,46 +380,77 @@ IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRaw(size_t Width, size_ if((Flags & IGraphics::TEXLOAD_NO_2D_TEXTURE) != 0) Cmd.m_Flags |= CCommandBuffer::TEXFLAG_NO_2D_TEXTURE; - // copy texture data - const size_t MemSize = Width * Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA); - void *pTmpData = malloc(MemSize); - if(!ConvertToRGBA((uint8_t *)pTmpData, (const uint8_t *)pData, Width, Height, Format)) + return Cmd; +} + +IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRaw(const CImageInfo &Image, int Flags, const char *pTexName) +{ + LoadTextureAddWarning(Image.m_Width, Image.m_Height, Flags, pTexName, m_vWarnings); + + if(Image.m_Width == 0 || Image.m_Height == 0) + return IGraphics::CTextureHandle(); + + IGraphics::CTextureHandle TextureHandle = FindFreeTextureIndex(); + CCommandBuffer::SCommand_Texture_Create Cmd = LoadTextureCreateCommand(TextureHandle.Id(), Image.m_Width, Image.m_Height, Flags); + + // Copy texture data and convert if necessary + uint8_t *pTmpData; + if(!ConvertToRgbaAlloc(pTmpData, Image)) { - dbg_msg("graphics", "converted image %s to RGBA, consider making its file format RGBA", pTexName ? pTexName : "(no name)"); + dbg_msg("graphics", "converted image '%s' to RGBA, consider making its file format RGBA", pTexName ? pTexName : "(no name)"); } Cmd.m_pData = pTmpData; + + AddCmd(Cmd); + + return TextureHandle; +} + +IGraphics::CTextureHandle CGraphics_Threaded::LoadTextureRawMove(CImageInfo &Image, int Flags, const char *pTexName) +{ + if(Image.m_Format != CImageInfo::FORMAT_RGBA) + { + // Moving not possible, texture needs to be converted + IGraphics::CTextureHandle TextureHandle = LoadTextureRaw(Image, Flags, pTexName); + Image.Free(); + return TextureHandle; + } + + LoadTextureAddWarning(Image.m_Width, Image.m_Height, Flags, pTexName, m_vWarnings); + + if(Image.m_Width == 0 || Image.m_Height == 0) + return IGraphics::CTextureHandle(); + + IGraphics::CTextureHandle TextureHandle = FindFreeTextureIndex(); + CCommandBuffer::SCommand_Texture_Create Cmd = LoadTextureCreateCommand(TextureHandle.Id(), Image.m_Width, Image.m_Height, Flags); + Cmd.m_pData = Image.m_pData; + Image.m_pData = nullptr; + Image.Free(); AddCmd(Cmd); return TextureHandle; } -// simple uncompressed RGBA loaders IGraphics::CTextureHandle CGraphics_Threaded::LoadTexture(const char *pFilename, int StorageType, int Flags) { dbg_assert(pFilename[0] != '\0', "Cannot load texture from file with empty filename"); // would cause Valgrind to crash otherwise - CImageInfo Img; - if(LoadPNG(&Img, pFilename, StorageType)) + CImageInfo Image; + if(LoadPng(Image, pFilename, StorageType)) { - CTextureHandle ID = LoadTextureRaw(Img.m_Width, Img.m_Height, Img.m_Format, Img.m_pData, Flags, pFilename); - FreePNG(&Img); - if(ID.IsValid()) + CTextureHandle Id = LoadTextureRawMove(Image, Flags, pFilename); + if(Id.IsValid()) { if(g_Config.m_Debug) dbg_msg("graphics/texture", "loaded %s", pFilename); - return ID; + return Id; } } return m_NullTexture; } -IGraphics::CTextureHandle CGraphics_Threaded::NullTexture() const -{ - return m_NullTexture; -} - -bool CGraphics_Threaded::LoadTextTextures(size_t Width, size_t Height, CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture, void *pTextData, void *pTextOutlineData) +bool CGraphics_Threaded::LoadTextTextures(size_t Width, size_t Height, CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture, uint8_t *pTextData, uint8_t *pTextOutlineData) { if(Width == 0 || Height == 0) return false; @@ -531,20 +484,17 @@ bool CGraphics_Threaded::UnloadTextTextures(CTextureHandle &TextTexture, CTextur return true; } -bool CGraphics_Threaded::UpdateTextTexture(CTextureHandle TextureID, int x, int y, size_t Width, size_t Height, const void *pData) +bool CGraphics_Threaded::UpdateTextTexture(CTextureHandle TextureId, int x, int y, size_t Width, size_t Height, const uint8_t *pData) { CCommandBuffer::SCommand_TextTexture_Update Cmd; - Cmd.m_Slot = TextureID.Id(); + Cmd.m_Slot = TextureId.Id(); Cmd.m_X = x; Cmd.m_Y = y; Cmd.m_Width = Width; Cmd.m_Height = Height; - // calculate memory usage const size_t MemSize = Width * Height; - - // copy texture data - void *pTmpData = malloc(MemSize); + uint8_t *pTmpData = static_cast(malloc(MemSize)); mem_copy(pTmpData, pData, MemSize); Cmd.m_pData = pTmpData; AddCmd(Cmd); @@ -552,168 +502,114 @@ bool CGraphics_Threaded::UpdateTextTexture(CTextureHandle TextureID, int x, int return true; } -bool CGraphics_Threaded::LoadPNG(CImageInfo *pImg, const char *pFilename, int StorageType) +static SWarning FormatPngliteIncompatibilityWarning(int PngliteIncompatible, const char *pContextName) { - char aCompleteFilename[IO_MAX_PATH_LENGTH]; - IOHANDLE File = m_pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType, aCompleteFilename, sizeof(aCompleteFilename)); - if(File) - { - io_seek(File, 0, IOSEEK_END); - unsigned int FileSize = io_tell(File); - io_seek(File, 0, IOSEEK_START); - - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); - - ByteBuffer.resize(FileSize); - io_read(File, &ByteBuffer.front(), FileSize); - - io_close(File); + SWarning Warning; + str_format(Warning.m_aWarningMsg, sizeof(Warning.m_aWarningMsg), Localize("\"%s\" is not compatible with pnglite and cannot be loaded by old DDNet versions: "), pContextName); + static const int FLAGS[] = {CImageLoader::PNGLITE_COLOR_TYPE, CImageLoader::PNGLITE_BIT_DEPTH, CImageLoader::PNGLITE_INTERLACE_TYPE, CImageLoader::PNGLITE_COMPRESSION_TYPE, CImageLoader::PNGLITE_FILTER_TYPE}; + static const char *const EXPLANATION[] = {"color type", "bit depth", "interlace type", "compression type", "filter type"}; - uint8_t *pImgBuffer = NULL; - EImageFormat ImageFormat; - int PngliteIncompatible; - if(::LoadPNG(ImageByteBuffer, pFilename, PngliteIncompatible, pImg->m_Width, pImg->m_Height, pImgBuffer, ImageFormat)) + bool First = true; + for(size_t i = 0; i < std::size(FLAGS); ++i) + { + if((PngliteIncompatible & FLAGS[i]) != 0) { - if(ImageFormat == IMAGE_FORMAT_RGB) - pImg->m_Format = CImageInfo::FORMAT_RGB; - else if(ImageFormat == IMAGE_FORMAT_RGBA) - pImg->m_Format = CImageInfo::FORMAT_RGBA; - else + if(!First) { - free(pImgBuffer); - log_error("game/png", "image had unsupported image format. filename='%s' format='%d'", pFilename, (int)ImageFormat); - return false; + str_append(Warning.m_aWarningMsg, ", "); } - pImg->m_pData = pImgBuffer; - - if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0) - { - SWarning Warning; - str_format(Warning.m_aWarningMsg, sizeof(Warning.m_aWarningMsg), Localize("\"%s\" is not compatible with pnglite and cannot be loaded by old DDNet versions: "), pFilename); - static const int FLAGS[] = {PNGLITE_COLOR_TYPE, PNGLITE_BIT_DEPTH, PNGLITE_INTERLACE_TYPE, PNGLITE_COMPRESSION_TYPE, PNGLITE_FILTER_TYPE}; - static const char *EXPLANATION[] = {"color type", "bit depth", "interlace type", "compression type", "filter type"}; - - bool First = true; - for(size_t i = 0; i < std::size(FLAGS); ++i) - { - if((PngliteIncompatible & FLAGS[i]) != 0) - { - if(!First) - { - str_append(Warning.m_aWarningMsg, ", "); - } - str_append(Warning.m_aWarningMsg, EXPLANATION[i]); - First = false; - } - } - str_append(Warning.m_aWarningMsg, " unsupported"); - m_vWarnings.emplace_back(Warning); - } - } - else - { - log_error("game/png", "failed to load file. filename='%s'", pFilename); - return false; + str_append(Warning.m_aWarningMsg, EXPLANATION[i]); + First = false; } } - else - { - log_error("game/png", "failed to open file. filename='%s'", pFilename); + str_append(Warning.m_aWarningMsg, " unsupported"); + return Warning; +} + +bool CGraphics_Threaded::LoadPng(CImageInfo &Image, const char *pFilename, int StorageType) +{ + IOHANDLE File = m_pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType); + + int PngliteIncompatible; + if(!CImageLoader::LoadPng(File, pFilename, Image, PngliteIncompatible)) return false; + + if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0) + { + m_vWarnings.emplace_back(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pFilename)); } return true; } -void CGraphics_Threaded::FreePNG(CImageInfo *pImg) +bool CGraphics_Threaded::LoadPng(CImageInfo &Image, const uint8_t *pData, size_t DataSize, const char *pContextName) { - free(pImg->m_pData); - pImg->m_pData = NULL; + CByteBufferReader Reader(pData, DataSize); + int PngliteIncompatible; + if(!CImageLoader::LoadPng(Reader, pContextName, Image, PngliteIncompatible)) + return false; + + if(m_WarnPngliteIncompatibleImages && PngliteIncompatible != 0) + { + m_vWarnings.emplace_back(FormatPngliteIncompatibilityWarning(PngliteIncompatible, pContextName)); + } + + return true; } -bool CGraphics_Threaded::CheckImageDivisibility(const char *pFileName, CImageInfo &Img, int DivX, int DivY, bool AllowResize) +bool CGraphics_Threaded::CheckImageDivisibility(const char *pContextName, CImageInfo &Image, int DivX, int DivY, bool AllowResize) { dbg_assert(DivX != 0 && DivY != 0, "Passing 0 to this function is not allowed."); bool ImageIsValid = true; - bool WidthBroken = Img.m_Width == 0 || (Img.m_Width % DivX) != 0; - bool HeightBroken = Img.m_Height == 0 || (Img.m_Height % DivY) != 0; + bool WidthBroken = Image.m_Width == 0 || (Image.m_Width % DivX) != 0; + bool HeightBroken = Image.m_Height == 0 || (Image.m_Height % DivY) != 0; if(WidthBroken || HeightBroken) { SWarning NewWarning; - str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg), Localize("The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs."), pFileName, DivX, DivY); - + char aContextNameQuoted[128]; + str_format(aContextNameQuoted, sizeof(aContextNameQuoted), "\"%s\"", pContextName); + str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg), + Localize("The width of texture %s is not divisible by %d, or the height is not divisible by %d, which might cause visual bugs."), aContextNameQuoted, DivX, DivY); m_vWarnings.emplace_back(NewWarning); - ImageIsValid = false; } - if(AllowResize && !ImageIsValid && Img.m_Width > 0 && Img.m_Height > 0) + if(AllowResize && !ImageIsValid && Image.m_Width > 0 && Image.m_Height > 0) { int NewWidth = DivX; int NewHeight = DivY; if(WidthBroken) { - NewWidth = maximum(HighestBit(Img.m_Width), DivX); + NewWidth = maximum(HighestBit(Image.m_Width), DivX); NewHeight = (NewWidth / DivX) * DivY; } else { - NewHeight = maximum(HighestBit(Img.m_Height), DivY); + NewHeight = maximum(HighestBit(Image.m_Height), DivY); NewWidth = (NewHeight / DivY) * DivX; } - - uint8_t *pNewImg = ResizeImage((uint8_t *)Img.m_pData, Img.m_Width, Img.m_Height, NewWidth, NewHeight, Img.PixelSize()); - free(Img.m_pData); - Img.m_pData = pNewImg; - Img.m_Width = NewWidth; - Img.m_Height = NewHeight; + ResizeImage(Image, NewWidth, NewHeight); ImageIsValid = true; } return ImageIsValid; } -bool CGraphics_Threaded::IsImageFormatRGBA(const char *pFileName, CImageInfo &Img) +bool CGraphics_Threaded::IsImageFormatRgba(const char *pContextName, const CImageInfo &Image) { - if(Img.m_Format != CImageInfo::FORMAT_RGBA) + if(Image.m_Format != CImageInfo::FORMAT_RGBA) { SWarning NewWarning; - char aText[128]; - aText[0] = '\0'; - if(pFileName) - { - str_format(aText, sizeof(aText), "\"%s\"", pFileName); - } + char aContextNameQuoted[128]; + str_format(aContextNameQuoted, sizeof(aContextNameQuoted), "\"%s\"", pContextName); str_format(NewWarning.m_aWarningMsg, sizeof(NewWarning.m_aWarningMsg), - Localize("The format of texture %s is not RGBA which will cause visual bugs."), aText); + Localize("The format of texture %s is not RGBA which will cause visual bugs."), aContextNameQuoted); m_vWarnings.emplace_back(NewWarning); return false; } return true; } -void CGraphics_Threaded::CopyTextureBufferSub(uint8_t *pDestBuffer, uint8_t *pSourceBuffer, size_t FullWidth, size_t FullHeight, size_t PixelSize, size_t SubOffsetX, size_t SubOffsetY, size_t SubCopyWidth, size_t SubCopyHeight) -{ - for(size_t Y = 0; Y < SubCopyHeight; ++Y) - { - const size_t ImgOffset = ((SubOffsetY + Y) * FullWidth * PixelSize) + (SubOffsetX * PixelSize); - const size_t CopySize = SubCopyWidth * PixelSize; - mem_copy(&pDestBuffer[ImgOffset], &pSourceBuffer[ImgOffset], CopySize); - } -} - -void CGraphics_Threaded::CopyTextureFromTextureBufferSub(uint8_t *pDestBuffer, size_t DestWidth, size_t DestHeight, uint8_t *pSourceBuffer, size_t SrcWidth, size_t SrcHeight, size_t PixelSize, size_t SrcSubOffsetX, size_t SrcSubOffsetY, size_t SrcSubCopyWidth, size_t SrcSubCopyHeight) -{ - for(size_t Y = 0; Y < SrcSubCopyHeight; ++Y) - { - const size_t SrcImgOffset = ((SrcSubOffsetY + Y) * SrcWidth * PixelSize) + (SrcSubOffsetX * PixelSize); - const size_t DstImgOffset = (Y * DestWidth * PixelSize); - const size_t CopySize = SrcSubCopyWidth * PixelSize; - mem_copy(&pDestBuffer[DstImgOffset], &pSourceBuffer[SrcImgOffset], CopySize); - } -} - void CGraphics_Threaded::KickCommandBuffer() { m_pBackend->RunBuffer(m_pCommandBuffer); @@ -740,24 +636,14 @@ class CScreenshotSaveJob : public IJob IStorage *m_pStorage; IConsole *m_pConsole; char m_aName[IO_MAX_PATH_LENGTH]; - int m_Width; - int m_Height; - void *m_pData; + CImageInfo m_Image; void Run() override { char aWholePath[IO_MAX_PATH_LENGTH]; char aBuf[64 + IO_MAX_PATH_LENGTH]; - IOHANDLE File = m_pStorage->OpenFile(m_aName, IOFLAG_WRITE, IStorage::TYPE_SAVE, aWholePath, sizeof(aWholePath)); - if(File) + if(CImageLoader::SavePng(m_pStorage->OpenFile(m_aName, IOFLAG_WRITE, IStorage::TYPE_SAVE, aWholePath, sizeof(aWholePath)), m_aName, m_Image)) { - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); - - if(SavePNG(IMAGE_FORMAT_RGBA, (const uint8_t *)m_pData, ImageByteBuffer, m_Width, m_Height)) - io_write(File, &ByteBuffer.front(), ByteBuffer.size()); - io_close(File); - str_format(aBuf, sizeof(aBuf), "saved screenshot to '%s'", aWholePath); } else @@ -768,51 +654,49 @@ class CScreenshotSaveJob : public IJob } public: - CScreenshotSaveJob(IStorage *pStorage, IConsole *pConsole, const char *pName, int Width, int Height, void *pData) : + CScreenshotSaveJob(IStorage *pStorage, IConsole *pConsole, const char *pName, CImageInfo Image) : m_pStorage(pStorage), m_pConsole(pConsole), - m_Width(Width), - m_Height(Height), - m_pData(pData) + m_Image(Image) { str_copy(m_aName, pName); } ~CScreenshotSaveJob() override { - free(m_pData); + m_Image.Free(); } }; -bool CGraphics_Threaded::ScreenshotDirect() +void CGraphics_Threaded::ScreenshotDirect(bool *pSwapped) { - // add swap command - CImageInfo Image; + if(!m_DoScreenshot) + return; + m_DoScreenshot = false; + if(!WindowActive()) + return; - bool DidSwap = false; + CImageInfo Image; CCommandBuffer::SCommand_TrySwapAndScreenshot Cmd; Cmd.m_pImage = &Image; - Cmd.m_pSwapped = &DidSwap; + Cmd.m_pSwapped = pSwapped; AddCmd(Cmd); - // kick the buffer and wait for the result KickCommandBuffer(); WaitForIdle(); if(Image.m_pData) { - m_pEngine->AddJob(std::make_shared(m_pStorage, m_pConsole, m_aScreenshotName, Image.m_Width, Image.m_Height, Image.m_pData)); + m_pEngine->AddJob(std::make_shared(m_pStorage, m_pConsole, m_aScreenshotName, Image)); } - - return DidSwap; } -void CGraphics_Threaded::TextureSet(CTextureHandle TextureID) +void CGraphics_Threaded::TextureSet(CTextureHandle TextureId) { dbg_assert(m_Drawing == 0, "called Graphics()->TextureSet within begin"); - dbg_assert(!TextureID.IsValid() || m_vTextureIndices[TextureID.Id()] == -1, "Texture handle was not invalid, but also did not correlate to an existing texture."); - m_State.m_Texture = TextureID.Id(); + dbg_assert(!TextureId.IsValid() || m_vTextureIndices[TextureId.Id()] == -1, "Texture handle was not invalid, but also did not correlate to an existing texture."); + m_State.m_Texture = TextureId.Id(); } void CGraphics_Threaded::Clear(float r, float g, float b, bool ForceClearNow) @@ -891,49 +775,36 @@ void CGraphics_Threaded::QuadsSetRotation(float Angle) m_Rotation = Angle; } -inline void clampf(float &Value, float Min, float Max) +static unsigned char NormalizeColorComponent(float ColorComponent) { - if(Value > Max) - Value = Max; - else if(Value < Min) - Value = Min; + return (unsigned char)(clamp(ColorComponent, 0.0f, 1.0f) * 255.0f + 0.5f); // +0.5f to round to nearest } -void CGraphics_Threaded::SetColorVertex(const CColorVertex *pArray, int Num) +void CGraphics_Threaded::SetColorVertex(const CColorVertex *pArray, size_t Num) { dbg_assert(m_Drawing != 0, "called Graphics()->SetColorVertex without begin"); - for(int i = 0; i < Num; ++i) + for(size_t i = 0; i < Num; ++i) { - float r = pArray[i].m_R, g = pArray[i].m_G, b = pArray[i].m_B, a = pArray[i].m_A; - clampf(r, 0.f, 1.f); - clampf(g, 0.f, 1.f); - clampf(b, 0.f, 1.f); - clampf(a, 0.f, 1.f); - m_aColor[pArray[i].m_Index].r = (unsigned char)(r * 255.f); - m_aColor[pArray[i].m_Index].g = (unsigned char)(g * 255.f); - m_aColor[pArray[i].m_Index].b = (unsigned char)(b * 255.f); - m_aColor[pArray[i].m_Index].a = (unsigned char)(a * 255.f); + const CColorVertex &Vertex = pArray[i]; + CCommandBuffer::SColor &Color = m_aColor[Vertex.m_Index]; + Color.r = NormalizeColorComponent(Vertex.m_R); + Color.g = NormalizeColorComponent(Vertex.m_G); + Color.b = NormalizeColorComponent(Vertex.m_B); + Color.a = NormalizeColorComponent(Vertex.m_A); } } void CGraphics_Threaded::SetColor(float r, float g, float b, float a) { - clampf(r, 0.f, 1.f); - clampf(g, 0.f, 1.f); - clampf(b, 0.f, 1.f); - clampf(a, 0.f, 1.f); - r *= 255.f; - g *= 255.f; - b *= 255.f; - a *= 255.f; - - for(auto &Color : m_aColor) + CCommandBuffer::SColor NewColor; + NewColor.r = NormalizeColorComponent(r); + NewColor.g = NormalizeColorComponent(g); + NewColor.b = NormalizeColorComponent(b); + NewColor.a = NormalizeColorComponent(a); + for(CCommandBuffer::SColor &Color : m_aColor) { - Color.r = (unsigned char)(r); - Color.g = (unsigned char)(g); - Color.b = (unsigned char)(b); - Color.a = (unsigned char)(a); + Color = NewColor; } } @@ -944,25 +815,20 @@ void CGraphics_Threaded::SetColor(ColorRGBA Color) void CGraphics_Threaded::SetColor4(ColorRGBA TopLeft, ColorRGBA TopRight, ColorRGBA BottomLeft, ColorRGBA BottomRight) { - dbg_assert(m_Drawing != 0, "called Graphics()->SetColor without begin"); - CColorVertex Array[4] = { - CColorVertex(0, TopLeft.r, TopLeft.g, TopLeft.b, TopLeft.a), - CColorVertex(1, TopRight.r, TopRight.g, TopRight.b, TopRight.a), - CColorVertex(2, BottomRight.r, BottomRight.g, BottomRight.b, BottomRight.a), - CColorVertex(3, BottomLeft.r, BottomLeft.g, BottomLeft.b, BottomLeft.a)}; - SetColorVertex(Array, 4); + CColorVertex aArray[] = { + CColorVertex(0, TopLeft), + CColorVertex(1, TopRight), + CColorVertex(2, BottomRight), + CColorVertex(3, BottomLeft)}; + SetColorVertex(aArray, std::size(aArray)); } void CGraphics_Threaded::ChangeColorOfCurrentQuadVertices(float r, float g, float b, float a) { - clampf(r, 0.f, 1.f); - clampf(g, 0.f, 1.f); - clampf(b, 0.f, 1.f); - clampf(a, 0.f, 1.f); - m_aColor[0].r = (unsigned char)(r * 255.f); - m_aColor[0].g = (unsigned char)(g * 255.f); - m_aColor[0].b = (unsigned char)(b * 255.f); - m_aColor[0].a = (unsigned char)(a * 255.f); + m_aColor[0].r = NormalizeColorComponent(r); + m_aColor[0].g = NormalizeColorComponent(g); + m_aColor[0].b = NormalizeColorComponent(b); + m_aColor[0].a = NormalizeColorComponent(a); for(int i = 0; i < m_NumVertices; ++i) { @@ -970,61 +836,13 @@ void CGraphics_Threaded::ChangeColorOfCurrentQuadVertices(float r, float g, floa } } -void CGraphics_Threaded::ChangeColorOfQuadVertices(int QuadOffset, unsigned char r, unsigned char g, unsigned char b, unsigned char a) +void CGraphics_Threaded::ChangeColorOfQuadVertices(size_t QuadOffset, unsigned char r, unsigned char g, unsigned char b, unsigned char a) { - if(g_Config.m_GfxQuadAsTriangle && !m_GLUseTrianglesAsQuad) + const CCommandBuffer::SColor Color(r, g, b, a); + const size_t VertNum = g_Config.m_GfxQuadAsTriangle && !m_GLUseTrianglesAsQuad ? 6 : 4; + for(size_t i = 0; i < VertNum; ++i) { - m_aVertices[QuadOffset * 6].m_Color.r = r; - m_aVertices[QuadOffset * 6].m_Color.g = g; - m_aVertices[QuadOffset * 6].m_Color.b = b; - m_aVertices[QuadOffset * 6].m_Color.a = a; - - m_aVertices[QuadOffset * 6 + 1].m_Color.r = r; - m_aVertices[QuadOffset * 6 + 1].m_Color.g = g; - m_aVertices[QuadOffset * 6 + 1].m_Color.b = b; - m_aVertices[QuadOffset * 6 + 1].m_Color.a = a; - - m_aVertices[QuadOffset * 6 + 2].m_Color.r = r; - m_aVertices[QuadOffset * 6 + 2].m_Color.g = g; - m_aVertices[QuadOffset * 6 + 2].m_Color.b = b; - m_aVertices[QuadOffset * 6 + 2].m_Color.a = a; - - m_aVertices[QuadOffset * 6 + 3].m_Color.r = r; - m_aVertices[QuadOffset * 6 + 3].m_Color.g = g; - m_aVertices[QuadOffset * 6 + 3].m_Color.b = b; - m_aVertices[QuadOffset * 6 + 3].m_Color.a = a; - - m_aVertices[QuadOffset * 6 + 4].m_Color.r = r; - m_aVertices[QuadOffset * 6 + 4].m_Color.g = g; - m_aVertices[QuadOffset * 6 + 4].m_Color.b = b; - m_aVertices[QuadOffset * 6 + 4].m_Color.a = a; - - m_aVertices[QuadOffset * 6 + 5].m_Color.r = r; - m_aVertices[QuadOffset * 6 + 5].m_Color.g = g; - m_aVertices[QuadOffset * 6 + 5].m_Color.b = b; - m_aVertices[QuadOffset * 6 + 5].m_Color.a = a; - } - else - { - m_aVertices[QuadOffset * 4].m_Color.r = r; - m_aVertices[QuadOffset * 4].m_Color.g = g; - m_aVertices[QuadOffset * 4].m_Color.b = b; - m_aVertices[QuadOffset * 4].m_Color.a = a; - - m_aVertices[QuadOffset * 4 + 1].m_Color.r = r; - m_aVertices[QuadOffset * 4 + 1].m_Color.g = g; - m_aVertices[QuadOffset * 4 + 1].m_Color.b = b; - m_aVertices[QuadOffset * 4 + 1].m_Color.a = a; - - m_aVertices[QuadOffset * 4 + 2].m_Color.r = r; - m_aVertices[QuadOffset * 4 + 2].m_Color.g = g; - m_aVertices[QuadOffset * 4 + 2].m_Color.b = b; - m_aVertices[QuadOffset * 4 + 2].m_Color.a = a; - - m_aVertices[QuadOffset * 4 + 3].m_Color.r = r; - m_aVertices[QuadOffset * 4 + 3].m_Color.g = g; - m_aVertices[QuadOffset * 4 + 3].m_Color.b = b; - m_aVertices[QuadOffset * 4 + 3].m_Color.a = a; + m_aVertices[QuadOffset * VertNum + i].m_Color = Color; } } @@ -1074,26 +892,14 @@ void CGraphics_Threaded::QuadsDrawTL(const CQuadItem *pArray, int Num) void CGraphics_Threaded::QuadsTex3DDrawTL(const CQuadItem *pArray, int Num) { - int CurNumVert = m_NumVertices; - - int VertNum = 0; - if(g_Config.m_GfxQuadAsTriangle && !m_GLUseTrianglesAsQuad) - { - VertNum = 6; - } - else - { - VertNum = 4; - } + const int VertNum = g_Config.m_GfxQuadAsTriangle && !m_GLUseTrianglesAsQuad ? 6 : 4; + const float CurIndex = Uses2DTextureArrays() ? m_CurIndex : (m_CurIndex + 0.5f) / 256.0f; for(int i = 0; i < Num; ++i) { for(int n = 0; n < VertNum; ++n) { - if(Uses2DTextureArrays()) - m_aVerticesTex3D[CurNumVert + VertNum * i + n].m_Tex.w = (float)m_CurIndex; - else - m_aVerticesTex3D[CurNumVert + VertNum * i + n].m_Tex.w = ((float)m_CurIndex + 0.5f) / 256.f; + m_aVerticesTex3D[m_NumVertices + VertNum * i + n].m_Tex.w = CurIndex; } } @@ -2082,7 +1888,7 @@ void *CGraphics_Threaded::AllocCommandBufferData(size_t AllocSize) if(pData == nullptr) { char aError[256]; - str_format(aError, sizeof(aError), "graphics: failed to allocate data (size %" PRIzu ") for command buffer", (size_t)AllocSize); + str_format(aError, sizeof(aError), "graphics: failed to allocate data (size %" PRIzu ") for command buffer", AllocSize); dbg_assert(false, aError); } } @@ -2360,7 +2166,7 @@ int CGraphics_Threaded::IssueInit() if(g_Config.m_GfxHighdpi) Flags |= IGraphicsBackend::INITFLAG_HIGHDPI; - int r = m_pBackend->Init("StormA Client", &g_Config.m_GfxScreen, &g_Config.m_GfxScreenWidth, &g_Config.m_GfxScreenHeight, &g_Config.m_GfxScreenRefreshRate, &g_Config.m_GfxFsaaSamples, Flags, &g_Config.m_GfxDesktopWidth, &g_Config.m_GfxDesktopHeight, &m_ScreenWidth, &m_ScreenHeight, m_pStorage); + int r = m_pBackend->Init("DDNet Client", &g_Config.m_GfxScreen, &g_Config.m_GfxScreenWidth, &g_Config.m_GfxScreenHeight, &g_Config.m_GfxScreenRefreshRate, &g_Config.m_GfxFsaaSamples, Flags, &g_Config.m_GfxDesktopWidth, &g_Config.m_GfxDesktopHeight, &m_ScreenWidth, &m_ScreenHeight, m_pStorage); AddBackEndWarningIfExists(); if(r == 0) { @@ -2604,9 +2410,14 @@ int CGraphics_Threaded::Init() mem_copy(&aNullTextureData[(y * NullTextureDimension + x) * PixelSize], pColor, PixelSize); } } + CImageInfo NullTextureInfo; + NullTextureInfo.m_Width = NullTextureDimension; + NullTextureInfo.m_Height = NullTextureDimension; + NullTextureInfo.m_Format = CImageInfo::FORMAT_RGBA; + NullTextureInfo.m_pData = aNullTextureData; const int TextureLoadFlags = Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; m_NullTexture.Invalidate(); - m_NullTexture = LoadTextureRaw(NullTextureDimension, NullTextureDimension, CImageInfo::FORMAT_RGBA, aNullTextureData, TextureLoadFlags); + m_NullTexture = LoadTextureRaw(NullTextureInfo, TextureLoadFlags, "null-texture"); dbg_assert(m_NullTexture.IsNullTexture(), "Null texture invalid"); } @@ -2671,9 +2482,9 @@ void CGraphics_Threaded::WarnPngliteIncompatibleImages(bool Warn) m_WarnPngliteIncompatibleImages = Warn; } -void CGraphics_Threaded::SetWindowParams(int FullscreenMode, bool IsBorderless, bool AllowResizing) +void CGraphics_Threaded::SetWindowParams(int FullscreenMode, bool IsBorderless) { - m_pBackend->SetWindowParams(FullscreenMode, IsBorderless, AllowResizing); + m_pBackend->SetWindowParams(FullscreenMode, IsBorderless); CVideoMode CurMode; m_pBackend->GetCurrentVideoMode(CurMode, m_ScreenHiDPIScale, g_Config.m_GfxDesktopWidth, g_Config.m_GfxDesktopHeight, g_Config.m_GfxScreen); GotResized(CurMode.m_WindowWidth, CurMode.m_WindowHeight, CurMode.m_RefreshRate); @@ -2694,6 +2505,7 @@ bool CGraphics_Threaded::SetWindowScreen(int Index) for(auto &PropChangedListener : m_vPropChangeListeners) PropChangedListener(); + return true; } @@ -2715,15 +2527,15 @@ void CGraphics_Threaded::Move(int x, int y) PropChangedListener(); } -void CGraphics_Threaded::Resize(int w, int h, int RefreshRate) +bool CGraphics_Threaded::Resize(int w, int h, int RefreshRate) { #if defined(CONF_VIDEORECORDER) if(IVideo::Current() && IVideo::Current()->IsRecording()) - return; + return false; #endif if(WindowWidth() == w && WindowHeight() == h && RefreshRate == m_ScreenRefreshRate) - return; + return false; // if the size is changed manually, only set the window resize, a window size changed event is triggered anyway if(m_pBackend->ResizeWindow(w, h, RefreshRate)) @@ -2731,7 +2543,20 @@ void CGraphics_Threaded::Resize(int w, int h, int RefreshRate) CVideoMode CurMode; m_pBackend->GetCurrentVideoMode(CurMode, m_ScreenHiDPIScale, g_Config.m_GfxDesktopWidth, g_Config.m_GfxDesktopHeight, g_Config.m_GfxScreen); GotResized(w, h, RefreshRate); + return true; } + return false; +} + +void CGraphics_Threaded::ResizeToScreen() +{ + if(Resize(g_Config.m_GfxScreenWidth, g_Config.m_GfxScreenHeight, g_Config.m_GfxScreenRefreshRate)) + return; + + // Revert config variables if the change was not accepted + g_Config.m_GfxScreenWidth = ScreenWidth(); + g_Config.m_GfxScreenHeight = ScreenHeight(); + g_Config.m_GfxScreenRefreshRate = m_ScreenRefreshRate; } void CGraphics_Threaded::GotResized(int w, int h, int RefreshRate) @@ -2787,12 +2612,12 @@ int CGraphics_Threaded::GetWindowScreen() return m_pBackend->GetWindowScreen(); } -void CGraphics_Threaded::WindowDestroyNtf(uint32_t WindowID) +void CGraphics_Threaded::WindowDestroyNtf(uint32_t WindowId) { - m_pBackend->WindowDestroyNtf(WindowID); + m_pBackend->WindowDestroyNtf(WindowId); CCommandBuffer::SCommand_WindowDestroyNtf Cmd; - Cmd.m_WindowID = WindowID; + Cmd.m_WindowId = WindowId; AddCmd(Cmd); // wait @@ -2800,12 +2625,12 @@ void CGraphics_Threaded::WindowDestroyNtf(uint32_t WindowID) WaitForIdle(); } -void CGraphics_Threaded::WindowCreateNtf(uint32_t WindowID) +void CGraphics_Threaded::WindowCreateNtf(uint32_t WindowId) { - m_pBackend->WindowCreateNtf(WindowID); + m_pBackend->WindowCreateNtf(WindowId); CCommandBuffer::SCommand_WindowCreateNtf Cmd; - Cmd.m_WindowID = WindowID; + Cmd.m_WindowId = WindowId; AddCmd(Cmd); // wait @@ -2825,12 +2650,38 @@ int CGraphics_Threaded::WindowOpen() void CGraphics_Threaded::SetWindowGrab(bool Grab) { - return m_pBackend->SetWindowGrab(Grab); + m_pBackend->SetWindowGrab(Grab); } void CGraphics_Threaded::NotifyWindow() { - return m_pBackend->NotifyWindow(); + m_pBackend->NotifyWindow(); +} + +void CGraphics_Threaded::ReadPixel(ivec2 Position, ColorRGBA *pColor) +{ + dbg_assert(Position.x >= 0 && Position.x < ScreenWidth(), "ReadPixel position x out of range"); + dbg_assert(Position.y >= 0 && Position.y < ScreenHeight(), "ReadPixel position y out of range"); + + m_ReadPixelPosition = Position; + m_pReadPixelColor = pColor; +} + +void CGraphics_Threaded::ReadPixelDirect(bool *pSwapped) +{ + if(m_pReadPixelColor == nullptr) + return; + + CCommandBuffer::SCommand_TrySwapAndReadPixel Cmd; + Cmd.m_Position = m_ReadPixelPosition; + Cmd.m_pColor = m_pReadPixelColor; + Cmd.m_pSwapped = pSwapped; + AddCmd(Cmd); + + KickCommandBuffer(); + WaitForIdle(); + + m_pReadPixelColor = nullptr; } void CGraphics_Threaded::TakeScreenshot(const char *pFilename) @@ -2859,23 +2710,16 @@ void CGraphics_Threaded::Swap() } } - bool TookScreenshotAndSwapped = false; + bool Swapped = false; + ScreenshotDirect(&Swapped); + ReadPixelDirect(&Swapped); - if(m_DoScreenshot) + if(!Swapped) { - if(WindowActive()) - TookScreenshotAndSwapped = ScreenshotDirect(); - m_DoScreenshot = false; - } - - if(!TookScreenshotAndSwapped) - { - // add swap command CCommandBuffer::SCommand_Swap Cmd; AddCmd(Cmd); } - // kick the command buffer KickCommandBuffer(); // TODO: Remove when https://github.com/libsdl-org/SDL/issues/5203 is fixed #ifdef CONF_PLATFORM_MACOS @@ -2997,6 +2841,11 @@ int CGraphics_Threaded::GetVideoModes(CVideoMode *pModes, int MaxModes, int Scre return NumModes; } +void CGraphics_Threaded::GetCurrentVideoMode(CVideoMode &CurMode, int Screen) +{ + m_pBackend->GetCurrentVideoMode(CurMode, m_ScreenHiDPIScale, g_Config.m_GfxDesktopWidth, g_Config.m_GfxDesktopHeight, Screen); +} + extern IEngineGraphics *CreateEngineGraphicsThreaded() { return new CGraphics_Threaded(); diff --git a/src/engine/client/graphics_threaded.h b/src/engine/client/graphics_threaded.h index eaf0570d66..01774d58fc 100644 --- a/src/engine/client/graphics_threaded.h +++ b/src/engine/client/graphics_threaded.h @@ -93,7 +93,6 @@ class CCommandBuffer // texture commands CMD_TEXTURE_CREATE, CMD_TEXTURE_DESTROY, - CMD_TEXTURE_UPDATE, CMD_TEXT_TEXTURES_CREATE, CMD_TEXT_TEXTURES_DESTROY, CMD_TEXT_TEXTURE_UPDATE, @@ -130,6 +129,7 @@ class CCommandBuffer // misc CMD_MULTISAMPLING, CMD_VSYNC, + CMD_TRY_SWAP_AND_READ_PIXEL, CMD_TRY_SWAP_AND_SCREENSHOT, CMD_UPDATE_VIEWPORT, @@ -462,12 +462,21 @@ class CCommandBuffer void *m_pOffset; }; + struct SCommand_TrySwapAndReadPixel : public SCommand + { + SCommand_TrySwapAndReadPixel() : + SCommand(CMD_TRY_SWAP_AND_READ_PIXEL) {} + ivec2 m_Position; + SColorf *m_pColor; // processor will fill this out + bool *m_pSwapped; // processor may set this to true, must be initialized to false by the caller + }; + struct SCommand_TrySwapAndScreenshot : public SCommand { SCommand_TrySwapAndScreenshot() : SCommand(CMD_TRY_SWAP_AND_SCREENSHOT) {} CImageInfo *m_pImage; // processor will fill this out, the one who adds this command must free the data as well - bool *m_pSwapped; + bool *m_pSwapped; // processor may set this to true, must be initialized to false by the caller }; struct SCommand_Swap : public SCommand @@ -517,26 +526,9 @@ class CCommandBuffer size_t m_Width; size_t m_Height; - int m_Format; - int m_StoreFormat; int m_Flags; - void *m_pData; // will be freed by the command processor - }; - - struct SCommand_Texture_Update : public SCommand - { - SCommand_Texture_Update() : - SCommand(CMD_TEXTURE_UPDATE) {} - - // texture information - int m_Slot; - - int m_X; - int m_Y; - size_t m_Width; - size_t m_Height; - int m_Format; - void *m_pData; // will be freed by the command processor + // data must be in RGBA format + uint8_t *m_pData; // will be freed by the command processor }; struct SCommand_Texture_Destroy : public SCommand @@ -560,8 +552,8 @@ class CCommandBuffer size_t m_Width; size_t m_Height; - void *m_pTextData; - void *m_pTextOutlineData; + uint8_t *m_pTextData; // will be freed by the command processor + uint8_t *m_pTextOutlineData; // will be freed by the command processor }; struct SCommand_TextTextures_Destroy : public SCommand @@ -586,7 +578,7 @@ class CCommandBuffer int m_Y; size_t m_Width; size_t m_Height; - void *m_pData; // will be freed by the command processor + uint8_t *m_pData; // will be freed by the command processor }; struct SCommand_WindowCreateNtf : public CCommandBuffer::SCommand @@ -594,7 +586,7 @@ class CCommandBuffer SCommand_WindowCreateNtf() : SCommand(CMD_WINDOW_CREATE_NTF) {} - uint32_t m_WindowID; + uint32_t m_WindowId; }; struct SCommand_WindowDestroyNtf : public CCommandBuffer::SCommand @@ -602,7 +594,7 @@ class CCommandBuffer SCommand_WindowDestroyNtf() : SCommand(CMD_WINDOW_DESTROY_NTF) {} - uint32_t m_WindowID; + uint32_t m_WindowId; }; // @@ -699,7 +691,7 @@ class IGraphicsBackend virtual uint64_t StreamedMemoryUsage() const = 0; virtual uint64_t StagingMemoryUsage() const = 0; - virtual const TTWGraphicsGPUList &GetGPUs() const = 0; + virtual const TTwGraphicsGpuList &GetGpus() const = 0; virtual void GetVideoModes(CVideoMode *pModes, int MaxModes, int *pNumModes, int HiDPIScale, int MaxWindowWidth, int MaxWindowHeight, int Screen) = 0; virtual void GetCurrentVideoMode(CVideoMode &CurMode, int HiDPIScale, int MaxWindowWidth, int MaxWindowHeight, int Screen) = 0; @@ -709,7 +701,7 @@ class IGraphicsBackend virtual void Minimize() = 0; virtual void Maximize() = 0; - virtual void SetWindowParams(int FullscreenMode, bool IsBorderless, bool AllowResizing) = 0; + virtual void SetWindowParams(int FullscreenMode, bool IsBorderless) = 0; virtual bool SetWindowScreen(int Index) = 0; virtual bool UpdateDisplayMode(int Index) = 0; virtual int GetWindowScreen() = 0; @@ -721,8 +713,8 @@ class IGraphicsBackend virtual void GetViewportSize(int &w, int &h) = 0; virtual void NotifyWindow() = 0; - virtual void WindowDestroyNtf(uint32_t WindowID) = 0; - virtual void WindowCreateNtf(uint32_t WindowID) = 0; + virtual void WindowDestroyNtf(uint32_t WindowId) = 0; + virtual void WindowCreateNtf(uint32_t WindowId) = 0; virtual void RunBuffer(CCommandBuffer *pBuffer) = 0; virtual void RunBufferSingleThreadedUnsafe(CCommandBuffer *pBuffer) = 0; @@ -806,8 +798,6 @@ class CGraphics_Threaded : public IEngineGraphics size_t m_FirstFreeTexture; int m_TextureMemoryUsage; - std::vector m_vSpriteHelper; - bool m_WarnPngliteIncompatibleImages = false; std::vector m_vWarnings; @@ -917,6 +907,11 @@ class CGraphics_Threaded : public IEngineGraphics void AdjustViewport(bool SendViewportChangeToBackend); + ivec2 m_ReadPixelPosition = ivec2(0, 0); + ColorRGBA *m_pReadPixelColor = nullptr; + void ReadPixelDirect(bool *pSwapped); + void ScreenshotDirect(bool *pSwapped); + int IssueInit(); int InitWindow(); @@ -938,7 +933,7 @@ class CGraphics_Threaded : public IEngineGraphics uint64_t StreamedMemoryUsage() const override; uint64_t StagingMemoryUsage() const override; - const TTWGraphicsGPUList &GetGPUs() const override; + const TTwGraphicsGpuList &GetGpus() const override; void MapScreen(float TopLeftX, float TopLeftY, float BottomRightX, float BottomRightY) override; void GetScreen(float *pTopLeftX, float *pTopLeftY, float *pBottomRightX, float *pBottomRightY) override; @@ -949,35 +944,28 @@ class CGraphics_Threaded : public IEngineGraphics IGraphics::CTextureHandle FindFreeTextureIndex(); void FreeTextureIndex(CTextureHandle *pIndex); - int UnloadTexture(IGraphics::CTextureHandle *pIndex) override; - IGraphics::CTextureHandle LoadTextureRaw(size_t Width, size_t Height, CImageInfo::EImageFormat Format, const void *pData, int Flags, const char *pTexName = nullptr) override; - int LoadTextureRawSub(IGraphics::CTextureHandle TextureID, int x, int y, size_t Width, size_t Height, CImageInfo::EImageFormat Format, const void *pData) override; - IGraphics::CTextureHandle NullTexture() const override; + void UnloadTexture(IGraphics::CTextureHandle *pIndex) override; + IGraphics::CTextureHandle LoadTextureRaw(const CImageInfo &Image, int Flags, const char *pTexName = nullptr) override; + IGraphics::CTextureHandle LoadTextureRawMove(CImageInfo &Image, int Flags, const char *pTexName = nullptr) override; - bool LoadTextTextures(size_t Width, size_t Height, CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture, void *pTextData, void *pTextOutlineData) override; + bool LoadTextTextures(size_t Width, size_t Height, CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture, uint8_t *pTextData, uint8_t *pTextOutlineData) override; bool UnloadTextTextures(CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture) override; - bool UpdateTextTexture(CTextureHandle TextureID, int x, int y, size_t Width, size_t Height, const void *pData) override; + bool UpdateTextTexture(CTextureHandle TextureId, int x, int y, size_t Width, size_t Height, const uint8_t *pData) override; - CTextureHandle LoadSpriteTextureImpl(CImageInfo &FromImageInfo, int x, int y, size_t w, size_t h); - CTextureHandle LoadSpriteTexture(CImageInfo &FromImageInfo, struct CDataSprite *pSprite) override; + CTextureHandle LoadSpriteTexture(const CImageInfo &FromImageInfo, const struct CDataSprite *pSprite) override; - bool IsImageSubFullyTransparent(CImageInfo &FromImageInfo, int x, int y, int w, int h) override; - bool IsSpriteTextureFullyTransparent(CImageInfo &FromImageInfo, struct CDataSprite *pSprite) override; + bool IsImageSubFullyTransparent(const CImageInfo &FromImageInfo, int x, int y, int w, int h) override; + bool IsSpriteTextureFullyTransparent(const CImageInfo &FromImageInfo, const struct CDataSprite *pSprite) override; // simple uncompressed RGBA loaders IGraphics::CTextureHandle LoadTexture(const char *pFilename, int StorageType, int Flags = 0) override; - bool LoadPNG(CImageInfo *pImg, const char *pFilename, int StorageType) override; - void FreePNG(CImageInfo *pImg) override; - - bool CheckImageDivisibility(const char *pFileName, CImageInfo &Img, int DivX, int DivY, bool AllowResize) override; - bool IsImageFormatRGBA(const char *pFileName, CImageInfo &Img) override; - - void CopyTextureBufferSub(uint8_t *pDestBuffer, uint8_t *pSourceBuffer, size_t FullWidth, size_t FullHeight, size_t PixelSize, size_t SubOffsetX, size_t SubOffsetY, size_t SubCopyWidth, size_t SubCopyHeight) override; - void CopyTextureFromTextureBufferSub(uint8_t *pDestBuffer, size_t DestWidth, size_t DestHeight, uint8_t *pSourceBuffer, size_t SrcWidth, size_t SrcHeight, size_t PixelSize, size_t SrcSubOffsetX, size_t SrcSubOffsetY, size_t SrcSubCopyWidth, size_t SrcSubCopyHeight) override; + bool LoadPng(CImageInfo &Image, const char *pFilename, int StorageType) override; + bool LoadPng(CImageInfo &Image, const uint8_t *pData, size_t DataSize, const char *pContextName) override; - bool ScreenshotDirect(); + bool CheckImageDivisibility(const char *pContextName, CImageInfo &Image, int DivX, int DivY, bool AllowResize) override; + bool IsImageFormatRgba(const char *pContextName, const CImageInfo &Image) override; - void TextureSet(CTextureHandle TextureID) override; + void TextureSet(CTextureHandle TextureId) override; void Clear(float r, float g, float b, bool ForceClearNow = false) override; @@ -998,14 +986,14 @@ class CGraphics_Threaded : public IEngineGraphics pVert->m_Color = m_aColor[ColorIndex]; } - void SetColorVertex(const CColorVertex *pArray, int Num) override; + void SetColorVertex(const CColorVertex *pArray, size_t Num) override; void SetColor(float r, float g, float b, float a) override; void SetColor(ColorRGBA Color) override; void SetColor4(ColorRGBA TopLeft, ColorRGBA TopRight, ColorRGBA BottomLeft, ColorRGBA BottomRight) override; // go through all vertices and change their color (only works for quads) void ChangeColorOfCurrentQuadVertices(float r, float g, float b, float a) override; - void ChangeColorOfQuadVertices(int QuadOffset, unsigned char r, unsigned char g, unsigned char b, unsigned char a) override; + void ChangeColorOfQuadVertices(size_t QuadOffset, unsigned char r, unsigned char g, unsigned char b, unsigned char a) override; void QuadsSetSubset(float TlU, float TlV, float BrU, float BrV) override; void QuadsSetSubsetFree( @@ -1221,18 +1209,19 @@ class CGraphics_Threaded : public IEngineGraphics void Minimize() override; void Maximize() override; void WarnPngliteIncompatibleImages(bool Warn) override; - void SetWindowParams(int FullscreenMode, bool IsBorderless, bool AllowResizing) override; + void SetWindowParams(int FullscreenMode, bool IsBorderless) override; bool SetWindowScreen(int Index) override; void Move(int x, int y) override; - void Resize(int w, int h, int RefreshRate) override; + bool Resize(int w, int h, int RefreshRate) override; + void ResizeToScreen() override; void GotResized(int w, int h, int RefreshRate) override; void UpdateViewport(int X, int Y, int W, int H, bool ByResize) override; void AddWindowResizeListener(WINDOW_RESIZE_FUNC pFunc) override; void AddWindowPropChangeListener(WINDOW_PROPS_CHANGED_FUNC pFunc) override; int GetWindowScreen() override; - void WindowDestroyNtf(uint32_t WindowID) override; - void WindowCreateNtf(uint32_t WindowID) override; + void WindowDestroyNtf(uint32_t WindowId) override; + void WindowCreateNtf(uint32_t WindowId) override; int WindowActive() override; int WindowOpen() override; @@ -1243,6 +1232,7 @@ class CGraphics_Threaded : public IEngineGraphics int Init() override; void Shutdown() override; + void ReadPixel(ivec2 Position, ColorRGBA *pColor) override; void TakeScreenshot(const char *pFilename) override; void TakeCustomScreenshot(const char *pFilename) override; void Swap() override; @@ -1250,6 +1240,7 @@ class CGraphics_Threaded : public IEngineGraphics bool SetMultiSampling(uint32_t ReqMultiSamplingCount, uint32_t &MultiSamplingCountBackend) override; int GetVideoModes(CVideoMode *pModes, int MaxModes, int Screen) override; + void GetCurrentVideoMode(CVideoMode &CurMode, int Screen) override; virtual int GetDesktopScreenWidth() const { return g_Config.m_GfxDesktopWidth; } virtual int GetDesktopScreenHeight() const { return g_Config.m_GfxDesktopHeight; } diff --git a/src/engine/client/input.cpp b/src/engine/client/input.cpp index 7de5a9e31d..f590ba8c02 100644 --- a/src/engine/client/input.cpp +++ b/src/engine/client/input.cpp @@ -25,7 +25,6 @@ #endif #if defined(CONF_FAMILY_WINDOWS) -#define WIN32_LEAN_AND_MEAN #include // windows.h must be included before imm.h, but clang-format requires includes to be sorted alphabetically, hence this comment. #include @@ -34,19 +33,25 @@ // for platform specific features that aren't available or are broken in SDL #include -void CInput::AddEvent(char *pText, int Key, int Flags) +void CInput::AddKeyEvent(int Key, int Flags) { - if(m_NumEvents != INPUT_BUFFER_SIZE) - { - m_aInputEvents[m_NumEvents].m_Key = Key; - m_aInputEvents[m_NumEvents].m_Flags = Flags; - if(pText == nullptr) - m_aInputEvents[m_NumEvents].m_aText[0] = '\0'; - else - str_copy(m_aInputEvents[m_NumEvents].m_aText, pText); - m_aInputEvents[m_NumEvents].m_InputCount = m_InputCounter; - m_NumEvents++; - } + dbg_assert((Flags & (FLAG_PRESS | FLAG_RELEASE)) != 0 && (Flags & ~(FLAG_PRESS | FLAG_RELEASE)) == 0, "Flags invalid"); + CEvent Event; + Event.m_Key = Key; + Event.m_Flags = Flags; + Event.m_aText[0] = '\0'; + Event.m_InputCount = m_InputCounter; + m_vInputEvents.emplace_back(Event); +} + +void CInput::AddTextEvent(const char *pText) +{ + CEvent Event; + Event.m_Key = KEY_UNKNOWN; + Event.m_Flags = FLAG_TEXT; + str_copy(Event.m_aText, pText); + Event.m_InputCount = m_InputCounter; + m_vInputEvents.emplace_back(Event); } CInput::CInput() @@ -54,20 +59,15 @@ CInput::CInput() mem_zero(m_aInputCount, sizeof(m_aInputCount)); mem_zero(m_aInputState, sizeof(m_aInputState)); + m_vInputEvents.reserve(32); m_LastUpdate = 0; m_UpdateTime = 0.0f; m_InputCounter = 1; m_InputGrabbed = false; - m_MouseDoubleClick = false; - - m_NumEvents = 0; m_MouseFocus = true; - m_pClipboardText = nullptr; - - m_CompositionLength = COMP_LENGTH_INACTIVE; m_CompositionCursor = 0; m_CandidateSelectedIndex = -1; @@ -80,6 +80,7 @@ void CInput::Init() m_pGraphics = Kernel()->RequestInterface(); m_pConsole = Kernel()->RequestInterface(); + m_pConfigManager = Kernel()->RequestInterface(); MouseModeRelative(); @@ -88,7 +89,6 @@ void CInput::Init() void CInput::Shutdown() { - SDL_free(m_pClipboardText); CloseJoysticks(); } @@ -175,7 +175,7 @@ CInput::CJoystick::CJoystick(CInput *pInput, int Index, SDL_Joystick *pDelegate) m_NumHats = SDL_JoystickNumHats(pDelegate); str_copy(m_aName, SDL_JoystickName(pDelegate)); SDL_JoystickGetGUIDString(SDL_JoystickGetGUID(pDelegate), m_aGUID, sizeof(m_aGUID)); - m_InstanceID = SDL_JoystickInstanceID(pDelegate); + m_InstanceId = SDL_JoystickInstanceID(pDelegate); } void CInput::CloseJoysticks() @@ -217,7 +217,7 @@ void CInput::CJoystick::GetJoystickHatKeys(int Hat, int HatValue, int (&HatKeys) void CInput::CJoystick::GetHatValue(int Hat, int (&HatKeys)[2]) { - return GetJoystickHatKeys(Hat, SDL_JoystickGetHat(m_pDelegate, Hat), HatKeys); + GetJoystickHatKeys(Hat, SDL_JoystickGetHat(m_pDelegate, Hat), HatKeys); } bool CInput::CJoystick::Relative(float *pX, float *pY) @@ -260,14 +260,7 @@ bool CInput::MouseRelative(float *pX, float *pY) return false; ivec2 Relative; -#if defined(CONF_PLATFORM_ANDROID) // No relative mouse on Android - ivec2 CurrentPos; - SDL_GetMouseState(&CurrentPos.x, &CurrentPos.y); - Relative = CurrentPos - m_LastMousePos; - m_LastMousePos = CurrentPos; -#else SDL_GetRelativeMouseState(&Relative.x, &Relative.y); -#endif *pX = Relative.x; *pY = Relative.y; @@ -284,40 +277,36 @@ void CInput::MouseModeAbsolute() void CInput::MouseModeRelative() { m_InputGrabbed = true; -#if !defined(CONF_PLATFORM_ANDROID) // No relative mouse on Android SDL_SetRelativeMouseMode(SDL_TRUE); -#endif Graphics()->SetWindowGrab(true); // Clear pending relative mouse motion SDL_GetRelativeMouseState(nullptr, nullptr); } -void CInput::NativeMousePos(int *pX, int *pY) const +vec2 CInput::NativeMousePos() const { - SDL_GetMouseState(pX, pY); + ivec2 Position; + SDL_GetMouseState(&Position.x, &Position.y); + return vec2(Position.x, Position.y); } -bool CInput::NativeMousePressed(int Index) +bool CInput::NativeMousePressed(int Index) const { int i = SDL_GetMouseState(nullptr, nullptr); return (i & SDL_BUTTON(Index)) != 0; } -bool CInput::MouseDoubleClick() +const std::vector &CInput::TouchFingerStates() const { - if(m_MouseDoubleClick) - { - m_MouseDoubleClick = false; - return true; - } - return false; + return m_vTouchFingerStates; } -const char *CInput::GetClipboardText() +std::string CInput::GetClipboardText() { - SDL_free(m_pClipboardText); - m_pClipboardText = SDL_GetClipboardText(); - return m_pClipboardText; + char *pClipboardText = SDL_GetClipboardText(); + std::string ClipboardText = pClipboardText; + SDL_free(pClipboardText); + return ClipboardText; } void CInput::SetClipboardText(const char *pText) @@ -337,17 +326,37 @@ void CInput::StopTextInput() SDL_StopTextInput(); // disable system messages for performance SDL_EventState(SDL_SYSWMEVENT, SDL_DISABLE); - m_CompositionLength = COMP_LENGTH_INACTIVE; + m_CompositionString = ""; m_CompositionCursor = 0; - m_aComposition[0] = '\0'; m_vCandidates.clear(); } +void CInput::ConsumeEvents(std::function Consumer) const +{ + for(const CEvent &Event : m_vInputEvents) + { + // Only propagate valid input events + if(Event.m_InputCount == m_InputCounter) + { + Consumer(Event); + } + } +} + void CInput::Clear() { mem_zero(m_aInputState, sizeof(m_aInputState)); mem_zero(m_aInputCount, sizeof(m_aInputCount)); - m_NumEvents = 0; + m_vInputEvents.clear(); + for(CTouchFingerState &TouchFingerState : m_vTouchFingerStates) + { + TouchFingerState.m_Delta = vec2(0.0f, 0.0f); + } +} + +float CInput::GetUpdateTime() const +{ + return m_UpdateTime; } bool CInput::KeyState(int Key) const @@ -357,6 +366,26 @@ bool CInput::KeyState(int Key) const return m_aInputState[Key]; } +int CInput::FindKeyByName(const char *pKeyName) const +{ + // check for numeric + if(pKeyName[0] == '&') + { + int Key = str_toint(pKeyName + 1); + if(Key > KEY_FIRST && Key < KEY_LAST) + return Key; // numeric + } + + // search for key + for(int Key = KEY_FIRST; Key < KEY_LAST; Key++) + { + if(str_comp_nocase(pKeyName, KeyName(Key)) == 0) + return Key; + } + + return KEY_UNKNOWN; +} + void CInput::UpdateMouseState() { const int MouseState = SDL_GetMouseState(nullptr, nullptr); @@ -412,7 +441,7 @@ void CInput::HandleJoystickAxisMotionEvent(const SDL_JoyAxisEvent &Event) if(!g_Config.m_InpControllerEnable) return; CJoystick *pJoystick = GetActiveJoystick(); - if(!pJoystick || pJoystick->GetInstanceID() != Event.which) + if(!pJoystick || pJoystick->GetInstanceId() != Event.which) return; if(Event.axis >= NUM_JOYSTICK_AXES) return; @@ -425,24 +454,24 @@ void CInput::HandleJoystickAxisMotionEvent(const SDL_JoyAxisEvent &Event) { m_aInputState[LeftKey] = true; m_aInputCount[LeftKey] = m_InputCounter; - AddEvent(nullptr, LeftKey, IInput::FLAG_PRESS); + AddKeyEvent(LeftKey, IInput::FLAG_PRESS); } else if(Event.value > SDL_JOYSTICK_AXIS_MIN * DeadZone && m_aInputState[LeftKey]) { m_aInputState[LeftKey] = false; - AddEvent(nullptr, LeftKey, IInput::FLAG_RELEASE); + AddKeyEvent(LeftKey, IInput::FLAG_RELEASE); } if(Event.value >= SDL_JOYSTICK_AXIS_MAX * DeadZone && !m_aInputState[RightKey]) { m_aInputState[RightKey] = true; m_aInputCount[RightKey] = m_InputCounter; - AddEvent(nullptr, RightKey, IInput::FLAG_PRESS); + AddKeyEvent(RightKey, IInput::FLAG_PRESS); } else if(Event.value < SDL_JOYSTICK_AXIS_MAX * DeadZone && m_aInputState[RightKey]) { m_aInputState[RightKey] = false; - AddEvent(nullptr, RightKey, IInput::FLAG_RELEASE); + AddKeyEvent(RightKey, IInput::FLAG_RELEASE); } } @@ -451,7 +480,7 @@ void CInput::HandleJoystickButtonEvent(const SDL_JoyButtonEvent &Event) if(!g_Config.m_InpControllerEnable) return; CJoystick *pJoystick = GetActiveJoystick(); - if(!pJoystick || pJoystick->GetInstanceID() != Event.which) + if(!pJoystick || pJoystick->GetInstanceId() != Event.which) return; if(Event.button >= NUM_JOYSTICK_BUTTONS) return; @@ -462,12 +491,12 @@ void CInput::HandleJoystickButtonEvent(const SDL_JoyButtonEvent &Event) { m_aInputState[Key] = true; m_aInputCount[Key] = m_InputCounter; - AddEvent(nullptr, Key, IInput::FLAG_PRESS); + AddKeyEvent(Key, IInput::FLAG_PRESS); } else if(Event.type == SDL_JOYBUTTONUP) { m_aInputState[Key] = false; - AddEvent(nullptr, Key, IInput::FLAG_RELEASE); + AddKeyEvent(Key, IInput::FLAG_RELEASE); } } @@ -476,7 +505,7 @@ void CInput::HandleJoystickHatMotionEvent(const SDL_JoyHatEvent &Event) if(!g_Config.m_InpControllerEnable) return; CJoystick *pJoystick = GetActiveJoystick(); - if(!pJoystick || pJoystick->GetInstanceID() != Event.which) + if(!pJoystick || pJoystick->GetInstanceId() != Event.which) return; if(Event.hat >= NUM_JOYSTICK_HATS) return; @@ -489,7 +518,7 @@ void CInput::HandleJoystickHatMotionEvent(const SDL_JoyHatEvent &Event) if(Key != HatKeys[0] && Key != HatKeys[1] && m_aInputState[Key]) { m_aInputState[Key] = false; - AddEvent(nullptr, Key, IInput::FLAG_RELEASE); + AddKeyEvent(Key, IInput::FLAG_RELEASE); } } @@ -499,7 +528,7 @@ void CInput::HandleJoystickHatMotionEvent(const SDL_JoyHatEvent &Event) { m_aInputState[CurrentKey] = true; m_aInputCount[CurrentKey] = m_InputCounter; - AddEvent(nullptr, CurrentKey, IInput::FLAG_PRESS); + AddKeyEvent(CurrentKey, IInput::FLAG_PRESS); } } } @@ -514,7 +543,7 @@ void CInput::HandleJoystickAddedEvent(const SDL_JoyDeviceEvent &Event) void CInput::HandleJoystickRemovedEvent(const SDL_JoyDeviceEvent &Event) { - auto RemovedJoystick = std::find_if(m_vJoysticks.begin(), m_vJoysticks.end(), [Event](const CJoystick &Joystick) -> bool { return Joystick.GetInstanceID() == Event.which; }); + auto RemovedJoystick = std::find_if(m_vJoysticks.begin(), m_vJoysticks.end(), [Event](const CJoystick &Joystick) -> bool { return Joystick.GetInstanceId() == Event.which; }); if(RemovedJoystick != m_vJoysticks.end()) { dbg_msg("joystick", "Closed joystick %d '%s'", (*RemovedJoystick).GetIndex(), (*RemovedJoystick).GetName()); @@ -529,6 +558,59 @@ void CInput::HandleJoystickRemovedEvent(const SDL_JoyDeviceEvent &Event) } } +void CInput::HandleTouchDownEvent(const SDL_TouchFingerEvent &Event) +{ + CTouchFingerState TouchFingerState; + TouchFingerState.m_Finger.m_DeviceId = Event.touchId; + TouchFingerState.m_Finger.m_FingerId = Event.fingerId; + TouchFingerState.m_Position = vec2(Event.x, Event.y); + TouchFingerState.m_Delta = vec2(Event.dx, Event.dy); + m_vTouchFingerStates.emplace_back(TouchFingerState); +} + +void CInput::HandleTouchUpEvent(const SDL_TouchFingerEvent &Event) +{ + auto FoundState = std::find_if(m_vTouchFingerStates.begin(), m_vTouchFingerStates.end(), [Event](const CTouchFingerState &State) { + return State.m_Finger.m_DeviceId == Event.touchId && State.m_Finger.m_FingerId == Event.fingerId; + }); + if(FoundState != m_vTouchFingerStates.end()) + { + m_vTouchFingerStates.erase(FoundState); + } +} + +void CInput::HandleTouchMotionEvent(const SDL_TouchFingerEvent &Event) +{ + auto FoundState = std::find_if(m_vTouchFingerStates.begin(), m_vTouchFingerStates.end(), [Event](const CTouchFingerState &State) { + return State.m_Finger.m_DeviceId == Event.touchId && State.m_Finger.m_FingerId == Event.fingerId; + }); + if(FoundState != m_vTouchFingerStates.end()) + { + FoundState->m_Position = vec2(Event.x, Event.y); + FoundState->m_Delta += vec2(Event.dx, Event.dy); + } +} + +void CInput::HandleTextEditingEvent(const char *pText, int Start, int Length) +{ + if(pText[0] != '\0') + { + m_CompositionString = pText; + m_CompositionCursor = 0; + for(int i = 0; i < Start; i++) + { + m_CompositionCursor = str_utf8_forward(m_CompositionString.c_str(), m_CompositionCursor); + } + // Length is currently unused on Windows and will always be 0, so we don't support selecting composition text + AddTextEvent(""); + } + else + { + m_CompositionString = ""; + m_CompositionCursor = 0; + } +} + void CInput::SetCompositionWindowPosition(float X, float Y, float H) { SDL_Rect Rect; @@ -539,6 +621,40 @@ void CInput::SetCompositionWindowPosition(float X, float Y, float H) SDL_SetTextInputRect(&Rect); } +static int TranslateScancode(const SDL_KeyboardEvent &KeyEvent) +{ + // See SDL_Keymod for possible modifiers: + // NONE = 0 + // LSHIFT = 1 + // RSHIFT = 2 + // LCTRL = 64 + // RCTRL = 128 + // LALT = 256 + // RALT = 512 + // LGUI = 1024 + // RGUI = 2048 + // NUM = 4096 + // CAPS = 8192 + // MODE = 16384 + // Sum if you want to ignore multiple modifiers. + if(KeyEvent.keysym.mod & g_Config.m_InpIgnoredModifiers) + { + return 0; + } + + int Scancode = g_Config.m_InpTranslatedKeys ? SDL_GetScancodeFromKey(KeyEvent.keysym.sym) : KeyEvent.keysym.scancode; + +#if defined(CONF_PLATFORM_ANDROID) + // Translate the Android back-button to the escape-key so it can be used to open/close the menu, close popups etc. + if(Scancode == KEY_AC_BACK) + { + Scancode = KEY_ESCAPE; + } +#endif + + return Scancode; +} + int CInput::Update() { const int64_t Now = time_get(); @@ -549,8 +665,8 @@ int CInput::Update() } m_LastUpdate = Now; - // keep the counter between 1..0xFFFF, 0 means not pressed - m_InputCounter = (m_InputCounter % 0xFFFF) + 1; + // keep the counter between 1..0xFFFFFFFF, 0 means not pressed + m_InputCounter = (m_InputCounter % std::numeric_limits::max()) + 1; // Ensure that we have the latest keyboard, mouse and joystick state SDL_PumpEvents(); @@ -579,57 +695,29 @@ int CInput::Update() break; case SDL_TEXTEDITING: - { - m_CompositionLength = str_length(Event.edit.text); - if(m_CompositionLength) - { - str_copy(m_aComposition, Event.edit.text); - m_CompositionCursor = 0; - for(int i = 0; i < Event.edit.start; i++) - m_CompositionCursor = str_utf8_forward(m_aComposition, m_CompositionCursor); - // Event.edit.length is currently unused on Windows and will always be 0, so we don't support selecting composition text - AddEvent(nullptr, KEY_UNKNOWN, IInput::FLAG_TEXT); - } - else - { - m_aComposition[0] = '\0'; - m_CompositionLength = 0; - m_CompositionCursor = 0; - } + HandleTextEditingEvent(Event.edit.text, Event.edit.start, Event.edit.length); break; - } + +#if SDL_VERSION_ATLEAST(2, 0, 22) + case SDL_TEXTEDITING_EXT: + HandleTextEditingEvent(Event.editExt.text, Event.editExt.start, Event.editExt.length); + SDL_free(Event.editExt.text); + break; +#endif case SDL_TEXTINPUT: - m_aComposition[0] = '\0'; - m_CompositionLength = COMP_LENGTH_INACTIVE; + m_CompositionString = ""; m_CompositionCursor = 0; - AddEvent(Event.text.text, KEY_UNKNOWN, IInput::FLAG_TEXT); + AddTextEvent(Event.text.text); break; // handle keys case SDL_KEYDOWN: - // See SDL_Keymod for possible modifiers: - // NONE = 0 - // LSHIFT = 1 - // RSHIFT = 2 - // LCTRL = 64 - // RCTRL = 128 - // LALT = 256 - // RALT = 512 - // LGUI = 1024 - // RGUI = 2048 - // NUM = 4096 - // CAPS = 8192 - // MODE = 16384 - // Sum if you want to ignore multiple modifiers. - if(!(Event.key.keysym.mod & g_Config.m_InpIgnoredModifiers)) - { - Scancode = g_Config.m_InpTranslatedKeys ? SDL_GetScancodeFromKey(Event.key.keysym.sym) : Event.key.keysym.scancode; - } + Scancode = TranslateScancode(Event.key); break; case SDL_KEYUP: Action = IInput::FLAG_RELEASE; - Scancode = g_Config.m_InpTranslatedKeys ? SDL_GetScancodeFromKey(Event.key.keysym.sym) : Event.key.keysym.scancode; + Scancode = TranslateScancode(Event.key); break; // handle the joystick events @@ -678,13 +766,6 @@ int CInput::Update() Scancode = KEY_MOUSE_8; if(Event.button.button == 9) Scancode = KEY_MOUSE_9; - if(Event.button.button == SDL_BUTTON_LEFT) - { - if(Event.button.clicks % 2 == 0) - m_MouseDoubleClick = true; - if(Event.button.clicks == 1) - m_MouseDoubleClick = false; - } break; case SDL_MOUSEWHEEL: @@ -699,6 +780,18 @@ int CInput::Update() Action |= IInput::FLAG_RELEASE; break; + case SDL_FINGERDOWN: + HandleTouchDownEvent(Event.tfinger); + break; + + case SDL_FINGERUP: + HandleTouchUpEvent(Event.tfinger); + break; + + case SDL_FINGERMOTION: + HandleTouchMotionEvent(Event.tfinger); + break; + case SDL_WINDOWEVENT: // Ignore keys following a focus gain as they may be part of global // shortcuts @@ -732,6 +825,9 @@ int CInput::Update() } break; case SDL_WINDOWEVENT_MINIMIZED: +#if defined(CONF_PLATFORM_ANDROID) // Save the config when minimized on Android. + m_pConfigManager->Save(); +#endif Graphics()->WindowDestroyNtf(Event.window.windowID); break; @@ -764,13 +860,10 @@ int CInput::Update() m_aInputState[Scancode] = 1; m_aInputCount[Scancode] = m_InputCounter; } - AddEvent(nullptr, Scancode, Action); + AddKeyEvent(Scancode, Action); } } - if(m_CompositionLength == 0) - m_CompositionLength = COMP_LENGTH_INACTIVE; - return 0; } diff --git a/src/engine/client/input.h b/src/engine/client/input.h index b0576b52c4..d0098abf1b 100644 --- a/src/engine/client/input.h +++ b/src/engine/client/input.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -13,6 +14,7 @@ #include class IEngineGraphics; +class IConfigManager; class CInput : public IEngineInput { @@ -25,7 +27,7 @@ class CInput : public IEngineInput int m_Index; char m_aName[64]; char m_aGUID[34]; - SDL_JoystickID m_InstanceID; + SDL_JoystickID m_InstanceId; int m_NumAxes; int m_NumButtons; int m_NumBalls; @@ -42,7 +44,7 @@ class CInput : public IEngineInput int GetIndex() const override { return m_Index; } const char *GetName() const override { return m_aName; } const char *GetGUID() const { return m_aGUID; } - SDL_JoystickID GetInstanceID() const { return m_InstanceID; } + SDL_JoystickID GetInstanceId() const { return m_InstanceId; } int GetNumAxes() const override { return m_NumAxes; } int GetNumButtons() const override { return m_NumButtons; } int GetNumBalls() const override { return m_NumBalls; } @@ -58,9 +60,10 @@ class CInput : public IEngineInput private: IEngineGraphics *m_pGraphics; IConsole *m_pConsole; + IConfigManager *m_pConfigManager; - IEngineGraphics *Graphics() { return m_pGraphics; } - IConsole *Console() { return m_pConsole; } + IEngineGraphics *Graphics() const { return m_pGraphics; } + IConsole *Console() const { return m_pConsole; } // joystick std::vector m_vJoysticks; @@ -73,29 +76,27 @@ class CInput : public IEngineInput float GetJoystickDeadzone(); bool m_InputGrabbed; - char *m_pClipboardText; bool m_MouseFocus; - bool m_MouseDoubleClick; -#if defined(CONF_PLATFORM_ANDROID) // No relative mouse on Android - ivec2 m_LastMousePos = ivec2(0, 0); -#endif // IME support - char m_aComposition[MAX_COMPOSITION_ARRAY_SIZE]; + std::string m_CompositionString; int m_CompositionCursor; - int m_CompositionLength; std::vector m_vCandidates; int m_CandidateSelectedIndex; - void AddEvent(char *pText, int Key, int Flags); - void Clear() override; - bool IsEventValid(const CEvent &Event) const override { return Event.m_InputCount == m_InputCounter; } + // events + std::vector m_vInputEvents; + int64_t m_LastUpdate; + float m_UpdateTime; + void AddKeyEvent(int Key, int Flags); + void AddTextEvent(const char *pText); // quick access to input - unsigned short m_aInputCount[g_MaxKeys]; // tw-KEY - unsigned char m_aInputState[g_MaxKeys]; // SDL_SCANCODE - int m_InputCounter; + uint32_t m_aInputCount[g_MaxKeys]; + unsigned char m_aInputState[g_MaxKeys]; + uint32_t m_InputCounter; + std::vector m_vTouchFingerStates; void UpdateMouseState(); void UpdateJoystickState(); @@ -104,6 +105,10 @@ class CInput : public IEngineInput void HandleJoystickHatMotionEvent(const SDL_JoyHatEvent &Event); void HandleJoystickAddedEvent(const SDL_JoyDeviceEvent &Event); void HandleJoystickRemovedEvent(const SDL_JoyDeviceEvent &Event); + void HandleTouchDownEvent(const SDL_TouchFingerEvent &Event); + void HandleTouchUpEvent(const SDL_TouchFingerEvent &Event); + void HandleTouchMotionEvent(const SDL_TouchFingerEvent &Event); + void HandleTextEditingEvent(const char *pText, int Start, int Length); char m_aDropFile[IO_MAX_PATH_LENGTH]; @@ -118,11 +123,16 @@ class CInput : public IEngineInput int Update() override; void Shutdown() override; + void ConsumeEvents(std::function Consumer) const override; + void Clear() override; + float GetUpdateTime() const override; + bool ModifierIsPressed() const override { return KeyState(KEY_LCTRL) || KeyState(KEY_RCTRL) || KeyState(KEY_LGUI) || KeyState(KEY_RGUI); } bool ShiftIsPressed() const override { return KeyState(KEY_LSHIFT) || KeyState(KEY_RSHIFT); } bool AltIsPressed() const override { return KeyState(KEY_LALT) || KeyState(KEY_RALT); } bool KeyIsPressed(int Key) const override { return KeyState(Key); } bool KeyPress(int Key, bool CheckCounter) const override { return CheckCounter ? (m_aInputCount[Key] == m_InputCounter) : m_aInputCount[Key]; } + int FindKeyByName(const char *pKeyName) const override; size_t NumJoysticks() const override { return m_vJoysticks.size(); } CJoystick *GetJoystick(size_t Index) override { return &m_vJoysticks[Index]; } @@ -132,19 +142,20 @@ class CInput : public IEngineInput bool MouseRelative(float *pX, float *pY) override; void MouseModeAbsolute() override; void MouseModeRelative() override; - void NativeMousePos(int *pX, int *pY) const override; - bool NativeMousePressed(int Index) override; - bool MouseDoubleClick() override; + vec2 NativeMousePos() const override; + bool NativeMousePressed(int Index) const override; + + const std::vector &TouchFingerStates() const override; - const char *GetClipboardText() override; + std::string GetClipboardText() override; void SetClipboardText(const char *pText) override; void StartTextInput() override; void StopTextInput() override; - const char *GetComposition() const override { return m_aComposition; } - bool HasComposition() const override { return m_CompositionLength != COMP_LENGTH_INACTIVE; } + const char *GetComposition() const override { return m_CompositionString.c_str(); } + bool HasComposition() const override { return !m_CompositionString.empty(); } int GetCompositionCursor() const override { return m_CompositionCursor; } - int GetCompositionLength() const override { return m_CompositionLength; } + int GetCompositionLength() const override { return m_CompositionString.length(); } const char *GetCandidate(int Index) const override { return m_vCandidates[Index].c_str(); } int GetCandidateCount() const override { return m_vCandidates.size(); } int GetCandidateSelectedIndex() const override { return m_CandidateSelectedIndex; } diff --git a/src/engine/client/keynames.h b/src/engine/client/keynames.h index 57005ff873..a41dd9d499 100644 --- a/src/engine/client/keynames.h +++ b/src/engine/client/keynames.h @@ -5,517 +5,518 @@ #endif const char g_aaKeyStrings[512][20] = // NOLINT(misc-definitions-in-headers) - { - "unknown", - "&1", - "&2", - "&3", - "a", - "b", - "c", - "d", - "e", - "f", - "g", - "h", - "i", - "j", - "k", - "l", - "m", - "n", - "o", - "p", - "q", - "r", - "s", - "t", - "u", - "v", - "w", - "x", - "y", - "z", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "0", - "return", - "escape", - "backspace", - "tab", - "space", - "minus", - "equals", - "leftbracket", - "rightbracket", - "backslash", - "nonushash", - "semicolon", - "apostrophe", - "grave", - "comma", - "period", - "slash", - "capslock", - "f1", - "f2", - "f3", - "f4", - "f5", - "f6", - "f7", - "f8", - "f9", - "f10", - "f11", - "f12", - "printscreen", - "scrolllock", - "pause", - "insert", - "home", - "pageup", - "delete", - "end", - "pagedown", - "right", - "left", - "down", - "up", - "numlockclear", - "kp_divide", - "kp_multiply", - "kp_minus", - "kp_plus", - "kp_enter", - "kp_1", - "kp_2", - "kp_3", - "kp_4", - "kp_5", - "kp_6", - "kp_7", - "kp_8", - "kp_9", - "kp_0", - "kp_period", - "nonusbackslash", - "application", - "power", - "kp_equals", - "f13", - "f14", - "f15", - "f16", - "f17", - "f18", - "f19", - "f20", - "f21", - "f22", - "f23", - "f24", - "execute", - "help", - "menu", - "select", - "stop", - "again", - "undo", - "cut", - "copy", - "paste", - "find", - "mute", - "volumeup", - "volumedown", - "&130", - "&131", - "&132", - "kp_comma", - "kp_equalsas400", - "international1", - "international2", - "international3", - "international4", - "international5", - "international6", - "international7", - "international8", - "international9", - "lang1", - "lang2", - "lang3", - "lang4", - "lang5", - "lang6", - "lang7", - "lang8", - "lang9", - "alterase", - "sysreq", - "cancel", - "clear", - "prior", - "return2", - "separator", - "out", - "oper", - "clearagain", - "crsel", - "exsel", - "&165", - "&166", - "&167", - "&168", - "&169", - "&170", - "&171", - "&172", - "&173", - "&174", - "&175", - "kp_00", - "kp_000", - "thousandsseparator", - "decimalseparator", - "currencyunit", - "currencysubunit", - "kp_leftparen", - "kp_rightparen", - "kp_leftbrace", - "kp_rightbrace", - "kp_tab", - "kp_backspace", - "kp_a", - "kp_b", - "kp_c", - "kp_d", - "kp_e", - "kp_f", - "kp_xor", - "kp_power", - "kp_percent", - "kp_less", - "kp_greater", - "kp_ampersand", - "kp_dblampersand", - "kp_verticalbar", - "kp_dblverticalbar", - "kp_colon", - "kp_hash", - "kp_space", - "kp_at", - "kp_exclam", - "kp_memstore", - "kp_memrecall", - "kp_memclear", - "kp_memadd", - "kp_memsubtract", - "kp_memmultiply", - "kp_memdivide", - "kp_plusminus", - "kp_clear", - "kp_clearentry", - "kp_binary", - "kp_octal", - "kp_decimal", - "kp_hexadecimal", - "&222", - "&223", - "lctrl", - "lshift", - "lalt", - "lgui", - "rctrl", - "rshift", - "ralt", - "rgui", - "&232", - "&233", - "&234", - "&235", - "&236", - "&237", - "&238", - "&239", - "&240", - "&241", - "&242", - "&243", - "&244", - "&245", - "&246", - "&247", - "&248", - "&249", - "&250", - "&251", - "&252", - "&253", - "&254", - "&255", - "&256", - "mode", - "audionext", - "audioprev", - "audiostop", - "audioplay", - "audiomute", - "mediaselect", - "www", - "mail", - "calculator", - "computer", - "ac_search", - "ac_home", - "ac_back", - "ac_forward", - "ac_stop", - "ac_refresh", - "ac_bookmarks", - "brightnessdown", - "brightnessup", - "displayswitch", - "kbdillumtoggle", - "kbdillumdown", - "kbdillumup", - "eject", - "sleep", - "app1", - "app2", - "audiorewind", - "audiofastforward", - "softleft", - "softright", - "call", - "endcall", - "mouse1", - "mouse2", - "mouse3", - "mouse4", - "mouse5", - "mouse6", - "mouse7", - "mouse8", - "mouse9", - "mousewheelup", - "mousewheeldown", - "mousewheelleft", - "mousewheelright", - "joystick0", - "joystick1", - "joystick2", - "joystick3", - "joystick4", - "joystick5", - "joystick6", - "joystick7", - "joystick8", - "joystick9", - "joystick10", - "joystick11", - "joy_hat0_up", - "joy_hat0_left", - "joy_hat0_right", - "joy_hat0_down", - "joy_hat1_up", - "joy_hat1_left", - "joy_hat1_right", - "joy_hat1_down", - "joy_axis0_left", - "joy_axis0_right", - "joy_axis1_left", - "joy_axis1_right", - "joy_axis2_left", - "joy_axis2_right", - "joy_axis3_left", - "joy_axis3_right", - "joy_axis4_left", - "joy_axis4_right", - "joy_axis5_left", - "joy_axis5_right", - "joy_axis6_left", - "joy_axis6_right", - "joy_axis7_left", - "joy_axis7_right", - "joy_axis8_left", - "joy_axis8_right", - "joy_axis9_left", - "joy_axis9_right", - "joy_axis10_left", - "joy_axis10_right", - "joy_axis11_left", - "joy_axis11_right", - "&348", - "&349", - "&350", - "&351", - "&352", - "&353", - "&354", - "&355", - "&356", - "&357", - "&358", - "&359", - "&360", - "&361", - "&362", - "&363", - "&364", - "&365", - "&366", - "&367", - "&368", - "&369", - "&370", - "&371", - "&372", - "&373", - "&374", - "&375", - "&376", - "&377", - "&378", - "&379", - "&380", - "&381", - "&382", - "&383", - "&384", - "&385", - "&386", - "&387", - "&388", - "&389", - "&390", - "&391", - "&392", - "&393", - "&394", - "&395", - "&396", - "&397", - "&398", - "&399", - "&400", - "&401", - "&402", - "&403", - "&404", - "&405", - "&406", - "&407", - "&408", - "&409", - "&410", - "&411", - "&412", - "&413", - "&414", - "&415", - "&416", - "&417", - "&418", - "&419", - "&420", - "&421", - "&422", - "&423", - "&424", - "&425", - "&426", - "&427", - "&428", - "&429", - "&430", - "&431", - "&432", - "&433", - "&434", - "&435", - "&436", - "&437", - "&438", - "&439", - "&440", - "&441", - "&442", - "&443", - "&444", - "&445", - "&446", - "&447", - "&448", - "&449", - "&450", - "&451", - "&452", - "&453", - "&454", - "&455", - "&456", - "&457", - "&458", - "&459", - "&460", - "&461", - "&462", - "&463", - "&464", - "&465", - "&466", - "&467", - "&468", - "&469", - "&470", - "&471", - "&472", - "&473", - "&474", - "&475", - "&476", - "&477", - "&478", - "&479", - "&480", - "&481", - "&482", - "&483", - "&484", - "&485", - "&486", - "&487", - "&488", - "&489", - "&490", - "&491", - "&492", - "&493", - "&494", - "&495", - "&496", - "&497", - "&498", - "&499", - "&500", - "&501", - "&502", - "&503", - "&504", - "&505", - "&506", - "&507", - "&508", - "&509", - "&510", - "&511", +{ + "unknown", + "&1", + "&2", + "&3", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "return", + "escape", + "backspace", + "tab", + "space", + "minus", + "equals", + "leftbracket", + "rightbracket", + "backslash", + "nonushash", + "semicolon", + "apostrophe", + "grave", + "comma", + "period", + "slash", + "capslock", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10", + "f11", + "f12", + "printscreen", + "scrolllock", + "pause", + "insert", + "home", + "pageup", + "delete", + "end", + "pagedown", + "right", + "left", + "down", + "up", + "numlockclear", + "kp_divide", + "kp_multiply", + "kp_minus", + "kp_plus", + "kp_enter", + "kp_1", + "kp_2", + "kp_3", + "kp_4", + "kp_5", + "kp_6", + "kp_7", + "kp_8", + "kp_9", + "kp_0", + "kp_period", + "nonusbackslash", + "application", + "power", + "kp_equals", + "f13", + "f14", + "f15", + "f16", + "f17", + "f18", + "f19", + "f20", + "f21", + "f22", + "f23", + "f24", + "execute", + "help", + "menu", + "select", + "stop", + "again", + "undo", + "cut", + "copy", + "paste", + "find", + "mute", + "volumeup", + "volumedown", + "&130", + "&131", + "&132", + "kp_comma", + "kp_equalsas400", + "international1", + "international2", + "international3", + "international4", + "international5", + "international6", + "international7", + "international8", + "international9", + "lang1", + "lang2", + "lang3", + "lang4", + "lang5", + "lang6", + "lang7", + "lang8", + "lang9", + "alterase", + "sysreq", + "cancel", + "clear", + "prior", + "return2", + "separator", + "out", + "oper", + "clearagain", + "crsel", + "exsel", + "&165", + "&166", + "&167", + "&168", + "&169", + "&170", + "&171", + "&172", + "&173", + "&174", + "&175", + "kp_00", + "kp_000", + "thousandsseparator", + "decimalseparator", + "currencyunit", + "currencysubunit", + "kp_leftparen", + "kp_rightparen", + "kp_leftbrace", + "kp_rightbrace", + "kp_tab", + "kp_backspace", + "kp_a", + "kp_b", + "kp_c", + "kp_d", + "kp_e", + "kp_f", + "kp_xor", + "kp_power", + "kp_percent", + "kp_less", + "kp_greater", + "kp_ampersand", + "kp_dblampersand", + "kp_verticalbar", + "kp_dblverticalbar", + "kp_colon", + "kp_hash", + "kp_space", + "kp_at", + "kp_exclam", + "kp_memstore", + "kp_memrecall", + "kp_memclear", + "kp_memadd", + "kp_memsubtract", + "kp_memmultiply", + "kp_memdivide", + "kp_plusminus", + "kp_clear", + "kp_clearentry", + "kp_binary", + "kp_octal", + "kp_decimal", + "kp_hexadecimal", + "&222", + "&223", + "lctrl", + "lshift", + "lalt", + "lgui", + "rctrl", + "rshift", + "ralt", + "rgui", + "&232", + "&233", + "&234", + "&235", + "&236", + "&237", + "&238", + "&239", + "&240", + "&241", + "&242", + "&243", + "&244", + "&245", + "&246", + "&247", + "&248", + "&249", + "&250", + "&251", + "&252", + "&253", + "&254", + "&255", + "&256", + "mode", + "audionext", + "audioprev", + "audiostop", + "audioplay", + "audiomute", + "mediaselect", + "www", + "mail", + "calculator", + "computer", + "ac_search", + "ac_home", + "ac_back", + "ac_forward", + "ac_stop", + "ac_refresh", + "ac_bookmarks", + "brightnessdown", + "brightnessup", + "displayswitch", + "kbdillumtoggle", + "kbdillumdown", + "kbdillumup", + "eject", + "sleep", + "app1", + "app2", + "audiorewind", + "audiofastforward", + "softleft", + "softright", + "call", + "endcall", + "mouse1", + "mouse2", + "mouse3", + "mouse4", + "mouse5", + "mouse6", + "mouse7", + "mouse8", + "mouse9", + "mousewheelup", + "mousewheeldown", + "mousewheelleft", + "mousewheelright", + "joystick0", + "joystick1", + "joystick2", + "joystick3", + "joystick4", + "joystick5", + "joystick6", + "joystick7", + "joystick8", + "joystick9", + "joystick10", + "joystick11", + "joy_hat0_up", + "joy_hat0_left", + "joy_hat0_right", + "joy_hat0_down", + "joy_hat1_up", + "joy_hat1_left", + "joy_hat1_right", + "joy_hat1_down", + "joy_axis0_left", + "joy_axis0_right", + "joy_axis1_left", + "joy_axis1_right", + "joy_axis2_left", + "joy_axis2_right", + "joy_axis3_left", + "joy_axis3_right", + "joy_axis4_left", + "joy_axis4_right", + "joy_axis5_left", + "joy_axis5_right", + "joy_axis6_left", + "joy_axis6_right", + "joy_axis7_left", + "joy_axis7_right", + "joy_axis8_left", + "joy_axis8_right", + "joy_axis9_left", + "joy_axis9_right", + "joy_axis10_left", + "joy_axis10_right", + "joy_axis11_left", + "joy_axis11_right", + "&348", + "&349", + "&350", + "&351", + "&352", + "&353", + "&354", + "&355", + "&356", + "&357", + "&358", + "&359", + "&360", + "&361", + "&362", + "&363", + "&364", + "&365", + "&366", + "&367", + "&368", + "&369", + "&370", + "&371", + "&372", + "&373", + "&374", + "&375", + "&376", + "&377", + "&378", + "&379", + "&380", + "&381", + "&382", + "&383", + "&384", + "&385", + "&386", + "&387", + "&388", + "&389", + "&390", + "&391", + "&392", + "&393", + "&394", + "&395", + "&396", + "&397", + "&398", + "&399", + "&400", + "&401", + "&402", + "&403", + "&404", + "&405", + "&406", + "&407", + "&408", + "&409", + "&410", + "&411", + "&412", + "&413", + "&414", + "&415", + "&416", + "&417", + "&418", + "&419", + "&420", + "&421", + "&422", + "&423", + "&424", + "&425", + "&426", + "&427", + "&428", + "&429", + "&430", + "&431", + "&432", + "&433", + "&434", + "&435", + "&436", + "&437", + "&438", + "&439", + "&440", + "&441", + "&442", + "&443", + "&444", + "&445", + "&446", + "&447", + "&448", + "&449", + "&450", + "&451", + "&452", + "&453", + "&454", + "&455", + "&456", + "&457", + "&458", + "&459", + "&460", + "&461", + "&462", + "&463", + "&464", + "&465", + "&466", + "&467", + "&468", + "&469", + "&470", + "&471", + "&472", + "&473", + "&474", + "&475", + "&476", + "&477", + "&478", + "&479", + "&480", + "&481", + "&482", + "&483", + "&484", + "&485", + "&486", + "&487", + "&488", + "&489", + "&490", + "&491", + "&492", + "&493", + "&494", + "&495", + "&496", + "&497", + "&498", + "&499", + "&500", + "&501", + "&502", + "&503", + "&504", + "&505", + "&506", + "&507", + "&508", + "&509", + "&510", + "&511", }; + diff --git a/src/engine/client/serverbrowser.cpp b/src/engine/client/serverbrowser.cpp index 22939fe1fa..07cd529da7 100644 --- a/src/engine/client/serverbrowser.cpp +++ b/src/engine/client/serverbrowser.cpp @@ -6,7 +6,6 @@ #include "serverbrowser_ping_cache.h" #include -#include #include #include @@ -25,6 +24,7 @@ #include #include #include +#include #include class CSortWrap @@ -50,9 +50,9 @@ bool matchesExactly(const char *a, const char *b) } CServerBrowser::CServerBrowser() : - m_CommunitiesFilter(g_Config.m_BrFilterExcludeCommunities, sizeof(g_Config.m_BrFilterExcludeCommunities)), - m_CountriesFilter(g_Config.m_BrFilterExcludeCountries, sizeof(g_Config.m_BrFilterExcludeCountries)), - m_TypesFilter(g_Config.m_BrFilterExcludeTypes, sizeof(g_Config.m_BrFilterExcludeTypes)) + m_CommunityCache(this), + m_CountriesFilter(&m_CommunityCache), + m_TypesFilter(&m_CommunityCache) { m_ppServerlist = nullptr; m_pSortedServerlist = nullptr; @@ -67,9 +67,6 @@ CServerBrowser::CServerBrowser() : m_BroadcastTime = 0; secure_random_fill(m_aTokenSeed, sizeof(m_aTokenSeed)); - m_pDDNetInfo = nullptr; - m_DDNetInfoUpdateTime = 0; - CleanUp(); } @@ -95,6 +92,7 @@ void CServerBrowser::SetBaseInfo(class CNetClient *pClient, const char *pNetVers m_pFavorites = Kernel()->RequestInterface(); m_pFriends = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); + m_pHttpClient = Kernel()->RequestInterface(); m_pPingCache = CreateServerBrowserPingCache(m_pConsole, m_pStorage); RegisterCommands(); @@ -102,17 +100,111 @@ void CServerBrowser::SetBaseInfo(class CNetClient *pClient, const char *pNetVers void CServerBrowser::OnInit() { - m_pHttp = CreateServerBrowserHttp(m_pEngine, m_pConsole, m_pStorage, g_Config.m_BrCachedBestServerinfoUrl); + m_pHttp = CreateServerBrowserHttp(m_pEngine, m_pStorage, m_pHttpClient, g_Config.m_BrCachedBestServerinfoUrl); } void CServerBrowser::RegisterCommands() { + m_pConfigManager->RegisterCallback(CServerBrowser::ConfigSaveCallback, this); + m_pConsole->Register("add_favorite_community", "s[community_id]", CFGFLAG_CLIENT, Con_AddFavoriteCommunity, this, "Add a community as a favorite"); + m_pConsole->Register("remove_favorite_community", "s[community_id]", CFGFLAG_CLIENT, Con_RemoveFavoriteCommunity, this, "Remove a community from the favorites"); + m_pConsole->Register("add_excluded_community", "s[community_id]", CFGFLAG_CLIENT, Con_AddExcludedCommunity, this, "Add a community to the exclusion filter"); + m_pConsole->Register("remove_excluded_community", "s[community_id]", CFGFLAG_CLIENT, Con_RemoveExcludedCommunity, this, "Remove a community from the exclusion filter"); + m_pConsole->Register("add_excluded_country", "s[community_id] s[country_code]", CFGFLAG_CLIENT, Con_AddExcludedCountry, this, "Add a country to the exclusion filter for a specific community"); + m_pConsole->Register("remove_excluded_country", "s[community_id] s[country_code]", CFGFLAG_CLIENT, Con_RemoveExcludedCountry, this, "Remove a country from the exclusion filter for a specific community"); + m_pConsole->Register("add_excluded_type", "s[community_id] s[type]", CFGFLAG_CLIENT, Con_AddExcludedType, this, "Add a type to the exclusion filter for a specific community"); + m_pConsole->Register("remove_excluded_type", "s[community_id] s[type]", CFGFLAG_CLIENT, Con_RemoveExcludedType, this, "Remove a type from the exclusion filter for a specific community"); m_pConsole->Register("leak_ip_address_to_all_servers", "", CFGFLAG_CLIENT, Con_LeakIpAddress, this, "Leaks your IP address to all servers by pinging each of them, also acquiring the latency in the process"); } +void CServerBrowser::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + pThis->FavoriteCommunitiesFilter().Save(pConfigManager); + pThis->CommunitiesFilter().Save(pConfigManager); + pThis->CountriesFilter().Save(pConfigManager); + pThis->TypesFilter().Save(pConfigManager); +} + +void CServerBrowser::Con_AddFavoriteCommunity(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + if(!pThis->ValidateCommunityId(pCommunityId)) + return; + pThis->FavoriteCommunitiesFilter().Add(pCommunityId); +} + +void CServerBrowser::Con_RemoveFavoriteCommunity(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + if(!pThis->ValidateCommunityId(pCommunityId)) + return; + pThis->FavoriteCommunitiesFilter().Remove(pCommunityId); +} + +void CServerBrowser::Con_AddExcludedCommunity(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + if(!pThis->ValidateCommunityId(pCommunityId)) + return; + pThis->CommunitiesFilter().Add(pCommunityId); +} + +void CServerBrowser::Con_RemoveExcludedCommunity(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + if(!pThis->ValidateCommunityId(pCommunityId)) + return; + pThis->CommunitiesFilter().Remove(pCommunityId); +} + +void CServerBrowser::Con_AddExcludedCountry(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + const char *pCountryName = pResult->GetString(1); + if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateCountryName(pCountryName)) + return; + pThis->CountriesFilter().Add(pCommunityId, pCountryName); +} + +void CServerBrowser::Con_RemoveExcludedCountry(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + const char *pCountryName = pResult->GetString(1); + if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateCountryName(pCountryName)) + return; + pThis->CountriesFilter().Remove(pCommunityId, pCountryName); +} + +void CServerBrowser::Con_AddExcludedType(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + const char *pTypeName = pResult->GetString(1); + if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateTypeName(pTypeName)) + return; + pThis->TypesFilter().Add(pCommunityId, pTypeName); +} + +void CServerBrowser::Con_RemoveExcludedType(IConsole::IResult *pResult, void *pUserData) +{ + CServerBrowser *pThis = static_cast(pUserData); + const char *pCommunityId = pResult->GetString(0); + const char *pTypeName = pResult->GetString(1); + if(!pThis->ValidateCommunityId(pCommunityId) || !pThis->ValidateTypeName(pTypeName)) + return; + pThis->TypesFilter().Remove(pCommunityId, pTypeName); +} + void CServerBrowser::Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserData) { - CServerBrowser *pThis = (CServerBrowser *)pUserData; + CServerBrowser *pThis = static_cast(pUserData); // We only consider the first address of every server. @@ -169,6 +261,50 @@ void CServerBrowser::Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserDa } } +static bool ValidIdentifier(const char *pId, size_t MaxLength) +{ + if(pId[0] == '\0' || (size_t)str_length(pId) >= MaxLength) + { + return false; + } + + for(int i = 0; pId[i] != '\0'; ++i) + { + if(pId[i] == '"' || pId[i] == '/' || pId[i] == '\\') + { + return false; + } + } + return true; +} + +static bool ValidateIdentifier(const char *pId, size_t MaxLength, const char *pContext, IConsole *pConsole) +{ + if(!ValidIdentifier(pId, MaxLength)) + { + char aError[32 + IConsole::CMDLINE_LENGTH]; + str_format(aError, sizeof(aError), "%s '%s' is not valid", pContext, pId); + pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowser", aError); + return false; + } + return true; +} + +bool CServerBrowser::ValidateCommunityId(const char *pCommunityId) const +{ + return ValidateIdentifier(pCommunityId, CServerInfo::MAX_COMMUNITY_ID_LENGTH, "Community ID", m_pConsole); +} + +bool CServerBrowser::ValidateCountryName(const char *pCountryName) const +{ + return ValidateIdentifier(pCountryName, CServerInfo::MAX_COMMUNITY_COUNTRY_LENGTH, "Country name", m_pConsole); +} + +bool CServerBrowser::ValidateTypeName(const char *pTypeName) const +{ + return ValidateIdentifier(pTypeName, CServerInfo::MAX_COMMUNITY_TYPE_LENGTH, "Type name", m_pConsole); +} + int CServerBrowser::Players(const CServerInfo &Item) const { return g_Config.m_BrFilterSpectators ? Item.m_NumPlayers : Item.m_NumClients; @@ -250,6 +386,17 @@ bool CServerBrowser::SortCompareNumClients(int Index1, int Index2) const return pIndex1->m_Info.m_NumClients > pIndex2->m_Info.m_NumClients; } +bool CServerBrowser::SortCompareNumFriends(int Index1, int Index2) const +{ + CServerEntry *pIndex1 = m_ppServerlist[Index1]; + CServerEntry *pIndex2 = m_ppServerlist[Index2]; + + if(pIndex1->m_Info.m_FriendNum == pIndex2->m_Info.m_FriendNum) + return pIndex1->m_Info.m_NumFilteredPlayers > pIndex2->m_Info.m_NumFilteredPlayers; + else + return pIndex1->m_Info.m_FriendNum > pIndex2->m_Info.m_FriendNum; +} + bool CServerBrowser::SortCompareNumPlayersAndPing(int Index1, int Index2) const { CServerEntry *pIndex1 = m_ppServerlist[Index1]; @@ -296,13 +443,22 @@ void CServerBrowser::Filter() Filtered = true; else if(g_Config.m_BrFilterUnfinishedMap && Info.m_HasRank == CServerInfo::RANK_RANKED) Filtered = true; + else if(g_Config.m_BrFilterLogin && Info.m_RequiresLogin) + Filtered = true; else { - if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES) + if(!Communities().empty()) { - Filtered = CommunitiesFilter().Filtered(Info.m_aCommunityId); - Filtered = Filtered || CountriesFilter().Filtered(Info.m_aCommunityCountry); - Filtered = Filtered || TypesFilter().Filtered(Info.m_aCommunityType); + if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES) + { + Filtered = CommunitiesFilter().Filtered(Info.m_aCommunityId); + } + if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES || + (m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5)) + { + Filtered = Filtered || CountriesFilter().Filtered(Info.m_aCommunityCountry); + Filtered = Filtered || TypesFilter().Filtered(Info.m_aCommunityType); + } } if(!Filtered && g_Config.m_BrFilterCountry) @@ -325,22 +481,26 @@ void CServerBrowser::Filter() const char *pStr = g_Config.m_BrFilterString; char aFilterStr[sizeof(g_Config.m_BrFilterString)]; + char aFilterStrTrimmed[sizeof(g_Config.m_BrFilterString)]; while((pStr = str_next_token(pStr, IServerBrowser::SEARCH_EXCLUDE_TOKEN, aFilterStr, sizeof(aFilterStr)))) { - if(aFilterStr[0] == '\0') + str_copy(aFilterStrTrimmed, str_utf8_skip_whitespaces(aFilterStr)); + str_utf8_trim_right(aFilterStrTrimmed); + + if(aFilterStrTrimmed[0] == '\0') { continue; } auto MatchesFn = matchesPart; - const int FilterLen = str_length(aFilterStr); - if(aFilterStr[0] == '"' && aFilterStr[FilterLen - 1] == '"') + const int FilterLen = str_length(aFilterStrTrimmed); + if(aFilterStrTrimmed[0] == '"' && aFilterStrTrimmed[FilterLen - 1] == '"') { - aFilterStr[FilterLen - 1] = '\0'; + aFilterStrTrimmed[FilterLen - 1] = '\0'; MatchesFn = matchesExactly; } // match against server name - if(MatchesFn(Info.m_aName, aFilterStr)) + if(MatchesFn(Info.m_aName, aFilterStrTrimmed)) { Info.m_QuickSearchHit |= IServerBrowser::QUICK_SERVERNAME; } @@ -348,8 +508,8 @@ void CServerBrowser::Filter() // match against players for(int p = 0; p < minimum(Info.m_NumClients, (int)MAX_CLIENTS); p++) { - if(MatchesFn(Info.m_aClients[p].m_aName, aFilterStr) || - MatchesFn(Info.m_aClients[p].m_aClan, aFilterStr)) + if(MatchesFn(Info.m_aClients[p].m_aName, aFilterStrTrimmed) || + MatchesFn(Info.m_aClients[p].m_aClan, aFilterStrTrimmed)) { if(g_Config.m_BrFilterConnectingPlayers && str_comp(Info.m_aClients[p].m_aName, "(connecting)") == 0 && @@ -363,7 +523,7 @@ void CServerBrowser::Filter() } // match against map - if(MatchesFn(Info.m_aMap, aFilterStr)) + if(MatchesFn(Info.m_aMap, aFilterStrTrimmed)) { Info.m_QuickSearchHit |= IServerBrowser::QUICK_MAPNAME; } @@ -377,36 +537,40 @@ void CServerBrowser::Filter() { const char *pStr = g_Config.m_BrExcludeString; char aExcludeStr[sizeof(g_Config.m_BrExcludeString)]; + char aExcludeStrTrimmed[sizeof(g_Config.m_BrExcludeString)]; while((pStr = str_next_token(pStr, IServerBrowser::SEARCH_EXCLUDE_TOKEN, aExcludeStr, sizeof(aExcludeStr)))) { - if(aExcludeStr[0] == '\0') + str_copy(aExcludeStrTrimmed, str_utf8_skip_whitespaces(aExcludeStr)); + str_utf8_trim_right(aExcludeStrTrimmed); + + if(aExcludeStrTrimmed[0] == '\0') { continue; } auto MatchesFn = matchesPart; - const int FilterLen = str_length(aExcludeStr); - if(aExcludeStr[0] == '"' && aExcludeStr[FilterLen - 1] == '"') + const int FilterLen = str_length(aExcludeStrTrimmed); + if(aExcludeStrTrimmed[0] == '"' && aExcludeStrTrimmed[FilterLen - 1] == '"') { - aExcludeStr[FilterLen - 1] = '\0'; + aExcludeStrTrimmed[FilterLen - 1] = '\0'; MatchesFn = matchesExactly; } // match against server name - if(MatchesFn(Info.m_aName, aExcludeStr)) + if(MatchesFn(Info.m_aName, aExcludeStrTrimmed)) { Filtered = true; break; } // match against map - if(MatchesFn(Info.m_aMap, aExcludeStr)) + if(MatchesFn(Info.m_aMap, aExcludeStrTrimmed)) { Filtered = true; break; } // match against gametype - if(MatchesFn(Info.m_aGameType, aExcludeStr)) + if(MatchesFn(Info.m_aGameType, aExcludeStrTrimmed)) { Filtered = true; break; @@ -441,6 +605,7 @@ int CServerBrowser::SortHash() const i |= g_Config.m_BrFilterUnfinishedMap << 13; i |= g_Config.m_BrFilterCountry << 14; i |= g_Config.m_BrFilterConnectingPlayers << 15; + i |= g_Config.m_BrFilterLogin << 16; return i; } @@ -464,6 +629,8 @@ void CServerBrowser::Sort() std::stable_sort(m_pSortedServerlist, m_pSortedServerlist + m_NumSortedServers, CSortWrap(this, &CServerBrowser::SortComparePing)); else if(g_Config.m_BrSort == IServerBrowser::SORT_MAP) std::stable_sort(m_pSortedServerlist, m_pSortedServerlist + m_NumSortedServers, CSortWrap(this, &CServerBrowser::SortCompareMap)); + else if(g_Config.m_BrSort == IServerBrowser::SORT_NUMFRIENDS) + std::stable_sort(m_pSortedServerlist, m_pSortedServerlist + m_NumSortedServers, CSortWrap(this, &CServerBrowser::SortCompareNumFriends)); else if(g_Config.m_BrSort == IServerBrowser::SORT_NUMPLAYERS) std::stable_sort(m_pSortedServerlist, m_pSortedServerlist + m_NumSortedServers, CSortWrap(this, &CServerBrowser::SortCompareNumPlayers)); else if(g_Config.m_BrSort == IServerBrowser::SORT_GAMETYPE) @@ -534,7 +701,22 @@ void ServerBrowserFormatAddresses(char *pBuffer, int BufferSize, NETADDR *pAddrs { return; } - net_addr_str(&pAddrs[i], pBuffer, BufferSize, true); + char aIpAddr[512]; + if(!net_addr_str(&pAddrs[i], aIpAddr, sizeof(aIpAddr), true)) + { + str_copy(pBuffer, aIpAddr, BufferSize); + return; + } + if(pAddrs[i].type & NETTYPE_TW7) + { + str_format( + pBuffer, + BufferSize, + "tw-0.7+udp://%s", + aIpAddr); + return; + } + str_copy(pBuffer, aIpAddr, BufferSize); int Length = str_length(pBuffer); pBuffer += Length; BufferSize -= Length; @@ -550,12 +732,14 @@ void CServerBrowser::SetInfo(CServerEntry *pEntry, const CServerInfo &Info) cons mem_copy(pEntry->m_Info.m_aAddresses, TmpInfo.m_aAddresses, sizeof(pEntry->m_Info.m_aAddresses)); pEntry->m_Info.m_NumAddresses = TmpInfo.m_NumAddresses; ServerBrowserFormatAddresses(pEntry->m_Info.m_aAddress, sizeof(pEntry->m_Info.m_aAddress), pEntry->m_Info.m_aAddresses, pEntry->m_Info.m_NumAddresses); - UpdateServerCommunity(&pEntry->m_Info); + str_copy(pEntry->m_Info.m_aCommunityId, TmpInfo.m_aCommunityId); + str_copy(pEntry->m_Info.m_aCommunityCountry, TmpInfo.m_aCommunityCountry); + str_copy(pEntry->m_Info.m_aCommunityType, TmpInfo.m_aCommunityType); UpdateServerRank(&pEntry->m_Info); if(pEntry->m_Info.m_ClientScoreKind == CServerInfo::CLIENT_SCORE_KIND_UNSPECIFIED) { - if((str_find_nocase(pEntry->m_Info.m_aGameType, "race") || str_find_nocase(pEntry->m_Info.m_aGameType, "fastcap")) && g_Config.m_ClDDRaceScoreBoard) + if(str_find_nocase(pEntry->m_Info.m_aGameType, "race") || str_find_nocase(pEntry->m_Info.m_aGameType, "fastcap")) { pEntry->m_Info.m_ClientScoreKind = CServerInfo::CLIENT_SCORE_KIND_TIME_BACKCOMPAT; } @@ -567,11 +751,11 @@ void CServerBrowser::SetInfo(CServerEntry *pEntry, const CServerInfo &Info) cons class CPlayerScoreNameLess { - const int ScoreKind; + const int m_ScoreKind; public: CPlayerScoreNameLess(int ClientScoreKind) : - ScoreKind(ClientScoreKind) + m_ScoreKind(ClientScoreKind) { } @@ -586,7 +770,7 @@ void CServerBrowser::SetInfo(CServerEntry *pEntry, const CServerInfo &Info) cons int Score0 = p0.m_Score; int Score1 = p1.m_Score; - if(ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME || ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME_BACKCOMPAT) + if(m_ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME || m_ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME_BACKCOMPAT) { // Sort unfinished (-9999) and still connecting players (-1) after others if(Score0 < 0 && Score1 >= 0) @@ -598,7 +782,7 @@ void CServerBrowser::SetInfo(CServerEntry *pEntry, const CServerInfo &Info) cons if(Score0 != Score1) { // Handle the sign change introduced with CLIENT_SCORE_KIND_TIME - if(ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME) + if(m_ScoreKind == CServerInfo::CLIENT_SCORE_KIND_TIME) return Score0 < Score1; else return Score0 > Score1; @@ -662,6 +846,7 @@ CServerBrowser::CServerEntry *CServerBrowser::Add(const NETADDR *pAddrs, int Num pEntry->m_Info.m_Latency = 999; pEntry->m_Info.m_HasRank = CServerInfo::RANK_UNAVAILABLE; ServerBrowserFormatAddresses(pEntry->m_Info.m_aAddress, sizeof(pEntry->m_Info.m_aAddress), pEntry->m_Info.m_aAddresses, pEntry->m_Info.m_NumAddresses); + UpdateServerCommunity(&pEntry->m_Info); str_copy(pEntry->m_Info.m_aName, pEntry->m_Info.m_aAddress, sizeof(pEntry->m_Info.m_aName)); // check if it's a favorite @@ -767,9 +952,9 @@ void CServerBrowser::OnServerInfoUpdate(const NETADDR &Addr, int Token, const CS RequestResort(); } -void CServerBrowser::Refresh(int Type) +void CServerBrowser::Refresh(int Type, bool Force) { - bool ServerListTypeChanged = m_ServerlistType != Type; + bool ServerListTypeChanged = Force || m_ServerlistType != Type; int OldServerListType = m_ServerlistType; m_ServerlistType = Type; secure_random_fill(m_aTokenSeed, sizeof(m_aTokenSeed)); @@ -783,7 +968,7 @@ void CServerBrowser::Refresh(int Type) CNetChunk Packet; /* do the broadcast version */ - Packet.m_ClientID = -1; + Packet.m_ClientId = -1; mem_zero(&Packet, sizeof(Packet)); Packet.m_Address.type = m_pNetClient->NetType() | NETTYPE_LINK_BROADCAST; Packet.m_Flags = NETSENDFLAG_CONNLESS | NETSENDFLAG_EXTENDED; @@ -800,16 +985,16 @@ void CServerBrowser::Refresh(int Type) m_BroadcastTime = time_get(); - for(int i = 8303; i <= 8310; i++) + for(int Port = LAN_PORT_BEGIN; Port <= LAN_PORT_END; Port++) { - Packet.m_Address.port = i; + Packet.m_Address.port = Port; m_pNetClient->Send(&Packet); } if(g_Config.m_Debug) m_pConsole->Print(IConsole::OUTPUT_LEVEL_DEBUG, "serverbrowser", "broadcasting for servers"); } - else if(Type == IServerBrowser::TYPE_FAVORITES || Type == IServerBrowser::TYPE_INTERNET) + else { m_pHttp->Refresh(); m_pPingCache->Load(); @@ -859,7 +1044,7 @@ void CServerBrowser::RequestImpl(const NETADDR &Addr, CServerEntry *pEntry, int aBuffer[sizeof(SERVERBROWSE_GETINFO)] = GetBasicToken(Token); CNetChunk Packet; - Packet.m_ClientID = -1; + Packet.m_ClientId = -1; Packet.m_Address = Addr; Packet.m_Flags = NETSENDFLAG_CONNLESS | NETSENDFLAG_EXTENDED; Packet.m_DataSize = sizeof(aBuffer); @@ -907,11 +1092,40 @@ void CServerBrowser::UpdateFromHttp() } int NumServers = m_pHttp->NumServers(); - int NumLegacyServers = m_pHttp->NumLegacyServers(); std::function Want = [](const NETADDR *pAddrs, int NumAddrs) { return true; }; if(m_ServerlistType == IServerBrowser::TYPE_FAVORITES) { - Want = [&](const NETADDR *pAddrs, int NumAddrs) -> bool { return m_pFavorites->IsFavorite(pAddrs, NumAddrs) != TRISTATE::NONE; }; + Want = [this](const NETADDR *pAddrs, int NumAddrs) -> bool { + return m_pFavorites->IsFavorite(pAddrs, NumAddrs) != TRISTATE::NONE; + }; + } + else if(m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5) + { + const size_t CommunityIndex = m_ServerlistType - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1; + std::vector vpFavoriteCommunities = FavoriteCommunities(); + dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid community index"); + const CCommunity *pWantedCommunity = vpFavoriteCommunities[CommunityIndex]; + const bool IsNoneCommunity = str_comp(pWantedCommunity->Id(), COMMUNITY_NONE) == 0; + Want = [this, pWantedCommunity, IsNoneCommunity](const NETADDR *pAddrs, int NumAddrs) -> bool { + for(int AddressIndex = 0; AddressIndex < NumAddrs; AddressIndex++) + { + const auto CommunityServer = m_CommunityServersByAddr.find(pAddrs[AddressIndex]); + if(CommunityServer != m_CommunityServersByAddr.end()) + { + if(IsNoneCommunity) + { + // Servers with community "none" are not present in m_CommunityServersByAddr, so we ignore + // any server that is found in this map to determine only the servers without community. + return false; + } + else if(str_comp(CommunityServer->second.CommunityId(), pWantedCommunity->Id()) == 0) + { + return true; + } + } + } + return IsNoneCommunity; + }; } for(int i = 0; i < NumServers; i++) @@ -936,16 +1150,6 @@ void CServerBrowser::UpdateFromHttp() pEntry->m_RequestIgnoreInfo = true; } - for(int i = 0; i < NumLegacyServers; i++) - { - NETADDR Addr = m_pHttp->LegacyServer(i); - if(!Want(&Addr, 1)) - { - continue; - } - QueueRequest(Add(&Addr, 1)); - } - if(m_ServerlistType == IServerBrowser::TYPE_FAVORITES) { const IFavorites::CEntry *pFavorites; @@ -968,7 +1172,7 @@ void CServerBrowser::UpdateFromHttp() } // (Also add favorites we're not allowed to ping.) CServerEntry *pEntry = Add(pFavorites[i].m_aAddrs, pFavorites[i].m_NumAddrs); - if(pFavorites->m_AllowPing) + if(pFavorites[i].m_AllowPing) { QueueRequest(pEntry); } @@ -1094,7 +1298,6 @@ const json_value *CServerBrowser::LoadDDNetInfo() UpdateServerCommunity(&m_ppServerlist[i]->m_Info); UpdateServerRank(&m_ppServerlist[i]->m_Info); } - m_DDNetInfoUpdateTime = time_get(); return m_pDDNetInfo; } @@ -1103,7 +1306,12 @@ void CServerBrowser::LoadDDNetInfoJson() void *pBuf; unsigned Length; if(!m_pStorage->ReadFile(DDNET_INFO_FILE, IStorage::TYPE_SAVE, &pBuf, &Length)) + { + m_DDNetInfoSha256 = SHA256_ZEROED; return; + } + + m_DDNetInfoSha256 = sha256(pBuf, Length); json_value_free(m_pDDNetInfo); json_settings JsonSettings{}; @@ -1229,14 +1437,12 @@ void CServerBrowser::LoadDDNetServers() if(!m_pDDNetInfo) { - CleanFilters(); return; } const json_value &Communities = (*m_pDDNetInfo)["communities"]; if(Communities.type != json_array) { - CleanFilters(); return; } @@ -1258,8 +1464,22 @@ void CServerBrowser::LoadDDNetServers() const json_value &IconSha256 = Icon["sha256"]; const json_value &IconUrl = Icon["url"]; const json_value &Name = Community["name"]; - const json_value *pFinishes = &Icon["finishes"]; - const json_value *pServers = &Icon["servers"]; + const json_value HasFinishes = Community["has_finishes"]; + const json_value *pFinishes = &Community["finishes"]; + const json_value *pServers = &Community["servers"]; + // We accidentally set finishes/servers to be part of icon in + // the past, so support that, too. Can be removed once we make + // a breaking change to the whole thing, necessitating a new + // endpoint. + if(pFinishes->type == json_none) + { + pServers = &Icon["finishes"]; + } + if(pServers->type == json_none) + { + pServers = &Icon["servers"]; + } + // Backward compatibility. if(pFinishes->type == json_none) { if(str_comp(Id, COMMUNITY_DDNET) == 0) @@ -1267,7 +1487,6 @@ void CServerBrowser::LoadDDNetServers() pFinishes = &(*m_pDDNetInfo)["maps"]; } } - // Backward compatibility. if(pServers->type == json_none) { if(str_comp(Id, COMMUNITY_DDNET) == 0) @@ -1284,6 +1503,7 @@ void CServerBrowser::LoadDDNetServers() IconSha256.type != json_string || IconUrl.type != json_string || Name.type != json_string || + HasFinishes.type != json_boolean || (pFinishes->type != json_array && pFinishes->type != json_none) || pServers->type != json_array) { @@ -1302,8 +1522,8 @@ void CServerBrowser::LoadDDNetServers() log_error("serverbrowser", "invalid community servers (CommunityId=%s)", NewCommunity.Id()); continue; } - NewCommunity.m_HasFinishes = pFinishes->type == json_array; - if(NewCommunity.m_HasFinishes && !ParseCommunityFinishes(&NewCommunity, *pFinishes)) + NewCommunity.m_HasFinishes = HasFinishes; + if(NewCommunity.m_HasFinishes && pFinishes->type == json_array && !ParseCommunityFinishes(&NewCommunity, *pFinishes)) { log_error("serverbrowser", "invalid community finishes (CommunityId=%s)", NewCommunity.Id()); continue; @@ -1320,7 +1540,12 @@ void CServerBrowser::LoadDDNetServers() } // Add default none community - m_vCommunities.emplace_back(COMMUNITY_NONE, "None", SHA256_ZEROED, ""); + { + CCommunity NoneCommunity(COMMUNITY_NONE, "None", SHA256_ZEROED, ""); + NoneCommunity.m_vCountries.emplace_back(COMMUNITY_COUNTRY_NONE, -1); + NoneCommunity.m_vTypes.emplace_back(COMMUNITY_TYPE_NONE); + m_vCommunities.push_back(std::move(NoneCommunity)); + } // Remove unknown elements from exclude lists CleanFilters(); @@ -1366,8 +1591,8 @@ void CServerBrowser::UpdateServerCommunity(CServerInfo *pInfo) const } } str_copy(pInfo->m_aCommunityId, COMMUNITY_NONE); - str_copy(pInfo->m_aCommunityCountry, ""); - str_copy(pInfo->m_aCommunityType, ""); + str_copy(pInfo->m_aCommunityCountry, COMMUNITY_COUNTRY_NONE); + str_copy(pInfo->m_aCommunityType, COMMUNITY_TYPE_NONE); } void CServerBrowser::UpdateServerRank(CServerInfo *pInfo) const @@ -1378,11 +1603,6 @@ void CServerBrowser::UpdateServerRank(CServerInfo *pInfo) const const char *CServerBrowser::GetTutorialServer() { - // Use internet tab as default after joining tutorial, also makes sure Find() actually works. - // Note that when no server info has been loaded yet, this will not return a result immediately. - m_pConfigManager->Reset("ui_page"); - Refresh(IServerBrowser::TYPE_INTERNET); - const CCommunity *pCommunity = Community(COMMUNITY_DDNET); if(pCommunity == nullptr) return nullptr; @@ -1393,10 +1613,10 @@ const char *CServerBrowser::GetTutorialServer() { for(const auto &Server : Country.Servers()) { - CServerEntry *pEntry = Find(Server.Address()); - if(!pEntry) + if(str_comp(Server.TypeName(), "Tutorial") != 0) continue; - if(str_find(pEntry->m_Info.m_aName, "(Tutorial)") == 0) + const CServerEntry *pEntry = Find(Server.Address()); + if(!pEntry) continue; if(pEntry->m_Info.m_NumPlayers > pEntry->m_Info.m_MaxPlayers - 10) continue; @@ -1429,6 +1649,20 @@ int CServerBrowser::LoadingProgression() const return 100.0f * Loaded / Servers; } +bool CCommunity::HasCountry(const char *pCountryName) const +{ + return std::find_if(Countries().begin(), Countries().end(), [pCountryName](const auto &Elem) { + return str_comp(Elem.Name(), pCountryName) == 0; + }) != Countries().end(); +} + +bool CCommunity::HasType(const char *pTypeName) const +{ + return std::find_if(Types().begin(), Types().end(), [pTypeName](const auto &Elem) { + return str_comp(Elem.Name(), pTypeName) == 0; + }) != Types().end(); +} + CServerInfo::ERankState CCommunity::HasRank(const char *pMap) const { if(!HasRanks()) @@ -1463,147 +1697,604 @@ std::vector CServerBrowser::SelectedCommunities() const return vpSelected; } -void CFilterList::Add(const char *pElement) +std::vector CServerBrowser::FavoriteCommunities() const { - if(Filtered(pElement)) - return; + // This is done differently than SelectedCommunities because the favorite + // communities should be returned in the order specified by the user. + std::vector vpFavorites; + for(const auto &CommunityId : FavoriteCommunitiesFilter().Entries()) + { + const CCommunity *pCommunity = Community(CommunityId.Id()); + if(pCommunity) + { + vpFavorites.push_back(pCommunity); + } + } + return vpFavorites; +} - if(m_pFilter[0] != '\0') - str_append(m_pFilter, ",", m_FilterSize); - str_append(m_pFilter, pElement, m_FilterSize); +std::vector CServerBrowser::CurrentCommunities() const +{ + if(m_ServerlistType == IServerBrowser::TYPE_INTERNET || m_ServerlistType == IServerBrowser::TYPE_FAVORITES) + { + return SelectedCommunities(); + } + else if(m_ServerlistType >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_ServerlistType <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5) + { + const size_t CommunityIndex = m_ServerlistType - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1; + std::vector vpFavoriteCommunities = FavoriteCommunities(); + dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid favorite community serverbrowser type"); + return {vpFavoriteCommunities[CommunityIndex]}; + } + else + { + return {}; + } +} + +unsigned CServerBrowser::CurrentCommunitiesHash() const +{ + std::vector vpCommunities = CurrentCommunities(); + unsigned Hash = 5381; + for(const CCommunity *pCommunity : CurrentCommunities()) + { + Hash = (Hash << 5) + Hash + str_quickhash(pCommunity->Id()); + } + return Hash; } -void CFilterList::Remove(const char *pElement) +void CCommunityCache::Update(bool Force) { - if(!Filtered(pElement)) + const unsigned CommunitiesHash = m_pServerBrowser->CurrentCommunitiesHash(); + const bool TypeChanged = m_LastType != m_pServerBrowser->GetCurrentType(); + const bool CurrentCommunitiesChanged = m_LastType == m_pServerBrowser->GetCurrentType() && m_SelectedCommunitiesHash != CommunitiesHash; + if(CurrentCommunitiesChanged && m_pServerBrowser->GetCurrentType() >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_pServerBrowser->GetCurrentType() <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5) + { + // Favorite community was changed while its type is active, + // refresh to get correct serverlist for updated community. + m_pServerBrowser->Refresh(m_pServerBrowser->GetCurrentType(), true); + } + + if(!Force && m_InfoSha256 == m_pServerBrowser->DDNetInfoSha256() && + !CurrentCommunitiesChanged && !TypeChanged) + { return; + } + + m_InfoSha256 = m_pServerBrowser->DDNetInfoSha256(); + m_LastType = m_pServerBrowser->GetCurrentType(); + m_SelectedCommunitiesHash = CommunitiesHash; + m_vpSelectedCommunities = m_pServerBrowser->CurrentCommunities(); + + m_vpSelectableCountries.clear(); + m_vpSelectableTypes.clear(); + for(const CCommunity *pCommunity : m_vpSelectedCommunities) + { + for(const auto &Country : pCommunity->Countries()) + { + const auto ExistingCountry = std::find_if(m_vpSelectableCountries.begin(), m_vpSelectableCountries.end(), [&](const CCommunityCountry *pOther) { + return str_comp(Country.Name(), pOther->Name()) == 0 && Country.FlagId() == pOther->FlagId(); + }); + if(ExistingCountry == m_vpSelectableCountries.end()) + { + m_vpSelectableCountries.push_back(&Country); + } + } + for(const auto &Type : pCommunity->Types()) + { + const auto ExistingType = std::find_if(m_vpSelectableTypes.begin(), m_vpSelectableTypes.end(), [&](const CCommunityType *pOther) { + return str_comp(Type.Name(), pOther->Name()) == 0; + }); + if(ExistingType == m_vpSelectableTypes.end()) + { + m_vpSelectableTypes.push_back(&Type); + } + } + } + + m_AnyRanksAvailable = std::any_of(m_vpSelectedCommunities.begin(), m_vpSelectedCommunities.end(), [](const CCommunity *pCommunity) { + return pCommunity->HasRanks(); + }); + + // Country/type filters not shown if there are no countries and types, or if only the none-community is selected + m_CountryTypesFilterAvailable = (!m_vpSelectableCountries.empty() || !m_vpSelectableTypes.empty()) && + (m_vpSelectedCommunities.size() != 1 || str_comp(m_vpSelectedCommunities[0]->Id(), IServerBrowser::COMMUNITY_NONE) != 0); + + if(m_pServerBrowser->GetCurrentType() >= IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 && m_pServerBrowser->GetCurrentType() <= IServerBrowser::TYPE_FAVORITE_COMMUNITY_5) + { + const size_t CommunityIndex = m_pServerBrowser->GetCurrentType() - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1; + std::vector vpFavoriteCommunities = m_pServerBrowser->FavoriteCommunities(); + dbg_assert(CommunityIndex < vpFavoriteCommunities.size(), "Invalid favorite community serverbrowser type"); + m_pCountryTypeFilterKey = vpFavoriteCommunities[CommunityIndex]->Id(); + } + else + { + m_pCountryTypeFilterKey = IServerBrowser::COMMUNITY_ALL; + } + + m_pServerBrowser->CleanFilters(); +} + +void CFavoriteCommunityFilterList::Add(const char *pCommunityId) +{ + // Remove community if it's already a favorite, so it will be added again at + // the end of the list, to allow setting the entire list easier with binds. + Remove(pCommunityId); + + // Ensure maximum number of favorite communities, by removing the least-recently + // added community from the beginning, when the maximum number of favorite + // communities has been reached. + constexpr size_t MaxFavoriteCommunities = IServerBrowser::TYPE_FAVORITE_COMMUNITY_5 - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 + 1; + if(m_vEntries.size() >= MaxFavoriteCommunities) + { + dbg_assert(m_vEntries.size() == MaxFavoriteCommunities, "Maximum number of communities can never be exceeded"); + m_vEntries.erase(m_vEntries.begin()); + } + m_vEntries.emplace_back(pCommunityId); +} + +void CFavoriteCommunityFilterList::Remove(const char *pCommunityId) +{ + auto FoundCommunity = std::find(m_vEntries.begin(), m_vEntries.end(), CCommunityId(pCommunityId)); + if(FoundCommunity != m_vEntries.end()) + { + m_vEntries.erase(FoundCommunity); + } +} + +void CFavoriteCommunityFilterList::Clear() +{ + m_vEntries.clear(); +} + +bool CFavoriteCommunityFilterList::Filtered(const char *pCommunityId) const +{ + return std::find(m_vEntries.begin(), m_vEntries.end(), CCommunityId(pCommunityId)) != m_vEntries.end(); +} + +bool CFavoriteCommunityFilterList::Empty() const +{ + return m_vEntries.empty(); +} + +void CFavoriteCommunityFilterList::Clean(const std::vector &vAllowedCommunities) +{ + auto It = std::remove_if(m_vEntries.begin(), m_vEntries.end(), [&](const auto &Community) { + return std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { + return str_comp(Community.Id(), AllowedCommunity.Id()) == 0; + }) == vAllowedCommunities.end(); + }); + m_vEntries.erase(It, m_vEntries.end()); +} + +void CFavoriteCommunityFilterList::Save(IConfigManager *pConfigManager) const +{ + char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH]; + for(const auto &FavoriteCommunity : m_vEntries) + { + str_copy(aBuf, "add_favorite_community \""); + str_append(aBuf, FavoriteCommunity.Id()); + str_append(aBuf, "\""); + pConfigManager->WriteLine(aBuf); + } +} + +const std::vector &CFavoriteCommunityFilterList::Entries() const +{ + return m_vEntries; +} + +template +static bool IsSubsetEquals(const std::vector &vpLeft, const std::unordered_set &Right) +{ + return vpLeft.size() <= Right.size() && std::all_of(vpLeft.begin(), vpLeft.end(), [&](const TNamedElement *pElem) { + return Right.count(TElementName(pElem->Name())) > 0; + }); +} + +void CExcludedCommunityFilterList::Add(const char *pCommunityId) +{ + m_Entries.emplace(pCommunityId); +} + +void CExcludedCommunityFilterList::Remove(const char *pCommunityId) +{ + m_Entries.erase(CCommunityId(pCommunityId)); +} + +void CExcludedCommunityFilterList::Clear() +{ + m_Entries.clear(); +} - // rewrite exclude/filter list - char aBuf[512]; +bool CExcludedCommunityFilterList::Filtered(const char *pCommunityId) const +{ + return std::find(m_Entries.begin(), m_Entries.end(), CCommunityId(pCommunityId)) != m_Entries.end(); +} - str_copy(aBuf, m_pFilter); - m_pFilter[0] = '\0'; +bool CExcludedCommunityFilterList::Empty() const +{ + return m_Entries.empty(); +} - char aToken[512]; - for(const char *pTok = aBuf; (pTok = str_next_token(pTok, ",", aToken, sizeof(aToken)));) +void CExcludedCommunityFilterList::Clean(const std::vector &vAllowedCommunities) +{ + for(auto It = m_Entries.begin(); It != m_Entries.end();) { - if(str_comp_nocase(pElement, aToken) != 0) + const bool Found = std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { + return str_comp(It->Id(), AllowedCommunity.Id()) == 0; + }) != vAllowedCommunities.end(); + if(Found) + { + ++It; + } + else { - if(m_pFilter[0] != '\0') - str_append(m_pFilter, ",", m_FilterSize); - str_append(m_pFilter, aToken, m_FilterSize); + It = m_Entries.erase(It); } } + // Prevent filter that would exclude all allowed communities + if(m_Entries.size() == vAllowedCommunities.size()) + { + m_Entries.clear(); + } } -void CFilterList::Clear() +void CExcludedCommunityFilterList::Save(IConfigManager *pConfigManager) const { - m_pFilter[0] = '\0'; + char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH]; + for(const auto &ExcludedCommunity : m_Entries) + { + str_copy(aBuf, "add_excluded_community \""); + str_append(aBuf, ExcludedCommunity.Id()); + str_append(aBuf, "\""); + pConfigManager->WriteLine(aBuf); + } } -bool CFilterList::Filtered(const char *pElement) const +void CExcludedCommunityCountryFilterList::Add(const char *pCountryName) { - // If the needle is not defined, we exclude it if there is any other - // exclusion, i.e. we only show those elements when the filter is empty. - if(pElement[0] == '\0') - return !Empty(); + // Handle special case that all selectable entries are currently filtered, + // where adding more entries to the exclusion list would have no effect. + auto CommunityEntry = m_Entries.find(m_pCommunityCache->CountryTypeFilterKey()); + if(CommunityEntry != m_Entries.end() && IsSubsetEquals(m_pCommunityCache->SelectableCountries(), CommunityEntry->second)) + { + for(const CCommunityCountry *pSelectableCountry : m_pCommunityCache->SelectableCountries()) + { + CommunityEntry->second.erase(pSelectableCountry->Name()); + } + } - // Special case: "*element" means anything except that element is excluded. - // Necessary because the default filter cannot exclude unknown elements, - // but we want to select only the DDNet community by default. - if(m_pFilter[0] == '*') - return str_comp(m_pFilter + 1, pElement) != 0; + Add(m_pCommunityCache->CountryTypeFilterKey(), pCountryName); +} - // Comma separated list of excluded elements. - return str_in_list(m_pFilter, ",", pElement); +void CExcludedCommunityCountryFilterList::Add(const char *pCommunityId, const char *pCountryName) +{ + CCommunityId CommunityId(pCommunityId); + if(m_Entries.find(CommunityId) == m_Entries.end()) + { + m_Entries[CommunityId] = {}; + } + m_Entries[CommunityId].emplace(pCountryName); +} + +void CExcludedCommunityCountryFilterList::Remove(const char *pCountryName) +{ + Remove(m_pCommunityCache->CountryTypeFilterKey(), pCountryName); +} + +void CExcludedCommunityCountryFilterList::Remove(const char *pCommunityId, const char *pCountryName) +{ + auto CommunityEntry = m_Entries.find(CCommunityId(pCommunityId)); + if(CommunityEntry != m_Entries.end()) + { + CommunityEntry->second.erase(pCountryName); + } +} + +void CExcludedCommunityCountryFilterList::Clear() +{ + auto CommunityEntry = m_Entries.find(m_pCommunityCache->CountryTypeFilterKey()); + if(CommunityEntry != m_Entries.end()) + { + CommunityEntry->second.clear(); + } } -bool CFilterList::Empty() const +bool CExcludedCommunityCountryFilterList::Filtered(const char *pCountryName) const { - return m_pFilter[0] == '\0'; + auto CommunityEntry = m_Entries.find(CCommunityId(m_pCommunityCache->CountryTypeFilterKey())); + if(CommunityEntry == m_Entries.end()) + return false; + + const auto &CountryEntries = CommunityEntry->second; + return !IsSubsetEquals(m_pCommunityCache->SelectableCountries(), CountryEntries) && + CountryEntries.find(CCommunityCountryName(pCountryName)) != CountryEntries.end(); } -void CFilterList::Clean(const std::vector &vpAllowedElements) +bool CExcludedCommunityCountryFilterList::Empty() const { - size_t NumFiltered = 0; - char aNewList[512]; - aNewList[0] = '\0'; + auto CommunityEntry = m_Entries.find(CCommunityId(m_pCommunityCache->CountryTypeFilterKey())); + return CommunityEntry == m_Entries.end() || + CommunityEntry->second.empty() || + IsSubsetEquals(m_pCommunityCache->SelectableCountries(), CommunityEntry->second); +} - for(const char *pElement : vpAllowedElements) +void CExcludedCommunityCountryFilterList::Clean(const std::vector &vAllowedCommunities) +{ + for(auto It = m_Entries.begin(); It != m_Entries.end();) { - if(Filtered(pElement)) + const bool AllEntry = str_comp(It->first.Id(), IServerBrowser::COMMUNITY_ALL) == 0; + const bool Found = AllEntry || std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { + return str_comp(It->first.Id(), AllowedCommunity.Id()) == 0; + }) != vAllowedCommunities.end(); + if(Found) + { + ++It; + } + else { - if(aNewList[0] != '\0') - str_append(aNewList, ","); - str_append(aNewList, pElement); - ++NumFiltered; + It = m_Entries.erase(It); } } - // Prevent filter that would exclude all allowed elements - if(NumFiltered == vpAllowedElements.size()) - m_pFilter[0] = '\0'; - else - str_copy(m_pFilter, aNewList, m_FilterSize); + for(const CCommunity &AllowedCommunity : vAllowedCommunities) + { + auto CommunityEntry = m_Entries.find(CCommunityId(AllowedCommunity.Id())); + if(CommunityEntry != m_Entries.end()) + { + auto &CountryEntries = CommunityEntry->second; + for(auto It = CountryEntries.begin(); It != CountryEntries.end();) + { + if(AllowedCommunity.HasCountry(It->Name())) + { + ++It; + } + else + { + It = CountryEntries.erase(It); + } + } + // Prevent filter that would exclude all allowed countries + if(CountryEntries.size() == AllowedCommunity.Countries().size()) + { + CountryEntries.clear(); + } + } + } + + auto AllCommunityEntry = m_Entries.find(CCommunityId(IServerBrowser::COMMUNITY_ALL)); + if(AllCommunityEntry != m_Entries.end()) + { + auto &CountryEntries = AllCommunityEntry->second; + for(auto It = CountryEntries.begin(); It != CountryEntries.end();) + { + if(std::any_of(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const auto &Community) { return Community.HasCountry(It->Name()); })) + { + ++It; + } + else + { + It = CountryEntries.erase(It); + } + } + // Prevent filter that would exclude all allowed countries + std::unordered_set UniqueCountries; + for(const CCommunity &AllowedCommunity : vAllowedCommunities) + { + for(const CCommunityCountry &Country : AllowedCommunity.Countries()) + { + UniqueCountries.emplace(Country.Name()); + } + } + if(CountryEntries.size() == UniqueCountries.size()) + { + CountryEntries.clear(); + } + } } -void CServerBrowser::CleanFilters() +void CExcludedCommunityCountryFilterList::Save(IConfigManager *pConfigManager) const { - CommunitiesFilterClean(); - CountriesFilterClean(); - TypesFilterClean(); + char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH + CServerInfo::MAX_COMMUNITY_COUNTRY_LENGTH]; + for(const auto &[Community, Countries] : m_Entries) + { + for(const auto &Country : Countries) + { + str_copy(aBuf, "add_excluded_country \""); + str_append(aBuf, Community.Id()); + str_append(aBuf, "\" \""); + str_append(aBuf, Country.Name()); + str_append(aBuf, "\""); + pConfigManager->WriteLine(aBuf); + } + } } -void CServerBrowser::CommunitiesFilterClean() +void CExcludedCommunityTypeFilterList::Add(const char *pTypeName) { - std::vector vpCommunityNames; - for(const auto &Community : Communities()) - vpCommunityNames.push_back(Community.Id()); - m_CommunitiesFilter.Clean(vpCommunityNames); + // Handle special case that all selectable entries are currently filtered, + // where adding more entries to the exclusion list would have no effect. + auto CommunityEntry = m_Entries.find(m_pCommunityCache->CountryTypeFilterKey()); + if(CommunityEntry != m_Entries.end() && IsSubsetEquals(m_pCommunityCache->SelectableTypes(), CommunityEntry->second)) + { + for(const CCommunityType *pSelectableType : m_pCommunityCache->SelectableTypes()) + { + CommunityEntry->second.erase(pSelectableType->Name()); + } + } + + Add(m_pCommunityCache->CountryTypeFilterKey(), pTypeName); } -void CServerBrowser::CountriesFilterClean() +void CExcludedCommunityTypeFilterList::Add(const char *pCommunityId, const char *pTypeName) { - std::vector vpCountryNames; - for(const auto &Community : Communities()) - for(const auto &Country : Community.Countries()) - vpCountryNames.push_back(Country.Name()); - m_CountriesFilter.Clean(vpCountryNames); + CCommunityId CommunityId(pCommunityId); + if(m_Entries.find(CommunityId) == m_Entries.end()) + { + m_Entries[CommunityId] = {}; + } + m_Entries[CommunityId].emplace(pTypeName); } -void CServerBrowser::TypesFilterClean() +void CExcludedCommunityTypeFilterList::Remove(const char *pTypeName) { - std::vector vpTypeNames; - for(const auto &Community : Communities()) - for(const auto &Type : Community.Types()) - vpTypeNames.push_back(Type.Name()); - m_TypesFilter.Clean(vpTypeNames); + Remove(m_pCommunityCache->CountryTypeFilterKey(), pTypeName); } -bool CServerBrowser::IsRegistered(const NETADDR &Addr) +void CExcludedCommunityTypeFilterList::Remove(const char *pCommunityId, const char *pTypeName) { - const int NumServers = m_pHttp->NumServers(); - for(int i = 0; i < NumServers; i++) + auto CommunityEntry = m_Entries.find(CCommunityId(pCommunityId)); + if(CommunityEntry != m_Entries.end()) { - const CServerInfo Info = m_pHttp->Server(i); - for(int j = 0; j < Info.m_NumAddresses; j++) + CommunityEntry->second.erase(pTypeName); + } +} + +void CExcludedCommunityTypeFilterList::Clear() +{ + auto CommunityEntry = m_Entries.find(m_pCommunityCache->CountryTypeFilterKey()); + if(CommunityEntry != m_Entries.end()) + { + CommunityEntry->second.clear(); + } +} + +bool CExcludedCommunityTypeFilterList::Filtered(const char *pTypeName) const +{ + auto CommunityEntry = m_Entries.find(CCommunityId(m_pCommunityCache->CountryTypeFilterKey())); + if(CommunityEntry == m_Entries.end()) + return false; + + const auto &TypeEntries = CommunityEntry->second; + return !IsSubsetEquals(m_pCommunityCache->SelectableTypes(), TypeEntries) && + TypeEntries.find(CCommunityTypeName(pTypeName)) != TypeEntries.end(); +} + +bool CExcludedCommunityTypeFilterList::Empty() const +{ + auto CommunityEntry = m_Entries.find(CCommunityId(m_pCommunityCache->CountryTypeFilterKey())); + return CommunityEntry == m_Entries.end() || + CommunityEntry->second.empty() || + IsSubsetEquals(m_pCommunityCache->SelectableTypes(), CommunityEntry->second); +} + +void CExcludedCommunityTypeFilterList::Clean(const std::vector &vAllowedCommunities) +{ + for(auto It = m_Entries.begin(); It != m_Entries.end();) + { + const bool AllEntry = str_comp(It->first.Id(), IServerBrowser::COMMUNITY_ALL) == 0; + const bool Found = AllEntry || std::find_if(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const CCommunity &AllowedCommunity) { + return str_comp(It->first.Id(), AllowedCommunity.Id()) == 0; + }) != vAllowedCommunities.end(); + if(Found) { - if(net_addr_comp(&Info.m_aAddresses[j], &Addr) == 0) + ++It; + } + else + { + It = m_Entries.erase(It); + } + } + + for(const CCommunity &AllowedCommunity : vAllowedCommunities) + { + auto CommunityEntry = m_Entries.find(CCommunityId(AllowedCommunity.Id())); + if(CommunityEntry != m_Entries.end()) + { + auto &TypeEntries = CommunityEntry->second; + for(auto It = TypeEntries.begin(); It != TypeEntries.end();) { - return true; + if(AllowedCommunity.HasType(It->Name())) + { + ++It; + } + else + { + It = TypeEntries.erase(It); + } + } + // Prevent filter that would exclude all allowed types + if(TypeEntries.size() == AllowedCommunity.Types().size()) + { + TypeEntries.clear(); } } } - const int NumLegacyServers = m_pHttp->NumLegacyServers(); - for(int i = 0; i < NumLegacyServers; i++) + auto AllCommunityEntry = m_Entries.find(CCommunityId(IServerBrowser::COMMUNITY_ALL)); + if(AllCommunityEntry != m_Entries.end()) { - if(net_addr_comp(&m_pHttp->LegacyServer(i), &Addr) == 0) + auto &TypeEntries = AllCommunityEntry->second; + for(auto It = TypeEntries.begin(); It != TypeEntries.end();) + { + if(std::any_of(vAllowedCommunities.begin(), vAllowedCommunities.end(), [&](const auto &Community) { return Community.HasType(It->Name()); })) + { + ++It; + } + else + { + It = TypeEntries.erase(It); + } + } + // Prevent filter that would exclude all allowed types + std::unordered_set UniqueTypes; + for(const CCommunity &AllowedCommunity : vAllowedCommunities) + { + for(const CCommunityType &Type : AllowedCommunity.Types()) + { + UniqueTypes.emplace(Type.Name()); + } + } + if(TypeEntries.size() == UniqueTypes.size()) { - return true; + TypeEntries.clear(); } } +} +void CExcludedCommunityTypeFilterList::Save(IConfigManager *pConfigManager) const +{ + char aBuf[32 + CServerInfo::MAX_COMMUNITY_ID_LENGTH + CServerInfo::MAX_COMMUNITY_TYPE_LENGTH]; + for(const auto &[Community, Types] : m_Entries) + { + for(const auto &Type : Types) + { + str_copy(aBuf, "add_excluded_type \""); + str_append(aBuf, Community.Id()); + str_append(aBuf, "\" \""); + str_append(aBuf, Type.Name()); + str_append(aBuf, "\""); + pConfigManager->WriteLine(aBuf); + } + } +} + +void CServerBrowser::CleanFilters() +{ + // Keep filters if we failed to load any communities + if(Communities().empty()) + return; + FavoriteCommunitiesFilter().Clean(Communities()); + CommunitiesFilter().Clean(Communities()); + CountriesFilter().Clean(Communities()); + TypesFilter().Clean(Communities()); +} + +bool CServerBrowser::IsRegistered(const NETADDR &Addr) +{ + const int NumServers = m_pHttp->NumServers(); + for(int i = 0; i < NumServers; i++) + { + const CServerInfo &Info = m_pHttp->Server(i); + for(int j = 0; j < Info.m_NumAddresses; j++) + { + if(net_addr_comp(&Info.m_aAddresses[j], &Addr) == 0) + { + return true; + } + } + } return false; } @@ -1649,16 +2340,3 @@ bool CServerInfo::ParseLocation(int *pResult, const char *pString) } return true; } - -void CServerInfo::InfoToString(char *pBuffer, int BufferSize) const -{ - str_format( - pBuffer, - BufferSize, - "%s\n" - "Address: ddnet://%s\n" - "My IGN: %s\n", - m_aName, - m_aAddress, - g_Config.m_PlayerName); -} diff --git a/src/engine/client/serverbrowser.h b/src/engine/client/serverbrowser.h index 1920b6c744..e54f2efd66 100644 --- a/src/engine/client/serverbrowser.h +++ b/src/engine/client/serverbrowser.h @@ -3,13 +3,16 @@ #ifndef ENGINE_CLIENT_SERVERBROWSER_H #define ENGINE_CLIENT_SERVERBROWSER_H +#include #include #include #include #include +#include #include +#include typedef struct _json_value json_value; class CNetClient; @@ -21,6 +24,88 @@ class IFriends; class IServerBrowserHttp; class IServerBrowserPingCache; class IStorage; +class IHttp; + +class CCommunityId +{ + char m_aId[CServerInfo::MAX_COMMUNITY_ID_LENGTH]; + +public: + CCommunityId(const char *pCommunityId) + { + str_copy(m_aId, pCommunityId); + } + + const char *Id() const { return m_aId; } + + bool operator==(const CCommunityId &Other) const + { + return str_comp(Id(), Other.Id()) == 0; + } +}; + +template<> +struct std::hash +{ + size_t operator()(const CCommunityId &Elem) const noexcept + { + return str_quickhash(Elem.Id()); + } +}; + +class CCommunityCountryName +{ + char m_aName[CServerInfo::MAX_COMMUNITY_COUNTRY_LENGTH]; + +public: + CCommunityCountryName(const char *pCountryName) + { + str_copy(m_aName, pCountryName); + } + + const char *Name() const { return m_aName; } + + bool operator==(const CCommunityCountryName &Other) const + { + return str_comp(Name(), Other.Name()) == 0; + } +}; + +template<> +struct std::hash +{ + size_t operator()(const CCommunityCountryName &Elem) const noexcept + { + return str_quickhash(Elem.Name()); + } +}; + +class CCommunityTypeName +{ + char m_aName[CServerInfo::MAX_COMMUNITY_TYPE_LENGTH]; + +public: + CCommunityTypeName(const char *pTypeName) + { + str_copy(m_aName, pTypeName); + } + + const char *Name() const { return m_aName; } + + bool operator==(const CCommunityTypeName &Other) const + { + return str_comp(Name(), Other.Name()) == 0; + } +}; + +template<> +struct std::hash +{ + size_t operator()(const CCommunityTypeName &Elem) const noexcept + { + return str_quickhash(Elem.Name()); + } +}; class CCommunityServer { @@ -41,45 +126,119 @@ class CCommunityServer const char *TypeName() const { return m_aTypeName; } }; -class CFilterList : public IFilterList +class CFavoriteCommunityFilterList : public IFilterList +{ +public: + void Add(const char *pCommunityId) override; + void Remove(const char *pCommunityId) override; + void Clear() override; + bool Filtered(const char *pCommunityId) const override; + bool Empty() const override; + void Clean(const std::vector &vAllowedCommunities); + void Save(IConfigManager *pConfigManager) const; + const std::vector &Entries() const; + +private: + std::vector m_vEntries; +}; + +class CExcludedCommunityFilterList : public IFilterList +{ +public: + void Add(const char *pCommunityId) override; + void Remove(const char *pCommunityId) override; + void Clear() override; + bool Filtered(const char *pCommunityId) const override; + bool Empty() const override; + void Clean(const std::vector &vAllowedCommunities); + void Save(IConfigManager *pConfigManager) const; + +private: + std::unordered_set m_Entries; +}; + +class CExcludedCommunityCountryFilterList : public IFilterList { - char *m_pFilter; - size_t m_FilterSize; +public: + CExcludedCommunityCountryFilterList(const ICommunityCache *pCommunityCache) : + m_pCommunityCache(pCommunityCache) + { + } + + void Add(const char *pCountryName) override; + void Add(const char *pCommunityId, const char *pCountryName); + void Remove(const char *pCountryName) override; + void Remove(const char *pCommunityId, const char *pCountryName); + void Clear() override; + bool Filtered(const char *pCountryName) const override; + bool Empty() const override; + void Clean(const std::vector &vAllowedCommunities); + void Save(IConfigManager *pConfigManager) const; +private: + const ICommunityCache *m_pCommunityCache; + std::unordered_map> m_Entries; +}; + +class CExcludedCommunityTypeFilterList : public IFilterList +{ public: - CFilterList(char *pFilter, size_t FilterSize) : - m_pFilter(pFilter), m_FilterSize(FilterSize) + CExcludedCommunityTypeFilterList(const ICommunityCache *pCommunityCache) : + m_pCommunityCache(pCommunityCache) { } - void Add(const char *pElement) override; - void Remove(const char *pElement) override; + void Add(const char *pTypeName) override; + void Add(const char *pCommunityId, const char *pTypeName); + void Remove(const char *pTypeName) override; + void Remove(const char *pCommunityId, const char *pTypeName); void Clear() override; - bool Filtered(const char *pElement) const override; + bool Filtered(const char *pTypeName) const override; bool Empty() const override; - void Clean(const std::vector &vpAllowedElements); + void Clean(const std::vector &vAllowedCommunities); + void Save(IConfigManager *pConfigManager) const; + +private: + const ICommunityCache *m_pCommunityCache; + std::unordered_map> m_Entries; }; -class CServerBrowser : public IServerBrowser +class CCommunityCache : public ICommunityCache { + IServerBrowser *m_pServerBrowser; + SHA256_DIGEST m_InfoSha256 = SHA256_ZEROED; + int m_LastType = IServerBrowser::NUM_TYPES; // initial value does not appear normally, marking uninitialized cache + unsigned m_SelectedCommunitiesHash = 0; + std::vector m_vpSelectedCommunities; + std::vector m_vpSelectableCountries; + std::vector m_vpSelectableTypes; + bool m_AnyRanksAvailable = false; + bool m_CountryTypesFilterAvailable = false; + const char *m_pCountryTypeFilterKey = IServerBrowser::COMMUNITY_ALL; + public: - class CServerEntry + CCommunityCache(IServerBrowser *pServerBrowser) : + m_pServerBrowser(pServerBrowser) { - public: - int64_t m_RequestTime; - bool m_RequestIgnoreInfo; - int m_GotInfo; - CServerInfo m_Info; + } - CServerEntry *m_pPrevReq; // request list - CServerEntry *m_pNextReq; - }; + void Update(bool Force) override; + const std::vector &SelectedCommunities() const override { return m_vpSelectedCommunities; } + const std::vector &SelectableCountries() const override { return m_vpSelectableCountries; } + const std::vector &SelectableTypes() const override { return m_vpSelectableTypes; } + bool AnyRanksAvailable() const override { return m_AnyRanksAvailable; } + bool CountriesTypesFilterAvailable() const override { return m_CountryTypesFilterAvailable; } + const char *CountryTypeFilterKey() const override { return m_pCountryTypeFilterKey; } +}; +class CServerBrowser : public IServerBrowser +{ +public: CServerBrowser(); virtual ~CServerBrowser(); // interface functions - void Refresh(int Type) override; + void Refresh(int Type, bool Force = false) override; bool IsRefreshing() const override; bool IsGettingServerlist() const override; int LoadingProgression() const override; @@ -105,20 +264,25 @@ class CServerBrowser : public IServerBrowser const std::vector &Communities() const override; const CCommunity *Community(const char *pCommunityId) const override; std::vector SelectedCommunities() const override; - int64_t DDNetInfoUpdateTime() const override { return m_DDNetInfoUpdateTime; } - - CFilterList &CommunitiesFilter() override { return m_CommunitiesFilter; } - CFilterList &CountriesFilter() override { return m_CountriesFilter; } - CFilterList &TypesFilter() override { return m_TypesFilter; } - const CFilterList &CommunitiesFilter() const override { return m_CommunitiesFilter; } - const CFilterList &CountriesFilter() const override { return m_CountriesFilter; } - const CFilterList &TypesFilter() const override { return m_TypesFilter; } + std::vector FavoriteCommunities() const override; + std::vector CurrentCommunities() const override; + unsigned CurrentCommunitiesHash() const override; + + bool DDNetInfoAvailable() const override { return m_pDDNetInfo != nullptr; } + SHA256_DIGEST DDNetInfoSha256() const override { return m_DDNetInfoSha256; } + + ICommunityCache &CommunityCache() override { return m_CommunityCache; } + const ICommunityCache &CommunityCache() const override { return m_CommunityCache; } + CFavoriteCommunityFilterList &FavoriteCommunitiesFilter() override { return m_FavoriteCommunitiesFilter; } + CExcludedCommunityFilterList &CommunitiesFilter() override { return m_CommunitiesFilter; } + CExcludedCommunityCountryFilterList &CountriesFilter() override { return m_CountriesFilter; } + CExcludedCommunityTypeFilterList &TypesFilter() override { return m_TypesFilter; } + const CFavoriteCommunityFilterList &FavoriteCommunitiesFilter() const override { return m_FavoriteCommunitiesFilter; } + const CExcludedCommunityFilterList &CommunitiesFilter() const override { return m_CommunitiesFilter; } + const CExcludedCommunityCountryFilterList &CountriesFilter() const override { return m_CountriesFilter; } + const CExcludedCommunityTypeFilterList &TypesFilter() const override { return m_TypesFilter; } void CleanFilters() override; - void CommunitiesFilterClean(); - void CountriesFilterClean(); - void TypesFilterClean(); - // void Update(); void OnServerInfoUpdate(const NETADDR &Addr, int Token, const CServerInfo *pInfo); @@ -131,7 +295,7 @@ class CServerBrowser : public IServerBrowser void OnInit(); void QueueRequest(CServerEntry *pEntry); - CServerEntry *Find(const NETADDR &Addr); + CServerEntry *Find(const NETADDR &Addr) override; int GetCurrentType() override { return m_ServerlistType; } bool IsRegistered(const NETADDR &Addr); @@ -143,6 +307,7 @@ class CServerBrowser : public IServerBrowser IFriends *m_pFriends = nullptr; IFavorites *m_pFavorites = nullptr; IStorage *m_pStorage = nullptr; + IHttp *m_pHttpClient = nullptr; char m_aNetVersion[128]; bool m_RefreshingHttp = false; @@ -160,12 +325,14 @@ class CServerBrowser : public IServerBrowser int m_OwnLocation = CServerInfo::LOC_UNKNOWN; - CFilterList m_CommunitiesFilter; - CFilterList m_CountriesFilter; - CFilterList m_TypesFilter; + CCommunityCache m_CommunityCache; + CFavoriteCommunityFilterList m_FavoriteCommunitiesFilter; + CExcludedCommunityFilterList m_CommunitiesFilter; + CExcludedCommunityCountryFilterList m_CountriesFilter; + CExcludedCommunityTypeFilterList m_TypesFilter; - json_value *m_pDDNetInfo; - int64_t m_DDNetInfoUpdateTime; + json_value *m_pDDNetInfo = nullptr; + SHA256_DIGEST m_DDNetInfoSha256 = SHA256_ZEROED; CServerEntry *m_pFirstReqServer; // request list CServerEntry *m_pLastReqServer; @@ -198,6 +365,7 @@ class CServerBrowser : public IServerBrowser bool SortCompareGametype(int Index1, int Index2) const; bool SortCompareNumPlayers(int Index1, int Index2) const; bool SortCompareNumClients(int Index1, int Index2) const; + bool SortCompareNumFriends(int Index1, int Index2) const; bool SortCompareNumPlayersAndPing(int Index1, int Index2) const; // @@ -215,8 +383,21 @@ class CServerBrowser : public IServerBrowser void RequestImpl(const NETADDR &Addr, CServerEntry *pEntry, int *pBasicToken, int *pToken, bool RandomToken) const; void RegisterCommands(); + static void ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData); + static void Con_AddFavoriteCommunity(IConsole::IResult *pResult, void *pUserData); + static void Con_RemoveFavoriteCommunity(IConsole::IResult *pResult, void *pUserData); + static void Con_AddExcludedCommunity(IConsole::IResult *pResult, void *pUserData); + static void Con_RemoveExcludedCommunity(IConsole::IResult *pResult, void *pUserData); + static void Con_AddExcludedCountry(IConsole::IResult *pResult, void *pUserData); + static void Con_RemoveExcludedCountry(IConsole::IResult *pResult, void *pUserData); + static void Con_AddExcludedType(IConsole::IResult *pResult, void *pUserData); + static void Con_RemoveExcludedType(IConsole::IResult *pResult, void *pUserData); static void Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserData); + bool ValidateCommunityId(const char *pCommunityId) const; + bool ValidateCountryName(const char *pCountryName) const; + bool ValidateTypeName(const char *pTypeName) const; + void SetInfo(CServerEntry *pEntry, const CServerInfo &Info) const; void SetLatency(NETADDR Addr, int Latency); diff --git a/src/engine/client/serverbrowser_http.cpp b/src/engine/client/serverbrowser_http.cpp index df92212920..95dce94981 100644 --- a/src/engine/client/serverbrowser_http.cpp +++ b/src/engine/client/serverbrowser_http.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -20,6 +21,28 @@ using namespace std::chrono_literals; +static int SanitizeAge(std::optional Age) +{ + // A year is of course pi*10**7 seconds. + if(!(Age && 0 <= *Age && *Age < 31415927)) + { + return 31415927; + } + return *Age; +} + +// Classify HTTP responses into buckets, treat 15 seconds as fresh, 1 minute as +// less fresh, etc. This ensures that differences in the order of seconds do +// not affect master choice. +static int ClassifyAge(int AgeSeconds) +{ + return 0 // + + (AgeSeconds >= 15) // 15 seconds + + (AgeSeconds >= 60) // 1 minute + + (AgeSeconds >= 300) // 5 minutes + + (AgeSeconds / 3600); // 1 hour +} + class CChooseMaster { public: @@ -29,12 +52,12 @@ class CChooseMaster { MAX_URLS = 16, }; - CChooseMaster(IEngine *pEngine, VALIDATOR pfnValidator, const char **ppUrls, int NumUrls, int PreviousBestIndex); + CChooseMaster(IEngine *pEngine, IHttp *pHttp, VALIDATOR pfnValidator, const char **ppUrls, int NumUrls, int PreviousBestIndex); virtual ~CChooseMaster(); bool GetBestUrl(const char **pBestUrl) const; void Reset(); - bool IsRefreshing() const { return m_pJob && m_pJob->Status() != IJob::STATE_DONE; } + bool IsRefreshing() const { return m_pJob && !m_pJob->Done(); } void Refresh(); private: @@ -51,26 +74,33 @@ class CChooseMaster }; class CJob : public IJob { + CChooseMaster *m_pParent; CLock m_Lock; std::shared_ptr m_pData; - std::unique_ptr m_pHead PT_GUARDED_BY(m_Lock); - std::unique_ptr m_pGet PT_GUARDED_BY(m_Lock); + std::shared_ptr m_pHead; + std::shared_ptr m_pGet; void Run() override REQUIRES(!m_Lock); public: - CJob(std::shared_ptr pData) : - m_pData(std::move(pData)) {} - void Abort() REQUIRES(!m_Lock); + CJob(CChooseMaster *pParent, std::shared_ptr pData) : + m_pParent(pParent), + m_pData(std::move(pData)) + { + Abortable(true); + } + bool Abort() override REQUIRES(!m_Lock); }; IEngine *m_pEngine; + IHttp *m_pHttp; int m_PreviousBestIndex; std::shared_ptr m_pData; std::shared_ptr m_pJob; }; -CChooseMaster::CChooseMaster(IEngine *pEngine, VALIDATOR pfnValidator, const char **ppUrls, int NumUrls, int PreviousBestIndex) : +CChooseMaster::CChooseMaster(IEngine *pEngine, IHttp *pHttp, VALIDATOR pfnValidator, const char **ppUrls, int NumUrls, int PreviousBestIndex) : m_pEngine(pEngine), + m_pHttp(pHttp), m_PreviousBestIndex(PreviousBestIndex) { dbg_assert(NumUrls >= 0, "no master URLs"); @@ -127,12 +157,20 @@ void CChooseMaster::Reset() void CChooseMaster::Refresh() { - if(m_pJob == nullptr || m_pJob->Status() == IJob::STATE_DONE) - m_pEngine->AddJob(m_pJob = std::make_shared(m_pData)); + if(m_pJob == nullptr || m_pJob->State() == IJob::STATE_DONE) + { + m_pJob = std::make_shared(this, m_pData); + m_pEngine->AddJob(m_pJob); + } } -void CChooseMaster::CJob::Abort() +bool CChooseMaster::CJob::Abort() { + if(!IJob::Abort()) + { + return false; + } + CLockScope ls(m_Lock); if(m_pHead != nullptr) { @@ -143,6 +181,8 @@ void CChooseMaster::CJob::Abort() { m_pGet->Abort(); } + + return true; } void CChooseMaster::CJob::Run() @@ -167,43 +207,51 @@ void CChooseMaster::CJob::Run() // fail. CTimeout Timeout{10000, 0, 8000, 10}; int aTimeMs[MAX_URLS]; + int aAgeS[MAX_URLS]; for(int i = 0; i < m_pData->m_NumUrls; i++) { aTimeMs[i] = -1; + aAgeS[i] = SanitizeAge({}); const char *pUrl = m_pData->m_aaUrls[aRandomized[i]]; - CHttpRequest *pHead = HttpHead(pUrl).release(); + std::shared_ptr pHead = HttpHead(pUrl); pHead->Timeout(Timeout); pHead->LogProgress(HTTPLOG::FAILURE); { CLockScope ls(m_Lock); - m_pHead = std::unique_ptr(pHead); + m_pHead = pHead; } - IEngine::RunJobBlocking(pHead); - if(pHead->State() == HTTP_ABORTED) + + m_pParent->m_pHttp->Run(pHead); + pHead->Wait(); + if(pHead->State() == EHttpState::ABORTED || State() == IJob::STATE_ABORTED) { - dbg_msg("serverbrowse_http", "master chooser aborted"); + log_debug("serverbrowser_http", "master chooser aborted"); return; } - if(pHead->State() != HTTP_DONE) + if(pHead->State() != EHttpState::DONE) { continue; } + auto StartTime = time_get_nanoseconds(); - CHttpRequest *pGet = HttpGet(pUrl).release(); + std::shared_ptr pGet = HttpGet(pUrl); pGet->Timeout(Timeout); pGet->LogProgress(HTTPLOG::FAILURE); { CLockScope ls(m_Lock); - m_pGet = std::unique_ptr(pGet); + m_pGet = pGet; } - IEngine::RunJobBlocking(pGet); + + m_pParent->m_pHttp->Run(pGet); + pGet->Wait(); + auto Time = std::chrono::duration_cast(time_get_nanoseconds() - StartTime); - if(pHead->State() == HTTP_ABORTED) + if(pGet->State() == EHttpState::ABORTED || State() == IJob::STATE_ABORTED) { - dbg_msg("serverbrowse_http", "master chooser aborted"); + log_debug("serverbrowser_http", "master chooser aborted"); return; } - if(pGet->State() != HTTP_DONE) + if(pGet->State() != EHttpState::DONE) { continue; } @@ -212,43 +260,51 @@ void CChooseMaster::CJob::Run() { continue; } + bool ParseFailure = m_pData->m_pfnValidator(pJson); json_value_free(pJson); if(ParseFailure) { continue; } - dbg_msg("serverbrowse_http", "found master, url='%s' time=%dms", pUrl, (int)Time.count()); + int AgeS = SanitizeAge(pGet->ResultAgeSeconds()); + log_info("serverbrowser_http", "found master, url='%s' time=%dms age=%ds", pUrl, (int)Time.count(), AgeS); + aTimeMs[i] = Time.count(); + aAgeS[i] = AgeS; } + // Determine index of the minimum time. int BestIndex = -1; int BestTime = 0; + int BestAge = 0; for(int i = 0; i < m_pData->m_NumUrls; i++) { if(aTimeMs[i] < 0) { continue; } - if(BestIndex == -1 || aTimeMs[i] < BestTime) + if(BestIndex == -1 || std::tuple(ClassifyAge(aAgeS[i]), aTimeMs[i]) < std::tuple(ClassifyAge(BestAge), BestTime)) { BestTime = aTimeMs[i]; + BestAge = aAgeS[i]; BestIndex = aRandomized[i]; } } if(BestIndex == -1) { - dbg_msg("serverbrowse_http", "WARNING: no usable masters found"); + log_error("serverbrowser_http", "WARNING: no usable masters found"); return; } - dbg_msg("serverbrowse_http", "determined best master, url='%s' time=%dms", m_pData->m_aaUrls[BestIndex], BestTime); + + log_info("serverbrowser_http", "determined best master, url='%s' time=%dms age=%ds", m_pData->m_aaUrls[BestIndex], BestTime, BestAge); m_pData->m_BestIndex.store(BestIndex); } class CServerBrowserHttp : public IServerBrowserHttp { public: - CServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, const char **ppUrls, int NumUrls, int PreviousBestIndex); + CServerBrowserHttp(IEngine *pEngine, IHttp *pHttp, const char **ppUrls, int NumUrls, int PreviousBestIndex); ~CServerBrowserHttp() override; void Update() override; bool IsRefreshing() override { return m_State != STATE_DONE; } @@ -263,14 +319,6 @@ class CServerBrowserHttp : public IServerBrowserHttp { return m_vServers[Index]; } - int NumLegacyServers() const override - { - return m_vLegacyServers.size(); - } - const NETADDR &LegacyServer(int Index) const override - { - return m_vLegacyServers[Index]; - } private: enum @@ -282,23 +330,20 @@ class CServerBrowserHttp : public IServerBrowserHttp }; static bool Validate(json_value *pJson); - static bool Parse(json_value *pJson, std::vector *pvServers, std::vector *pvLegacyServers); + static bool Parse(json_value *pJson, std::vector *pvServers); - IEngine *m_pEngine; - IConsole *m_pConsole; + IHttp *m_pHttp; int m_State = STATE_DONE; std::shared_ptr m_pGetServers; std::unique_ptr m_pChooseMaster; std::vector m_vServers; - std::vector m_vLegacyServers; }; -CServerBrowserHttp::CServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, const char **ppUrls, int NumUrls, int PreviousBestIndex) : - m_pEngine(pEngine), - m_pConsole(pConsole), - m_pChooseMaster(new CChooseMaster(pEngine, Validate, ppUrls, NumUrls, PreviousBestIndex)) +CServerBrowserHttp::CServerBrowserHttp(IEngine *pEngine, IHttp *pHttp, const char **ppUrls, int NumUrls, int PreviousBestIndex) : + m_pHttp(pHttp), + m_pChooseMaster(new CChooseMaster(pEngine, pHttp, Validate, ppUrls, NumUrls, PreviousBestIndex)) { m_pChooseMaster->Refresh(); } @@ -320,7 +365,7 @@ void CServerBrowserHttp::Update() { if(!m_pChooseMaster->IsRefreshing()) { - m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_http", "no working serverlist URL found"); + log_error("serverbrowser_http", "no working serverlist URL found"); m_State = STATE_NO_MASTER; } return; @@ -328,12 +373,12 @@ void CServerBrowserHttp::Update() m_pGetServers = HttpGet(pBestUrl); // 10 seconds connection timeout, lower than 8KB/s for 10 seconds to fail. m_pGetServers->Timeout(CTimeout{10000, 0, 8000, 10}); - m_pEngine->AddJob(m_pGetServers); + m_pHttp->Run(m_pGetServers); m_State = STATE_REFRESHING; } else if(m_State == STATE_REFRESHING) { - if(m_pGetServers->State() == HTTP_QUEUED || m_pGetServers->State() == HTTP_RUNNING) + if(!m_pGetServers->Done()) { return; } @@ -342,16 +387,27 @@ void CServerBrowserHttp::Update() std::swap(m_pGetServers, pGetServers); bool Success = true; - json_value *pJson = pGetServers->ResultJson(); + json_value *pJson = pGetServers->State() == EHttpState::DONE ? pGetServers->ResultJson() : nullptr; Success = Success && pJson; - Success = Success && !Parse(pJson, &m_vServers, &m_vLegacyServers); + Success = Success && !Parse(pJson, &m_vServers); json_value_free(pJson); if(!Success) { - m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "serverbrowse_http", "failed getting serverlist, trying to find best URL"); + log_error("serverbrowser_http", "failed getting serverlist, trying to find best URL"); m_pChooseMaster->Reset(); m_pChooseMaster->Refresh(); } + else + { + // Try to find new master if the current one returns + // results that are 5 minutes old. + int Age = SanitizeAge(pGetServers->ResultAgeSeconds()); + if(Age > 300) + { + log_info("serverbrowser_http", "got stale serverlist, age=%ds, trying to find best URL", Age); + m_pChooseMaster->Refresh(); + } + } } } void CServerBrowserHttp::Refresh() @@ -368,23 +424,23 @@ void CServerBrowserHttp::Refresh() } bool ServerbrowserParseUrl(NETADDR *pOut, const char *pUrl) { - return net_addr_from_url(pOut, pUrl, nullptr, 0) != 0; + int Failure = net_addr_from_url(pOut, pUrl, nullptr, 0); + if(Failure || pOut->port == 0) + return true; + return false; } bool CServerBrowserHttp::Validate(json_value *pJson) { std::vector vServers; - std::vector vLegacyServers; - return Parse(pJson, &vServers, &vLegacyServers); + return Parse(pJson, &vServers); } -bool CServerBrowserHttp::Parse(json_value *pJson, std::vector *pvServers, std::vector *pvLegacyServers) +bool CServerBrowserHttp::Parse(json_value *pJson, std::vector *pvServers) { std::vector vServers; - std::vector vLegacyServers; const json_value &Json = *pJson; const json_value &Servers = Json["servers"]; - const json_value &LegacyServers = Json["servers_legacy"]; - if(Servers.type != json_array || (LegacyServers.type != json_array && LegacyServers.type != json_none)) + if(Servers.type != json_array) { return true; } @@ -409,7 +465,7 @@ bool CServerBrowserHttp::Parse(json_value *pJson, std::vector *pvSe } if(CServerInfo2::FromJson(&ParsedInfo, &Info)) { - //dbg_msg("dbg/serverbrowser", "skipped due to info, i=%d", i); + log_debug("serverbrowser_http", "skipped due to info, i=%d", i); // Only skip the current server on parsing // failure; the server info is "user input" by // the game server and can be set to arbitrary @@ -419,6 +475,7 @@ bool CServerBrowserHttp::Parse(json_value *pJson, std::vector *pvSe CServerInfo SetInfo = ParsedInfo; SetInfo.m_Location = ParsedLocation; SetInfo.m_NumAddresses = 0; + bool GotVersion6 = false; for(unsigned int a = 0; a < Addresses.u.array.length; a++) { const json_value &Address = Addresses[a]; @@ -426,10 +483,27 @@ bool CServerBrowserHttp::Parse(json_value *pJson, std::vector *pvSe { return true; } + if(str_startswith(Addresses[a], "tw-0.6+udp://")) + { + GotVersion6 = true; + break; + } + } + for(unsigned int a = 0; a < Addresses.u.array.length; a++) + { + const json_value &Address = Addresses[a]; + if(Address.type != json_string) + { + return true; + } + if(GotVersion6 && str_startswith(Addresses[a], "tw-0.7+udp://")) + { + continue; + } NETADDR ParsedAddr; if(ServerbrowserParseUrl(&ParsedAddr, Addresses[a])) { - //dbg_msg("dbg/serverbrowser", "unknown address, i=%d a=%d", i, a); + // log_debug("serverbrowser_http", "unknown address, i=%d a=%d", i, a); // Skip unknown addresses. continue; } @@ -444,21 +518,7 @@ bool CServerBrowserHttp::Parse(json_value *pJson, std::vector *pvSe vServers.push_back(SetInfo); } } - if(LegacyServers.type == json_array) - { - for(unsigned int i = 0; i < LegacyServers.u.array.length; i++) - { - const json_value &Address = LegacyServers[i]; - NETADDR ParsedAddr; - if(Address.type != json_string || net_addr_from_str(&ParsedAddr, Address)) - { - return true; - } - vLegacyServers.push_back(ParsedAddr); - } - } *pvServers = vServers; - *pvLegacyServers = vLegacyServers; return false; } @@ -469,21 +529,18 @@ static const char *DEFAULT_SERVERLIST_URLS[] = { "https://master4.ddnet.org/ddnet/15/servers.json", }; -IServerBrowserHttp *CreateServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, IStorage *pStorage, const char *pPreviousBestUrl) +IServerBrowserHttp *CreateServerBrowserHttp(IEngine *pEngine, IStorage *pStorage, IHttp *pHttp, const char *pPreviousBestUrl) { char aaUrls[CChooseMaster::MAX_URLS][256]; const char *apUrls[CChooseMaster::MAX_URLS] = {0}; const char **ppUrls = apUrls; int NumUrls = 0; - IOHANDLE File = pStorage->OpenFile("ddnet-serverlist-urls.cfg", IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL); - if(File) + CLineReader LineReader; + if(LineReader.OpenFile(pStorage->OpenFile("ddnet-serverlist-urls.cfg", IOFLAG_READ, IStorage::TYPE_ALL))) { - CLineReader Lines; - Lines.Init(File); - while(NumUrls < CChooseMaster::MAX_URLS) + while(const char *pLine = LineReader.Get()) { - const char *pLine = Lines.Get(); - if(!pLine) + if(NumUrls == CChooseMaster::MAX_URLS) { break; } @@ -506,5 +563,5 @@ IServerBrowserHttp *CreateServerBrowserHttp(IEngine *pEngine, IConsole *pConsole break; } } - return new CServerBrowserHttp(pEngine, pConsole, ppUrls, NumUrls, PreviousBestIndex); + return new CServerBrowserHttp(pEngine, pHttp, ppUrls, NumUrls, PreviousBestIndex); } diff --git a/src/engine/client/serverbrowser_http.h b/src/engine/client/serverbrowser_http.h index b6884f869a..18f7a62d68 100644 --- a/src/engine/client/serverbrowser_http.h +++ b/src/engine/client/serverbrowser_http.h @@ -3,9 +3,9 @@ #include class CServerInfo; -class IConsole; class IEngine; class IStorage; +class IHttp; class IServerBrowserHttp { @@ -21,9 +21,7 @@ class IServerBrowserHttp virtual int NumServers() const = 0; virtual const CServerInfo &Server(int Index) const = 0; - virtual int NumLegacyServers() const = 0; - virtual const NETADDR &LegacyServer(int Index) const = 0; }; -IServerBrowserHttp *CreateServerBrowserHttp(IEngine *pEngine, IConsole *pConsole, IStorage *pStorage, const char *pPreviousBestUrl); +IServerBrowserHttp *CreateServerBrowserHttp(IEngine *pEngine, IStorage *pStorage, IHttp *pHttp, const char *pPreviousBestUrl); #endif // ENGINE_CLIENT_SERVERBROWSER_HTTP_H diff --git a/src/engine/client/sixup_translate_system.cpp b/src/engine/client/sixup_translate_system.cpp new file mode 100644 index 0000000000..f6398cfcb5 --- /dev/null +++ b/src/engine/client/sixup_translate_system.cpp @@ -0,0 +1,114 @@ +#include + +int CClient::TranslateSysMsg(int *pMsgId, bool System, CUnpacker *pUnpacker, CPacker *pPacker, CNetChunk *pPacket, bool *pIsExMsg) +{ + *pIsExMsg = false; + if(!System) + return -1; + + // ddnet ex + if(*pMsgId > NETMSG_WHATIS && *pMsgId < NETMSG_RCON_CMD_GROUP_END) + { + *pIsExMsg = true; + return 0; + } + + pPacker->Reset(); + + if(*pMsgId == protocol7::NETMSG_MAP_CHANGE) + { + *pMsgId = NETMSG_MAP_CHANGE; + const char *pMapName = pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES); + int MapCrc = pUnpacker->GetInt(); + int Size = pUnpacker->GetInt(); + m_TranslationContext.m_MapDownloadChunksPerRequest = pUnpacker->GetInt(); + int ChunkSize = pUnpacker->GetInt(); + // void *pSha256 = pUnpacker->GetRaw(); // probably safe to ignore + pPacker->AddString(pMapName, 0); + pPacker->AddInt(MapCrc); + pPacker->AddInt(Size); + m_TranslationContext.m_MapdownloadTotalsize = Size; + m_TranslationContext.m_MapDownloadChunkSize = ChunkSize; + return 0; + } + else if(*pMsgId == protocol7::NETMSG_SERVERINFO) + { + // side effect only + // this is a 0.7 only message and not handled in 0.6 code + *pMsgId = -1; + net_addr_str(&pPacket->m_Address, m_CurrentServerInfo.m_aAddress, sizeof(m_CurrentServerInfo.m_aAddress), true); + str_copy(m_CurrentServerInfo.m_aVersion, pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES)); + str_copy(m_CurrentServerInfo.m_aName, pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES)); + str_clean_whitespaces(m_CurrentServerInfo.m_aName); + pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES); // Hostname + str_copy(m_CurrentServerInfo.m_aMap, pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES)); + str_copy(m_CurrentServerInfo.m_aGameType, pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES)); + int Flags = pUnpacker->GetInt(); + if(Flags & SERVER_FLAG_PASSWORD) + m_CurrentServerInfo.m_Flags |= SERVER_FLAG_PASSWORD; + // ddnets http master server handles timescore for us already + // if(Flags&SERVER_FLAG_TIMESCORE) + // m_CurrentServerInfo.m_Flags |= SERVER_FLAG_TIMESCORE; + pUnpacker->GetInt(); // Server level + m_CurrentServerInfo.m_NumPlayers = pUnpacker->GetInt(); + m_CurrentServerInfo.m_MaxPlayers = pUnpacker->GetInt(); + m_CurrentServerInfo.m_NumClients = pUnpacker->GetInt(); + m_CurrentServerInfo.m_MaxClients = pUnpacker->GetInt(); + return 0; + } + else if(*pMsgId == protocol7::NETMSG_RCON_AUTH_ON) + { + *pMsgId = NETMSG_RCON_AUTH_STATUS; + pPacker->AddInt(1); // authed + pPacker->AddInt(1); // cmdlist + return 0; + } + else if(*pMsgId == protocol7::NETMSG_RCON_AUTH_OFF) + { + *pMsgId = NETMSG_RCON_AUTH_STATUS; + pPacker->AddInt(0); // authed + pPacker->AddInt(0); // cmdlist + return 0; + } + else if(*pMsgId == protocol7::NETMSG_MAP_DATA) + { + // not binary compatible but translation happens on unpack + *pMsgId = NETMSG_MAP_DATA; + } + else if(*pMsgId >= protocol7::NETMSG_CON_READY && *pMsgId <= protocol7::NETMSG_INPUTTIMING) + { + *pMsgId = *pMsgId - 1; + } + else if(*pMsgId == protocol7::NETMSG_RCON_LINE) + { + *pMsgId = NETMSG_RCON_LINE; + } + else if(*pMsgId == protocol7::NETMSG_RCON_CMD_ADD) + { + *pMsgId = NETMSG_RCON_CMD_ADD; + } + else if(*pMsgId == protocol7::NETMSG_RCON_CMD_REM) + { + *pMsgId = NETMSG_RCON_CMD_REM; + } + else if(*pMsgId == protocol7::NETMSG_PING_REPLY) + { + *pMsgId = NETMSG_PING_REPLY; + } + else if(*pMsgId == protocol7::NETMSG_MAPLIST_ENTRY_ADD || *pMsgId == protocol7::NETMSG_MAPLIST_ENTRY_REM) + { + // This is just a nice to have so silently dropping that is fine + return -1; + } + else if(*pMsgId >= NETMSG_INFO && *pMsgId <= NETMSG_MAP_DATA) + { + return -1; // same in 0.6 and 0.7 + } + else if(*pMsgId < OFFSET_UUID) + { + dbg_msg("sixup", "drop unknown sys msg=%d", *pMsgId); + return -1; + } + + return -1; +} diff --git a/src/engine/client/sound.cpp b/src/engine/client/sound.cpp index 8654077305..63df6cdb50 100644 --- a/src/engine/client/sound.cpp +++ b/src/engine/client/sound.cpp @@ -21,6 +21,9 @@ extern "C" { #include +static constexpr int SAMPLE_INDEX_USED = -2; +static constexpr int SAMPLE_INDEX_FULL = -1; + void CSound::Mix(short *pFinalOut, unsigned Frames) { Frames = minimum(Frames, m_MaxFrames); @@ -60,12 +63,10 @@ void CSound::Mix(short *pFinalOut, unsigned Frames) if(Voice.m_Flags & ISound::FLAG_POS && Voice.m_pChannel->m_Pan) { // TODO: we should respect the channel panning value - const int dx = Voice.m_X - m_CenterX.load(std::memory_order_relaxed); - const int dy = Voice.m_Y - m_CenterY.load(std::memory_order_relaxed); - float FalloffX = 0.0f; - float FalloffY = 0.0f; + const vec2 Delta = Voice.m_Position - vec2(m_ListenerPositionX.load(std::memory_order_relaxed), m_ListenerPositionY.load(std::memory_order_relaxed)); + vec2 Falloff = vec2(0.0f, 0.0f); - int RangeX = 0; // for panning + float RangeX = 0.0f; // for panning bool InVoiceField = false; switch(Voice.m_Shape) @@ -75,50 +76,34 @@ void CSound::Mix(short *pFinalOut, unsigned Frames) const float Radius = Voice.m_Circle.m_Radius; RangeX = Radius; - // dx and dy can be larger than 46341 and thus the calculation would go beyond the limits of a integer, - // therefore we cast them into float - const int Dist = (int)length(vec2(dx, dy)); + const float Dist = length(Delta); if(Dist < Radius) { InVoiceField = true; // falloff - int FalloffDistance = Radius * Voice.m_Falloff; - if(Dist > FalloffDistance) - FalloffX = FalloffY = (Radius - Dist) / (Radius - FalloffDistance); - else - FalloffX = FalloffY = 1.0f; + const float FalloffDistance = Radius * Voice.m_Falloff; + Falloff.x = Falloff.y = Dist > FalloffDistance ? (Radius - Dist) / (Radius - FalloffDistance) : 1.0f; } - else - InVoiceField = false; - break; } case ISound::SHAPE_RECTANGLE: { - RangeX = Voice.m_Rectangle.m_Width / 2.0f; - - const int abs_dx = absolute(dx); - const int abs_dy = absolute(dy); + const vec2 AbsoluteDelta = vec2(absolute(Delta.x), absolute(Delta.y)); + const float w = Voice.m_Rectangle.m_Width / 2.0f; + const float h = Voice.m_Rectangle.m_Height / 2.0f; + RangeX = w; - const int w = Voice.m_Rectangle.m_Width / 2.0f; - const int h = Voice.m_Rectangle.m_Height / 2.0f; - - if(abs_dx < w && abs_dy < h) + if(AbsoluteDelta.x < w && AbsoluteDelta.y < h) { InVoiceField = true; // falloff - int fx = Voice.m_Falloff * w; - int fy = Voice.m_Falloff * h; - - FalloffX = abs_dx > fx ? (float)(w - abs_dx) / (w - fx) : 1.0f; - FalloffY = abs_dy > fy ? (float)(h - abs_dy) / (h - fy) : 1.0f; + const vec2 FalloffDistance = vec2(w, h) * Voice.m_Falloff; + Falloff.x = AbsoluteDelta.x > FalloffDistance.x ? (w - AbsoluteDelta.x) / (w - FalloffDistance.x) : 1.0f; + Falloff.y = AbsoluteDelta.y > FalloffDistance.y ? (h - AbsoluteDelta.y) / (h - FalloffDistance.y) : 1.0f; } - else - InVoiceField = false; - break; } }; @@ -128,15 +113,15 @@ void CSound::Mix(short *pFinalOut, unsigned Frames) // panning if(!(Voice.m_Flags & ISound::FLAG_NO_PANNING)) { - if(dx > 0) - VolumeL = ((RangeX - absolute(dx)) * VolumeL) / RangeX; + if(Delta.x > 0) + VolumeL = ((RangeX - absolute(Delta.x)) * VolumeL) / RangeX; else - VolumeR = ((RangeX - absolute(dx)) * VolumeR) / RangeX; + VolumeR = ((RangeX - absolute(Delta.x)) * VolumeR) / RangeX; } { - VolumeL *= FalloffX * FalloffY; - VolumeR *= FalloffX * FalloffY; + VolumeL *= Falloff.x * Falloff.y; + VolumeR *= Falloff.x * Falloff.y; } } else @@ -204,6 +189,18 @@ int CSound::Init() m_pGraphics = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); + // Initialize sample indices. We always need them to load sounds in + // the editor even if sound is disabled or failed to be enabled. + m_FirstFreeSampleIndex = 0; + for(size_t i = 0; i < std::size(m_aSamples) - 1; ++i) + { + m_aSamples[i].m_Index = i; + m_aSamples[i].m_NextFreeSampleIndex = i + 1; + m_aSamples[i].m_pData = nullptr; + } + m_aSamples[std::size(m_aSamples) - 1].m_Index = std::size(m_aSamples) - 1; + m_aSamples[std::size(m_aSamples) - 1].m_NextFreeSampleIndex = SAMPLE_INDEX_FULL; + if(!g_Config.m_SndEnable) return 0; @@ -213,10 +210,8 @@ int CSound::Init() return -1; } - m_MixingRate = g_Config.m_SndRate; - SDL_AudioSpec Format, FormatOut; - Format.freq = m_MixingRate; + Format.freq = g_Config.m_SndRate; Format.format = AUDIO_S16; Format.channels = 2; Format.samples = g_Config.m_SndBufferSize; @@ -224,8 +219,7 @@ int CSound::Init() Format.userdata = this; // Open the audio device and start playing sound! - m_Device = SDL_OpenAudioDevice(nullptr, 0, &Format, &FormatOut, 0); - + m_Device = SDL_OpenAudioDevice(nullptr, 0, &Format, &FormatOut, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE); if(m_Device == 0) { dbg_msg("sound", "unable to open audio: %s", SDL_GetError()); @@ -234,6 +228,7 @@ int CSound::Init() else dbg_msg("sound", "sound init successful using audio driver '%s'", SDL_GetCurrentAudioDriver()); + m_MixingRate = FormatOut.freq; m_MaxFrames = FormatOut.samples * 2; #if defined(CONF_VIDEORECORDER) m_MaxFrames = maximum(m_MaxFrames, 1024 * 2); // make the buffer bigger just in case @@ -263,9 +258,9 @@ void CSound::UpdateVolume() void CSound::Shutdown() { - for(unsigned SampleID = 0; SampleID < NUM_SAMPLES; SampleID++) + for(unsigned SampleId = 0; SampleId < NUM_SAMPLES; SampleId++) { - UnloadSample(SampleID); + UnloadSample(SampleId); } SDL_CloseAudioDevice(m_Device); @@ -274,22 +269,29 @@ void CSound::Shutdown() m_pMixBuffer = nullptr; } -int CSound::AllocID() +CSample *CSound::AllocSample() { - // TODO: linear search, get rid of it - for(unsigned SampleID = 0; SampleID < NUM_SAMPLES; SampleID++) + if(m_FirstFreeSampleIndex == SAMPLE_INDEX_FULL) + return nullptr; + + CSample *pSample = &m_aSamples[m_FirstFreeSampleIndex]; + if(pSample->m_pData != nullptr || pSample->m_NextFreeSampleIndex == SAMPLE_INDEX_USED) { - if(m_aSamples[SampleID].m_pData == nullptr) - return SampleID; + char aError[128]; + str_format(aError, sizeof(aError), "Sample was not unloaded (index=%d, next=%d, duration=%f, data=%p)", + pSample->m_Index, pSample->m_NextFreeSampleIndex, pSample->TotalTime(), pSample->m_pData); + dbg_assert(false, aError); } - - return -1; + m_FirstFreeSampleIndex = pSample->m_NextFreeSampleIndex; + pSample->m_NextFreeSampleIndex = SAMPLE_INDEX_USED; + return pSample; } void CSound::RateConvert(CSample &Sample) const { + dbg_assert(Sample.m_pData != nullptr, "Sample is not loaded"); // make sure that we need to convert this sound - if(!Sample.m_pData || Sample.m_Rate == m_MixingRate) + if(Sample.m_Rate == m_MixingRate) return; // allocate new data @@ -323,29 +325,36 @@ void CSound::RateConvert(CSample &Sample) const bool CSound::DecodeOpus(CSample &Sample, const void *pData, unsigned DataSize) const { - OggOpusFile *pOpusFile = op_open_memory((const unsigned char *)pData, DataSize, nullptr); + int OpusError = 0; + OggOpusFile *pOpusFile = op_open_memory((const unsigned char *)pData, DataSize, &OpusError); if(pOpusFile) { const int NumChannels = op_channel_count(pOpusFile, -1); - const int NumSamples = op_pcm_total(pOpusFile, -1); // per channel! - - Sample.m_Channels = NumChannels; - - if(Sample.m_Channels > 2) + if(NumChannels > 2) { + op_free(pOpusFile); dbg_msg("sound/opus", "file is not mono or stereo."); return false; } - Sample.m_pData = (short *)calloc((size_t)NumSamples * NumChannels, sizeof(short)); + const int NumSamples = op_pcm_total(pOpusFile, -1); // per channel! + if(NumSamples < 0) + { + op_free(pOpusFile); + dbg_msg("sound/opus", "failed to get number of samples, error %d", NumSamples); + return false; + } + + short *pSampleData = (short *)calloc((size_t)NumSamples * NumChannels, sizeof(short)); int Pos = 0; while(Pos < NumSamples) { - const int Read = op_read(pOpusFile, Sample.m_pData + Pos * NumChannels, NumSamples * NumChannels, nullptr); + const int Read = op_read(pOpusFile, pSampleData + Pos * NumChannels, (NumSamples - Pos) * NumChannels, nullptr); if(Read < 0) { - free(Sample.m_pData); + free(pSampleData); + op_free(pOpusFile); dbg_msg("sound/opus", "op_read error %d at %d", Read, Pos); return false; } @@ -354,15 +363,19 @@ bool CSound::DecodeOpus(CSample &Sample, const void *pData, unsigned DataSize) c Pos += Read; } + op_free(pOpusFile); + + Sample.m_pData = pSampleData; Sample.m_NumFrames = Pos; Sample.m_Rate = 48000; + Sample.m_Channels = NumChannels; Sample.m_LoopStart = -1; Sample.m_LoopEnd = -1; Sample.m_PausedAt = 0; } else { - dbg_msg("sound/opus", "failed to decode sample"); + dbg_msg("sound/opus", "failed to decode sample, error %d", OpusError); return false; } @@ -441,10 +454,7 @@ bool CSound::DecodeWV(CSample &Sample, const void *pData, unsigned DataSize) con const unsigned int SampleRate = WavpackGetSampleRate(pContext); const int NumChannels = WavpackGetNumChannels(pContext); - Sample.m_Channels = NumChannels; - Sample.m_Rate = SampleRate; - - if(Sample.m_Channels > 2) + if(NumChannels > 2) { dbg_msg("sound/wv", "file is not mono or stereo."); s_pWVBuffer = nullptr; @@ -480,6 +490,8 @@ bool CSound::DecodeWV(CSample &Sample, const void *pData, unsigned DataSize) con #endif Sample.m_NumFrames = NumSamples; + Sample.m_Rate = SampleRate; + Sample.m_Channels = NumChannels; Sample.m_LoopStart = -1; Sample.m_LoopEnd = -1; Sample.m_PausedAt = 0; @@ -505,8 +517,8 @@ int CSound::LoadOpus(const char *pFilename, int StorageType) if(!m_pStorage) return -1; - const int SampleID = AllocID(); - if(SampleID < 0) + CSample *pSample = AllocSample(); + if(!pSample) { dbg_msg("sound/opus", "failed to allocate sample ID. filename='%s'", pFilename); return -1; @@ -516,20 +528,24 @@ int CSound::LoadOpus(const char *pFilename, int StorageType) unsigned DataSize; if(!m_pStorage->ReadFile(pFilename, StorageType, &pData, &DataSize)) { + UnloadSample(pSample->m_Index); dbg_msg("sound/opus", "failed to open file. filename='%s'", pFilename); return -1; } - const bool DecodeSuccess = DecodeOpus(m_aSamples[SampleID], pData, DataSize); + const bool DecodeSuccess = DecodeOpus(*pSample, pData, DataSize); free(pData); if(!DecodeSuccess) + { + UnloadSample(pSample->m_Index); return -1; + } if(g_Config.m_Debug) dbg_msg("sound/opus", "loaded %s", pFilename); - RateConvert(m_aSamples[SampleID]); - return SampleID; + RateConvert(*pSample); + return pSample->m_Index; } int CSound::LoadWV(const char *pFilename, int StorageType) @@ -541,8 +557,8 @@ int CSound::LoadWV(const char *pFilename, int StorageType) if(!m_pStorage) return -1; - const int SampleID = AllocID(); - if(SampleID < 0) + CSample *pSample = AllocSample(); + if(!pSample) { dbg_msg("sound/wv", "failed to allocate sample ID. filename='%s'", pFilename); return -1; @@ -552,20 +568,24 @@ int CSound::LoadWV(const char *pFilename, int StorageType) unsigned DataSize; if(!m_pStorage->ReadFile(pFilename, StorageType, &pData, &DataSize)) { + UnloadSample(pSample->m_Index); dbg_msg("sound/wv", "failed to open file. filename='%s'", pFilename); return -1; } - const bool DecodeSuccess = DecodeWV(m_aSamples[SampleID], pData, DataSize); + const bool DecodeSuccess = DecodeWV(*pSample, pData, DataSize); free(pData); if(!DecodeSuccess) + { + UnloadSample(pSample->m_Index); return -1; + } if(g_Config.m_Debug) dbg_msg("sound/wv", "loaded %s", pFilename); - RateConvert(m_aSamples[SampleID]); - return SampleID; + RateConvert(*pSample); + return pSample->m_Index; } int CSound::LoadOpusFromMem(const void *pData, unsigned DataSize, bool FromEditor = false) @@ -577,15 +597,18 @@ int CSound::LoadOpusFromMem(const void *pData, unsigned DataSize, bool FromEdito if(!pData) return -1; - const int SampleID = AllocID(); - if(SampleID < 0) + CSample *pSample = AllocSample(); + if(!pSample) return -1; - if(!DecodeOpus(m_aSamples[SampleID], pData, DataSize)) + if(!DecodeOpus(*pSample, pData, DataSize)) + { + UnloadSample(pSample->m_Index); return -1; + } - RateConvert(m_aSamples[SampleID]); - return SampleID; + RateConvert(*pSample); + return pSample->m_Index; } int CSound::LoadWVFromMem(const void *pData, unsigned DataSize, bool FromEditor = false) @@ -597,87 +620,95 @@ int CSound::LoadWVFromMem(const void *pData, unsigned DataSize, bool FromEditor if(!pData) return -1; - const int SampleID = AllocID(); - if(SampleID < 0) + CSample *pSample = AllocSample(); + if(!pSample) return -1; - if(!DecodeWV(m_aSamples[SampleID], pData, DataSize)) + if(!DecodeWV(*pSample, pData, DataSize)) + { + UnloadSample(pSample->m_Index); return -1; + } - RateConvert(m_aSamples[SampleID]); - return SampleID; + RateConvert(*pSample); + return pSample->m_Index; } -void CSound::UnloadSample(int SampleID) +void CSound::UnloadSample(int SampleId) { - if(SampleID == -1 || SampleID >= NUM_SAMPLES) + if(SampleId == -1 || SampleId >= NUM_SAMPLES) return; - Stop(SampleID); - free(m_aSamples[SampleID].m_pData); - m_aSamples[SampleID].m_pData = nullptr; + Stop(SampleId); + + // Free data + CSample &Sample = m_aSamples[SampleId]; + free(Sample.m_pData); + Sample.m_pData = nullptr; + + // Free slot + if(Sample.m_NextFreeSampleIndex == SAMPLE_INDEX_USED) + { + Sample.m_NextFreeSampleIndex = m_FirstFreeSampleIndex; + m_FirstFreeSampleIndex = Sample.m_Index; + } } -float CSound::GetSampleTotalTime(int SampleID) +float CSound::GetSampleTotalTime(int SampleId) { - if(SampleID == -1 || SampleID >= NUM_SAMPLES) + if(SampleId == -1 || SampleId >= NUM_SAMPLES) return 0.0f; - return (m_aSamples[SampleID].m_NumFrames / (float)m_aSamples[SampleID].m_Rate); + return m_aSamples[SampleId].TotalTime(); } -float CSound::GetSampleCurrentTime(int SampleID) +float CSound::GetSampleCurrentTime(int SampleId) { - if(SampleID == -1 || SampleID >= NUM_SAMPLES) + if(SampleId == -1 || SampleId >= NUM_SAMPLES) return 0.0f; - CSample *pSample = &m_aSamples[SampleID]; - if(IsPlaying(SampleID)) + const CLockScope LockScope(m_SoundLock); + CSample *pSample = &m_aSamples[SampleId]; + for(auto &Voice : m_aVoices) { - for(auto &Voice : m_aVoices) + if(Voice.m_pSample == pSample) { - if(Voice.m_pSample == pSample) - { - return (Voice.m_Tick / (float)pSample->m_Rate); - } + return Voice.m_Tick / (float)pSample->m_Rate; } } - return (pSample->m_PausedAt / (float)pSample->m_Rate); + return pSample->m_PausedAt / (float)pSample->m_Rate; } -void CSound::SetSampleCurrentTime(int SampleID, float Time) +void CSound::SetSampleCurrentTime(int SampleId, float Time) { - if(SampleID == -1 || SampleID >= NUM_SAMPLES) + if(SampleId == -1 || SampleId >= NUM_SAMPLES) return; - CSample *pSample = &m_aSamples[SampleID]; - if(IsPlaying(SampleID)) + const CLockScope LockScope(m_SoundLock); + CSample *pSample = &m_aSamples[SampleId]; + for(auto &Voice : m_aVoices) { - for(auto &Voice : m_aVoices) + if(Voice.m_pSample == pSample) { - if(Voice.m_pSample == pSample) - { - Voice.m_Tick = pSample->m_NumFrames * Time; - } + Voice.m_Tick = pSample->m_NumFrames * Time; + return; } } - else - { - pSample->m_PausedAt = pSample->m_NumFrames * Time; - } + + pSample->m_PausedAt = pSample->m_NumFrames * Time; } -void CSound::SetChannel(int ChannelID, float Vol, float Pan) +void CSound::SetChannel(int ChannelId, float Vol, float Pan) { - m_aChannels[ChannelID].m_Vol = (int)(Vol * 255.0f); - m_aChannels[ChannelID].m_Pan = (int)(Pan * 255.0f); // TODO: this is only on and off right now + m_aChannels[ChannelId].m_Vol = (int)(Vol * 255.0f); + m_aChannels[ChannelId].m_Pan = (int)(Pan * 255.0f); // TODO: this is only on and off right now } -void CSound::SetListenerPos(float x, float y) +void CSound::SetListenerPosition(vec2 Position) { - m_CenterX.store((int)x, std::memory_order_relaxed); - m_CenterY.store((int)y, std::memory_order_relaxed); + m_ListenerPositionX.store(Position.x, std::memory_order_relaxed); + m_ListenerPositionY.store(Position.y, std::memory_order_relaxed); } void CSound::SetVoiceVolume(CVoiceHandle Voice, float Volume) @@ -685,14 +716,14 @@ void CSound::SetVoiceVolume(CVoiceHandle Voice, float Volume) if(!Voice.IsValid()) return; - int VoiceID = Voice.Id(); + int VoiceId = Voice.Id(); const CLockScope LockScope(m_SoundLock); - if(m_aVoices[VoiceID].m_Age != Voice.Age()) + if(m_aVoices[VoiceId].m_Age != Voice.Age()) return; Volume = clamp(Volume, 0.0f, 1.0f); - m_aVoices[VoiceID].m_Vol = (int)(Volume * 255.0f); + m_aVoices[VoiceId].m_Vol = (int)(Volume * 255.0f); } void CSound::SetVoiceFalloff(CVoiceHandle Voice, float Falloff) @@ -700,29 +731,28 @@ void CSound::SetVoiceFalloff(CVoiceHandle Voice, float Falloff) if(!Voice.IsValid()) return; - int VoiceID = Voice.Id(); + int VoiceId = Voice.Id(); const CLockScope LockScope(m_SoundLock); - if(m_aVoices[VoiceID].m_Age != Voice.Age()) + if(m_aVoices[VoiceId].m_Age != Voice.Age()) return; Falloff = clamp(Falloff, 0.0f, 1.0f); - m_aVoices[VoiceID].m_Falloff = Falloff; + m_aVoices[VoiceId].m_Falloff = Falloff; } -void CSound::SetVoiceLocation(CVoiceHandle Voice, float x, float y) +void CSound::SetVoicePosition(CVoiceHandle Voice, vec2 Position) { if(!Voice.IsValid()) return; - int VoiceID = Voice.Id(); + int VoiceId = Voice.Id(); const CLockScope LockScope(m_SoundLock); - if(m_aVoices[VoiceID].m_Age != Voice.Age()) + if(m_aVoices[VoiceId].m_Age != Voice.Age()) return; - m_aVoices[VoiceID].m_X = x; - m_aVoices[VoiceID].m_Y = y; + m_aVoices[VoiceId].m_Position = Position; } void CSound::SetVoiceTimeOffset(CVoiceHandle Voice, float TimeOffset) @@ -730,31 +760,31 @@ void CSound::SetVoiceTimeOffset(CVoiceHandle Voice, float TimeOffset) if(!Voice.IsValid()) return; - int VoiceID = Voice.Id(); + int VoiceId = Voice.Id(); const CLockScope LockScope(m_SoundLock); - if(m_aVoices[VoiceID].m_Age != Voice.Age()) + if(m_aVoices[VoiceId].m_Age != Voice.Age()) return; - if(!m_aVoices[VoiceID].m_pSample) + if(!m_aVoices[VoiceId].m_pSample) return; int Tick = 0; - bool IsLooping = m_aVoices[VoiceID].m_Flags & ISound::FLAG_LOOP; - uint64_t TickOffset = m_aVoices[VoiceID].m_pSample->m_Rate * TimeOffset; - if(m_aVoices[VoiceID].m_pSample->m_NumFrames > 0 && IsLooping) - Tick = TickOffset % m_aVoices[VoiceID].m_pSample->m_NumFrames; + bool IsLooping = m_aVoices[VoiceId].m_Flags & ISound::FLAG_LOOP; + uint64_t TickOffset = m_aVoices[VoiceId].m_pSample->m_Rate * TimeOffset; + if(m_aVoices[VoiceId].m_pSample->m_NumFrames > 0 && IsLooping) + Tick = TickOffset % m_aVoices[VoiceId].m_pSample->m_NumFrames; else - Tick = clamp(TickOffset, (uint64_t)0, (uint64_t)m_aVoices[VoiceID].m_pSample->m_NumFrames); + Tick = clamp(TickOffset, (uint64_t)0, (uint64_t)m_aVoices[VoiceId].m_pSample->m_NumFrames); // at least 200msec off, else depend on buffer size - float Threshold = maximum(0.2f * m_aVoices[VoiceID].m_pSample->m_Rate, (float)m_MaxFrames); - if(absolute(m_aVoices[VoiceID].m_Tick - Tick) > Threshold) + float Threshold = maximum(0.2f * m_aVoices[VoiceId].m_pSample->m_Rate, (float)m_MaxFrames); + if(absolute(m_aVoices[VoiceId].m_Tick - Tick) > Threshold) { // take care of looping (modulo!) - if(!(IsLooping && (minimum(m_aVoices[VoiceID].m_Tick, Tick) + m_aVoices[VoiceID].m_pSample->m_NumFrames - maximum(m_aVoices[VoiceID].m_Tick, Tick)) <= Threshold)) + if(!(IsLooping && (minimum(m_aVoices[VoiceId].m_Tick, Tick) + m_aVoices[VoiceId].m_pSample->m_NumFrames - maximum(m_aVoices[VoiceId].m_Tick, Tick)) <= Threshold)) { - m_aVoices[VoiceID].m_Tick = Tick; + m_aVoices[VoiceId].m_Tick = Tick; } } } @@ -764,14 +794,14 @@ void CSound::SetVoiceCircle(CVoiceHandle Voice, float Radius) if(!Voice.IsValid()) return; - int VoiceID = Voice.Id(); + int VoiceId = Voice.Id(); const CLockScope LockScope(m_SoundLock); - if(m_aVoices[VoiceID].m_Age != Voice.Age()) + if(m_aVoices[VoiceId].m_Age != Voice.Age()) return; - m_aVoices[VoiceID].m_Shape = ISound::SHAPE_CIRCLE; - m_aVoices[VoiceID].m_Circle.m_Radius = maximum(0.0f, Radius); + m_aVoices[VoiceId].m_Shape = ISound::SHAPE_CIRCLE; + m_aVoices[VoiceId].m_Circle.m_Radius = maximum(0.0f, Radius); } void CSound::SetVoiceRectangle(CVoiceHandle Voice, float Width, float Height) @@ -779,81 +809,80 @@ void CSound::SetVoiceRectangle(CVoiceHandle Voice, float Width, float Height) if(!Voice.IsValid()) return; - int VoiceID = Voice.Id(); + int VoiceId = Voice.Id(); const CLockScope LockScope(m_SoundLock); - if(m_aVoices[VoiceID].m_Age != Voice.Age()) + if(m_aVoices[VoiceId].m_Age != Voice.Age()) return; - m_aVoices[VoiceID].m_Shape = ISound::SHAPE_RECTANGLE; - m_aVoices[VoiceID].m_Rectangle.m_Width = maximum(0.0f, Width); - m_aVoices[VoiceID].m_Rectangle.m_Height = maximum(0.0f, Height); + m_aVoices[VoiceId].m_Shape = ISound::SHAPE_RECTANGLE; + m_aVoices[VoiceId].m_Rectangle.m_Width = maximum(0.0f, Width); + m_aVoices[VoiceId].m_Rectangle.m_Height = maximum(0.0f, Height); } -ISound::CVoiceHandle CSound::Play(int ChannelID, int SampleID, int Flags, float x, float y) +ISound::CVoiceHandle CSound::Play(int ChannelId, int SampleId, int Flags, float Volume, vec2 Position) { const CLockScope LockScope(m_SoundLock); // search for voice - int VoiceID = -1; + int VoiceId = -1; for(int i = 0; i < NUM_VOICES; i++) { - int NextID = (m_NextVoice + i) % NUM_VOICES; - if(!m_aVoices[NextID].m_pSample) + int NextId = (m_NextVoice + i) % NUM_VOICES; + if(!m_aVoices[NextId].m_pSample) { - VoiceID = NextID; - m_NextVoice = NextID + 1; + VoiceId = NextId; + m_NextVoice = NextId + 1; break; } } // voice found, use it int Age = -1; - if(VoiceID != -1) + if(VoiceId != -1) { - m_aVoices[VoiceID].m_pSample = &m_aSamples[SampleID]; - m_aVoices[VoiceID].m_pChannel = &m_aChannels[ChannelID]; + m_aVoices[VoiceId].m_pSample = &m_aSamples[SampleId]; + m_aVoices[VoiceId].m_pChannel = &m_aChannels[ChannelId]; if(Flags & FLAG_LOOP) { - m_aVoices[VoiceID].m_Tick = m_aSamples[SampleID].m_PausedAt; + m_aVoices[VoiceId].m_Tick = m_aSamples[SampleId].m_PausedAt; } else if(Flags & FLAG_PREVIEW) { - m_aVoices[VoiceID].m_Tick = m_aSamples[SampleID].m_PausedAt; - m_aSamples[SampleID].m_PausedAt = 0; + m_aVoices[VoiceId].m_Tick = m_aSamples[SampleId].m_PausedAt; + m_aSamples[SampleId].m_PausedAt = 0; } else { - m_aVoices[VoiceID].m_Tick = 0; + m_aVoices[VoiceId].m_Tick = 0; } - m_aVoices[VoiceID].m_Vol = 255; - m_aVoices[VoiceID].m_Flags = Flags; - m_aVoices[VoiceID].m_X = (int)x; - m_aVoices[VoiceID].m_Y = (int)y; - m_aVoices[VoiceID].m_Falloff = 0.0f; - m_aVoices[VoiceID].m_Shape = ISound::SHAPE_CIRCLE; - m_aVoices[VoiceID].m_Circle.m_Radius = 1500; - Age = m_aVoices[VoiceID].m_Age; + m_aVoices[VoiceId].m_Vol = (int)(clamp(Volume, 0.0f, 1.0f) * 255.0f); + m_aVoices[VoiceId].m_Flags = Flags; + m_aVoices[VoiceId].m_Position = Position; + m_aVoices[VoiceId].m_Falloff = 0.0f; + m_aVoices[VoiceId].m_Shape = ISound::SHAPE_CIRCLE; + m_aVoices[VoiceId].m_Circle.m_Radius = 1500; + Age = m_aVoices[VoiceId].m_Age; } - return CreateVoiceHandle(VoiceID, Age); + return CreateVoiceHandle(VoiceId, Age); } -ISound::CVoiceHandle CSound::PlayAt(int ChannelID, int SampleID, int Flags, float x, float y) +ISound::CVoiceHandle CSound::PlayAt(int ChannelId, int SampleId, int Flags, float Volume, vec2 Position) { - return Play(ChannelID, SampleID, Flags | ISound::FLAG_POS, x, y); + return Play(ChannelId, SampleId, Flags | ISound::FLAG_POS, Volume, Position); } -ISound::CVoiceHandle CSound::Play(int ChannelID, int SampleID, int Flags) +ISound::CVoiceHandle CSound::Play(int ChannelId, int SampleId, int Flags, float Volume) { - return Play(ChannelID, SampleID, Flags, 0, 0); + return Play(ChannelId, SampleId, Flags, Volume, vec2(0.0f, 0.0f)); } -void CSound::Pause(int SampleID) +void CSound::Pause(int SampleId) { // TODO: a nice fade out const CLockScope LockScope(m_SoundLock); - CSample *pSample = &m_aSamples[SampleID]; + CSample *pSample = &m_aSamples[SampleId]; for(auto &Voice : m_aVoices) { if(Voice.m_pSample == pSample) @@ -864,11 +893,11 @@ void CSound::Pause(int SampleID) } } -void CSound::Stop(int SampleID) +void CSound::Stop(int SampleId) { // TODO: a nice fade out const CLockScope LockScope(m_SoundLock); - CSample *pSample = &m_aSamples[SampleID]; + CSample *pSample = &m_aSamples[SampleId]; for(auto &Voice : m_aVoices) { if(Voice.m_pSample == pSample) @@ -904,20 +933,20 @@ void CSound::StopVoice(CVoiceHandle Voice) if(!Voice.IsValid()) return; - int VoiceID = Voice.Id(); + int VoiceId = Voice.Id(); const CLockScope LockScope(m_SoundLock); - if(m_aVoices[VoiceID].m_Age != Voice.Age()) + if(m_aVoices[VoiceId].m_Age != Voice.Age()) return; - m_aVoices[VoiceID].m_pSample = nullptr; - m_aVoices[VoiceID].m_Age++; + m_aVoices[VoiceId].m_pSample = nullptr; + m_aVoices[VoiceId].m_Age++; } -bool CSound::IsPlaying(int SampleID) +bool CSound::IsPlaying(int SampleId) { const CLockScope LockScope(m_SoundLock); - const CSample *pSample = &m_aSamples[SampleID]; + const CSample *pSample = &m_aSamples[SampleId]; return std::any_of(std::begin(m_aVoices), std::end(m_aVoices), [pSample](const auto &Voice) { return Voice.m_pSample == pSample; }); } diff --git a/src/engine/client/sound.h b/src/engine/client/sound.h index ea5eb5766f..f7cc4b82cc 100644 --- a/src/engine/client/sound.h +++ b/src/engine/client/sound.h @@ -13,6 +13,9 @@ struct CSample { + int m_Index; + int m_NextFreeSampleIndex; + short *m_pData; int m_NumFrames; int m_Rate; @@ -20,6 +23,11 @@ struct CSample int m_LoopStart; int m_LoopEnd; int m_PausedAt; + + float TotalTime() const + { + return m_NumFrames / (float)m_Rate; + } }; struct CChannel @@ -36,7 +44,7 @@ struct CVoice int m_Tick; int m_Vol; // 0 - 255 int m_Flags; - int m_X, m_Y; + vec2 m_Position; float m_Falloff; // [0.0, 1.0] int m_Shape; @@ -61,13 +69,17 @@ class CSound : public IEngineSound CLock m_SoundLock; CSample m_aSamples[NUM_SAMPLES] = {{0}}; + int m_FirstFreeSampleIndex = 0; + CVoice m_aVoices[NUM_VOICES] = {{0}}; CChannel m_aChannels[NUM_CHANNELS] = {{255, 0}}; int m_NextVoice = 0; uint32_t m_MaxFrames = 0; - std::atomic m_CenterX = 0; - std::atomic m_CenterY = 0; + // This is not an std::atomic as this would require linking with + // libatomic with clang x86 as there is no native support for this. + std::atomic m_ListenerPositionX = 0.0f; + std::atomic m_ListenerPositionY = 0.0f; std::atomic m_SoundVolume = 100; int m_MixingRate = 48000; @@ -76,7 +88,7 @@ class CSound : public IEngineSound int *m_pMixBuffer = nullptr; - int AllocID(); + CSample *AllocSample(); void RateConvert(CSample &Sample) const; bool DecodeOpus(CSample &Sample, const void *pData, unsigned DataSize) const; @@ -91,37 +103,39 @@ class CSound : public IEngineSound bool IsSoundEnabled() override { return m_SoundEnabled; } - int LoadOpus(const char *pFilename, int StorageType = IStorage::TYPE_ALL) override; - int LoadWV(const char *pFilename, int StorageType = IStorage::TYPE_ALL) override; - int LoadOpusFromMem(const void *pData, unsigned DataSize, bool FromEditor) override; - int LoadWVFromMem(const void *pData, unsigned DataSize, bool FromEditor) override; - void UnloadSample(int SampleID) override REQUIRES(!m_SoundLock); + int LoadOpus(const char *pFilename, int StorageType = IStorage::TYPE_ALL) override REQUIRES(!m_SoundLock); + int LoadWV(const char *pFilename, int StorageType = IStorage::TYPE_ALL) override REQUIRES(!m_SoundLock); + int LoadOpusFromMem(const void *pData, unsigned DataSize, bool FromEditor) override REQUIRES(!m_SoundLock); + int LoadWVFromMem(const void *pData, unsigned DataSize, bool FromEditor) override REQUIRES(!m_SoundLock); + void UnloadSample(int SampleId) override REQUIRES(!m_SoundLock); - float GetSampleTotalTime(int SampleID) override; // in s - float GetSampleCurrentTime(int SampleID) override REQUIRES(!m_SoundLock); // in s - void SetSampleCurrentTime(int SampleID, float Time) override REQUIRES(!m_SoundLock); + float GetSampleTotalTime(int SampleId) override; // in s + float GetSampleCurrentTime(int SampleId) override REQUIRES(!m_SoundLock); // in s + void SetSampleCurrentTime(int SampleId, float Time) override REQUIRES(!m_SoundLock); - void SetChannel(int ChannelID, float Vol, float Pan) override; - void SetListenerPos(float x, float y) override; + void SetChannel(int ChannelId, float Vol, float Pan) override; + void SetListenerPosition(vec2 Position) override; void SetVoiceVolume(CVoiceHandle Voice, float Volume) override REQUIRES(!m_SoundLock); void SetVoiceFalloff(CVoiceHandle Voice, float Falloff) override REQUIRES(!m_SoundLock); - void SetVoiceLocation(CVoiceHandle Voice, float x, float y) override REQUIRES(!m_SoundLock); + void SetVoicePosition(CVoiceHandle Voice, vec2 Position) override REQUIRES(!m_SoundLock); void SetVoiceTimeOffset(CVoiceHandle Voice, float TimeOffset) override REQUIRES(!m_SoundLock); // in s void SetVoiceCircle(CVoiceHandle Voice, float Radius) override REQUIRES(!m_SoundLock); void SetVoiceRectangle(CVoiceHandle Voice, float Width, float Height) override REQUIRES(!m_SoundLock); - CVoiceHandle Play(int ChannelID, int SampleID, int Flags, float x, float y) REQUIRES(!m_SoundLock); - CVoiceHandle PlayAt(int ChannelID, int SampleID, int Flags, float x, float y) override REQUIRES(!m_SoundLock); - CVoiceHandle Play(int ChannelID, int SampleID, int Flags) override REQUIRES(!m_SoundLock); - void Pause(int SampleID) override REQUIRES(!m_SoundLock); - void Stop(int SampleID) override REQUIRES(!m_SoundLock); + CVoiceHandle Play(int ChannelId, int SampleId, int Flags, float Volume, vec2 Position) REQUIRES(!m_SoundLock); + CVoiceHandle PlayAt(int ChannelId, int SampleId, int Flags, float Volume, vec2 Position) override REQUIRES(!m_SoundLock); + CVoiceHandle Play(int ChannelId, int SampleId, int Flags, float Volume) override REQUIRES(!m_SoundLock); + void Pause(int SampleId) override REQUIRES(!m_SoundLock); + void Stop(int SampleId) override REQUIRES(!m_SoundLock); void StopAll() override REQUIRES(!m_SoundLock); void StopVoice(CVoiceHandle Voice) override REQUIRES(!m_SoundLock); - bool IsPlaying(int SampleID) override REQUIRES(!m_SoundLock); + bool IsPlaying(int SampleId) override REQUIRES(!m_SoundLock); + int MixingRate() const override { return m_MixingRate; } void Mix(short *pFinalOut, unsigned Frames) override REQUIRES(!m_SoundLock); + void PauseAudioDevice() override; void UnpauseAudioDevice() override; }; diff --git a/src/engine/client/text.cpp b/src/engine/client/text.cpp index 91457099dd..1337d32932 100644 --- a/src/engine/client/text.cpp +++ b/src/engine/client/text.cpp @@ -391,8 +391,8 @@ class CGlyphMap void UploadTextures() { const size_t NewTextureSize = m_TextureDimension * m_TextureDimension; - void *pTmpTextFillData = malloc(NewTextureSize); - void *pTmpTextOutlineData = malloc(NewTextureSize); + uint8_t *pTmpTextFillData = static_cast(malloc(NewTextureSize)); + uint8_t *pTmpTextOutlineData = static_cast(malloc(NewTextureSize)); mem_copy(pTmpTextFillData, m_apTextureData[FONT_TEXTURE_FILL], NewTextureSize); mem_copy(pTmpTextOutlineData, m_apTextureData[FONT_TEXTURE_OUTLINE], NewTextureSize); Graphics()->LoadTextTextures(m_TextureDimension, m_TextureDimension, m_aTextures[FONT_TEXTURE_FILL], m_aTextures[FONT_TEXTURE_OUTLINE], pTmpTextFillData, pTmpTextOutlineData); @@ -465,8 +465,8 @@ class CGlyphMap if(GetX >= 0 && GetY >= 0 && GetX < w && GetY < h) { int Index = GetY * w + GetX; - if(pIn[Index] > c) - c = pIn[Index]; + float Mask = 1.f - clamp(length(vec2(sx, sy)) - OutlineCount, 0.f, 1.f); + c = maximum(c, int(pIn[Index] * Mask)); } } } @@ -729,11 +729,12 @@ class CGlyphMap return vec2(0.0f, 0.0f); } - void UploadEntityLayerText(void *pTexBuff, size_t PixelSize, size_t TexWidth, size_t TexHeight, int TexSubWidth, int TexSubHeight, const char *pText, int Length, float x, float y, int FontSize) + void UploadEntityLayerText(const CImageInfo &TextImage, int TexSubWidth, int TexSubHeight, const char *pText, int Length, float x, float y, int FontSize) { if(FontSize < 1) return; + const size_t PixelSize = TextImage.PixelSize(); const char *pCurrent = pText; const char *pEnd = pCurrent + Length; int WidthLastChars = 0; @@ -770,24 +771,23 @@ class CGlyphMap else mem_zero(m_aaGlyphData[FONT_TEXTURE_FILL], GlyphDataSize); - uint8_t *pImageBuff = (uint8_t *)pTexBuff; for(unsigned OffY = 0; OffY < pBitmap->rows; ++OffY) { for(unsigned OffX = 0; OffX < pBitmap->width; ++OffX) { const int ImgOffX = clamp(x + OffX + WidthLastChars, x, (x + TexSubWidth) - 1); const int ImgOffY = clamp(y + OffY, y, (y + TexSubHeight) - 1); - const size_t ImageOffset = ImgOffY * (TexWidth * PixelSize) + ImgOffX * PixelSize; + const size_t ImageOffset = ImgOffY * (TextImage.m_Width * PixelSize) + ImgOffX * PixelSize; const size_t GlyphOffset = OffY * pBitmap->width + OffX; for(size_t i = 0; i < PixelSize; ++i) { if(i != PixelSize - 1) { - *(pImageBuff + ImageOffset + i) = 255; + *(TextImage.m_pData + ImageOffset + i) = 255; } else { - *(pImageBuff + ImageOffset + i) = *(m_aaGlyphData[FONT_TEXTURE_FILL] + GlyphOffset); + *(TextImage.m_pData + ImageOffset + i) = *(m_aaGlyphData[FONT_TEXTURE_FILL] + GlyphOffset); } } } @@ -1268,7 +1268,9 @@ class CTextRender : public IEngineTextRender pCursor->m_GlyphCount = 0; pCursor->m_CharCount = 0; pCursor->m_MaxLines = 0; + pCursor->m_LineSpacing = 0; + pCursor->m_AlignedLineSpacing = 0; pCursor->m_StartX = x; pCursor->m_StartY = y; @@ -1307,6 +1309,7 @@ class CTextRender : public IEngineTextRender pCursor->m_X = x; pCursor->m_Y = y; } + void Text(float x, float y, float Size, const char *pText, float LineWidth = -1.0f) override { CTextCursor Cursor; @@ -1314,6 +1317,7 @@ class CTextRender : public IEngineTextRender Cursor.m_LineWidth = LineWidth; TextEx(&Cursor, pText, -1); } + float TextWidth(float Size, const char *pText, int StrLength = -1, float LineWidth = -1.0f, int Flags = 0, const STextSizeProperties &TextSizeProps = {}) override { CTextCursor Cursor; @@ -1349,9 +1353,9 @@ class CTextRender : public IEngineTextRender m_Color.a = a; } - void TextColor(ColorRGBA rgb) override + void TextColor(ColorRGBA Color) override { - m_Color = rgb; + m_Color = Color; } void TextOutlineColor(float r, float g, float b, float a) override @@ -1362,9 +1366,9 @@ class CTextRender : public IEngineTextRender m_OutlineColor.a = a; } - void TextOutlineColor(ColorRGBA rgb) override + void TextOutlineColor(ColorRGBA Color) override { - m_OutlineColor = rgb; + m_OutlineColor = Color; } void TextSelectionColor(float r, float g, float b, float a) override @@ -1375,9 +1379,9 @@ class CTextRender : public IEngineTextRender m_SelectionColor.a = a; } - void TextSelectionColor(ColorRGBA rgb) override + void TextSelectionColor(ColorRGBA Color) override { - m_SelectionColor = rgb; + m_SelectionColor = Color; } ColorRGBA GetTextColor() const override @@ -1479,7 +1483,7 @@ class CTextRender : public IEngineTextRender const float CursorY = round_to_int(pCursor->m_Y * FakeToScreen.y) / FakeToScreen.y; const int ActualSize = round_truncate(pCursor->m_FontSize * FakeToScreen.y); pCursor->m_AlignedFontSize = ActualSize / FakeToScreen.y; - const float LineSpacing = pCursor->m_LineSpacing; + pCursor->m_AlignedLineSpacing = round_truncate(pCursor->m_LineSpacing * FakeToScreen.y) / FakeToScreen.y; // string length if(Length < 0) @@ -1528,6 +1532,7 @@ class CTextRender : public IEngineTextRender const float CursorOuterInnerDiff = (CursorOuterWidth - CursorInnerWidth) / 2; std::vector vSelectionQuads; + int SelectionQuadLine = -1; bool SelectionStarted = false; bool SelectionUsedPress = false; bool SelectionUsedRelease = false; @@ -1537,38 +1542,34 @@ class CTextRender : public IEngineTextRender const auto &&CheckInsideChar = [&](bool CheckOuter, vec2 CursorPos, float LastCharX, float LastCharWidth, float CharX, float CharWidth, float CharY) -> bool { return (LastCharX - LastCharWidth / 2 <= CursorPos.x && CharX + CharWidth / 2 > CursorPos.x && - CharY - pCursor->m_AlignedFontSize - LineSpacing <= CursorPos.y && - CharY + LineSpacing > CursorPos.y) || + CursorPos.y >= CharY - pCursor->m_AlignedFontSize && + CursorPos.y < CharY + pCursor->m_AlignedLineSpacing) || (CheckOuter && - CharY - pCursor->m_AlignedFontSize + LineSpacing > CursorPos.y); + CursorPos.y <= CharY - pCursor->m_AlignedFontSize); }; const auto &&CheckSelectionStart = [&](bool CheckOuter, vec2 CursorPos, int &SelectionChar, bool &SelectionUsedCase, float LastCharX, float LastCharWidth, float CharX, float CharWidth, float CharY) { - if(!SelectionStarted && !SelectionUsedCase) + if(!SelectionStarted && !SelectionUsedCase && + CheckInsideChar(CheckOuter, CursorPos, LastCharX, LastCharWidth, CharX, CharWidth, CharY)) { - if(CheckInsideChar(CheckOuter, CursorPos, LastCharX, LastCharWidth, CharX, CharWidth, CharY)) - { - SelectionChar = pCursor->m_GlyphCount; - SelectionStarted = !SelectionStarted; - SelectionUsedCase = true; - } + SelectionChar = pCursor->m_GlyphCount; + SelectionStarted = !SelectionStarted; + SelectionUsedCase = true; } }; const auto &&CheckOutsideChar = [&](bool CheckOuter, vec2 CursorPos, float CharX, float CharWidth, float CharY) -> bool { return (CharX + CharWidth / 2 > CursorPos.x && - CharY - pCursor->m_AlignedFontSize - LineSpacing <= CursorPos.y && - CharY + LineSpacing > CursorPos.y) || + CursorPos.y >= CharY - pCursor->m_AlignedFontSize && + CursorPos.y < CharY + pCursor->m_AlignedLineSpacing) || (CheckOuter && - CharY - LineSpacing <= CursorPos.y); + CursorPos.y >= CharY + pCursor->m_AlignedLineSpacing); }; const auto &&CheckSelectionEnd = [&](bool CheckOuter, vec2 CursorPos, int &SelectionChar, bool &SelectionUsedCase, float CharX, float CharWidth, float CharY) { - if(SelectionStarted && !SelectionUsedCase) + if(SelectionStarted && !SelectionUsedCase && + CheckOutsideChar(CheckOuter, CursorPos, CharX, CharWidth, CharY)) { - if(CheckOutsideChar(CheckOuter, CursorPos, CharX, CharWidth, CharY)) - { - SelectionChar = pCursor->m_GlyphCount; - SelectionStarted = !SelectionStarted; - SelectionUsedCase = true; - } + SelectionChar = pCursor->m_GlyphCount; + SelectionStarted = !SelectionStarted; + SelectionUsedCase = true; } }; @@ -1583,7 +1584,7 @@ class CTextRender : public IEngineTextRender return false; DrawX = pCursor->m_StartX; - DrawY += pCursor->m_AlignedFontSize + pCursor->m_LineSpacing; + DrawY += pCursor->m_AlignedFontSize + pCursor->m_AlignedLineSpacing; if((RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT) == 0) { DrawX = round_to_int(DrawX * FakeToScreen.x) / FakeToScreen.x; // realign @@ -1611,7 +1612,6 @@ class CTextRender : public IEngineTextRender bool HasCursor = false; const SGlyph *pLastGlyph = nullptr; - bool GotNewLine = false; bool GotNewLineLast = false; int ColorOption = 0; @@ -1753,10 +1753,18 @@ class CTextRender : public IEngineTextRender if(ColorOption < (int)pCursor->m_vColorSplits.size()) { STextColorSplit &Split = pCursor->m_vColorSplits.at(ColorOption); - if(PrevCharCount >= Split.m_CharIndex && PrevCharCount < Split.m_CharIndex + Split.m_Length) + if(PrevCharCount >= Split.m_CharIndex && (Split.m_Length == -1 || PrevCharCount < Split.m_CharIndex + Split.m_Length)) Color = Split.m_Color; - if(PrevCharCount >= (Split.m_CharIndex + Split.m_Length - 1)) + if(Split.m_Length != -1 && PrevCharCount >= (Split.m_CharIndex + Split.m_Length - 1)) + { ColorOption++; + if(ColorOption < (int)pCursor->m_vColorSplits.size()) + { // Handle splits that are + Split = pCursor->m_vColorSplits.at(ColorOption); + if(PrevCharCount >= Split.m_CharIndex) + Color = Split.m_Color; + } + } } // don't add text that isn't drawn, the color overwrite is used for that @@ -1830,13 +1838,13 @@ class CTextRender : public IEngineTextRender } if(pCursor->m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_SET) { - if((int)pCursor->m_GlyphCount == pCursor->m_SelectionStart) + if(pCursor->m_GlyphCount == pCursor->m_SelectionStart) { SelectionStarted = !SelectionStarted; SelectionStartChar = pCursor->m_GlyphCount; SelectionUsedPress = true; } - if((int)pCursor->m_GlyphCount == pCursor->m_SelectionEnd) + if(pCursor->m_GlyphCount == pCursor->m_SelectionEnd) { SelectionStarted = !SelectionStarted; SelectionEndChar = pCursor->m_GlyphCount; @@ -1846,7 +1854,7 @@ class CTextRender : public IEngineTextRender if(pCursor->m_CursorMode != TEXT_CURSOR_CURSOR_MODE_NONE) { - if((int)pCursor->m_GlyphCount == pCursor->m_CursorCharacter) + if(pCursor->m_GlyphCount == pCursor->m_CursorCharacter) { HasCursor = true; aCursorQuads[0] = IGraphics::CQuadItem(SelX - CursorOuterInnerDiff, DrawY, CursorOuterWidth, pCursor->m_AlignedFontSize); @@ -1866,7 +1874,18 @@ class CTextRender : public IEngineTextRender if(SelectionStarted && IsRendered) { - vSelectionQuads.emplace_back(SelX, DrawY + (1.0f - pCursor->m_SelectionHeightFactor) * pCursor->m_AlignedFontSize, SelWidth, pCursor->m_SelectionHeightFactor * pCursor->m_AlignedFontSize); + if(!vSelectionQuads.empty() && SelectionQuadLine == LineCount) + { + vSelectionQuads.back().m_Width += SelWidth; + } + else + { + const float SelectionHeight = pCursor->m_AlignedFontSize + pCursor->m_AlignedLineSpacing; + const float SelectionY = DrawY + (1.0f - pCursor->m_SelectionHeightFactor) * SelectionHeight; + const float ScaledSelectionHeight = pCursor->m_SelectionHeightFactor * SelectionHeight; + vSelectionQuads.emplace_back(SelX, SelectionY, SelWidth, ScaledSelectionHeight); + SelectionQuadLine = LineCount; + } } LastSelX = SelX; @@ -1882,7 +1901,6 @@ class CTextRender : public IEngineTextRender { if(!StartNewLine()) break; - GotNewLine = true; GotNewLineLast = true; } else @@ -1918,13 +1936,13 @@ class CTextRender : public IEngineTextRender } else if(pCursor->m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_SET) { - if((int)pCursor->m_GlyphCount == pCursor->m_SelectionStart) + if(pCursor->m_GlyphCount == pCursor->m_SelectionStart) { SelectionStarted = !SelectionStarted; SelectionStartChar = pCursor->m_GlyphCount; SelectionUsedPress = true; } - if((int)pCursor->m_GlyphCount == pCursor->m_SelectionEnd) + if(pCursor->m_GlyphCount == pCursor->m_SelectionEnd) { SelectionStarted = !SelectionStarted; SelectionEndChar = pCursor->m_GlyphCount; @@ -1939,7 +1957,7 @@ class CTextRender : public IEngineTextRender pCursor->m_CursorCharacter = pCursor->m_GlyphCount; } - if((int)pCursor->m_GlyphCount == pCursor->m_CursorCharacter) + if(pCursor->m_GlyphCount == pCursor->m_CursorCharacter) { HasCursor = true; aCursorQuads[0] = IGraphics::CQuadItem((LastSelX + LastSelWidth) - CursorOuterInnerDiff, DrawY, CursorOuterWidth, pCursor->m_AlignedFontSize); @@ -1955,9 +1973,9 @@ class CTextRender : public IEngineTextRender if(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex == -1) TextContainer.m_StringInfo.m_SelectionQuadContainerIndex = Graphics()->CreateQuadContainer(false); if(HasCursor) - Graphics()->QuadContainerAddQuads(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, aCursorQuads, 2); + Graphics()->QuadContainerAddQuads(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, aCursorQuads, std::size(aCursorQuads)); if(HasSelection) - Graphics()->QuadContainerAddQuads(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, vSelectionQuads.data(), (int)vSelectionQuads.size()); + Graphics()->QuadContainerAddQuads(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex, vSelectionQuads.data(), vSelectionQuads.size()); Graphics()->QuadContainerUpload(TextContainer.m_StringInfo.m_SelectionQuadContainerIndex); TextContainer.m_HasCursor = HasCursor; @@ -1978,11 +1996,9 @@ class CTextRender : public IEngineTextRender // even if no text is drawn the cursor position will be adjusted pCursor->m_X = DrawX; + pCursor->m_Y = DrawY; pCursor->m_LineCount = LineCount; - if(GotNewLine) - pCursor->m_Y = DrawY; - TextContainer.m_BoundingBox = pCursor->BoundingBox(); } @@ -2161,9 +2177,9 @@ class CTextRender : public IEngineTextRender return TextContainer.m_BoundingBox; } - void UploadEntityLayerText(void *pTexBuff, size_t PixelSize, size_t TexWidth, size_t TexHeight, int TexSubWidth, int TexSubHeight, const char *pText, int Length, float x, float y, int FontSize) override + void UploadEntityLayerText(const CImageInfo &TextImage, int TexSubWidth, int TexSubHeight, const char *pText, int Length, float x, float y, int FontSize) override { - m_pGlyphMap->UploadEntityLayerText(pTexBuff, PixelSize, TexWidth, TexHeight, TexSubWidth, TexSubHeight, pText, Length, x, y, FontSize); + m_pGlyphMap->UploadEntityLayerText(TextImage, TexSubWidth, TexSubHeight, pText, Length, x, y, FontSize); } int AdjustFontSize(const char *pText, int TextLength, int MaxSize, int MaxWidth) const override diff --git a/src/engine/client/updater.cpp b/src/engine/client/updater.cpp index 3a6b6e389b..7829567858 100644 --- a/src/engine/client/updater.cpp +++ b/src/engine/client/updater.cpp @@ -1,7 +1,6 @@ -#include "updater.h" - #include +#include "updater.h" #include #include #include @@ -13,7 +12,6 @@ #include // system -using std::map; using std::string; class CUpdaterFetchTask : public CHttpRequest @@ -25,7 +23,7 @@ class CUpdaterFetchTask : public CHttpRequest void OnProgress() override; protected: - int OnCompletion(int State) override; + void OnCompletion(EHttpState State) override; public: CUpdaterFetchTask(CUpdater *pUpdater, const char *pFile, const char *pDestPath); @@ -57,14 +55,11 @@ CUpdaterFetchTask::CUpdaterFetchTask(CUpdater *pUpdater, const char *pFile, cons void CUpdaterFetchTask::OnProgress() { CLockScope ls(m_pUpdater->m_Lock); - str_copy(m_pUpdater->m_aStatus, Dest()); m_pUpdater->m_Percent = Progress(); } -int CUpdaterFetchTask::OnCompletion(int State) +void CUpdaterFetchTask::OnCompletion(EHttpState State) { - State = CHttpRequest::OnCompletion(State); - const char *pFileName = 0; for(const char *pPath = Dest(); *pPath; pPath++) if(*pPath == '/') @@ -72,48 +67,44 @@ int CUpdaterFetchTask::OnCompletion(int State) pFileName = pFileName ? pFileName : Dest(); if(!str_comp(pFileName, "update.json")) { - if(State == HTTP_DONE) + if(State == EHttpState::DONE) m_pUpdater->SetCurrentState(IUpdater::GOT_MANIFEST); - else if(State == HTTP_ERROR) + else if(State == EHttpState::ERROR) m_pUpdater->SetCurrentState(IUpdater::FAIL); } - else if(!str_comp(pFileName, m_pUpdater->m_aLastFile)) - { - if(State == HTTP_DONE) - m_pUpdater->SetCurrentState(IUpdater::MOVE_FILES); - else if(State == HTTP_ERROR) - m_pUpdater->SetCurrentState(IUpdater::FAIL); - } - - return State; } CUpdater::CUpdater() { - m_pClient = NULL; - m_pStorage = NULL; - m_pEngine = NULL; + m_pClient = nullptr; + m_pStorage = nullptr; + m_pEngine = nullptr; + m_pHttp = nullptr; m_State = CLEAN; m_Percent = 0; + m_pCurrentTask = nullptr; + + m_ClientUpdate = m_ServerUpdate = m_ClientFetched = m_ServerFetched = false; IStorage::FormatTmpPath(m_aClientExecTmp, sizeof(m_aClientExecTmp), CLIENT_EXEC); IStorage::FormatTmpPath(m_aServerExecTmp, sizeof(m_aServerExecTmp), SERVER_EXEC); } -void CUpdater::Init() +void CUpdater::Init(CHttp *pHttp) { m_pClient = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); m_pEngine = Kernel()->RequestInterface(); + m_pHttp = pHttp; } -void CUpdater::SetCurrentState(int NewState) +void CUpdater::SetCurrentState(EUpdaterState NewState) { CLockScope ls(m_Lock); m_State = NewState; } -int CUpdater::GetCurrentState() +IUpdater::EUpdaterState CUpdater::GetCurrentState() { CLockScope ls(m_Lock); return m_State; @@ -133,7 +124,10 @@ int CUpdater::GetCurrentPercent() void CUpdater::FetchFile(const char *pFile, const char *pDestPath) { - m_pEngine->AddJob(std::make_shared(this, pFile, pDestPath)); + CLockScope ls(m_Lock); + m_pCurrentTask = std::make_shared(this, pFile, pDestPath); + str_copy(m_aStatus, m_pCurrentTask->Dest()); + m_pHttp->Run(m_pCurrentTask); } bool CUpdater::MoveFile(const char *pFile) @@ -170,11 +164,14 @@ bool CUpdater::MoveFile(const char *pFile) void CUpdater::Update() { - switch(m_State) + switch(GetCurrentState()) { case IUpdater::GOT_MANIFEST: PerformUpdate(); break; + case IUpdater::DOWNLOADING: + RunningUpdate(); + break; case IUpdater::MOVE_FILES: CommitUpdate(); break; @@ -185,7 +182,7 @@ void CUpdater::Update() void CUpdater::AddFileJob(const char *pFile, bool Job) { - m_FileJobs[string(pFile)] = Job; + m_FileJobs.emplace_front(pFile, Job); } bool CUpdater::ReplaceClient() @@ -274,37 +271,44 @@ void CUpdater::ParseUpdate() break; } } + json_value_free(pVersions); } void CUpdater::InitiateUpdate() { - m_State = GETTING_MANIFEST; + SetCurrentState(IUpdater::GETTING_MANIFEST); FetchFile("update.json"); } void CUpdater::PerformUpdate() { - m_State = PARSING_UPDATE; + SetCurrentState(IUpdater::PARSING_UPDATE); dbg_msg("updater", "parsing update.json"); ParseUpdate(); - m_State = DOWNLOADING; + m_CurrentJob = m_FileJobs.begin(); + SetCurrentState(IUpdater::DOWNLOADING); +} - const char *pLastFile; - pLastFile = ""; - for(map::reverse_iterator it = m_FileJobs.rbegin(); it != m_FileJobs.rend(); ++it) +void CUpdater::RunningUpdate() +{ + if(m_pCurrentTask) { - if(it->second) + if(!m_pCurrentTask->Done()) + { + return; + } + else if(m_pCurrentTask->State() == EHttpState::ERROR || m_pCurrentTask->State() == EHttpState::ABORTED) { - pLastFile = it->first.c_str(); - break; + SetCurrentState(IUpdater::FAIL); } } - for(auto &FileJob : m_FileJobs) + if(m_CurrentJob != m_FileJobs.end()) { - if(FileJob.second) + auto &Job = *m_CurrentJob; + if(Job.second) { - const char *pFile = FileJob.first.c_str(); + const char *pFile = Job.first.c_str(); size_t len = str_length(pFile); if(!str_comp_nocase(pFile + len - 4, ".dll")) { @@ -332,24 +336,32 @@ void CUpdater::PerformUpdate() { FetchFile(pFile); } - pLastFile = pFile; } else - m_pStorage->RemoveBinaryFile(FileJob.first.c_str()); - } + { + m_pStorage->RemoveBinaryFile(Job.first.c_str()); + } - if(m_ServerUpdate) - { - FetchFile(PLAT_SERVER_DOWN, m_aServerExecTmp); - pLastFile = m_aServerExecTmp; + m_CurrentJob++; } - if(m_ClientUpdate) + else { - FetchFile(PLAT_CLIENT_DOWN, m_aClientExecTmp); - pLastFile = m_aClientExecTmp; - } + if(m_ServerUpdate && !m_ServerFetched) + { + FetchFile(PLAT_SERVER_DOWN, m_aServerExecTmp); + m_ServerFetched = true; + return; + } - str_copy(m_aLastFile, pLastFile); + if(m_ClientUpdate && !m_ClientFetched) + { + FetchFile(PLAT_CLIENT_DOWN, m_aClientExecTmp); + m_ClientFetched = true; + return; + } + + SetCurrentState(IUpdater::MOVE_FILES); + } } void CUpdater::CommitUpdate() @@ -365,9 +377,9 @@ void CUpdater::CommitUpdate() if(m_ServerUpdate) Success &= ReplaceServer(); if(!Success) - m_State = FAIL; + SetCurrentState(IUpdater::FAIL); else if(m_pClient->State() == IClient::STATE_ONLINE || m_pClient->EditorHasUnsavedData()) - m_State = NEED_RESTART; + SetCurrentState(IUpdater::NEED_RESTART); else { m_pClient->Restart(); diff --git a/src/engine/client/updater.h b/src/engine/client/updater.h index 0b5c71c1d5..4127ca5e3c 100644 --- a/src/engine/client/updater.h +++ b/src/engine/client/updater.h @@ -1,11 +1,13 @@ #ifndef ENGINE_CLIENT_UPDATER_H #define ENGINE_CLIENT_UPDATER_H +#include #include #include -#include +#include +#include #include #define CLIENT_EXEC "DDNet" @@ -24,6 +26,9 @@ #define PLAT_NAME CONF_PLATFORM_STRING "-unsupported" #endif #else +#if defined(AUTOUPDATE) +#error Compiling with autoupdater on an unsupported platform +#endif #define PLAT_EXT "" #define PLAT_NAME "unsupported-unsupported" #endif @@ -34,6 +39,8 @@ #define PLAT_CLIENT_EXEC CLIENT_EXEC PLAT_EXT #define PLAT_SERVER_EXEC SERVER_EXEC PLAT_EXT +class CUpdaterFetchTask; + class CUpdater : public IUpdater { friend class CUpdaterFetchTask; @@ -41,44 +48,50 @@ class CUpdater : public IUpdater class IClient *m_pClient; class IStorage *m_pStorage; class IEngine *m_pEngine; + class CHttp *m_pHttp; CLock m_Lock; - int m_State; + EUpdaterState m_State GUARDED_BY(m_Lock); char m_aStatus[256] GUARDED_BY(m_Lock); int m_Percent GUARDED_BY(m_Lock); - char m_aLastFile[256]; char m_aClientExecTmp[64]; char m_aServerExecTmp[64]; + std::forward_list> m_FileJobs; + std::shared_ptr m_pCurrentTask; + decltype(m_FileJobs)::iterator m_CurrentJob; + bool m_ClientUpdate; bool m_ServerUpdate; - std::map m_FileJobs; + bool m_ClientFetched; + bool m_ServerFetched; void AddFileJob(const char *pFile, bool Job); - void FetchFile(const char *pFile, const char *pDestPath = nullptr); + void FetchFile(const char *pFile, const char *pDestPath = nullptr) REQUIRES(!m_Lock); bool MoveFile(const char *pFile); - void ParseUpdate(); - void PerformUpdate(); - void CommitUpdate(); + void ParseUpdate() REQUIRES(!m_Lock); + void PerformUpdate() REQUIRES(!m_Lock); + void RunningUpdate() REQUIRES(!m_Lock); + void CommitUpdate() REQUIRES(!m_Lock); bool ReplaceClient(); bool ReplaceServer(); - void SetCurrentState(int NewState) REQUIRES(!m_Lock); + void SetCurrentState(EUpdaterState NewState) REQUIRES(!m_Lock); public: CUpdater(); - int GetCurrentState() override REQUIRES(!m_Lock); + EUpdaterState GetCurrentState() override REQUIRES(!m_Lock); void GetCurrentFile(char *pBuf, int BufSize) override REQUIRES(!m_Lock); int GetCurrentPercent() override REQUIRES(!m_Lock); - void InitiateUpdate() override; - void Init(); - void Update() override; + void InitiateUpdate() REQUIRES(!m_Lock) override; + void Init(CHttp *pHttp); + void Update() REQUIRES(!m_Lock) override; }; #endif diff --git a/src/engine/client/video.cpp b/src/engine/client/video.cpp index 62eeaf8598..d5b914fce8 100644 --- a/src/engine/client/video.cpp +++ b/src/engine/client/video.cpp @@ -2,7 +2,9 @@ #include "video.h" -#include +#include + +#include #include #include #include @@ -34,17 +36,57 @@ using namespace std::chrono_literals; const size_t FORMAT_GL_NCHANNELS = 4; CLock g_WriteLock; -CVideo::CVideo(CGraphics_Threaded *pGraphics, ISound *pSound, IStorage *pStorage, int Width, int Height, const char *pName) : +static LEVEL AvLevelToLogLevel(int Level) +{ + switch(Level) + { + case AV_LOG_PANIC: + case AV_LOG_FATAL: + case AV_LOG_ERROR: + return LEVEL_ERROR; + case AV_LOG_WARNING: + return LEVEL_WARN; + case AV_LOG_INFO: + return LEVEL_INFO; + case AV_LOG_VERBOSE: + case AV_LOG_DEBUG: + return LEVEL_DEBUG; + case AV_LOG_TRACE: + return LEVEL_TRACE; + default: + dbg_assert(false, "invalid log level"); + dbg_break(); + } +} + +void AvLogCallback(void *pUser, int Level, const char *pFormat, va_list VarArgs) + GNUC_ATTRIBUTE((format(printf, 3, 0))); + +void AvLogCallback(void *pUser, int Level, const char *pFormat, va_list VarArgs) +{ + const LEVEL LogLevel = AvLevelToLogLevel(Level); + if(LogLevel <= LEVEL_INFO) + { + log_log_v(LogLevel, "videorecorder/libav", pFormat, VarArgs); + } +} + +void CVideo::Init() +{ + av_log_set_callback(AvLogCallback); +} + +CVideo::CVideo(IGraphics *pGraphics, ISound *pSound, IStorage *pStorage, int Width, int Height, const char *pName) : m_pGraphics(pGraphics), m_pStorage(pStorage), m_pSound(pSound) { - m_pFormatContext = 0; - m_pFormat = 0; - m_pOptDict = 0; + m_pFormatContext = nullptr; + m_pFormat = nullptr; + m_pOptDict = nullptr; - m_pVideoCodec = 0; - m_pAudioCodec = 0; + m_pVideoCodec = nullptr; + m_pAudioCodec = nullptr; m_Width = Width; m_Height = Height; @@ -54,12 +96,13 @@ CVideo::CVideo(CGraphics_Threaded *pGraphics, ISound *pSound, IStorage *pStorage m_Recording = false; m_Started = false; + m_Stopped = false; m_ProcessingVideoFrame = 0; m_ProcessingAudioFrame = 0; - m_HasAudio = g_Config.m_ClVideoSndEnable; + m_HasAudio = m_pSound->IsSoundEnabled() && g_Config.m_ClVideoSndEnable; - dbg_assert(ms_pCurrentVideo == 0, "ms_pCurrentVideo is NOT set to NULL while creating a new Video."); + dbg_assert(ms_pCurrentVideo == nullptr, "ms_pCurrentVideo is NOT set to nullptr while creating a new Video."); ms_TickTime = time_freq() / m_FPS; ms_pCurrentVideo = this; @@ -67,43 +110,38 @@ CVideo::CVideo(CGraphics_Threaded *pGraphics, ISound *pSound, IStorage *pStorage CVideo::~CVideo() { - ms_pCurrentVideo = 0; + ms_pCurrentVideo = nullptr; } -void CVideo::Start() +bool CVideo::Start() { + dbg_assert(!m_Started, "Already started"); + // wait for the graphic thread to idle m_pGraphics->WaitForIdle(); m_AudioStream = {}; m_VideoStream = {}; - char aDate[20]; - str_timestamp(aDate, sizeof(aDate)); - char aBuf[256]; - if(str_length(m_aName) != 0) - str_format(aBuf, sizeof(aBuf), "videos/%s", m_aName); - else - str_format(aBuf, sizeof(aBuf), "videos/%s.mp4", aDate); - - char aWholePath[1024]; - IOHANDLE File = m_pStorage->OpenFile(aBuf, IOFLAG_WRITE, IStorage::TYPE_SAVE, aWholePath, sizeof(aWholePath)); - + char aWholePath[IO_MAX_PATH_LENGTH]; + IOHANDLE File = m_pStorage->OpenFile(m_aName, IOFLAG_WRITE, IStorage::TYPE_SAVE, aWholePath, sizeof(aWholePath)); if(File) { io_close(File); } else { - dbg_msg("video_recorder", "Failed to open file for recoding video."); - return; + log_error("videorecorder", "Could not open file '%s'", aWholePath); + return false; } - avformat_alloc_output_context2(&m_pFormatContext, 0, "mp4", aWholePath); - if(!m_pFormatContext) + const int FormatAllocResult = avformat_alloc_output_context2(&m_pFormatContext, nullptr, "mp4", aWholePath); + if(FormatAllocResult < 0 || !m_pFormatContext) { - dbg_msg("video_recorder", "Failed to create formatcontext for recoding video."); - return; + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(FormatAllocResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not create format context: %s", aError); + return false; } m_pFormat = m_pFormatContext->oformat; @@ -122,115 +160,118 @@ void CVideo::Start() m_CurAudioThreadIndex = 0; size_t GLNVals = FORMAT_GL_NCHANNELS * m_Width * m_Height; - m_vPixelHelper.resize(m_VideoThreads); + m_vVideoBuffers.resize(m_VideoThreads); for(size_t i = 0; i < m_VideoThreads; ++i) { - m_vPixelHelper[i].resize(GLNVals * sizeof(uint8_t)); + m_vVideoBuffers[i].m_vBuffer.resize(GLNVals * sizeof(uint8_t)); } - m_vBuffer.resize(m_AudioThreads); + m_vAudioBuffers.resize(m_AudioThreads); /* Add the audio and video streams using the default format codecs * and initialize the codecs. */ if(m_pFormat->video_codec != AV_CODEC_ID_NONE) { if(!AddStream(&m_VideoStream, m_pFormatContext, &m_pVideoCodec, m_pFormat->video_codec)) - return; + return false; } else { - dbg_msg("video_recorder", "Failed to add VideoStream for recoding video."); + log_error("videorecorder", "Could not determine default video stream codec"); + return false; } - if(m_HasAudio && m_pFormat->audio_codec != AV_CODEC_ID_NONE) - { - if(!AddStream(&m_AudioStream, m_pFormatContext, &m_pAudioCodec, m_pFormat->audio_codec)) - return; - } - else + if(m_HasAudio) { - dbg_msg("video_recorder", "No audio."); + if(m_pFormat->audio_codec != AV_CODEC_ID_NONE) + { + if(!AddStream(&m_AudioStream, m_pFormatContext, &m_pAudioCodec, m_pFormat->audio_codec)) + return false; + } + else + { + log_error("videorecorder", "Could not determine default audio stream codec"); + return false; + } } - m_vVideoThreads.resize(m_VideoThreads); + m_vpVideoThreads.resize(m_VideoThreads); for(size_t i = 0; i < m_VideoThreads; ++i) { - m_vVideoThreads[i] = std::make_unique(); + m_vpVideoThreads[i] = std::make_unique(); } for(size_t i = 0; i < m_VideoThreads; ++i) { - std::unique_lock Lock(m_vVideoThreads[i]->m_Mutex); - m_vVideoThreads[i]->m_Thread = std::thread([this, i]() REQUIRES(!g_WriteLock) { RunVideoThread(i == 0 ? (m_VideoThreads - 1) : (i - 1), i); }); - m_vVideoThreads[i]->m_Cond.wait(Lock, [this, i]() -> bool { return m_vVideoThreads[i]->m_Started; }); + std::unique_lock Lock(m_vpVideoThreads[i]->m_Mutex); + m_vpVideoThreads[i]->m_Thread = std::thread([this, i]() REQUIRES(!g_WriteLock) { RunVideoThread(i == 0 ? (m_VideoThreads - 1) : (i - 1), i); }); + m_vpVideoThreads[i]->m_Cond.wait(Lock, [this, i]() -> bool { return m_vpVideoThreads[i]->m_Started; }); } - m_vAudioThreads.resize(m_AudioThreads); + m_vpAudioThreads.resize(m_AudioThreads); for(size_t i = 0; i < m_AudioThreads; ++i) { - m_vAudioThreads[i] = std::make_unique(); + m_vpAudioThreads[i] = std::make_unique(); } for(size_t i = 0; i < m_AudioThreads; ++i) { - std::unique_lock Lock(m_vAudioThreads[i]->m_Mutex); - m_vAudioThreads[i]->m_Thread = std::thread([this, i]() REQUIRES(!g_WriteLock) { RunAudioThread(i == 0 ? (m_AudioThreads - 1) : (i - 1), i); }); - m_vAudioThreads[i]->m_Cond.wait(Lock, [this, i]() -> bool { return m_vAudioThreads[i]->m_Started; }); + std::unique_lock Lock(m_vpAudioThreads[i]->m_Mutex); + m_vpAudioThreads[i]->m_Thread = std::thread([this, i]() REQUIRES(!g_WriteLock) { RunAudioThread(i == 0 ? (m_AudioThreads - 1) : (i - 1), i); }); + m_vpAudioThreads[i]->m_Cond.wait(Lock, [this, i]() -> bool { return m_vpAudioThreads[i]->m_Started; }); } /* Now that all the parameters are set, we can open the audio and * video codecs and allocate the necessary encode buffers. */ if(!OpenVideo()) - return; - - if(m_HasAudio) - if(!OpenAudio()) - return; + return false; - // TODO: remove/comment: - av_dump_format(m_pFormatContext, 0, aWholePath, 1); + if(m_HasAudio && !OpenAudio()) + return false; /* open the output file, if needed */ if(!(m_pFormat->flags & AVFMT_NOFILE)) { - int Ret = avio_open(&m_pFormatContext->pb, aWholePath, AVIO_FLAG_WRITE); - if(Ret < 0) + const int OpenResult = avio_open(&m_pFormatContext->pb, aWholePath, AVIO_FLAG_WRITE); + if(OpenResult < 0) { char aError[AV_ERROR_MAX_STRING_SIZE]; - av_strerror(Ret, aError, sizeof(aError)); - dbg_msg("video_recorder", "Could not open '%s': %s", aWholePath, aError); - return; + av_strerror(OpenResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not open file '%s': %s", aWholePath, aError); + return false; } } - m_VideoStream.m_vpSwsCtxs.reserve(m_VideoThreads); + m_VideoStream.m_vpSwsContexts.reserve(m_VideoThreads); for(size_t i = 0; i < m_VideoThreads; ++i) { - if(m_VideoStream.m_vpSwsCtxs.size() <= i) - m_VideoStream.m_vpSwsCtxs.emplace_back(nullptr); + if(m_VideoStream.m_vpSwsContexts.size() <= i) + m_VideoStream.m_vpSwsContexts.emplace_back(nullptr); - if(!m_VideoStream.m_vpSwsCtxs[i]) + if(!m_VideoStream.m_vpSwsContexts[i]) { - m_VideoStream.m_vpSwsCtxs[i] = sws_getCachedContext( - m_VideoStream.m_vpSwsCtxs[i], - m_VideoStream.pEnc->width, m_VideoStream.pEnc->height, AV_PIX_FMT_RGBA, - m_VideoStream.pEnc->width, m_VideoStream.pEnc->height, AV_PIX_FMT_YUV420P, + m_VideoStream.m_vpSwsContexts[i] = sws_getCachedContext( + m_VideoStream.m_vpSwsContexts[i], + m_VideoStream.m_pCodecContext->width, m_VideoStream.m_pCodecContext->height, AV_PIX_FMT_RGBA, + m_VideoStream.m_pCodecContext->width, m_VideoStream.m_pCodecContext->height, AV_PIX_FMT_YUV420P, 0, 0, 0, 0); } } /* Write the stream header, if any. */ - int Ret = avformat_write_header(m_pFormatContext, &m_pOptDict); - if(Ret < 0) + const int WriteHeaderResult = avformat_write_header(m_pFormatContext, &m_pOptDict); + if(WriteHeaderResult < 0) { char aError[AV_ERROR_MAX_STRING_SIZE]; - av_strerror(Ret, aError, sizeof(aError)); - dbg_msg("video_recorder", "Error occurred when opening output file: %s", aError); - return; + av_strerror(WriteHeaderResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not write header: %s", aError); + return false; } + m_Recording = true; m_Started = true; + m_Stopped = false; ms_Time = time_get(); - m_Vframe = 0; + return true; } void CVideo::Pause(bool Pause) @@ -241,31 +282,34 @@ void CVideo::Pause(bool Pause) void CVideo::Stop() { + dbg_assert(!m_Stopped, "Already stopped"); + m_Stopped = true; + m_pGraphics->WaitForIdle(); - for(size_t i = 0; i < m_VideoThreads; ++i) + for(auto &pVideoThread : m_vpVideoThreads) { { - std::unique_lock Lock(m_vVideoThreads[i]->m_Mutex); - m_vVideoThreads[i]->m_Finished = true; - m_vVideoThreads[i]->m_Cond.notify_all(); + std::unique_lock Lock(pVideoThread->m_Mutex); + pVideoThread->m_Finished = true; + pVideoThread->m_Cond.notify_all(); } - m_vVideoThreads[i]->m_Thread.join(); + pVideoThread->m_Thread.join(); } - m_vVideoThreads.clear(); + m_vpVideoThreads.clear(); - for(size_t i = 0; i < m_AudioThreads; ++i) + for(auto &pAudioThread : m_vpAudioThreads) { { - std::unique_lock Lock(m_vAudioThreads[i]->m_Mutex); - m_vAudioThreads[i]->m_Finished = true; - m_vAudioThreads[i]->m_Cond.notify_all(); + std::unique_lock Lock(pAudioThread->m_Mutex); + pAudioThread->m_Finished = true; + pAudioThread->m_Cond.notify_all(); } - m_vAudioThreads[i]->m_Thread.join(); + pAudioThread->m_Thread.join(); } - m_vAudioThreads.clear(); + m_vpAudioThreads.clear(); while(m_ProcessingVideoFrame > 0 || m_ProcessingAudioFrame > 0) std::this_thread::sleep_for(10us); @@ -277,19 +321,21 @@ void CVideo::Stop() if(m_HasAudio) FinishFrames(&m_AudioStream); - av_write_trailer(m_pFormatContext); + if(m_pFormatContext && m_Started) + av_write_trailer(m_pFormatContext); CloseStream(&m_VideoStream); if(m_HasAudio) CloseStream(&m_AudioStream); - //fclose(m_dbgfile); - - if(!(m_pFormat->flags & AVFMT_NOFILE)) - avio_closep(&m_pFormatContext->pb); if(m_pFormatContext) + { + if(!(m_pFormat->flags & AVFMT_NOFILE)) + avio_closep(&m_pFormatContext->pb); + avformat_free_context(m_pFormatContext); + } ISound *volatile pSound = m_pSound; @@ -302,11 +348,8 @@ void CVideo::NextVideoFrameThread() { if(m_Recording) { - // #ifdef CONF_PLATFORM_MACOS - // CAutoreleasePool AutoreleasePool; - // #endif - m_VSeq += 1; - if(m_VSeq >= 2) + m_VideoFrameIndex += 1; + if(m_VideoFrameIndex >= 2) { m_ProcessingVideoFrame.fetch_add(1); @@ -315,9 +358,8 @@ void CVideo::NextVideoFrameThread() NextVideoThreadIndex = 0; // always wait for the next video thread too, to prevent a dead lock - { - auto *pVideoThread = m_vVideoThreads[NextVideoThreadIndex].get(); + auto *pVideoThread = m_vpVideoThreads[NextVideoThreadIndex].get(); std::unique_lock Lock(pVideoThread->m_Mutex); if(pVideoThread->m_HasVideoFrame) @@ -326,11 +368,9 @@ void CVideo::NextVideoFrameThread() } } - //dbg_msg("video_recorder", "vframe: %d", m_VideoStream.pEnc->FRAME_NUM); - // after reading the graphic libraries' frame buffer, go threaded { - auto *pVideoThread = m_vVideoThreads[m_CurVideoThreadIndex].get(); + auto *pVideoThread = m_vpVideoThreads[m_CurVideoThreadIndex].get(); std::unique_lock Lock(pVideoThread->m_Mutex); if(pVideoThread->m_HasVideoFrame) @@ -338,12 +378,12 @@ void CVideo::NextVideoFrameThread() pVideoThread->m_Cond.wait(Lock, [&pVideoThread]() -> bool { return !pVideoThread->m_HasVideoFrame; }); } - ReadRGBFromGL(m_CurVideoThreadIndex); + UpdateVideoBufferFromGraphics(m_CurVideoThreadIndex); pVideoThread->m_HasVideoFrame = true; { std::unique_lock LockParent(pVideoThread->m_VideoFillMutex); - pVideoThread->m_VideoFrameToFill = m_VSeq; + pVideoThread->m_VideoFrameToFill = m_VideoFrameIndex; } pVideoThread->m_Cond.notify_all(); } @@ -352,9 +392,6 @@ void CVideo::NextVideoFrameThread() if(m_CurVideoThreadIndex == m_VideoThreads) m_CurVideoThreadIndex = 0; } - - // sync_barrier(); - // m_Semaphore.signal(); } } @@ -364,7 +401,6 @@ void CVideo::NextVideoFrame() { ms_Time += ms_TickTime; ms_LocalTime = (ms_Time - ms_LocalStartTime) / (float)time_freq(); - m_Vframe += 1; } } @@ -372,8 +408,7 @@ void CVideo::NextAudioFrameTimeline(ISoundMixFunc Mix) { if(m_Recording && m_HasAudio) { - //if(m_VideoStream.pEnc->FRAME_NUM * (double)m_AudioStream.pEnc->sample_rate / m_FPS >= (double)m_AudioStream.pEnc->FRAME_NUM * m_AudioStream.pEnc->frame_size) - double SamplesPerFrame = (double)m_AudioStream.pEnc->sample_rate / m_FPS; + double SamplesPerFrame = (double)m_AudioStream.m_pCodecContext->sample_rate / m_FPS; while(m_AudioStream.m_SamplesFrameCount >= m_AudioStream.m_SamplesCount) { NextAudioFrame(Mix); @@ -386,7 +421,7 @@ void CVideo::NextAudioFrame(ISoundMixFunc Mix) { if(m_Recording && m_HasAudio) { - m_ASeq += 1; + m_AudioFrameIndex += 1; m_ProcessingAudioFrame.fetch_add(1); @@ -397,7 +432,7 @@ void CVideo::NextAudioFrame(ISoundMixFunc Mix) // always wait for the next Audio thread too, to prevent a dead lock { - auto *pAudioThread = m_vAudioThreads[NextAudioThreadIndex].get(); + auto *pAudioThread = m_vpAudioThreads[NextAudioThreadIndex].get(); std::unique_lock Lock(pAudioThread->m_Mutex); if(pAudioThread->m_HasAudioFrame) @@ -408,7 +443,7 @@ void CVideo::NextAudioFrame(ISoundMixFunc Mix) // after reading the graphic libraries' frame buffer, go threaded { - auto *pAudioThread = m_vAudioThreads[m_CurAudioThreadIndex].get(); + auto *pAudioThread = m_vpAudioThreads[m_CurAudioThreadIndex].get(); std::unique_lock Lock(pAudioThread->m_Mutex); @@ -417,13 +452,13 @@ void CVideo::NextAudioFrame(ISoundMixFunc Mix) pAudioThread->m_Cond.wait(Lock, [&pAudioThread]() -> bool { return !pAudioThread->m_HasAudioFrame; }); } - Mix(m_vBuffer[m_CurAudioThreadIndex].m_aBuffer, ALEN / 2); // two channels + Mix(m_vAudioBuffers[m_CurAudioThreadIndex].m_aBuffer, std::size(m_vAudioBuffers[m_CurAudioThreadIndex].m_aBuffer) / 2 / 2); // two channels int64_t DstNbSamples = av_rescale_rnd( - swr_get_delay(m_AudioStream.m_vpSwrCtxs[m_CurAudioThreadIndex], m_AudioStream.pEnc->sample_rate) + + swr_get_delay(m_AudioStream.m_vpSwrContexts[m_CurAudioThreadIndex], m_AudioStream.m_pCodecContext->sample_rate) + m_AudioStream.m_vpFrames[m_CurAudioThreadIndex]->nb_samples, - m_AudioStream.pEnc->sample_rate, - m_AudioStream.pEnc->sample_rate, AV_ROUND_UP); + m_AudioStream.m_pCodecContext->sample_rate, + m_AudioStream.m_pCodecContext->sample_rate, AV_ROUND_UP); pAudioThread->m_SampleCountStart = m_AudioStream.m_SamplesCount; m_AudioStream.m_SamplesCount += DstNbSamples; @@ -431,7 +466,7 @@ void CVideo::NextAudioFrame(ISoundMixFunc Mix) pAudioThread->m_HasAudioFrame = true; { std::unique_lock LockParent(pAudioThread->m_AudioFillMutex); - pAudioThread->m_AudioFrameToFill = m_ASeq; + pAudioThread->m_AudioFrameToFill = m_AudioFrameIndex; } pAudioThread->m_Cond.notify_all(); } @@ -444,8 +479,8 @@ void CVideo::NextAudioFrame(ISoundMixFunc Mix) void CVideo::RunAudioThread(size_t ParentThreadIndex, size_t ThreadIndex) { - auto *pThreadData = m_vAudioThreads[ThreadIndex].get(); - auto *pParentThreadData = m_vAudioThreads[ParentThreadIndex].get(); + auto *pThreadData = m_vpAudioThreads[ThreadIndex].get(); + auto *pParentThreadData = m_vpAudioThreads[ParentThreadIndex].get(); std::unique_lock Lock(pThreadData->m_Mutex); pThreadData->m_Started = true; pThreadData->m_Cond.notify_all(); @@ -472,7 +507,7 @@ void CVideo::RunAudioThread(size_t ParentThreadIndex, size_t ThreadIndex) { CLockScope ls(g_WriteLock); - m_AudioStream.m_vpFrames[ThreadIndex]->pts = av_rescale_q(pThreadData->m_SampleCountStart, AVRational{1, m_AudioStream.pEnc->sample_rate}, m_AudioStream.pEnc->time_base); + m_AudioStream.m_vpFrames[ThreadIndex]->pts = av_rescale_q(pThreadData->m_SampleCountStart, AVRational{1, m_AudioStream.m_pCodecContext->sample_rate}, m_AudioStream.m_pCodecContext->time_base); WriteFrame(&m_AudioStream, ThreadIndex); } @@ -489,45 +524,52 @@ void CVideo::RunAudioThread(size_t ParentThreadIndex, size_t ThreadIndex) void CVideo::FillAudioFrame(size_t ThreadIndex) { - av_samples_fill_arrays( + const int FillArrayResult = av_samples_fill_arrays( (uint8_t **)m_AudioStream.m_vpTmpFrames[ThreadIndex]->data, - 0, // pointer to linesize (int*) - (const uint8_t *)m_vBuffer[ThreadIndex].m_aBuffer, + nullptr, // pointer to linesize (int*) + (const uint8_t *)m_vAudioBuffers[ThreadIndex].m_aBuffer, 2, // channels m_AudioStream.m_vpTmpFrames[ThreadIndex]->nb_samples, AV_SAMPLE_FMT_S16, 0 // align ); + if(FillArrayResult < 0) + { + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(FillArrayResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not fill audio frame: %s", aError); + return; + } - // dbg_msg("video_recorder", "DstNbSamples: %d", DstNbSamples); - // fwrite(m_aBuffer, sizeof(short), 2048, m_dbgfile); - - int Ret = av_frame_make_writable(m_AudioStream.m_vpFrames[ThreadIndex]); - if(Ret < 0) + const int MakeWriteableResult = av_frame_make_writable(m_AudioStream.m_vpFrames[ThreadIndex]); + if(MakeWriteableResult < 0) { - dbg_msg("video_recorder", "Error making frame writable"); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(MakeWriteableResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not make audio frame writeable: %s", aError); return; } /* convert to destination format */ - Ret = swr_convert( - m_AudioStream.m_vpSwrCtxs[ThreadIndex], + const int ConvertResult = swr_convert( + m_AudioStream.m_vpSwrContexts[ThreadIndex], m_AudioStream.m_vpFrames[ThreadIndex]->data, m_AudioStream.m_vpFrames[ThreadIndex]->nb_samples, (const uint8_t **)m_AudioStream.m_vpTmpFrames[ThreadIndex]->data, m_AudioStream.m_vpTmpFrames[ThreadIndex]->nb_samples); - - if(Ret < 0) + if(ConvertResult < 0) { - dbg_msg("video_recorder", "Error while converting"); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(ConvertResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not convert audio frame: %s", aError); return; } } void CVideo::RunVideoThread(size_t ParentThreadIndex, size_t ThreadIndex) { - auto *pThreadData = m_vVideoThreads[ThreadIndex].get(); - auto *pParentThreadData = m_vVideoThreads[ParentThreadIndex].get(); + auto *pThreadData = m_vpVideoThreads[ThreadIndex].get(); + auto *pParentThreadData = m_vpVideoThreads[ParentThreadIndex].get(); std::unique_lock Lock(pThreadData->m_Mutex); pThreadData->m_Started = true; pThreadData->m_Cond.notify_all(); @@ -553,7 +595,7 @@ void CVideo::RunVideoThread(size_t ParentThreadIndex, size_t ThreadIndex) std::unique_lock LockVideo(pThreadData->m_VideoFillMutex); { CLockScope ls(g_WriteLock); - m_VideoStream.m_vpFrames[ThreadIndex]->pts = (int64_t)m_VideoStream.pEnc->FRAME_NUM; + m_VideoStream.m_vpFrames[ThreadIndex]->pts = (int64_t)m_VideoStream.m_pCodecContext->FRAME_NUM; WriteFrame(&m_VideoStream, ThreadIndex); } @@ -570,38 +612,42 @@ void CVideo::RunVideoThread(size_t ParentThreadIndex, size_t ThreadIndex) void CVideo::FillVideoFrame(size_t ThreadIndex) { - const int InLineSize = 4 * m_VideoStream.pEnc->width; - auto *pRGBAData = m_vPixelHelper[ThreadIndex].data(); - sws_scale(m_VideoStream.m_vpSwsCtxs[ThreadIndex], (const uint8_t *const *)&pRGBAData, &InLineSize, 0, - m_VideoStream.pEnc->height, m_VideoStream.m_vpFrames[ThreadIndex]->data, m_VideoStream.m_vpFrames[ThreadIndex]->linesize); + const int InLineSize = 4 * m_VideoStream.m_pCodecContext->width; + auto *pRGBAData = m_vVideoBuffers[ThreadIndex].m_vBuffer.data(); + sws_scale(m_VideoStream.m_vpSwsContexts[ThreadIndex], (const uint8_t *const *)&pRGBAData, &InLineSize, 0, + m_VideoStream.m_pCodecContext->height, m_VideoStream.m_vpFrames[ThreadIndex]->data, m_VideoStream.m_vpFrames[ThreadIndex]->linesize); } -void CVideo::ReadRGBFromGL(size_t ThreadIndex) +void CVideo::UpdateVideoBufferFromGraphics(size_t ThreadIndex) { uint32_t Width; uint32_t Height; CImageInfo::EImageFormat Format; - m_pGraphics->GetReadPresentedImageDataFuncUnsafe()(Width, Height, Format, m_vPixelHelper[ThreadIndex]); + m_pGraphics->GetReadPresentedImageDataFuncUnsafe()(Width, Height, Format, m_vVideoBuffers[ThreadIndex].m_vBuffer); + dbg_assert((int)Width == m_Width && (int)Height == m_Height, "Size mismatch between video and graphics"); + dbg_assert(Format == CImageInfo::FORMAT_RGBA, "Unexpected image format"); } AVFrame *CVideo::AllocPicture(enum AVPixelFormat PixFmt, int Width, int Height) { - AVFrame *pPicture; - int Ret; - - pPicture = av_frame_alloc(); + AVFrame *pPicture = av_frame_alloc(); if(!pPicture) - return NULL; + { + log_error("videorecorder", "Could not allocate video frame"); + return nullptr; + } pPicture->format = PixFmt; pPicture->width = Width; pPicture->height = Height; /* allocate the buffers for the frame data */ - Ret = av_frame_get_buffer(pPicture, 32); - if(Ret < 0) + const int FrameBufferAllocResult = av_frame_get_buffer(pPicture, 32); + if(FrameBufferAllocResult < 0) { - dbg_msg("video_recorder", "Could not allocate frame data."); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(FrameBufferAllocResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not allocate video frame buffer: %s", aError); return nullptr; } @@ -611,11 +657,9 @@ AVFrame *CVideo::AllocPicture(enum AVPixelFormat PixFmt, int Width, int Height) AVFrame *CVideo::AllocAudioFrame(enum AVSampleFormat SampleFmt, uint64_t ChannelLayout, int SampleRate, int NbSamples) { AVFrame *pFrame = av_frame_alloc(); - int Ret; - if(!pFrame) { - dbg_msg("video_recorder", "Error allocating an audio frame"); + log_error("videorecorder", "Could not allocate audio frame"); return nullptr; } @@ -630,10 +674,12 @@ AVFrame *CVideo::AllocAudioFrame(enum AVSampleFormat SampleFmt, uint64_t Channel if(NbSamples) { - Ret = av_frame_get_buffer(pFrame, 0); - if(Ret < 0) + const int FrameBufferAllocResult = av_frame_get_buffer(pFrame, 0); + if(FrameBufferAllocResult < 0) { - dbg_msg("video_recorder", "Error allocating an audio buffer"); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(FrameBufferAllocResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not allocate audio frame buffer: %s", aError); return nullptr; } } @@ -643,20 +689,18 @@ AVFrame *CVideo::AllocAudioFrame(enum AVSampleFormat SampleFmt, uint64_t Channel bool CVideo::OpenVideo() { - int Ret; - AVCodecContext *pContext = m_VideoStream.pEnc; - AVDictionary *pOptions = 0; - + AVCodecContext *pContext = m_VideoStream.m_pCodecContext; + AVDictionary *pOptions = nullptr; av_dict_copy(&pOptions, m_pOptDict, 0); /* open the codec */ - Ret = avcodec_open2(pContext, m_pVideoCodec, &pOptions); + const int VideoOpenResult = avcodec_open2(pContext, m_pVideoCodec, &pOptions); av_dict_free(&pOptions); - if(Ret < 0) + if(VideoOpenResult < 0) { - char aBuf[AV_ERROR_MAX_STRING_SIZE]; - av_strerror(Ret, aBuf, sizeof(aBuf)); - dbg_msg("video_recorder", "Could not open video codec: %s", aBuf); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(VideoOpenResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not open video codec: %s", aError); return false; } @@ -670,7 +714,6 @@ bool CVideo::OpenVideo() m_VideoStream.m_vpFrames[i] = AllocPicture(pContext->pix_fmt, pContext->width, pContext->height); if(!m_VideoStream.m_vpFrames[i]) { - dbg_msg("video_recorder", "Could not allocate video frame"); return false; } } @@ -690,45 +733,42 @@ bool CVideo::OpenVideo() m_VideoStream.m_vpTmpFrames[i] = AllocPicture(AV_PIX_FMT_YUV420P, pContext->width, pContext->height); if(!m_VideoStream.m_vpTmpFrames[i]) { - dbg_msg("video_recorder", "Could not allocate temporary video frame"); return false; } } } /* copy the stream parameters to the muxer */ - Ret = avcodec_parameters_from_context(m_VideoStream.pSt->codecpar, pContext); - if(Ret < 0) + const int AudioStreamCopyResult = avcodec_parameters_from_context(m_VideoStream.m_pStream->codecpar, pContext); + if(AudioStreamCopyResult < 0) { - dbg_msg("video_recorder", "Could not copy the stream parameters"); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(AudioStreamCopyResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not copy video stream parameters: %s", aError); return false; } - m_VSeq = 0; + m_VideoFrameIndex = 0; return true; } bool CVideo::OpenAudio() { - AVCodecContext *pContext; - int NbSamples; - int Ret; - AVDictionary *pOptions = NULL; - - pContext = m_AudioStream.pEnc; + AVCodecContext *pContext = m_AudioStream.m_pCodecContext; + AVDictionary *pOptions = nullptr; + av_dict_copy(&pOptions, m_pOptDict, 0); /* open it */ - //m_dbgfile = fopen("/tmp/pcm_dbg", "wb"); - av_dict_copy(&pOptions, m_pOptDict, 0); - Ret = avcodec_open2(pContext, m_pAudioCodec, &pOptions); + const int AudioOpenResult = avcodec_open2(pContext, m_pAudioCodec, &pOptions); av_dict_free(&pOptions); - if(Ret < 0) + if(AudioOpenResult < 0) { - char aBuf[AV_ERROR_MAX_STRING_SIZE]; - av_strerror(Ret, aBuf, sizeof(aBuf)); - dbg_msg("video_recorder", "Could not open audio codec: %s", aBuf); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(AudioOpenResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not open audio codec: %s", aError); return false; } + int NbSamples; if(pContext->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) NbSamples = 10000; else @@ -751,91 +791,99 @@ bool CVideo::OpenAudio() #endif if(!m_AudioStream.m_vpFrames[i]) { - dbg_msg("video_recorder", "Could not allocate audio frame"); return false; } m_AudioStream.m_vpTmpFrames.emplace_back(nullptr); - m_AudioStream.m_vpTmpFrames[i] = AllocAudioFrame(AV_SAMPLE_FMT_S16, AV_CH_LAYOUT_STEREO, g_Config.m_SndRate, NbSamples); + m_AudioStream.m_vpTmpFrames[i] = AllocAudioFrame(AV_SAMPLE_FMT_S16, AV_CH_LAYOUT_STEREO, m_pSound->MixingRate(), NbSamples); if(!m_AudioStream.m_vpTmpFrames[i]) { - dbg_msg("video_recorder", "Could not allocate audio frame"); return false; } } /* copy the stream parameters to the muxer */ - Ret = avcodec_parameters_from_context(m_AudioStream.pSt->codecpar, pContext); - if(Ret < 0) + const int AudioStreamCopyResult = avcodec_parameters_from_context(m_AudioStream.m_pStream->codecpar, pContext); + if(AudioStreamCopyResult < 0) { - dbg_msg("video_recorder", "Could not copy the stream parameters"); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(AudioStreamCopyResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not copy audio stream parameters: %s", aError); return false; } - /* create resampler context */ - m_AudioStream.m_vpSwrCtxs.clear(); - m_AudioStream.m_vpSwrCtxs.resize(m_AudioThreads); + /* create resampling context */ + m_AudioStream.m_vpSwrContexts.clear(); + m_AudioStream.m_vpSwrContexts.resize(m_AudioThreads); for(size_t i = 0; i < m_AudioThreads; ++i) { - m_AudioStream.m_vpSwrCtxs[i] = swr_alloc(); - if(!m_AudioStream.m_vpSwrCtxs[i]) + m_AudioStream.m_vpSwrContexts[i] = swr_alloc(); + if(!m_AudioStream.m_vpSwrContexts[i]) { - dbg_msg("video_recorder", "Could not allocate resampler context"); + log_error("videorecorder", "Could not allocate resampling context"); return false; } /* set options */ - av_opt_set_int(m_AudioStream.m_vpSwrCtxs[i], "in_channel_count", 2, 0); - av_opt_set_int(m_AudioStream.m_vpSwrCtxs[i], "in_sample_rate", g_Config.m_SndRate, 0); - av_opt_set_sample_fmt(m_AudioStream.m_vpSwrCtxs[i], "in_sample_fmt", AV_SAMPLE_FMT_S16, 0); #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 24, 100) - av_opt_set_int(m_AudioStream.m_vpSwrCtxs[i], "out_channel_count", pContext->ch_layout.nb_channels, 0); + dbg_assert(av_opt_set_chlayout(m_AudioStream.m_vpSwrContexts[i], "in_chlayout", &pContext->ch_layout, 0) == 0, "invalid option"); #else - av_opt_set_int(m_AudioStream.m_vpSwrCtxs[i], "out_channel_count", pContext->channels, 0); + dbg_assert(av_opt_set_int(m_AudioStream.m_vpSwrContexts[i], "in_channel_count", pContext->channels, 0) == 0, "invalid option"); #endif - av_opt_set_int(m_AudioStream.m_vpSwrCtxs[i], "out_sample_rate", pContext->sample_rate, 0); - av_opt_set_sample_fmt(m_AudioStream.m_vpSwrCtxs[i], "out_sample_fmt", pContext->sample_fmt, 0); + if(av_opt_set_int(m_AudioStream.m_vpSwrContexts[i], "in_sample_rate", m_pSound->MixingRate(), 0) != 0) + { + log_error("videorecorder", "Could not set audio sample rate to %d", m_pSound->MixingRate()); + return false; + } + dbg_assert(av_opt_set_sample_fmt(m_AudioStream.m_vpSwrContexts[i], "in_sample_fmt", AV_SAMPLE_FMT_S16, 0) == 0, "invalid option"); +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 24, 100) + dbg_assert(av_opt_set_chlayout(m_AudioStream.m_vpSwrContexts[i], "out_chlayout", &pContext->ch_layout, 0) == 0, "invalid option"); +#else + dbg_assert(av_opt_set_int(m_AudioStream.m_vpSwrContexts[i], "out_channel_count", pContext->channels, 0) == 0, "invalid option"); +#endif + dbg_assert(av_opt_set_int(m_AudioStream.m_vpSwrContexts[i], "out_sample_rate", pContext->sample_rate, 0) == 0, "invalid option"); + dbg_assert(av_opt_set_sample_fmt(m_AudioStream.m_vpSwrContexts[i], "out_sample_fmt", pContext->sample_fmt, 0) == 0, "invalid option"); /* initialize the resampling context */ - if(swr_init(m_AudioStream.m_vpSwrCtxs[i]) < 0) + const int ResamplingContextInitResult = swr_init(m_AudioStream.m_vpSwrContexts[i]); + if(ResamplingContextInitResult < 0) { - dbg_msg("video_recorder", "Failed to initialize the resampling context"); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(ResamplingContextInitResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not initialize resampling context: %s", aError); return false; } } - m_ASeq = 0; + m_AudioFrameIndex = 0; return true; } /* Add an output stream. */ -bool CVideo::AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCodec **ppCodec, enum AVCodecID CodecId) const +bool CVideo::AddStream(COutputStream *pStream, AVFormatContext *pFormatContext, const AVCodec **ppCodec, enum AVCodecID CodecId) const { - AVCodecContext *pContext; - /* find the encoder */ *ppCodec = avcodec_find_encoder(CodecId); if(!(*ppCodec)) { - dbg_msg("video_recorder", "Could not find encoder for '%s'", - avcodec_get_name(CodecId)); + log_error("videorecorder", "Could not find encoder for codec '%s'", avcodec_get_name(CodecId)); return false; } - pStream->pSt = avformat_new_stream(pOC, NULL); - if(!pStream->pSt) + pStream->m_pStream = avformat_new_stream(pFormatContext, nullptr); + if(!pStream->m_pStream) { - dbg_msg("video_recorder", "Could not allocate stream"); + log_error("videorecorder", "Could not allocate stream"); return false; } - pStream->pSt->id = pOC->nb_streams - 1; - pContext = avcodec_alloc_context3(*ppCodec); + pStream->m_pStream->id = pFormatContext->nb_streams - 1; + AVCodecContext *pContext = avcodec_alloc_context3(*ppCodec); if(!pContext) { - dbg_msg("video_recorder", "Could not alloc an encoding context"); + log_error("videorecorder", "Could not allocate encoding context"); return false; } - pStream->pEnc = pContext; + pStream->m_pCodecContext = pContext; #if defined(CONF_ARCH_IA32) || defined(CONF_ARCH_ARM) // use only 1 ffmpeg thread on 32-bit to save memory @@ -845,21 +893,34 @@ bool CVideo::AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCode switch((*ppCodec)->type) { case AVMEDIA_TYPE_AUDIO: - pContext->sample_fmt = (*ppCodec)->sample_fmts ? (*ppCodec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP; - pContext->bit_rate = g_Config.m_SndRate * 2 * 16; - pContext->sample_rate = g_Config.m_SndRate; - if((*ppCodec)->supported_samplerates) + { + const AVSampleFormat *pSampleFormats = nullptr; + const int *pSampleRates = nullptr; +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(61, 13, 100) + avcodec_get_supported_config(pContext, *ppCodec, AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, (const void **)&pSampleFormats, nullptr); + avcodec_get_supported_config(pContext, *ppCodec, AV_CODEC_CONFIG_SAMPLE_RATE, 0, (const void **)&pSampleRates, nullptr); +#else + pSampleFormats = (*ppCodec)->sample_fmts; + pSampleRates = (*ppCodec)->supported_samplerates; +#endif + pContext->sample_fmt = pSampleFormats ? pSampleFormats[0] : AV_SAMPLE_FMT_FLTP; + if(pSampleRates) { - pContext->sample_rate = (*ppCodec)->supported_samplerates[0]; - for(int i = 0; (*ppCodec)->supported_samplerates[i]; i++) + pContext->sample_rate = pSampleRates[0]; + for(int i = 0; pSampleRates[i]; i++) { - if((*ppCodec)->supported_samplerates[i] == g_Config.m_SndRate) + if(pSampleRates[i] == m_pSound->MixingRate()) { - pContext->sample_rate = g_Config.m_SndRate; + pContext->sample_rate = m_pSound->MixingRate(); break; } } } + else + { + pContext->sample_rate = m_pSound->MixingRate(); + } + pContext->bit_rate = pContext->sample_rate * 2 * 16; #if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 24, 100) dbg_assert(av_channel_layout_from_mask(&pContext->ch_layout, AV_CH_LAYOUT_STEREO) == 0, "Failed to set channel layout"); #else @@ -867,9 +928,10 @@ bool CVideo::AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCode pContext->channel_layout = AV_CH_LAYOUT_STEREO; #endif - pStream->pSt->time_base.num = 1; - pStream->pSt->time_base.den = pContext->sample_rate; + pStream->m_pStream->time_base.num = 1; + pStream->m_pStream->time_base.den = pContext->sample_rate; break; + } case AVMEDIA_TYPE_VIDEO: pContext->codec_id = CodecId; @@ -879,12 +941,12 @@ bool CVideo::AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCode pContext->width = m_Width; pContext->height = m_Height % 2 == 0 ? m_Height : m_Height - 1; /* timebase: This is the fundamental unit of time (in seconds) in terms - * of which frame timestamps are represented. For fixed-fps content, - * timebase should be 1/framerate and timestamp increments should be - * identical to 1. */ - pStream->pSt->time_base.num = 1; - pStream->pSt->time_base.den = m_FPS; - pContext->time_base = pStream->pSt->time_base; + * of which frame timestamps are represented. For fixed-fps content, + * timebase should be 1/framerate and timestamp increments should be + * identical to 1. */ + pStream->m_pStream->time_base.num = 1; + pStream->m_pStream->time_base.den = m_FPS; + pContext->time_base = pStream->m_pStream->time_base; pContext->gop_size = 12; /* emit one intra frame every twelve frames at most */ pContext->pix_fmt = STREAM_PIX_FMT; @@ -896,15 +958,16 @@ bool CVideo::AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCode if(pContext->codec_id == AV_CODEC_ID_MPEG1VIDEO) { /* Needed to avoid using macroblocks in which some coeffs overflow. - * This does not happen with normal video, it just happens here as - * the motion of the chroma plane does not match the luma plane. */ + * This does not happen with normal video, it just happens here as + * the motion of the chroma plane does not match the luma plane. */ pContext->mb_decision = 2; } if(CodecId == AV_CODEC_ID_H264) { static const char *s_apPresets[10] = {"ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow", "placebo"}; - av_opt_set(pContext->priv_data, "preset", s_apPresets[g_Config.m_ClVideoX264Preset], 0); - av_opt_set_int(pContext->priv_data, "crf", g_Config.m_ClVideoX264Crf, 0); + dbg_assert(g_Config.m_ClVideoX264Preset < (int)std::size(s_apPresets), "preset index invalid"); + dbg_assert(av_opt_set(pContext->priv_data, "preset", s_apPresets[g_Config.m_ClVideoX264Preset], 0) == 0, "invalid option"); + dbg_assert(av_opt_set_int(pContext->priv_data, "crf", g_Config.m_ClVideoX264Crf, 0) == 0, "invalid option"); } break; @@ -913,102 +976,109 @@ bool CVideo::AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCode } /* Some formats want stream headers to be separate. */ - if(pOC->oformat->flags & AVFMT_GLOBALHEADER) + if(pFormatContext->oformat->flags & AVFMT_GLOBALHEADER) pContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; return true; } -void CVideo::WriteFrame(OutputStream *pStream, size_t ThreadIndex) +void CVideo::WriteFrame(COutputStream *pStream, size_t ThreadIndex) { - int RetRecv = 0; - AVPacket *pPacket = av_packet_alloc(); if(pPacket == nullptr) { - dbg_msg("video_recorder", "Failed allocating packet"); + log_error("videorecorder", "Could not allocate packet"); return; } pPacket->data = 0; pPacket->size = 0; - avcodec_send_frame(pStream->pEnc, pStream->m_vpFrames[ThreadIndex]); + avcodec_send_frame(pStream->m_pCodecContext, pStream->m_vpFrames[ThreadIndex]); + int RecvResult = 0; do { - RetRecv = avcodec_receive_packet(pStream->pEnc, pPacket); - if(!RetRecv) + RecvResult = avcodec_receive_packet(pStream->m_pCodecContext, pPacket); + if(!RecvResult) { /* rescale output packet timestamp values from codec to stream timebase */ - av_packet_rescale_ts(pPacket, pStream->pEnc->time_base, pStream->pSt->time_base); - pPacket->stream_index = pStream->pSt->index; + av_packet_rescale_ts(pPacket, pStream->m_pCodecContext->time_base, pStream->m_pStream->time_base); + pPacket->stream_index = pStream->m_pStream->index; - if(int Ret = av_interleaved_write_frame(m_pFormatContext, pPacket)) + const int WriteFrameResult = av_interleaved_write_frame(m_pFormatContext, pPacket); + if(WriteFrameResult < 0) { - char aBuf[AV_ERROR_MAX_STRING_SIZE]; - av_strerror(Ret, aBuf, sizeof(aBuf)); - dbg_msg("video_recorder", "Error while writing video frame: %s", aBuf); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(WriteFrameResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not write video frame: %s", aError); } } else break; } while(true); - if(RetRecv && RetRecv != AVERROR(EAGAIN)) + if(RecvResult && RecvResult != AVERROR(EAGAIN)) { - dbg_msg("video_recorder", "Error encoding frame, error: %d", RetRecv); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(RecvResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not encode video frame: %s", aError); } av_packet_free(&pPacket); } -void CVideo::FinishFrames(OutputStream *pStream) +void CVideo::FinishFrames(COutputStream *pStream) { - dbg_msg("video_recorder", "------------"); - int RetRecv = 0; + if(!pStream->m_pCodecContext || !avcodec_is_open(pStream->m_pCodecContext)) + return; AVPacket *pPacket = av_packet_alloc(); if(pPacket == nullptr) { - dbg_msg("video_recorder", "Failed allocating packet"); + log_error("videorecorder", "Could not allocate packet"); return; } pPacket->data = 0; pPacket->size = 0; - avcodec_send_frame(pStream->pEnc, 0); + avcodec_send_frame(pStream->m_pCodecContext, 0); + int RecvResult = 0; do { - RetRecv = avcodec_receive_packet(pStream->pEnc, pPacket); - if(!RetRecv) + RecvResult = avcodec_receive_packet(pStream->m_pCodecContext, pPacket); + if(!RecvResult) { /* rescale output packet timestamp values from codec to stream timebase */ - av_packet_rescale_ts(pPacket, pStream->pEnc->time_base, pStream->pSt->time_base); - pPacket->stream_index = pStream->pSt->index; + av_packet_rescale_ts(pPacket, pStream->m_pCodecContext->time_base, pStream->m_pStream->time_base); + pPacket->stream_index = pStream->m_pStream->index; - if(int Ret = av_interleaved_write_frame(m_pFormatContext, pPacket)) + const int WriteFrameResult = av_interleaved_write_frame(m_pFormatContext, pPacket); + if(WriteFrameResult < 0) { - char aBuf[AV_ERROR_MAX_STRING_SIZE]; - av_strerror(Ret, aBuf, sizeof(aBuf)); - dbg_msg("video_recorder", "Error while writing video frame: %s", aBuf); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(WriteFrameResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not write video frame: %s", aError); } } else break; } while(true); - if(RetRecv && RetRecv != AVERROR_EOF) + if(RecvResult && RecvResult != AVERROR_EOF) { - dbg_msg("video_recorder", "failed to finish recoding, error: %d", RetRecv); + char aError[AV_ERROR_MAX_STRING_SIZE]; + av_strerror(RecvResult, aError, sizeof(aError)); + log_error("videorecorder", "Could not finish recording: %s", aError); } av_packet_free(&pPacket); } -void CVideo::CloseStream(OutputStream *pStream) +void CVideo::CloseStream(COutputStream *pStream) { - avcodec_free_context(&pStream->pEnc); + avcodec_free_context(&pStream->m_pCodecContext); + for(auto *pFrame : pStream->m_vpFrames) av_frame_free(&pFrame); pStream->m_vpFrames.clear(); @@ -1017,13 +1087,13 @@ void CVideo::CloseStream(OutputStream *pStream) av_frame_free(&pFrame); pStream->m_vpTmpFrames.clear(); - for(auto *pSwsContext : pStream->m_vpSwsCtxs) + for(auto *pSwsContext : pStream->m_vpSwsContexts) sws_freeContext(pSwsContext); - pStream->m_vpSwsCtxs.clear(); + pStream->m_vpSwsContexts.clear(); - for(auto *pSwrContext : pStream->m_vpSwrCtxs) + for(auto *pSwrContext : pStream->m_vpSwrContexts) swr_free(&pSwrContext); - pStream->m_vpSwrCtxs.clear(); + pStream->m_vpSwrContexts.clear(); } #endif diff --git a/src/engine/client/video.h b/src/engine/client/video.h index 0575cdee7e..63ddc27e65 100644 --- a/src/engine/client/video.h +++ b/src/engine/client/video.h @@ -15,39 +15,38 @@ extern "C" { #include #include #include -#define ALEN 2048 -class CGraphics_Threaded; +class IGraphics; class ISound; class IStorage; extern CLock g_WriteLock; // a wrapper around a single output AVStream -struct OutputStream +class COutputStream { - AVStream *pSt = nullptr; - AVCodecContext *pEnc = nullptr; +public: + AVStream *m_pStream = nullptr; + AVCodecContext *m_pCodecContext = nullptr; /* pts of the next frame that will be generated */ - int64_t NextPts = 0; int64_t m_SamplesCount = 0; int64_t m_SamplesFrameCount = 0; std::vector m_vpFrames; std::vector m_vpTmpFrames; - std::vector m_vpSwsCtxs; - std::vector m_vpSwrCtxs; + std::vector m_vpSwsContexts; + std::vector m_vpSwrContexts; }; class CVideo : public IVideo { public: - CVideo(CGraphics_Threaded *pGraphics, ISound *pSound, IStorage *pStorage, int Width, int Height, const char *pName); + CVideo(IGraphics *pGraphics, ISound *pSound, IStorage *pStorage, int Width, int Height, const char *pName); ~CVideo(); - void Start() override REQUIRES(!g_WriteLock); + bool Start() override REQUIRES(!g_WriteLock); void Stop() override; void Pause(bool Pause) override; bool IsRecording() override { return m_Recording; } @@ -60,12 +59,12 @@ class CVideo : public IVideo static IVideo *Current() { return IVideo::ms_pCurrentVideo; } - static void Init() { av_log_set_level(AV_LOG_DEBUG); } + static void Init(); private: void RunVideoThread(size_t ParentThreadIndex, size_t ThreadIndex) REQUIRES(!g_WriteLock); void FillVideoFrame(size_t ThreadIndex) REQUIRES(!g_WriteLock); - void ReadRGBFromGL(size_t ThreadIndex); + void UpdateVideoBufferFromGraphics(size_t ThreadIndex); void RunAudioThread(size_t ParentThreadIndex, size_t ThreadIndex) REQUIRES(!g_WriteLock); void FillAudioFrame(size_t ThreadIndex); @@ -75,27 +74,26 @@ class CVideo : public IVideo AVFrame *AllocPicture(enum AVPixelFormat PixFmt, int Width, int Height); AVFrame *AllocAudioFrame(enum AVSampleFormat SampleFmt, uint64_t ChannelLayout, int SampleRate, int NbSamples); - void WriteFrame(OutputStream *pStream, size_t ThreadIndex) REQUIRES(g_WriteLock); - void FinishFrames(OutputStream *pStream); - void CloseStream(OutputStream *pStream); + void WriteFrame(COutputStream *pStream, size_t ThreadIndex) REQUIRES(g_WriteLock); + void FinishFrames(COutputStream *pStream); + void CloseStream(COutputStream *pStream); - bool AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCodec **ppCodec, enum AVCodecID CodecId) const; + bool AddStream(COutputStream *pStream, AVFormatContext *pFormatContext, const AVCodec **ppCodec, enum AVCodecID CodecId) const; - CGraphics_Threaded *m_pGraphics; + IGraphics *m_pGraphics; IStorage *m_pStorage; ISound *m_pSound; int m_Width; int m_Height; char m_aName[256]; - //FILE *m_dbgfile; - uint64_t m_VSeq = 0; - uint64_t m_ASeq = 0; - uint64_t m_Vframe; + uint64_t m_VideoFrameIndex = 0; + uint64_t m_AudioFrameIndex = 0; int m_FPS; bool m_Started; + bool m_Stopped; bool m_Recording; size_t m_VideoThreads = 2; @@ -103,8 +101,9 @@ class CVideo : public IVideo size_t m_AudioThreads = 2; size_t m_CurAudioThreadIndex = 0; - struct SVideoRecorderThread + class CVideoRecorderThread { + public: std::thread m_Thread; std::mutex m_Mutex; std::condition_variable m_Cond; @@ -118,10 +117,11 @@ class CVideo : public IVideo uint64_t m_VideoFrameToFill = 0; }; - std::vector> m_vVideoThreads; + std::vector> m_vpVideoThreads; - struct SAudioRecorderThread + class CAudioRecorderThread { + public: std::thread m_Thread; std::mutex m_Mutex; std::condition_variable m_Cond; @@ -136,22 +136,28 @@ class CVideo : public IVideo int64_t m_SampleCountStart = 0; }; - std::vector> m_vAudioThreads; + std::vector> m_vpAudioThreads; std::atomic m_ProcessingVideoFrame; std::atomic m_ProcessingAudioFrame; bool m_HasAudio; - struct SVideoSoundBuffer + class CVideoBuffer + { + public: + std::vector m_vBuffer; + }; + std::vector m_vVideoBuffers; + class CAudioBuffer { - int16_t m_aBuffer[ALEN * 2]; + public: + int16_t m_aBuffer[4096]; }; - std::vector m_vBuffer; - std::vector> m_vPixelHelper; + std::vector m_vAudioBuffers; - OutputStream m_VideoStream; - OutputStream m_AudioStream; + COutputStream m_VideoStream; + COutputStream m_AudioStream; const AVCodec *m_pVideoCodec; const AVCodec *m_pAudioCodec; diff --git a/src/engine/client/warning.cpp b/src/engine/client/warning.cpp index 0ed4a9daae..b15ed4c1fd 100644 --- a/src/engine/client/warning.cpp +++ b/src/engine/client/warning.cpp @@ -4,7 +4,6 @@ SWarning::SWarning(const char *pMsg) { - str_copy(m_aWarningTitle, ""); str_copy(m_aWarningMsg, pMsg); } SWarning::SWarning(const char *pTitle, const char *pMsg) diff --git a/src/engine/config.h b/src/engine/config.h index bccf50437b..4123bad7e2 100644 --- a/src/engine/config.h +++ b/src/engine/config.h @@ -10,6 +10,7 @@ class IConfigManager : public IInterface MACRO_INTERFACE("config") public: typedef void (*SAVECALLBACKFUNC)(IConfigManager *pConfig, void *pUserData); + typedef void (*POSSIBLECFGFUNC)(const struct SConfigVariable *, void *pUserData); virtual void Init() = 0; virtual void Reset(const char *pScriptName) = 0; @@ -23,6 +24,8 @@ class IConfigManager : public IInterface virtual void WriteLine(const char *pLine) = 0; virtual void StoreUnknownCommand(const char *pCommand) = 0; + + virtual void PossibleConfigVariables(const char *pStr, int FlagMask, POSSIBLECFGFUNC pfnCallback, void *pUserData) = 0; }; extern IConfigManager *CreateConfigManager(); diff --git a/src/engine/console.h b/src/engine/console.h index 774e063583..23996c95e9 100644 --- a/src/engine/console.h +++ b/src/engine/console.h @@ -30,7 +30,7 @@ class IConsole : public IInterface ACCESS_LEVEL_HELPER, ACCESS_LEVEL_USER, - TEMPCMD_NAME_LENGTH = 32, + TEMPCMD_NAME_LENGTH = 64, TEMPCMD_HELP_LENGTH = 192, TEMPCMD_PARAMS_LENGTH = 96, @@ -53,12 +53,12 @@ class IConsole : public IInterface virtual int GetInteger(unsigned Index) const = 0; virtual float GetFloat(unsigned Index) const = 0; virtual const char *GetString(unsigned Index) const = 0; - virtual ColorHSLA GetColor(unsigned Index, bool Light) const = 0; + virtual std::optional GetColor(unsigned Index, float DarkestLighting) const = 0; virtual void RemoveArgument(unsigned Index) = 0; int NumArguments() const { return m_NumArgs; } - int m_ClientID; + int m_ClientId; // DDRace @@ -82,7 +82,7 @@ class IConsole : public IInterface int GetAccessLevel() const { return m_AccessLevel; } }; - typedef void (*FTeeHistorianCommandCallback)(int ClientID, int FlagMask, const char *pCmd, IResult *pResult, void *pUser); + typedef void (*FTeeHistorianCommandCallback)(int ClientId, int FlagMask, const char *pCmd, IResult *pResult, void *pUser); typedef void (*FPrintCallback)(const char *pStr, void *pUser, ColorRGBA PrintColor); typedef void (*FPossibleCallback)(int Index, const char *pCmd, void *pUser); typedef void (*FCommandCallback)(IResult *pResult, void *pUserData); @@ -106,10 +106,10 @@ class IConsole : public IInterface virtual void StoreCommands(bool Store) = 0; virtual bool LineIsValid(const char *pStr) = 0; - virtual void ExecuteLine(const char *pStr, int ClientID = -1, bool InterpretSemicolons = true) = 0; - virtual void ExecuteLineFlag(const char *pStr, int FlasgMask, int ClientID = -1, bool InterpretSemicolons = true) = 0; - virtual void ExecuteLineStroked(int Stroke, const char *pStr, int ClientID = -1, bool InterpretSemicolons = true) = 0; - virtual bool ExecuteFile(const char *pFilename, int ClientID = -1, bool LogFailure = false, int StorageType = IStorage::TYPE_ALL) = 0; + virtual void ExecuteLine(const char *pStr, int ClientId = -1, bool InterpretSemicolons = true) = 0; + virtual void ExecuteLineFlag(const char *pStr, int FlasgMask, int ClientId = -1, bool InterpretSemicolons = true) = 0; + virtual void ExecuteLineStroked(int Stroke, const char *pStr, int ClientId = -1, bool InterpretSemicolons = true) = 0; + virtual bool ExecuteFile(const char *pFilename, int ClientId = -1, bool LogFailure = false, int StorageType = IStorage::TYPE_ALL) = 0; virtual char *Format(char *pBuf, int Size, const char *pFrom, const char *pStr) = 0; virtual void Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor = gs_ConsoleDefaultColor) const = 0; diff --git a/src/engine/console.rs b/src/engine/console.rs index 055c0802fa..624127bc06 100644 --- a/src/engine/console.rs +++ b/src/engine/console.rs @@ -177,29 +177,29 @@ mod ffi { /// # unsafe { *user.cast_mut::() = true; } /// let result: &IConsole_IResult /* = `command "$f00" $ffa500 $12345 shiny cyan -16777216 $f008 $00ffff80` */; /// # result = result_param; - /// assert_eq!(result.GetColor(0, false), ColorHSLA { h: 0.0, s: 1.0, l: 0.5, a: 1.0 }); // red - /// assert_eq!(result.GetColor(1, false), ColorHSLA { h: 0.10784314, s: 1.0, l: 0.5, a: 1.0 }); // DDNet logo color - /// assert_eq!(result.GetColor(2, false), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // cannot be parsed => black - /// assert_eq!(result.GetColor(3, false), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // unknown color name => black - /// assert_eq!(result.GetColor(4, false), ColorHSLA { h: 0.5, s: 1.0, l: 0.5, a: 1.0 }); // cyan - /// assert_eq!(result.GetColor(5, false), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // black - /// assert_eq!(result.GetColor(6, false), ColorHSLA { h: 0.0, s: 1.0, l: 0.5, a: 0.53333336 }); // red with alpha specified - /// assert_eq!(result.GetColor(7, false), ColorHSLA { h: 0.5, s: 1.0, l: 0.5, a: 0.5019608 }); // cyan with alpha specified - /// assert_eq!(result.GetColor(8, false), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // out of range => black - /// - /// assert_eq!(result.GetColor(0, true), result.GetColor(0, false)); - /// assert_eq!(result.GetColor(1, true), result.GetColor(1, false)); - /// assert_eq!(result.GetColor(2, true), result.GetColor(2, false)); - /// assert_eq!(result.GetColor(3, true), result.GetColor(3, false)); - /// assert_eq!(result.GetColor(4, true), result.GetColor(4, false)); - /// assert_eq!(result.GetColor(5, true), ColorHSLA { h: 0.0, s: 0.0, l: 0.5, a: 1.0 }); // black, but has the `Light` parameter set - /// assert_eq!(result.GetColor(6, true), result.GetColor(6, false)); - /// assert_eq!(result.GetColor(7, true), result.GetColor(7, false)); - /// assert_eq!(result.GetColor(8, true), result.GetColor(8, false)); + /// assert_eq!(result.GetColor(0, 0.0), ColorHSLA { h: 0.0, s: 1.0, l: 0.5, a: 1.0 }); // red + /// assert_eq!(result.GetColor(1, 0.0), ColorHSLA { h: 0.10784314, s: 1.0, l: 0.5, a: 1.0 }); // DDNet logo color + /// assert_eq!(result.GetColor(2, 0.0), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // cannot be parsed => black + /// assert_eq!(result.GetColor(3, 0.0), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // unknown color name => black + /// assert_eq!(result.GetColor(4, 0.0), ColorHSLA { h: 0.5, s: 1.0, l: 0.5, a: 1.0 }); // cyan + /// assert_eq!(result.GetColor(5, 0.0), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // black + /// assert_eq!(result.GetColor(6, 0.0), ColorHSLA { h: 0.0, s: 1.0, l: 0.5, a: 0.53333336 }); // red with alpha specified + /// assert_eq!(result.GetColor(7, 0.0), ColorHSLA { h: 0.5, s: 1.0, l: 0.5, a: 0.5019608 }); // cyan with alpha specified + /// assert_eq!(result.GetColor(8, 0.0), ColorHSLA { h: 0.0, s: 0.0, l: 0.0, a: 1.0 }); // out of range => black + /// + /// assert_eq!(result.GetColor(0, 0.5), result.GetColor(0, 0.0)); + /// assert_eq!(result.GetColor(1, 0.5), result.GetColor(1, 0.0)); + /// assert_eq!(result.GetColor(2, 0.5), result.GetColor(2, 0.0)); + /// assert_eq!(result.GetColor(3, 0.5), result.GetColor(3, 0.0)); + /// assert_eq!(result.GetColor(4, 0.5), result.GetColor(4, 0.0)); + /// assert_eq!(result.GetColor(5, 0.5), ColorHSLA { h: 0.0, s: 0.0, l: 0.5, a: 1.0 }); // black, but has the `Light` parameter set + /// assert_eq!(result.GetColor(6, 0.5), result.GetColor(6, 0.0)); + /// assert_eq!(result.GetColor(7, 0.5), result.GetColor(7, 0.0)); + /// assert_eq!(result.GetColor(8, 0.5), result.GetColor(8, 0.0)); /// # } /// # assert!(executed); /// ``` - pub fn GetColor(self: &IConsole_IResult, Index: u32, Light: bool) -> ColorHSLA; + pub fn GetColor(self: &IConsole_IResult, Index: u32, DarkestLighting: f32) -> ColorHSLA; /// Get the number of parameters passed. /// diff --git a/src/engine/demo.h b/src/engine/demo.h index f42193e9b0..9a0e74d039 100644 --- a/src/engine/demo.h +++ b/src/engine/demo.h @@ -91,6 +91,7 @@ class IDemoPlayer : public IInterface virtual int SetPos(int WantedTick) = 0; virtual void Pause() = 0; virtual void Unpause() = 0; + virtual const char *ErrorMessage() const = 0; virtual bool IsPlaying() const = 0; virtual const CInfo *BaseInfo() const = 0; virtual void GetDemoName(char *pBuffer, size_t BufferSize) const = 0; @@ -101,18 +102,24 @@ class IDemoRecorder : public IInterface { MACRO_INTERFACE("demorecorder") public: + enum class EStopMode + { + KEEP_FILE, + REMOVE_FILE, + }; + virtual ~IDemoRecorder() {} virtual bool IsRecording() const = 0; - virtual int Stop() = 0; + virtual int Stop(IDemoRecorder::EStopMode Mode, const char *pTargetFilename = "") = 0; virtual int Length() const = 0; - virtual char *GetCurrentFilename() = 0; + virtual const char *CurrentFilename() const = 0; }; class IDemoEditor : public IInterface { MACRO_INTERFACE("demoeditor") public: - virtual void Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser) = 0; + virtual bool Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser) = 0; }; #endif diff --git a/src/engine/discord.h b/src/engine/discord.h index aed8a15f2e..5b5f0a8269 100644 --- a/src/engine/discord.h +++ b/src/engine/discord.h @@ -9,12 +9,11 @@ class IDiscord : public IInterface MACRO_INTERFACE("discord") public: virtual void Update() = 0; - virtual void Start() = 0; virtual void ClearGameInfo() = 0; - virtual void SetGameInfo(const NETADDR &ServerAddr, const char *pMapName, bool AnnounceAddr, const char *pText, const char *pImage, const char *pPlayerName) = 0; + virtual void SetGameInfo(const NETADDR &ServerAddr, const char *pMapName, bool AnnounceAddr) = 0; }; IDiscord *CreateDiscord(); -#endif // ENGINE_DISCORD_H \ No newline at end of file +#endif // ENGINE_DISCORD_H diff --git a/src/engine/engine.h b/src/engine/engine.h index 937a2a2722..c763e7f6ed 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -4,25 +4,24 @@ #define ENGINE_ENGINE_H #include "kernel.h" -#include + +#include class CFutureLogger; +class IJob; class ILogger; class IEngine : public IInterface { MACRO_INTERFACE("engine") -protected: - class CJobPool m_JobPool; - public: virtual ~IEngine() = default; virtual void Init() = 0; virtual void AddJob(std::shared_ptr pJob) = 0; + virtual void ShutdownJobs() = 0; virtual void SetAdditionalLogger(std::shared_ptr &&pLogger) = 0; - static void RunJobBlocking(IJob *pJob); }; extern IEngine *CreateEngine(const char *pAppname, std::shared_ptr pFutureLogger, int Jobs); diff --git a/src/engine/gfx/image.cpp b/src/engine/gfx/image.cpp new file mode 100644 index 0000000000..78475aae06 --- /dev/null +++ b/src/engine/gfx/image.cpp @@ -0,0 +1,120 @@ +#include + +#include + +void CImageInfo::Free() +{ + m_Width = 0; + m_Height = 0; + m_Format = FORMAT_UNDEFINED; + free(m_pData); + m_pData = nullptr; +} + +size_t CImageInfo::PixelSize(EImageFormat Format) +{ + dbg_assert(Format != FORMAT_UNDEFINED, "Format undefined"); + static const size_t s_aSizes[] = {3, 4, 1, 2}; + return s_aSizes[(int)Format]; +} + +const char *CImageInfo::FormatName(EImageFormat Format) +{ + static const char *s_apNames[] = {"UNDEFINED", "RGBA", "RGB", "R", "RA"}; + return s_apNames[(int)Format + 1]; +} + +size_t CImageInfo::PixelSize() const +{ + return PixelSize(m_Format); +} + +const char *CImageInfo::FormatName() const +{ + return FormatName(m_Format); +} + +size_t CImageInfo::DataSize() const +{ + return m_Width * m_Height * PixelSize(m_Format); +} + +bool CImageInfo::DataEquals(const CImageInfo &Other) const +{ + if(m_Width != Other.m_Width || m_Height != Other.m_Height || m_Format != Other.m_Format) + return false; + if(m_pData == nullptr && Other.m_pData == nullptr) + return true; + if(m_pData == nullptr || Other.m_pData == nullptr) + return false; + return mem_comp(m_pData, Other.m_pData, DataSize()) == 0; +} + +ColorRGBA CImageInfo::PixelColor(size_t x, size_t y) const +{ + const size_t PixelStartIndex = (x + m_Width * y) * PixelSize(); + + ColorRGBA Color; + if(m_Format == FORMAT_R) + { + Color.r = Color.g = Color.b = 1.0f; + Color.a = m_pData[PixelStartIndex] / 255.0f; + } + else if(m_Format == FORMAT_RA) + { + Color.r = Color.g = Color.b = m_pData[PixelStartIndex] / 255.0f; + Color.a = m_pData[PixelStartIndex + 1] / 255.0f; + } + else + { + Color.r = m_pData[PixelStartIndex + 0] / 255.0f; + Color.g = m_pData[PixelStartIndex + 1] / 255.0f; + Color.b = m_pData[PixelStartIndex + 2] / 255.0f; + if(m_Format == FORMAT_RGBA) + { + Color.a = m_pData[PixelStartIndex + 3] / 255.0f; + } + else + { + Color.a = 1.0f; + } + } + return Color; +} + +void CImageInfo::SetPixelColor(size_t x, size_t y, ColorRGBA Color) const +{ + const size_t PixelStartIndex = (x + m_Width * y) * PixelSize(); + + if(m_Format == FORMAT_R) + { + m_pData[PixelStartIndex] = round_to_int(Color.a * 255.0f); + } + else if(m_Format == FORMAT_RA) + { + m_pData[PixelStartIndex] = round_to_int((Color.r + Color.g + Color.b) / 3.0f * 255.0f); + m_pData[PixelStartIndex + 1] = round_to_int(Color.a * 255.0f); + } + else + { + m_pData[PixelStartIndex + 0] = round_to_int(Color.r * 255.0f); + m_pData[PixelStartIndex + 1] = round_to_int(Color.g * 255.0f); + m_pData[PixelStartIndex + 2] = round_to_int(Color.b * 255.0f); + if(m_Format == FORMAT_RGBA) + { + m_pData[PixelStartIndex + 3] = round_to_int(Color.a * 255.0f); + } + } +} + +void CImageInfo::CopyRectFrom(const CImageInfo &SrcImage, size_t SrcX, size_t SrcY, size_t Width, size_t Height, size_t DestX, size_t DestY) const +{ + const size_t PixelSize = SrcImage.PixelSize(); + const size_t CopySize = Width * PixelSize; + for(size_t Y = 0; Y < Height; ++Y) + { + const size_t SrcOffset = ((SrcY + Y) * SrcImage.m_Width + SrcX) * PixelSize; + const size_t DestOffset = ((DestY + Y) * m_Width + DestX) * PixelSize; + mem_copy(&m_pData[DestOffset], &SrcImage.m_pData[SrcOffset], CopySize); + } +} diff --git a/src/engine/gfx/image_loader.cpp b/src/engine/gfx/image_loader.cpp index eefb597cdb..79d95956d8 100644 --- a/src/engine/gfx/image_loader.cpp +++ b/src/engine/gfx/image_loader.cpp @@ -1,89 +1,94 @@ #include "image_loader.h" + #include #include + #include #include #include -struct SLibPNGWarningItem +bool CByteBufferReader::Read(void *pData, size_t Size) { - SImageByteBuffer *m_pByteLoader; - const char *m_pFileName; - std::jmp_buf m_Buf; -}; + if(m_Error) + return false; -[[noreturn]] static void LibPNGError(png_structp png_ptr, png_const_charp error_msg) -{ - SLibPNGWarningItem *pUserStruct = (SLibPNGWarningItem *)png_get_error_ptr(png_ptr); - pUserStruct->m_pByteLoader->m_Err = -1; - dbg_msg("png", "error for file \"%s\": %s", pUserStruct->m_pFileName, error_msg); - std::longjmp(pUserStruct->m_Buf, 1); + if(m_ReadOffset + Size <= m_Size) + { + mem_copy(pData, &m_pData[m_ReadOffset], Size); + m_ReadOffset += Size; + return true; + } + else + { + m_Error = true; + return false; + } } -static void LibPNGWarning(png_structp png_ptr, png_const_charp warning_msg) +void CByteBufferWriter::Write(const void *pData, size_t Size) { - SLibPNGWarningItem *pUserStruct = (SLibPNGWarningItem *)png_get_error_ptr(png_ptr); - dbg_msg("png", "warning for file \"%s\": %s", pUserStruct->m_pFileName, warning_msg); -} + if(!Size) + return; -static bool FileMatchesImageType(SImageByteBuffer &ByteLoader) -{ - if(ByteLoader.m_pvLoadedImageBytes->size() >= 8) - return png_sig_cmp((png_bytep)ByteLoader.m_pvLoadedImageBytes->data(), 0, 8) == 0; - return false; + const size_t WriteOffset = m_vBuffer.size(); + m_vBuffer.resize(WriteOffset + Size); + mem_copy(&m_vBuffer[WriteOffset], pData, Size); } -static void ReadDataFromLoadedBytes(png_structp pPNGStruct, png_bytep pOutBytes, png_size_t ByteCountToRead) +class CUserErrorStruct { - png_voidp pIO_Ptr = png_get_io_ptr(pPNGStruct); +public: + CByteBufferReader *m_pReader; + const char *m_pContextName; + std::jmp_buf m_JmpBuf; +}; - SImageByteBuffer *pByteLoader = (SImageByteBuffer *)pIO_Ptr; +[[noreturn]] static void PngErrorCallback(png_structp pPngStruct, png_const_charp pErrorMessage) +{ + CUserErrorStruct *pUserStruct = static_cast(png_get_error_ptr(pPngStruct)); + log_error("png", "error for file \"%s\": %s", pUserStruct->m_pContextName, pErrorMessage); + std::longjmp(pUserStruct->m_JmpBuf, 1); +} - if(pByteLoader->m_pvLoadedImageBytes->size() >= pByteLoader->m_LoadOffset + (size_t)ByteCountToRead) - { - mem_copy(pOutBytes, &(*pByteLoader->m_pvLoadedImageBytes)[pByteLoader->m_LoadOffset], (size_t)ByteCountToRead); +static void PngWarningCallback(png_structp pPngStruct, png_const_charp pWarningMessage) +{ + CUserErrorStruct *pUserStruct = static_cast(png_get_error_ptr(pPngStruct)); + log_warn("png", "warning for file \"%s\": %s", pUserStruct->m_pContextName, pWarningMessage); +} - pByteLoader->m_LoadOffset += (size_t)ByteCountToRead; - } - else +static void PngReadDataCallback(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToRead) +{ + CByteBufferReader *pReader = static_cast(png_get_io_ptr(pPngStruct)); + if(!pReader->Read(pOutBytes, ByteCountToRead)) { - pByteLoader->m_Err = -1; - dbg_msg("png", "could not read bytes, file was too small."); + png_error(pPngStruct, "Could not read all bytes, file was too small"); } } -static EImageFormat LibPNGGetImageFormat(int ColorChannelCount) +static CImageInfo::EImageFormat ImageFormatFromChannelCount(int ColorChannelCount) { switch(ColorChannelCount) { case 1: - return IMAGE_FORMAT_R; + return CImageInfo::FORMAT_R; case 2: - return IMAGE_FORMAT_RA; + return CImageInfo::FORMAT_RA; case 3: - return IMAGE_FORMAT_RGB; + return CImageInfo::FORMAT_RGB; case 4: - return IMAGE_FORMAT_RGBA; + return CImageInfo::FORMAT_RGBA; default: dbg_assert(false, "ColorChannelCount invalid"); dbg_break(); } } -static void LibPNGDeleteReadStruct(png_structp pPNGStruct, png_infop pPNGInfo) -{ - if(pPNGInfo != nullptr) - png_destroy_info_struct(pPNGStruct, &pPNGInfo); - png_destroy_read_struct(&pPNGStruct, nullptr, nullptr); -} - -static int PngliteIncompatibility(png_structp pPNGStruct, png_infop pPNGInfo) +static int PngliteIncompatibility(png_structp pPngStruct, png_infop pPngInfo) { - int ColorType = png_get_color_type(pPNGStruct, pPNGInfo); - int BitDepth = png_get_bit_depth(pPNGStruct, pPNGInfo); - int InterlaceType = png_get_interlace_type(pPNGStruct, pPNGInfo); int Result = 0; + + const int ColorType = png_get_color_type(pPngStruct, pPngInfo); switch(ColorType) { case PNG_COLOR_TYPE_GRAY: @@ -93,9 +98,10 @@ static int PngliteIncompatibility(png_structp pPNGStruct, png_infop pPNGInfo) break; default: log_debug("png", "color type %d unsupported by pnglite", ColorType); - Result |= PNGLITE_COLOR_TYPE; + Result |= CImageLoader::PNGLITE_COLOR_TYPE; } + const int BitDepth = png_get_bit_depth(pPngStruct, pPngInfo); switch(BitDepth) { case 8: @@ -103,125 +109,137 @@ static int PngliteIncompatibility(png_structp pPNGStruct, png_infop pPNGInfo) break; default: log_debug("png", "bit depth %d unsupported by pnglite", BitDepth); - Result |= PNGLITE_BIT_DEPTH; + Result |= CImageLoader::PNGLITE_BIT_DEPTH; } + const int InterlaceType = png_get_interlace_type(pPngStruct, pPngInfo); if(InterlaceType != PNG_INTERLACE_NONE) { log_debug("png", "interlace type %d unsupported by pnglite", InterlaceType); - Result |= PNGLITE_INTERLACE_TYPE; + Result |= CImageLoader::PNGLITE_INTERLACE_TYPE; } - if(png_get_compression_type(pPNGStruct, pPNGInfo) != PNG_COMPRESSION_TYPE_BASE) + + if(png_get_compression_type(pPngStruct, pPngInfo) != PNG_COMPRESSION_TYPE_BASE) { log_debug("png", "non-default compression type unsupported by pnglite"); - Result |= PNGLITE_COMPRESSION_TYPE; + Result |= CImageLoader::PNGLITE_COMPRESSION_TYPE; } - if(png_get_filter_type(pPNGStruct, pPNGInfo) != PNG_FILTER_TYPE_BASE) + + if(png_get_filter_type(pPngStruct, pPngInfo) != PNG_FILTER_TYPE_BASE) { log_debug("png", "non-default filter type unsupported by pnglite"); - Result |= PNGLITE_FILTER_TYPE; + Result |= CImageLoader::PNGLITE_FILTER_TYPE; } + return Result; } -bool LoadPNG(SImageByteBuffer &ByteLoader, const char *pFileName, int &PngliteIncompatible, int &Width, int &Height, uint8_t *&pImageBuff, EImageFormat &ImageFormat) +bool CImageLoader::LoadPng(CByteBufferReader &Reader, const char *pContextName, CImageInfo &Image, int &PngliteIncompatible) { - SLibPNGWarningItem UserErrorStruct = {&ByteLoader, pFileName, {}}; + CUserErrorStruct UserErrorStruct = {&Reader, pContextName, {}}; - png_structp pPNGStruct = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); - - if(pPNGStruct == nullptr) + png_structp pPngStruct = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if(pPngStruct == nullptr) { - dbg_msg("png", "libpng internal failure: png_create_read_struct failed."); + log_error("png", "libpng internal failure: png_create_read_struct failed."); return false; } - png_infop pPNGInfo = nullptr; + png_infop pPngInfo = nullptr; png_bytepp pRowPointers = nullptr; - Height = 0; // ensure this is not undefined for the error handler - if(setjmp(UserErrorStruct.m_Buf)) - { + int Height = 0; // ensure this is not undefined for the Cleanup function + const auto &&Cleanup = [&]() { if(pRowPointers != nullptr) { - for(int i = 0; i < Height; ++i) + for(int y = 0; y < Height; ++y) { - delete[] pRowPointers[i]; + delete[] pRowPointers[y]; } } delete[] pRowPointers; - LibPNGDeleteReadStruct(pPNGStruct, pPNGInfo); + if(pPngInfo != nullptr) + { + png_destroy_info_struct(pPngStruct, &pPngInfo); + } + png_destroy_read_struct(&pPngStruct, nullptr, nullptr); + }; + if(setjmp(UserErrorStruct.m_JmpBuf)) + { + Cleanup(); return false; } - png_set_error_fn(pPNGStruct, &UserErrorStruct, LibPNGError, LibPNGWarning); - - pPNGInfo = png_create_info_struct(pPNGStruct); + png_set_error_fn(pPngStruct, &UserErrorStruct, PngErrorCallback, PngWarningCallback); - if(pPNGInfo == nullptr) + pPngInfo = png_create_info_struct(pPngStruct); + if(pPngInfo == nullptr) { - png_destroy_read_struct(&pPNGStruct, nullptr, nullptr); - dbg_msg("png", "libpng internal failure: png_create_info_struct failed."); + Cleanup(); + log_error("png", "libpng internal failure: png_create_info_struct failed."); return false; } - if(!FileMatchesImageType(ByteLoader)) + png_byte aSignature[8]; + if(!Reader.Read(aSignature, sizeof(aSignature)) || png_sig_cmp(aSignature, 0, sizeof(aSignature)) != 0) { - LibPNGDeleteReadStruct(pPNGStruct, pPNGInfo); - dbg_msg("png", "file does not match image type."); + Cleanup(); + log_error("png", "file is not a valid PNG file (signature mismatch)."); return false; } - ByteLoader.m_LoadOffset = 8; + png_set_read_fn(pPngStruct, (png_bytep)&Reader, PngReadDataCallback); + png_set_sig_bytes(pPngStruct, sizeof(aSignature)); - png_set_read_fn(pPNGStruct, (png_bytep)&ByteLoader, ReadDataFromLoadedBytes); + png_read_info(pPngStruct, pPngInfo); - png_set_sig_bytes(pPNGStruct, 8); + if(Reader.Error()) + { + // error already logged + Cleanup(); + return false; + } - png_read_info(pPNGStruct, pPNGInfo); + const int Width = png_get_image_width(pPngStruct, pPngInfo); + Height = png_get_image_height(pPngStruct, pPngInfo); + const png_byte BitDepth = png_get_bit_depth(pPngStruct, pPngInfo); + const int ColorType = png_get_color_type(pPngStruct, pPngInfo); - if(ByteLoader.m_Err != 0) + if(Width == 0 || Height == 0) { - LibPNGDeleteReadStruct(pPNGStruct, pPNGInfo); - dbg_msg("png", "byte loader error."); + log_error("png", "image has width (%d) or height (%d) of 0.", Width, Height); + Cleanup(); return false; } - Width = png_get_image_width(pPNGStruct, pPNGInfo); - Height = png_get_image_height(pPNGStruct, pPNGInfo); - const int ColorType = png_get_color_type(pPNGStruct, pPNGInfo); - const png_byte BitDepth = png_get_bit_depth(pPNGStruct, pPNGInfo); - PngliteIncompatible = PngliteIncompatibility(pPNGStruct, pPNGInfo); - if(BitDepth == 16) { - png_set_strip_16(pPNGStruct); + png_set_strip_16(pPngStruct); } - else if(BitDepth > 8) + else if(BitDepth > 8 || BitDepth == 0) { - dbg_msg("png", "non supported bit depth."); - LibPNGDeleteReadStruct(pPNGStruct, pPNGInfo); + log_error("png", "bit depth %d not supported.", BitDepth); + Cleanup(); return false; } - if(Width == 0 || Height == 0 || BitDepth == 0) + if(ColorType == PNG_COLOR_TYPE_PALETTE) { - dbg_msg("png", "image had width, height or bit depth of 0."); - LibPNGDeleteReadStruct(pPNGStruct, pPNGInfo); - return false; + png_set_palette_to_rgb(pPngStruct); } - if(ColorType == PNG_COLOR_TYPE_PALETTE) - png_set_palette_to_rgb(pPNGStruct); - if(ColorType == PNG_COLOR_TYPE_GRAY && BitDepth < 8) - png_set_expand_gray_1_2_4_to_8(pPNGStruct); + { + png_set_expand_gray_1_2_4_to_8(pPngStruct); + } - if(png_get_valid(pPNGStruct, pPNGInfo, PNG_INFO_tRNS)) - png_set_tRNS_to_alpha(pPNGStruct); + if(png_get_valid(pPngStruct, pPngInfo, PNG_INFO_tRNS)) + { + png_set_tRNS_to_alpha(pPngStruct); + } - png_read_update_info(pPNGStruct, pPNGInfo); + png_read_update_info(pPngStruct, pPngInfo); - const int ColorChannelCount = png_get_channels(pPNGStruct, pPNGInfo); - const int BytesInRow = png_get_rowbytes(pPNGStruct, pPNGInfo); + const int ColorChannelCount = png_get_channels(pPngStruct, pPngInfo); + const int BytesInRow = png_get_rowbytes(pPngStruct, pPngInfo); dbg_assert(BytesInRow == Width * ColorChannelCount, "bytes in row incorrect."); pRowPointers = new png_bytep[Height]; @@ -230,131 +248,155 @@ bool LoadPNG(SImageByteBuffer &ByteLoader, const char *pFileName, int &PngliteIn pRowPointers[y] = new png_byte[BytesInRow]; } - png_read_image(pPNGStruct, pRowPointers); - - if(ByteLoader.m_Err == 0) - pImageBuff = (uint8_t *)malloc((size_t)Height * (size_t)Width * (size_t)ColorChannelCount * sizeof(uint8_t)); + png_read_image(pPngStruct, pRowPointers); - for(int i = 0; i < Height; ++i) + if(!Reader.Error()) { - if(ByteLoader.m_Err == 0) - mem_copy(&pImageBuff[i * BytesInRow], pRowPointers[i], BytesInRow); - delete[] pRowPointers[i]; + Image.m_Width = Width; + Image.m_Height = Height; + Image.m_Format = ImageFormatFromChannelCount(ColorChannelCount); + Image.m_pData = static_cast(malloc(Image.DataSize())); + for(int y = 0; y < Height; ++y) + { + mem_copy(&Image.m_pData[y * BytesInRow], pRowPointers[y], BytesInRow); + } + PngliteIncompatible = PngliteIncompatibility(pPngStruct, pPngInfo); } - delete[] pRowPointers; - pRowPointers = nullptr; - if(ByteLoader.m_Err != 0) + Cleanup(); + + return !Reader.Error(); +} + +bool CImageLoader::LoadPng(IOHANDLE File, const char *pFilename, CImageInfo &Image, int &PngliteIncompatible) +{ + if(!File) { - LibPNGDeleteReadStruct(pPNGStruct, pPNGInfo); - dbg_msg("png", "byte loader error."); + log_error("png", "failed to open file for reading. filename='%s'", pFilename); return false; } - ImageFormat = LibPNGGetImageFormat(ColorChannelCount); + void *pFileData; + unsigned FileDataSize; + io_read_all(File, &pFileData, &FileDataSize); + io_close(File); - png_destroy_info_struct(pPNGStruct, &pPNGInfo); - png_destroy_read_struct(&pPNGStruct, nullptr, nullptr); - - return true; -} + CByteBufferReader ImageReader(static_cast(pFileData), FileDataSize); -static void WriteDataFromLoadedBytes(png_structp pPNGStruct, png_bytep pOutBytes, png_size_t ByteCountToWrite) -{ - if(ByteCountToWrite > 0) + const bool LoadResult = CImageLoader::LoadPng(ImageReader, pFilename, Image, PngliteIncompatible); + free(pFileData); + if(!LoadResult) { - png_voidp pIO_Ptr = png_get_io_ptr(pPNGStruct); + log_error("png", "failed to load image from file. filename='%s'", pFilename); + return false; + } - SImageByteBuffer *pByteLoader = (SImageByteBuffer *)pIO_Ptr; + if(Image.m_Format != CImageInfo::FORMAT_RGB && Image.m_Format != CImageInfo::FORMAT_RGBA) + { + log_error("png", "image has unsupported format. filename='%s' format='%s'", pFilename, Image.FormatName()); + Image.Free(); + return false; + } - size_t NewSize = pByteLoader->m_LoadOffset + (size_t)ByteCountToWrite; - pByteLoader->m_pvLoadedImageBytes->resize(NewSize); + return true; +} - mem_copy(&(*pByteLoader->m_pvLoadedImageBytes)[pByteLoader->m_LoadOffset], pOutBytes, (size_t)ByteCountToWrite); - pByteLoader->m_LoadOffset = NewSize; - } +static void PngWriteDataCallback(png_structp pPngStruct, png_bytep pOutBytes, png_size_t ByteCountToWrite) +{ + CByteBufferWriter *pWriter = static_cast(png_get_io_ptr(pPngStruct)); + pWriter->Write(pOutBytes, ByteCountToWrite); } -static void FlushPNGWrite(png_structp png_ptr) {} +static void PngOutputFlushCallback(png_structp pPngStruct) +{ + // no need to flush memory buffer +} -static int ImageLoaderHelperFormatToColorChannel(EImageFormat Format) +static int PngColorTypeFromFormat(CImageInfo::EImageFormat Format) { switch(Format) { - case IMAGE_FORMAT_R: - return 1; - case IMAGE_FORMAT_RA: - return 2; - case IMAGE_FORMAT_RGB: - return 3; - case IMAGE_FORMAT_RGBA: - return 4; + case CImageInfo::FORMAT_R: + return PNG_COLOR_TYPE_GRAY; + case CImageInfo::FORMAT_RA: + return PNG_COLOR_TYPE_GRAY_ALPHA; + case CImageInfo::FORMAT_RGB: + return PNG_COLOR_TYPE_RGB; + case CImageInfo::FORMAT_RGBA: + return PNG_COLOR_TYPE_RGBA; default: dbg_assert(false, "Format invalid"); dbg_break(); } } -bool SavePNG(EImageFormat ImageFormat, const uint8_t *pRawBuffer, SImageByteBuffer &WrittenBytes, int Width, int Height) +bool CImageLoader::SavePng(CByteBufferWriter &Writer, const CImageInfo &Image) { - png_structp pPNGStruct = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); - - if(pPNGStruct == nullptr) + png_structp pPngStruct = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if(pPngStruct == nullptr) { - dbg_msg("png", "libpng internal failure: png_create_write_struct failed."); + log_error("png", "libpng internal failure: png_create_write_struct failed."); return false; } - png_infop pPNGInfo = png_create_info_struct(pPNGStruct); - - if(pPNGInfo == nullptr) + png_infop pPngInfo = png_create_info_struct(pPngStruct); + if(pPngInfo == nullptr) { - png_destroy_read_struct(&pPNGStruct, nullptr, nullptr); - dbg_msg("png", "libpng internal failure: png_create_info_struct failed."); + png_destroy_read_struct(&pPngStruct, nullptr, nullptr); + log_error("png", "libpng internal failure: png_create_info_struct failed."); return false; } - WrittenBytes.m_LoadOffset = 0; - WrittenBytes.m_pvLoadedImageBytes->clear(); + png_set_write_fn(pPngStruct, (png_bytep)&Writer, PngWriteDataCallback, PngOutputFlushCallback); - png_set_write_fn(pPNGStruct, (png_bytep)&WrittenBytes, WriteDataFromLoadedBytes, FlushPNGWrite); + png_set_IHDR(pPngStruct, pPngInfo, Image.m_Width, Image.m_Height, 8, PngColorTypeFromFormat(Image.m_Format), PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + png_write_info(pPngStruct, pPngInfo); - int ColorType = PNG_COLOR_TYPE_RGB; - int WriteBytesPerPixel = ImageLoaderHelperFormatToColorChannel(ImageFormat); - if(ImageFormat == IMAGE_FORMAT_R) + png_bytepp pRowPointers = new png_bytep[Image.m_Height]; + const int WidthBytes = Image.m_Width * Image.PixelSize(); + ptrdiff_t BufferOffset = 0; + for(size_t y = 0; y < Image.m_Height; ++y) { - ColorType = PNG_COLOR_TYPE_GRAY; + pRowPointers[y] = new png_byte[WidthBytes]; + mem_copy(pRowPointers[y], Image.m_pData + BufferOffset, WidthBytes); + BufferOffset += (ptrdiff_t)WidthBytes; } - else if(ImageFormat == IMAGE_FORMAT_RGBA) + png_write_image(pPngStruct, pRowPointers); + png_write_end(pPngStruct, pPngInfo); + + for(size_t y = 0; y < Image.m_Height; ++y) { - ColorType = PNG_COLOR_TYPE_RGBA; + delete[] pRowPointers[y]; } + delete[] pRowPointers; - png_set_IHDR(pPNGStruct, pPNGInfo, Width, Height, 8, ColorType, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE); + png_destroy_info_struct(pPngStruct, &pPngInfo); + png_destroy_write_struct(&pPngStruct, nullptr); - png_write_info(pPNGStruct, pPNGInfo); + return true; +} - png_bytepp pRowPointers = new png_bytep[Height]; - int WidthBytes = Width * WriteBytesPerPixel; - ptrdiff_t BufferOffset = 0; - for(int y = 0; y < Height; ++y) +bool CImageLoader::SavePng(IOHANDLE File, const char *pFilename, const CImageInfo &Image) +{ + if(!File) { - pRowPointers[y] = new png_byte[WidthBytes]; - mem_copy(pRowPointers[y], pRawBuffer + BufferOffset, WidthBytes); - BufferOffset += (ptrdiff_t)WidthBytes; + log_error("png", "failed to open file for writing. filename='%s'", pFilename); + return false; } - png_write_image(pPNGStruct, pRowPointers); - - png_write_end(pPNGStruct, pPNGInfo); - for(int y = 0; y < Height; ++y) + CByteBufferWriter Writer; + if(!CImageLoader::SavePng(Writer, Image)) { - delete[](pRowPointers[y]); + // error already logged + io_close(File); + return false; } - delete[](pRowPointers); - - png_destroy_info_struct(pPNGStruct, &pPNGInfo); - png_destroy_write_struct(&pPNGStruct, nullptr); - return true; + const bool WriteSuccess = io_write(File, Writer.Data(), Writer.Size()) == Writer.Size(); + if(!WriteSuccess) + { + log_error("png", "failed to write PNG data to file. filename='%s'", pFilename); + } + io_close(File); + return WriteSuccess; } diff --git a/src/engine/gfx/image_loader.h b/src/engine/gfx/image_loader.h index 0da2474a0f..c2658a6e3c 100644 --- a/src/engine/gfx/image_loader.h +++ b/src/engine/gfx/image_loader.h @@ -1,38 +1,57 @@ #ifndef ENGINE_GFX_IMAGE_LOADER_H #define ENGINE_GFX_IMAGE_LOADER_H -#include -#include +#include + +#include + #include -enum EImageFormat +class CByteBufferReader { - IMAGE_FORMAT_R = 0, - IMAGE_FORMAT_RA, - IMAGE_FORMAT_RGB, - IMAGE_FORMAT_RGBA, + const uint8_t *m_pData; + size_t m_Size; + size_t m_ReadOffset = 0; + bool m_Error = false; + +public: + CByteBufferReader(const uint8_t *pData, size_t Size) : + m_pData(pData), + m_Size(Size) {} + + bool Read(void *pData, size_t Size); + bool Error() const { return m_Error; } }; -typedef std::vector TImageByteBuffer; -struct SImageByteBuffer +class CByteBufferWriter { - SImageByteBuffer(std::vector *pvBuff) : - m_LoadOffset(0), m_pvLoadedImageBytes(pvBuff), m_Err(0) {} - size_t m_LoadOffset; - std::vector *m_pvLoadedImageBytes; - int m_Err; + std::vector m_vBuffer; + +public: + void Write(const void *pData, size_t Size); + const uint8_t *Data() const { return m_vBuffer.data(); } + size_t Size() const { return m_vBuffer.size(); } }; -enum +class CImageLoader { - PNGLITE_COLOR_TYPE = 1 << 0, - PNGLITE_BIT_DEPTH = 1 << 1, - PNGLITE_INTERLACE_TYPE = 1 << 2, - PNGLITE_COMPRESSION_TYPE = 1 << 3, - PNGLITE_FILTER_TYPE = 1 << 4, -}; + CImageLoader() = delete; + +public: + enum + { + PNGLITE_COLOR_TYPE = 1 << 0, + PNGLITE_BIT_DEPTH = 1 << 1, + PNGLITE_INTERLACE_TYPE = 1 << 2, + PNGLITE_COMPRESSION_TYPE = 1 << 3, + PNGLITE_FILTER_TYPE = 1 << 4, + }; -bool LoadPNG(SImageByteBuffer &ByteLoader, const char *pFileName, int &PngliteIncompatible, int &Width, int &Height, uint8_t *&pImageBuff, EImageFormat &ImageFormat); -bool SavePNG(EImageFormat ImageFormat, const uint8_t *pRawBuffer, SImageByteBuffer &WrittenBytes, int Width, int Height); + static bool LoadPng(CByteBufferReader &Reader, const char *pContextName, CImageInfo &Image, int &PngliteIncompatible); + static bool LoadPng(IOHANDLE File, const char *pFilename, CImageInfo &Image, int &PngliteIncompatible); + + static bool SavePng(CByteBufferWriter &Writer, const CImageInfo &Image); + static bool SavePng(IOHANDLE File, const char *pFilename, const CImageInfo &Image); +}; #endif // ENGINE_GFX_IMAGE_LOADER_H diff --git a/src/engine/gfx/image_manipulation.cpp b/src/engine/gfx/image_manipulation.cpp index 992ab7311a..3b7ab1aac8 100644 --- a/src/engine/gfx/image_manipulation.cpp +++ b/src/engine/gfx/image_manipulation.cpp @@ -1,39 +1,117 @@ #include "image_manipulation.h" + #include #include -#define TW_DILATE_ALPHA_THRESHOLD 10 +bool ConvertToRgba(uint8_t *pDest, const CImageInfo &SourceImage) +{ + if(SourceImage.m_Format == CImageInfo::FORMAT_RGBA) + { + mem_copy(pDest, SourceImage.m_pData, SourceImage.DataSize()); + return true; + } + else + { + const size_t SrcChannelCount = CImageInfo::PixelSize(SourceImage.m_Format); + const size_t DstChannelCount = CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA); + for(size_t Y = 0; Y < SourceImage.m_Height; ++Y) + { + for(size_t X = 0; X < SourceImage.m_Width; ++X) + { + size_t ImgOffsetSrc = (Y * SourceImage.m_Width * SrcChannelCount) + (X * SrcChannelCount); + size_t ImgOffsetDest = (Y * SourceImage.m_Width * DstChannelCount) + (X * DstChannelCount); + if(SourceImage.m_Format == CImageInfo::FORMAT_RGB) + { + mem_copy(&pDest[ImgOffsetDest], &SourceImage.m_pData[ImgOffsetSrc], SrcChannelCount); + pDest[ImgOffsetDest + 3] = 255; + } + else if(SourceImage.m_Format == CImageInfo::FORMAT_RA) + { + pDest[ImgOffsetDest + 0] = SourceImage.m_pData[ImgOffsetSrc]; + pDest[ImgOffsetDest + 1] = SourceImage.m_pData[ImgOffsetSrc]; + pDest[ImgOffsetDest + 2] = SourceImage.m_pData[ImgOffsetSrc]; + pDest[ImgOffsetDest + 3] = SourceImage.m_pData[ImgOffsetSrc + 1]; + } + else if(SourceImage.m_Format == CImageInfo::FORMAT_R) + { + pDest[ImgOffsetDest + 0] = 255; + pDest[ImgOffsetDest + 1] = 255; + pDest[ImgOffsetDest + 2] = 255; + pDest[ImgOffsetDest + 3] = SourceImage.m_pData[ImgOffsetSrc]; + } + else + { + dbg_assert(false, "SourceImage.m_Format invalid"); + } + } + } + return false; + } +} + +bool ConvertToRgbaAlloc(uint8_t *&pDest, const CImageInfo &SourceImage) +{ + pDest = static_cast(malloc(SourceImage.m_Width * SourceImage.m_Height * CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA))); + return ConvertToRgba(pDest, SourceImage); +} + +bool ConvertToRgba(CImageInfo &Image) +{ + if(Image.m_Format == CImageInfo::FORMAT_RGBA) + return true; + + uint8_t *pRgbaData; + ConvertToRgbaAlloc(pRgbaData, Image); + free(Image.m_pData); + Image.m_pData = pRgbaData; + Image.m_Format = CImageInfo::FORMAT_RGBA; + return false; +} -static void Dilate(int w, int h, const unsigned char *pSrc, unsigned char *pDest, unsigned char AlphaThreshold = TW_DILATE_ALPHA_THRESHOLD) +void ConvertToGrayscale(const CImageInfo &Image) +{ + if(Image.m_Format == CImageInfo::FORMAT_R || Image.m_Format == CImageInfo::FORMAT_RA) + return; + + const size_t Step = Image.PixelSize(); + for(size_t i = 0; i < Image.m_Width * Image.m_Height; ++i) + { + const int Average = (Image.m_pData[i * Step] + Image.m_pData[i * Step + 1] + Image.m_pData[i * Step + 2]) / 3; + Image.m_pData[i * Step] = Average; + Image.m_pData[i * Step + 1] = Average; + Image.m_pData[i * Step + 2] = Average; + } +} + +static constexpr int DILATE_BPP = 4; // RGBA assumed +static constexpr uint8_t DILATE_ALPHA_THRESHOLD = 10; + +static void Dilate(int w, int h, const uint8_t *pSrc, uint8_t *pDest) { - const int BPP = 4; // RGBA assumed - int ix, iy; const int aDirX[] = {0, -1, 1, 0}; const int aDirY[] = {-1, 0, 0, 1}; - int AlphaCompIndex = BPP - 1; - int m = 0; for(int y = 0; y < h; y++) { - for(int x = 0; x < w; x++, m += BPP) + for(int x = 0; x < w; x++, m += DILATE_BPP) { - for(int i = 0; i < BPP; ++i) + for(int i = 0; i < DILATE_BPP; ++i) pDest[m + i] = pSrc[m + i]; - if(pSrc[m + AlphaCompIndex] > AlphaThreshold) + if(pSrc[m + DILATE_BPP - 1] > DILATE_ALPHA_THRESHOLD) continue; int aSumOfOpaque[] = {0, 0, 0}; int Counter = 0; for(int c = 0; c < 4; c++) { - ix = clamp(x + aDirX[c], 0, w - 1); - iy = clamp(y + aDirY[c], 0, h - 1); - int k = iy * w * BPP + ix * BPP; - if(pSrc[k + AlphaCompIndex] > AlphaThreshold) + const int ClampedX = clamp(x + aDirX[c], 0, w - 1); + const int ClampedY = clamp(y + aDirY[c], 0, h - 1); + const int SrcIndex = ClampedY * w * DILATE_BPP + ClampedX * DILATE_BPP; + if(pSrc[SrcIndex + DILATE_BPP - 1] > DILATE_ALPHA_THRESHOLD) { - for(int p = 0; p < BPP - 1; ++p) - aSumOfOpaque[p] += pSrc[k + p]; + for(int p = 0; p < DILATE_BPP - 1; ++p) + aSumOfOpaque[p] += pSrc[SrcIndex + p]; ++Counter; break; } @@ -41,77 +119,80 @@ static void Dilate(int w, int h, const unsigned char *pSrc, unsigned char *pDest if(Counter > 0) { - for(int i = 0; i < BPP - 1; ++i) + for(int i = 0; i < DILATE_BPP - 1; ++i) { aSumOfOpaque[i] /= Counter; - pDest[m + i] = (unsigned char)aSumOfOpaque[i]; + pDest[m + i] = (uint8_t)aSumOfOpaque[i]; } - pDest[m + AlphaCompIndex] = 255; + pDest[m + DILATE_BPP - 1] = 255; } } } } -static void CopyColorValues(int w, int h, int BPP, const unsigned char *pSrc, unsigned char *pDest) +static void CopyColorValues(int w, int h, const uint8_t *pSrc, uint8_t *pDest) { int m = 0; for(int y = 0; y < h; y++) { - for(int x = 0; x < w; x++, m += BPP) + for(int x = 0; x < w; x++, m += DILATE_BPP) { - for(int i = 0; i < BPP - 1; ++i) + if(pDest[m + DILATE_BPP - 1] == 0) { - if(pDest[m + 3] == 0) - pDest[m + i] = pSrc[m + i]; + mem_copy(&pDest[m], &pSrc[m], DILATE_BPP - 1); } } } } -void DilateImage(unsigned char *pImageBuff, int w, int h) +void DilateImage(uint8_t *pImageBuff, int w, int h) { DilateImageSub(pImageBuff, w, h, 0, 0, w, h); } -void DilateImageSub(unsigned char *pImageBuff, int w, int h, int x, int y, int sw, int sh) +void DilateImage(const CImageInfo &Image) { - const int BPP = 4; // RGBA assumed - unsigned char *apBuffer[2] = {NULL, NULL}; + dbg_assert(Image.m_Format == CImageInfo::FORMAT_RGBA, "Dilate requires RGBA format"); + DilateImage(Image.m_pData, Image.m_Width, Image.m_Height); +} - apBuffer[0] = (unsigned char *)malloc((size_t)sw * sh * sizeof(unsigned char) * BPP); - apBuffer[1] = (unsigned char *)malloc((size_t)sw * sh * sizeof(unsigned char) * BPP); - unsigned char *pBufferOriginal = (unsigned char *)malloc((size_t)sw * sh * sizeof(unsigned char) * BPP); +void DilateImageSub(uint8_t *pImageBuff, int w, int h, int x, int y, int SubWidth, int SubHeight) +{ + uint8_t *apBuffer[2] = {nullptr, nullptr}; - unsigned char *pPixelBuff = (unsigned char *)pImageBuff; + const size_t ImageSize = (size_t)SubWidth * SubHeight * sizeof(uint8_t) * DILATE_BPP; + apBuffer[0] = (uint8_t *)malloc(ImageSize); + apBuffer[1] = (uint8_t *)malloc(ImageSize); + uint8_t *pBufferOriginal = (uint8_t *)malloc(ImageSize); - for(int Y = 0; Y < sh; ++Y) + for(int Y = 0; Y < SubHeight; ++Y) { - int SrcImgOffset = ((y + Y) * w * BPP) + (x * BPP); - int DstImgOffset = (Y * sw * BPP); - int CopySize = sw * BPP; - mem_copy(&pBufferOriginal[DstImgOffset], &pPixelBuff[SrcImgOffset], CopySize); + int SrcImgOffset = ((y + Y) * w * DILATE_BPP) + (x * DILATE_BPP); + int DstImgOffset = (Y * SubWidth * DILATE_BPP); + int CopySize = SubWidth * DILATE_BPP; + mem_copy(&pBufferOriginal[DstImgOffset], &pImageBuff[SrcImgOffset], CopySize); } - Dilate(sw, sh, pBufferOriginal, apBuffer[0]); + Dilate(SubWidth, SubHeight, pBufferOriginal, apBuffer[0]); for(int i = 0; i < 5; i++) { - Dilate(sw, sh, apBuffer[0], apBuffer[1]); - Dilate(sw, sh, apBuffer[1], apBuffer[0]); + Dilate(SubWidth, SubHeight, apBuffer[0], apBuffer[1]); + Dilate(SubWidth, SubHeight, apBuffer[1], apBuffer[0]); } - CopyColorValues(sw, sh, BPP, apBuffer[0], pBufferOriginal); + CopyColorValues(SubWidth, SubHeight, apBuffer[0], pBufferOriginal); free(apBuffer[0]); free(apBuffer[1]); - for(int Y = 0; Y < sh; ++Y) + for(int Y = 0; Y < SubHeight; ++Y) { - int SrcImgOffset = ((y + Y) * w * BPP) + (x * BPP); - int DstImgOffset = (Y * sw * BPP); - int CopySize = sw * BPP; - mem_copy(&pPixelBuff[SrcImgOffset], &pBufferOriginal[DstImgOffset], CopySize); + int SrcImgOffset = ((y + Y) * w * DILATE_BPP) + (x * DILATE_BPP); + int DstImgOffset = (Y * SubWidth * DILATE_BPP); + int CopySize = SubWidth * DILATE_BPP; + mem_copy(&pImageBuff[SrcImgOffset], &pBufferOriginal[DstImgOffset], CopySize); } free(pBufferOriginal); @@ -127,18 +208,15 @@ static float CubicHermite(float A, float B, float C, float D, float t) return (a * t * t * t) + (b * t * t) + (c * t) + d; } -static void GetPixelClamped(const uint8_t *pSourceImage, int x, int y, uint32_t W, uint32_t H, size_t BPP, uint8_t aTmp[]) +static void GetPixelClamped(const uint8_t *pSourceImage, int x, int y, uint32_t W, uint32_t H, size_t BPP, uint8_t aSample[4]) { x = clamp(x, 0, (int)W - 1); y = clamp(y, 0, (int)H - 1); - for(size_t i = 0; i < BPP; i++) - { - aTmp[i] = pSourceImage[x * BPP + (W * BPP * y) + i]; - } + mem_copy(aSample, &pSourceImage[x * BPP + (W * BPP * y)], BPP); } -static void SampleBicubic(const uint8_t *pSourceImage, float u, float v, uint32_t W, uint32_t H, size_t BPP, uint8_t aSample[]) +static void SampleBicubic(const uint8_t *pSourceImage, float u, float v, uint32_t W, uint32_t H, size_t BPP, uint8_t aSample[4]) { float X = (u * W) - 0.5f; int xInt = (int)X; @@ -148,79 +226,37 @@ static void SampleBicubic(const uint8_t *pSourceImage, float u, float v, uint32_ int yInt = (int)Y; float yFract = Y - std::floor(Y); - uint8_t aPX00[4]; - uint8_t aPX10[4]; - uint8_t aPX20[4]; - uint8_t aPX30[4]; - - uint8_t aPX01[4]; - uint8_t aPX11[4]; - uint8_t aPX21[4]; - uint8_t aPX31[4]; - - uint8_t aPX02[4]; - uint8_t aPX12[4]; - uint8_t aPX22[4]; - uint8_t aPX32[4]; - - uint8_t aPX03[4]; - uint8_t aPX13[4]; - uint8_t aPX23[4]; - uint8_t aPX33[4]; - - GetPixelClamped(pSourceImage, xInt - 1, yInt - 1, W, H, BPP, aPX00); - GetPixelClamped(pSourceImage, xInt + 0, yInt - 1, W, H, BPP, aPX10); - GetPixelClamped(pSourceImage, xInt + 1, yInt - 1, W, H, BPP, aPX20); - GetPixelClamped(pSourceImage, xInt + 2, yInt - 1, W, H, BPP, aPX30); - - GetPixelClamped(pSourceImage, xInt - 1, yInt + 0, W, H, BPP, aPX01); - GetPixelClamped(pSourceImage, xInt + 0, yInt + 0, W, H, BPP, aPX11); - GetPixelClamped(pSourceImage, xInt + 1, yInt + 0, W, H, BPP, aPX21); - GetPixelClamped(pSourceImage, xInt + 2, yInt + 0, W, H, BPP, aPX31); - - GetPixelClamped(pSourceImage, xInt - 1, yInt + 1, W, H, BPP, aPX02); - GetPixelClamped(pSourceImage, xInt + 0, yInt + 1, W, H, BPP, aPX12); - GetPixelClamped(pSourceImage, xInt + 1, yInt + 1, W, H, BPP, aPX22); - GetPixelClamped(pSourceImage, xInt + 2, yInt + 1, W, H, BPP, aPX32); - - GetPixelClamped(pSourceImage, xInt - 1, yInt + 2, W, H, BPP, aPX03); - GetPixelClamped(pSourceImage, xInt + 0, yInt + 2, W, H, BPP, aPX13); - GetPixelClamped(pSourceImage, xInt + 1, yInt + 2, W, H, BPP, aPX23); - GetPixelClamped(pSourceImage, xInt + 2, yInt + 2, W, H, BPP, aPX33); + uint8_t aaaSamples[4][4][4]; + for(int y = 0; y < 4; ++y) + { + for(int x = 0; x < 4; ++x) + { + GetPixelClamped(pSourceImage, xInt + x - 1, yInt + y - 1, W, H, BPP, aaaSamples[x][y]); + } + } for(size_t i = 0; i < BPP; i++) { - float Clmn0 = CubicHermite(aPX00[i], aPX10[i], aPX20[i], aPX30[i], xFract); - float Clmn1 = CubicHermite(aPX01[i], aPX11[i], aPX21[i], aPX31[i], xFract); - float Clmn2 = CubicHermite(aPX02[i], aPX12[i], aPX22[i], aPX32[i], xFract); - float Clmn3 = CubicHermite(aPX03[i], aPX13[i], aPX23[i], aPX33[i], xFract); - - float Valuef = CubicHermite(Clmn0, Clmn1, Clmn2, Clmn3, yFract); - - Valuef = clamp(Valuef, 0.0f, 255.0f); - - aSample[i] = (uint8_t)Valuef; + float aRows[4]; + for(int y = 0; y < 4; ++y) + { + aRows[y] = CubicHermite(aaaSamples[0][y][i], aaaSamples[1][y][i], aaaSamples[2][y][i], aaaSamples[3][y][i], xFract); + } + aSample[i] = (uint8_t)clamp(CubicHermite(aRows[0], aRows[1], aRows[2], aRows[3], yFract), 0.0f, 255.0f); } } static void ResizeImage(const uint8_t *pSourceImage, uint32_t SW, uint32_t SH, uint8_t *pDestinationImage, uint32_t W, uint32_t H, size_t BPP) { - uint8_t aSample[4]; - int y, x; - - for(y = 0; y < (int)H; ++y) + for(int y = 0; y < (int)H; ++y) { float v = (float)y / (float)(H - 1); - - for(x = 0; x < (int)W; ++x) + for(int x = 0; x < (int)W; ++x) { float u = (float)x / (float)(W - 1); + uint8_t aSample[4]; SampleBicubic(pSourceImage, u, v, SW, SH, BPP, aSample); - - for(size_t i = 0; i < BPP; ++i) - { - pDestinationImage[x * BPP + ((W * BPP) * y) + i] = aSample[i]; - } + mem_copy(&pDestinationImage[x * BPP + ((W * BPP) * y)], aSample, BPP); } } } @@ -232,6 +268,15 @@ uint8_t *ResizeImage(const uint8_t *pImageData, int Width, int Height, int NewWi return pTmpData; } +void ResizeImage(CImageInfo &Image, int NewWidth, int NewHeight) +{ + uint8_t *pNewData = ResizeImage(Image.m_pData, Image.m_Width, Image.m_Height, NewWidth, NewHeight, Image.PixelSize()); + free(Image.m_pData); + Image.m_pData = pNewData; + Image.m_Width = NewWidth; + Image.m_Height = NewHeight; +} + int HighestBit(int OfVar) { if(!OfVar) diff --git a/src/engine/gfx/image_manipulation.h b/src/engine/gfx/image_manipulation.h index 58a1335402..ae39144e24 100644 --- a/src/engine/gfx/image_manipulation.h +++ b/src/engine/gfx/image_manipulation.h @@ -1,14 +1,29 @@ #ifndef ENGINE_GFX_IMAGE_MANIPULATION_H #define ENGINE_GFX_IMAGE_MANIPULATION_H +#include + #include +// Destination must have appropriate size for RGBA data +bool ConvertToRgba(uint8_t *pDest, const CImageInfo &SourceImage); +// Allocates appropriate buffer with malloc, must be freed by caller +bool ConvertToRgbaAlloc(uint8_t *&pDest, const CImageInfo &SourceImage); +// Replaces existing image data with RGBA data (unless already RGBA) +bool ConvertToRgba(CImageInfo &Image); + +// Changes the image data (not the format) +void ConvertToGrayscale(const CImageInfo &Image); + // These functions assume that the image data is 4 bytes per pixel RGBA -void DilateImage(unsigned char *pImageBuff, int w, int h); -void DilateImageSub(unsigned char *pImageBuff, int w, int h, int x, int y, int sw, int sh); +void DilateImage(uint8_t *pImageBuff, int w, int h); +void DilateImage(const CImageInfo &Image); +void DilateImageSub(uint8_t *pImageBuff, int w, int h, int x, int y, int SubWidth, int SubHeight); -// returned pointer is allocated with malloc +// Returned buffer is allocated with malloc, must be freed by caller uint8_t *ResizeImage(const uint8_t *pImageData, int Width, int Height, int NewWidth, int NewHeight, int BPP); +// Replaces existing image data with resized buffer +void ResizeImage(CImageInfo &Image, int NewWidth, int NewHeight); int HighestBit(int OfVar); diff --git a/src/engine/graphics.h b/src/engine/graphics.h index 951748b117..3699c3ea73 100644 --- a/src/engine/graphics.h +++ b/src/engine/graphics.h @@ -3,6 +3,7 @@ #ifndef ENGINE_GRAPHICS_H #define ENGINE_GRAPHICS_H +#include "image.h" #include "kernel.h" #include "warning.h" @@ -64,59 +65,6 @@ struct SGraphicTileTexureCoords ubvec4 m_TexCoordBottomLeft; }; -class CImageInfo -{ -public: - enum EImageFormat - { - FORMAT_ERROR = -1, - FORMAT_RGB = 0, - FORMAT_RGBA = 1, - FORMAT_SINGLE_COMPONENT = 2, - }; - - /** - * Contains the width of the image - */ - int m_Width = 0; - - /** - * Contains the height of the image - */ - int m_Height = 0; - - /** - * Contains the format of the image. - * - * @see EImageFormat - */ - EImageFormat m_Format = FORMAT_ERROR; - - /** - * Pointer to the image data. - */ - void *m_pData = nullptr; - - static size_t PixelSize(EImageFormat Format) - { - dbg_assert(Format != FORMAT_ERROR, "Format invalid"); - static const size_t s_aSizes[] = {3, 4, 1}; - return s_aSizes[(int)Format]; - } - - size_t PixelSize() const - { - return PixelSize(m_Format); - } - - static EImageFormat ImageFormatFromInt(int Format) - { - if(Format < (int)FORMAT_RGB || Format > (int)FORMAT_SINGLE_COMPONENT) - return FORMAT_ERROR; - return (EImageFormat)Format; - } -}; - /* Structure: CVideoMode */ @@ -202,9 +150,9 @@ enum EBackendType BACKEND_TYPE_COUNT, }; -struct STWGraphicGPU +struct STWGraphicGpu { - enum ETWGraphicsGPUType + enum ETWGraphicsGpuType { GRAPHICS_GPU_TYPE_DISCRETE = 0, GRAPHICS_GPU_TYPE_INTEGRATED, @@ -215,16 +163,16 @@ struct STWGraphicGPU GRAPHICS_GPU_TYPE_INVALID, }; - struct STWGraphicGPUItem + struct STWGraphicGpuItem { char m_aName[256]; - ETWGraphicsGPUType m_GPUType; + ETWGraphicsGpuType m_GpuType; }; - std::vector m_vGPUs; - STWGraphicGPUItem m_AutoGPU; + std::vector m_vGpus; + STWGraphicGpuItem m_AutoGpu; }; -typedef STWGraphicGPU TTWGraphicsGPUList; +typedef STWGraphicGpu TTwGraphicsGpuList; typedef std::function WINDOW_RESIZE_FUNC; typedef std::function WINDOW_PROPS_CHANGED_FUNC; @@ -243,11 +191,9 @@ class IGraphics : public IInterface public: enum { - TEXLOAD_NOMIPMAPS = 1 << 1, - TEXLOAD_NO_COMPRESSION = 1 << 2, - TEXLOAD_TO_3D_TEXTURE = (1 << 3), - TEXLOAD_TO_2D_ARRAY_TEXTURE = (1 << 4), - TEXLOAD_NO_2D_TEXTURE = (1 << 5), + TEXLOAD_TO_3D_TEXTURE = 1 << 0, + TEXLOAD_TO_2D_ARRAY_TEXTURE = 1 << 1, + TEXLOAD_NO_2D_TEXTURE = 1 << 2, }; class CTextureHandle @@ -275,13 +221,14 @@ class IGraphics : public IInterface int WindowHeight() const { return m_ScreenHeight / m_ScreenHiDPIScale; } virtual void WarnPngliteIncompatibleImages(bool Warn) = 0; - virtual void SetWindowParams(int FullscreenMode, bool IsBorderless, bool AllowResizing) = 0; + virtual void SetWindowParams(int FullscreenMode, bool IsBorderless) = 0; virtual bool SetWindowScreen(int Index) = 0; virtual bool SetVSync(bool State) = 0; virtual bool SetMultiSampling(uint32_t ReqMultiSamplingCount, uint32_t &MultiSamplingCountBackend) = 0; virtual int GetWindowScreen() = 0; virtual void Move(int x, int y) = 0; - virtual void Resize(int w, int h, int RefreshRate) = 0; + virtual bool Resize(int w, int h, int RefreshRate) = 0; + virtual void ResizeToScreen() = 0; virtual void GotResized(int w, int h, int RefreshRate) = 0; virtual void UpdateViewport(int X, int Y, int W, int H, bool ByResize) = 0; @@ -295,8 +242,8 @@ class IGraphics : public IInterface */ virtual void AddWindowPropChangeListener(WINDOW_PROPS_CHANGED_FUNC pFunc) = 0; - virtual void WindowDestroyNtf(uint32_t WindowID) = 0; - virtual void WindowCreateNtf(uint32_t WindowID) = 0; + virtual void WindowDestroyNtf(uint32_t WindowId) = 0; + virtual void WindowCreateNtf(uint32_t WindowId) = 0; // ForceClearNow forces the backend to trigger a clear, even at performance cost, else it might be delayed by one frame virtual void Clear(float r, float g, float b, bool ForceClearNow = false) = 0; @@ -319,37 +266,30 @@ class IGraphics : public IInterface virtual uint64_t StreamedMemoryUsage() const = 0; virtual uint64_t StagingMemoryUsage() const = 0; - virtual const TTWGraphicsGPUList &GetGPUs() const = 0; - - virtual bool LoadPNG(CImageInfo *pImg, const char *pFilename, int StorageType) = 0; - virtual void FreePNG(CImageInfo *pImg) = 0; - - virtual bool CheckImageDivisibility(const char *pFileName, CImageInfo &Img, int DivX, int DivY, bool AllowResize) = 0; - virtual bool IsImageFormatRGBA(const char *pFileName, CImageInfo &Img) = 0; + virtual const TTwGraphicsGpuList &GetGpus() const = 0; - // destination and source buffer require to have the same width and height - virtual void CopyTextureBufferSub(uint8_t *pDestBuffer, uint8_t *pSourceBuffer, size_t FullWidth, size_t FullHeight, size_t PixelSize, size_t SubOffsetX, size_t SubOffsetY, size_t SubCopyWidth, size_t SubCopyHeight) = 0; + virtual bool LoadPng(CImageInfo &Image, const char *pFilename, int StorageType) = 0; + virtual bool LoadPng(CImageInfo &Image, const uint8_t *pData, size_t DataSize, const char *pContextName) = 0; - // destination width must be equal to the subwidth of the source - virtual void CopyTextureFromTextureBufferSub(uint8_t *pDestBuffer, size_t DestWidth, size_t DestHeight, uint8_t *pSourceBuffer, size_t SrcWidth, size_t SrcHeight, size_t PixelSize, size_t SrcSubOffsetX, size_t SrcSubOffsetY, size_t SrcSubCopyWidth, size_t SrcSubCopyHeight) = 0; + virtual bool CheckImageDivisibility(const char *pContextName, CImageInfo &Image, int DivX, int DivY, bool AllowResize) = 0; + virtual bool IsImageFormatRgba(const char *pContextName, const CImageInfo &Image) = 0; - virtual int UnloadTexture(CTextureHandle *pIndex) = 0; - virtual CTextureHandle LoadTextureRaw(size_t Width, size_t Height, CImageInfo::EImageFormat Format, const void *pData, int Flags, const char *pTexName = nullptr) = 0; - virtual int LoadTextureRawSub(CTextureHandle TextureID, int x, int y, size_t Width, size_t Height, CImageInfo::EImageFormat Format, const void *pData) = 0; + virtual void UnloadTexture(CTextureHandle *pIndex) = 0; + virtual CTextureHandle LoadTextureRaw(const CImageInfo &Image, int Flags, const char *pTexName = nullptr) = 0; + virtual CTextureHandle LoadTextureRawMove(CImageInfo &Image, int Flags, const char *pTexName = nullptr) = 0; virtual CTextureHandle LoadTexture(const char *pFilename, int StorageType, int Flags = 0) = 0; - virtual CTextureHandle NullTexture() const = 0; virtual void TextureSet(CTextureHandle Texture) = 0; void TextureClear() { TextureSet(CTextureHandle()); } // pTextData & pTextOutlineData are automatically free'd - virtual bool LoadTextTextures(size_t Width, size_t Height, CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture, void *pTextData, void *pTextOutlineData) = 0; + virtual bool LoadTextTextures(size_t Width, size_t Height, CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture, uint8_t *pTextData, uint8_t *pTextOutlineData) = 0; virtual bool UnloadTextTextures(CTextureHandle &TextTexture, CTextureHandle &TextOutlineTexture) = 0; - virtual bool UpdateTextTexture(CTextureHandle TextureID, int x, int y, size_t Width, size_t Height, const void *pData) = 0; + virtual bool UpdateTextTexture(CTextureHandle TextureId, int x, int y, size_t Width, size_t Height, const uint8_t *pData) = 0; - virtual CTextureHandle LoadSpriteTexture(CImageInfo &FromImageInfo, struct CDataSprite *pSprite) = 0; + virtual CTextureHandle LoadSpriteTexture(const CImageInfo &FromImageInfo, const struct CDataSprite *pSprite) = 0; - virtual bool IsImageSubFullyTransparent(CImageInfo &FromImageInfo, int x, int y, int w, int h) = 0; - virtual bool IsSpriteTextureFullyTransparent(CImageInfo &FromImageInfo, struct CDataSprite *pSprite) = 0; + virtual bool IsImageSubFullyTransparent(const CImageInfo &FromImageInfo, int x, int y, int w, int h) = 0; + virtual bool IsSpriteTextureFullyTransparent(const CImageInfo &FromImageInfo, const struct CDataSprite *pSprite) = 0; virtual void FlushVertices(bool KeepVertices = false) = 0; virtual void FlushVerticesTex3D() = 0; @@ -503,18 +443,29 @@ class IGraphics : public IInterface CColorVertex() {} CColorVertex(int i, float r, float g, float b, float a) : m_Index(i), m_R(r), m_G(g), m_B(b), m_A(a) {} + CColorVertex(int i, ColorRGBA Color) : + m_Index(i), m_R(Color.r), m_G(Color.g), m_B(Color.b), m_A(Color.a) {} }; - virtual void SetColorVertex(const CColorVertex *pArray, int Num) = 0; + virtual void SetColorVertex(const CColorVertex *pArray, size_t Num) = 0; virtual void SetColor(float r, float g, float b, float a) = 0; virtual void SetColor(ColorRGBA Color) = 0; virtual void SetColor4(ColorRGBA TopLeft, ColorRGBA TopRight, ColorRGBA BottomLeft, ColorRGBA BottomRight) = 0; virtual void ChangeColorOfCurrentQuadVertices(float r, float g, float b, float a) = 0; - virtual void ChangeColorOfQuadVertices(int QuadOffset, unsigned char r, unsigned char g, unsigned char b, unsigned char a) = 0; + virtual void ChangeColorOfQuadVertices(size_t QuadOffset, unsigned char r, unsigned char g, unsigned char b, unsigned char a) = 0; + /** + * Reads the color at the specified position from the backbuffer once, + * after the next swap operation. + * + * @param Position The pixel position to read. + * @param pColor Pointer that will receive the read pixel color. + * The pointer must be valid until the next swap operation. + */ + virtual void ReadPixel(ivec2 Position, ColorRGBA *pColor) = 0; virtual void TakeScreenshot(const char *pFilename) = 0; virtual void TakeCustomScreenshot(const char *pFilename) = 0; virtual int GetVideoModes(CVideoMode *pModes, int MaxModes, int Screen) = 0; - + virtual void GetCurrentVideoMode(CVideoMode &CurMode, int Screen) = 0; virtual void Swap() = 0; virtual int GetNumScreens() const = 0; virtual const char *GetScreenName(int Screen) const = 0; diff --git a/src/engine/http.h b/src/engine/http.h new file mode 100644 index 0000000000..7aac63181c --- /dev/null +++ b/src/engine/http.h @@ -0,0 +1,19 @@ +#ifndef ENGINE_HTTP_H +#define ENGINE_HTTP_H + +#include "kernel.h" +#include + +class IHttpRequest +{ +}; + +class IHttp : public IInterface +{ + MACRO_INTERFACE("http") + +public: + virtual void Run(std::shared_ptr pRequest) = 0; +}; + +#endif diff --git a/src/engine/image.h b/src/engine/image.h new file mode 100644 index 0000000000..6b794952aa --- /dev/null +++ b/src/engine/image.h @@ -0,0 +1,137 @@ +#ifndef ENGINE_IMAGE_H +#define ENGINE_IMAGE_H + +#include + +#include + +/** + * Represents an image that has been loaded into main memory. + */ +class CImageInfo +{ +public: + /** + * Defines the format of image data. + */ + enum EImageFormat + { + FORMAT_UNDEFINED = -1, + FORMAT_RGB = 0, + FORMAT_RGBA = 1, + FORMAT_R = 2, + FORMAT_RA = 3, + }; + + /** + * Width of the image. + */ + size_t m_Width = 0; + + /** + * Height of the image. + */ + size_t m_Height = 0; + + /** + * Format of the image. + * + * @see EImageFormat + */ + EImageFormat m_Format = FORMAT_UNDEFINED; + + /** + * Pointer to the image data. + */ + uint8_t *m_pData = nullptr; + + /** + * Frees the image data and clears all info. + */ + void Free(); + + /** + * Returns the pixel size in bytes for the given image format. + * + * @param Format Image format, must not be `FORMAT_UNDEFINED`. + * + * @return Size of one pixel with the given image format. + */ + static size_t PixelSize(EImageFormat Format); + + /** + * Returns a readable name for the given image format. + * + * @param Format Image format. + * + * @return Readable name for the given image format. + */ + static const char *FormatName(EImageFormat Format); + + /** + * Returns the pixel size in bytes for the format of this image. + * + * @return Size of one pixel with the format of this image. + * + * @remark The format must not be `FORMAT_UNDEFINED`. + */ + size_t PixelSize() const; + + /** + * Returns a readable name for the format of this image. + * + * @return Readable name for the format of this image. + */ + const char *FormatName() const; + + /** + * Returns the size of the data, as derived from the width, height and pixel size. + * + * @return Expected size of the image data. + */ + size_t DataSize() const; + + /** + * Returns whether this image is equal to the given image + * in width, height, format and data. + * + * @param Other The image to compare with. + * + * @return `true` if the images are identical, `false` otherwise. + */ + bool DataEquals(const CImageInfo &Other) const; + + /** + * Returns the color of the pixel at the specified position. + * + * @param x The x-coordinate to read from. + * @param y The y-coordinate to read from. + * + * @return Pixel color converted to normalized RGBA. + */ + ColorRGBA PixelColor(size_t x, size_t y) const; + + /** + * Sets the color of the pixel at the specified position. + * + * @param x The x-coordinate to write to. + * @param y The y-coordinate to write to. + * @param Color The normalized RGBA color to write. + */ + void SetPixelColor(size_t x, size_t y, ColorRGBA Color) const; + + /** + * Copies a rectangle of image data from the given image to this image. + * + * @param SrcImage The image to copy data from. + * @param SrcX The x-offset in the source image. + * @param SrcY The y-offset in the source image. + * @param Width The width of the rectangle to copy. + * @param Height The height of the rectangle to copy. + * @param DestX The x-offset in the destination image (this). + * @param DestY The y-offset in the destination image (this). + */ + void CopyRectFrom(const CImageInfo &SrcImage, size_t SrcX, size_t SrcY, size_t Width, size_t Height, size_t DestX, size_t DestY) const; +}; + +#endif diff --git a/src/engine/input.h b/src/engine/input.h index 87740c2fa2..9aa8733582 100644 --- a/src/engine/input.h +++ b/src/engine/input.h @@ -4,7 +4,14 @@ #define ENGINE_INPUT_H #include "kernel.h" -#include + +#include +#include + +#include +#include +#include +#include const int g_MaxKeys = 512; extern const char g_aaKeyStrings[g_MaxKeys][20]; @@ -13,33 +20,15 @@ class IInput : public IInterface { MACRO_INTERFACE("input") public: - enum - { - INPUT_TEXT_SIZE = 32 * UTF8_BYTE_LENGTH + 1, - }; - class CEvent { public: int m_Flags; int m_Key; - char m_aText[INPUT_TEXT_SIZE]; - int m_InputCount; - }; - -protected: - enum - { - INPUT_BUFFER_SIZE = 32 + uint32_t m_InputCount; + char m_aText[32]; // SDL_TEXTINPUTEVENT_TEXT_SIZE }; - // quick access to events - size_t m_NumEvents; - CEvent m_aInputEvents[INPUT_BUFFER_SIZE]; - int64_t m_LastUpdate; - float m_UpdateTime; - -public: enum { FLAG_PRESS = 1 << 0, @@ -52,27 +41,16 @@ class IInput : public IInterface CURSOR_MOUSE, CURSOR_JOYSTICK, }; - enum - { - MAX_COMPOSITION_ARRAY_SIZE = 32, // SDL2 limitation - - COMP_LENGTH_INACTIVE = -1, - }; // events - size_t NumEvents() const { return m_NumEvents; } - virtual bool IsEventValid(const CEvent &Event) const = 0; - const CEvent &GetEvent(size_t Index) const - { - dbg_assert(Index < m_NumEvents, "Index invalid"); - return m_aInputEvents[Index]; - } + virtual void ConsumeEvents(std::function Consumer) const = 0; + virtual void Clear() = 0; /** * @return Rolling average of the time in seconds between * calls of the Update function. */ - float GetUpdateTime() const { return m_UpdateTime; } + virtual float GetUpdateTime() const = 0; // keys virtual bool ModifierIsPressed() const = 0; @@ -81,7 +59,7 @@ class IInput : public IInterface virtual bool KeyIsPressed(int Key) const = 0; virtual bool KeyPress(int Key, bool CheckCounter = false) const = 0; const char *KeyName(int Key) const { return (Key >= 0 && Key < g_MaxKeys) ? g_aaKeyStrings[Key] : g_aaKeyStrings[0]; } - virtual void Clear() = 0; + virtual int FindKeyByName(const char *pKeyName) const = 0; // joystick class IJoystick @@ -104,15 +82,64 @@ class IInput : public IInterface virtual void SetActiveJoystick(size_t Index) = 0; // mouse - virtual void NativeMousePos(int *pX, int *pY) const = 0; - virtual bool NativeMousePressed(int Index) = 0; + virtual vec2 NativeMousePos() const = 0; + virtual bool NativeMousePressed(int Index) const = 0; virtual void MouseModeRelative() = 0; virtual void MouseModeAbsolute() = 0; - virtual bool MouseDoubleClick() = 0; virtual bool MouseRelative(float *pX, float *pY) = 0; + // touch + /** + * Represents a unique finger for a current touch event. If there are multiple touch input devices, they + * are handled transparently like different fingers. The concrete values of the member variables of this + * class are arbitrary based on the touch device driver and should only be used to uniquely identify touch + * fingers. Note that once a finger has been released, the same finger value may also be reused again. + */ + class CTouchFinger + { + friend class CInput; + + int64_t m_DeviceId; + int64_t m_FingerId; + + public: + bool operator==(const CTouchFinger &Other) const { return m_DeviceId == Other.m_DeviceId && m_FingerId == Other.m_FingerId; } + bool operator!=(const CTouchFinger &Other) const { return !(*this == Other); } + }; + /** + * Represents the state of a particular touch finger currently being pressed down on a touch device. + */ + class CTouchFingerState + { + public: + /** + * The unique finger which this state is associated with. + */ + CTouchFinger m_Finger; + /** + * The current position of the finger. The x- and y-components of the position are normalized to the + * range `0.0f`-`1.0f` representing the absolute position of the finger on the current touch device. + */ + vec2 m_Position; + /** + * The current delta of the finger. The x- and y-components of the delta are normalized to the + * range `0.0f`-`1.0f` representing the absolute delta of the finger on the current touch device. + * + * @remark This is reset to zero at the end of each frame. + */ + vec2 m_Delta; + }; + /** + * Returns a vector of the states of all touch fingers currently being pressed down on touch devices. + * Note that this only contains fingers which are pressed down, i.e. released fingers are never stored. + * The order of the fingers in this vector is based on the order in which the fingers where pressed. + * + * @return vector of all touch finger states + */ + virtual const std::vector &TouchFingerStates() const = 0; + // clipboard - virtual const char *GetClipboardText() = 0; + virtual std::string GetClipboardText() = 0; virtual void SetClipboardText(const char *pText) = 0; // text editing diff --git a/src/engine/kernel.h b/src/engine/kernel.h index 3a2c4362bc..7412142a0d 100644 --- a/src/engine/kernel.h +++ b/src/engine/kernel.h @@ -33,8 +33,8 @@ public: \ class IKernel { // hide the implementation - virtual bool RegisterInterfaceImpl(const char *pInterfaceName, IInterface *pInterface, bool Destroy) = 0; - virtual bool ReregisterInterfaceImpl(const char *pInterfaceName, IInterface *pInterface) = 0; + virtual void RegisterInterfaceImpl(const char *pInterfaceName, IInterface *pInterface, bool Destroy) = 0; + virtual void ReregisterInterfaceImpl(const char *pInterfaceName, IInterface *pInterface) = 0; virtual IInterface *RequestInterfaceImpl(const char *pInterfaceName) = 0; public: @@ -44,14 +44,14 @@ class IKernel // templated access to handle pointer conversions and interface names template - bool RegisterInterface(TINTERFACE *pInterface, bool Destroy = true) + void RegisterInterface(TINTERFACE *pInterface, bool Destroy = true) { - return RegisterInterfaceImpl(TINTERFACE::InterfaceName(), pInterface, Destroy); + RegisterInterfaceImpl(TINTERFACE::InterfaceName(), pInterface, Destroy); } template - bool ReregisterInterface(TINTERFACE *pInterface) + void ReregisterInterface(TINTERFACE *pInterface) { - return ReregisterInterfaceImpl(TINTERFACE::InterfaceName(), pInterface); + ReregisterInterfaceImpl(TINTERFACE::InterfaceName(), pInterface); } // Usage example: diff --git a/src/engine/keys.h b/src/engine/keys.h index 88db9778ab..246fffff06 100644 --- a/src/engine/keys.h +++ b/src/engine/keys.h @@ -1,7 +1,7 @@ #ifndef ENGINE_KEYS_H #define ENGINE_KEYS_H #if defined(CONF_FAMILY_WINDOWS) -#undef KEY_EXECUTE + #undef KEY_EXECUTE #endif /* AUTO GENERATED! DO NOT EDIT MANUALLY! */ enum diff --git a/src/engine/map.h b/src/engine/map.h index 47c2c5ed8a..10a996110e 100644 --- a/src/engine/map.h +++ b/src/engine/map.h @@ -24,10 +24,10 @@ class IMap : public IInterface virtual int NumData() const = 0; virtual int GetItemSize(int Index) = 0; - virtual void *GetItem(int Index, int *pType = nullptr, int *pID = nullptr) = 0; + virtual void *GetItem(int Index, int *pType = nullptr, int *pId = nullptr) = 0; virtual void GetType(int Type, int *pStart, int *pNum) = 0; - virtual int FindItemIndex(int Type, int ID) = 0; - virtual void *FindItem(int Type, int ID) = 0; + virtual int FindItemIndex(int Type, int Id) = 0; + virtual void *FindItem(int Type, int Id) = 0; virtual int NumItems() const = 0; }; diff --git a/src/engine/message.h b/src/engine/message.h index d862b8178b..2a24e3c7c0 100644 --- a/src/engine/message.h +++ b/src/engine/message.h @@ -9,18 +9,18 @@ class CMsgPacker : public CPacker { public: - int m_MsgID; + int m_MsgId; bool m_System; bool m_NoTranslate; CMsgPacker(int Type, bool System = false, bool NoTranslate = false) : - m_MsgID(Type), m_System(System), m_NoTranslate(NoTranslate) + m_MsgId(Type), m_System(System), m_NoTranslate(NoTranslate) { Reset(); } template CMsgPacker(const T *, bool System = false, bool NoTranslate = false) : - CMsgPacker(T::ms_MsgID, System, NoTranslate) + CMsgPacker(T::ms_MsgId, System, NoTranslate) { } }; diff --git a/src/engine/server.h b/src/engine/server.h index 1e81f77397..42e2463194 100644 --- a/src/engine/server.h +++ b/src/engine/server.h @@ -12,6 +12,7 @@ #include "kernel.h" #include "message.h" +#include #include #include #include @@ -19,7 +20,7 @@ struct CAntibotRoundData; -// When recording a demo on the server, the ClientID -1 is used +// When recording a demo on the server, the ClientId -1 is used enum { SERVER_DEMO_CLIENT = -1 @@ -42,7 +43,7 @@ class IServer : public IInterface bool m_GotDDNetVersion; int m_DDNetVersion; const char *m_pDDNetVersionStr; - const CUuid *m_pConnectionID; + const CUuid *m_pConnectionId; }; int Tick() const { return m_CurrentGameTick; } @@ -52,33 +53,34 @@ class IServer : public IInterface virtual int MaxClients() const = 0; virtual int ClientCount() const = 0; virtual int DistinctClientCount() const = 0; - virtual const char *ClientName(int ClientID) const = 0; - virtual const char *ClientClan(int ClientID) const = 0; - virtual int ClientCountry(int ClientID) const = 0; - virtual bool ClientIngame(int ClientID) const = 0; - virtual bool ClientAuthed(int ClientID) const = 0; - virtual bool GetClientInfo(int ClientID, CClientInfo *pInfo) const = 0; - virtual void SetClientDDNetVersion(int ClientID, int DDNetVersion) = 0; - virtual void GetClientAddr(int ClientID, char *pAddrStr, int Size) const = 0; + virtual const char *ClientName(int ClientId) const = 0; + virtual const char *ClientClan(int ClientId) const = 0; + virtual int ClientCountry(int ClientId) const = 0; + virtual bool ClientSlotEmpty(int ClientId) const = 0; + virtual bool ClientIngame(int ClientId) const = 0; + virtual bool ClientAuthed(int ClientId) const = 0; + virtual bool GetClientInfo(int ClientId, CClientInfo *pInfo) const = 0; + virtual void SetClientDDNetVersion(int ClientId, int DDNetVersion) = 0; + virtual void GetClientAddr(int ClientId, char *pAddrStr, int Size) const = 0; /** * Returns the version of the client with the given client ID. * - * @param ClientID the client ID, which must be between 0 and + * @param ClientId the client Id, which must be between 0 and * MAX_CLIENTS - 1, or equal to SERVER_DEMO_CLIENT for server demos. * * @return The version of the client with the given client ID. * For server demos this is always the latest client version. * On errors, VERSION_NONE is returned. */ - virtual int GetClientVersion(int ClientID) const = 0; - virtual int SendMsg(CMsgPacker *pMsg, int Flags, int ClientID) = 0; + virtual int GetClientVersion(int ClientId) const = 0; + virtual int SendMsg(CMsgPacker *pMsg, int Flags, int ClientId) = 0; template::value, int>::type = 0> - inline int SendPackMsg(const T *pMsg, int Flags, int ClientID) + inline int SendPackMsg(const T *pMsg, int Flags, int ClientId) { int Result = 0; - if(ClientID == -1) + if(ClientId == -1) { for(int i = 0; i < MaxClients(); i++) if(ClientIngame(i)) @@ -86,86 +88,101 @@ class IServer : public IInterface } else { - Result = SendPackMsgTranslate(pMsg, Flags, ClientID); + Result = SendPackMsgTranslate(pMsg, Flags, ClientId); } return Result; } template::value, int>::type = 1> - inline int SendPackMsg(const T *pMsg, int Flags, int ClientID) + inline int SendPackMsg(const T *pMsg, int Flags, int ClientId) { int Result = 0; - if(ClientID == -1) + if(ClientId == -1) { for(int i = 0; i < MaxClients(); i++) if(ClientIngame(i) && IsSixup(i)) Result = SendPackMsgOne(pMsg, Flags, i); } - else if(IsSixup(ClientID)) - Result = SendPackMsgOne(pMsg, Flags, ClientID); + else if(IsSixup(ClientId)) + Result = SendPackMsgOne(pMsg, Flags, ClientId); return Result; } template - int SendPackMsgTranslate(const T *pMsg, int Flags, int ClientID) + int SendPackMsgTranslate(const T *pMsg, int Flags, int ClientId) { - return SendPackMsgOne(pMsg, Flags, ClientID); + return SendPackMsgOne(pMsg, Flags, ClientId); } - int SendPackMsgTranslate(const CNetMsg_Sv_Emoticon *pMsg, int Flags, int ClientID) + int SendPackMsgTranslate(const CNetMsg_Sv_Emoticon *pMsg, int Flags, int ClientId) { CNetMsg_Sv_Emoticon MsgCopy; mem_copy(&MsgCopy, pMsg, sizeof(MsgCopy)); - return Translate(MsgCopy.m_ClientID, ClientID) && SendPackMsgOne(&MsgCopy, Flags, ClientID); + return Translate(MsgCopy.m_ClientId, ClientId) && SendPackMsgOne(&MsgCopy, Flags, ClientId); } - int SendPackMsgTranslate(const CNetMsg_Sv_Chat *pMsg, int Flags, int ClientID) + int SendPackMsgTranslate(const CNetMsg_Sv_Chat *pMsg, int Flags, int ClientId) { CNetMsg_Sv_Chat MsgCopy; mem_copy(&MsgCopy, pMsg, sizeof(MsgCopy)); char aBuf[1000]; - if(MsgCopy.m_ClientID >= 0 && !Translate(MsgCopy.m_ClientID, ClientID)) + if(MsgCopy.m_ClientId >= 0 && !Translate(MsgCopy.m_ClientId, ClientId)) { - str_format(aBuf, sizeof(aBuf), "%s: %s", ClientName(MsgCopy.m_ClientID), MsgCopy.m_pMessage); + str_format(aBuf, sizeof(aBuf), "%s: %s", ClientName(MsgCopy.m_ClientId), MsgCopy.m_pMessage); MsgCopy.m_pMessage = aBuf; - MsgCopy.m_ClientID = VANILLA_MAX_CLIENTS - 1; + MsgCopy.m_ClientId = VANILLA_MAX_CLIENTS - 1; } - if(IsSixup(ClientID)) + if(IsSixup(ClientId)) { protocol7::CNetMsg_Sv_Chat Msg7; - Msg7.m_ClientID = MsgCopy.m_ClientID; + Msg7.m_ClientId = MsgCopy.m_ClientId; Msg7.m_pMessage = MsgCopy.m_pMessage; Msg7.m_Mode = MsgCopy.m_Team > 0 ? protocol7::CHAT_TEAM : protocol7::CHAT_ALL; - Msg7.m_TargetID = -1; - return SendPackMsgOne(&Msg7, Flags, ClientID); + Msg7.m_TargetId = -1; + return SendPackMsgOne(&Msg7, Flags, ClientId); } - return SendPackMsgOne(&MsgCopy, Flags, ClientID); + return SendPackMsgOne(&MsgCopy, Flags, ClientId); } - int SendPackMsgTranslate(const CNetMsg_Sv_KillMsg *pMsg, int Flags, int ClientID) + int SendPackMsgTranslate(const CNetMsg_Sv_KillMsg *pMsg, int Flags, int ClientId) { CNetMsg_Sv_KillMsg MsgCopy; mem_copy(&MsgCopy, pMsg, sizeof(MsgCopy)); - if(!Translate(MsgCopy.m_Victim, ClientID)) + if(!Translate(MsgCopy.m_Victim, ClientId)) return 0; - if(!Translate(MsgCopy.m_Killer, ClientID)) + if(!Translate(MsgCopy.m_Killer, ClientId)) MsgCopy.m_Killer = MsgCopy.m_Victim; - return SendPackMsgOne(&MsgCopy, Flags, ClientID); + return SendPackMsgOne(&MsgCopy, Flags, ClientId); + } + + int SendPackMsgTranslate(const CNetMsg_Sv_RaceFinish *pMsg, int Flags, int ClientId) + { + if(IsSixup(ClientId)) + { + protocol7::CNetMsg_Sv_RaceFinish Msg7; + Msg7.m_ClientId = pMsg->m_ClientId; + Msg7.m_Diff = pMsg->m_Diff; + Msg7.m_Time = pMsg->m_Time; + Msg7.m_RecordPersonal = pMsg->m_RecordPersonal; + Msg7.m_RecordServer = pMsg->m_RecordServer; + return SendPackMsgOne(&Msg7, Flags, ClientId); + } + return SendPackMsgOne(pMsg, Flags, ClientId); } template - int SendPackMsgOne(const T *pMsg, int Flags, int ClientID) + int SendPackMsgOne(const T *pMsg, int Flags, int ClientId) { - dbg_assert(ClientID != -1, "SendPackMsgOne called with -1"); - CMsgPacker Packer(T::ms_MsgID, false, protocol7::is_sixup::value); + dbg_assert(ClientId != -1, "SendPackMsgOne called with -1"); + CMsgPacker Packer(T::ms_MsgId, false, protocol7::is_sixup::value); if(pMsg->Pack(&Packer)) return -1; - return SendMsg(&Packer, Flags, ClientID); + return SendMsg(&Packer, Flags, ClientId); } bool Translate(int &Target, int Client) @@ -204,23 +221,23 @@ class IServer : public IInterface virtual void GetMapInfo(char *pMapName, int MapNameSize, int *pMapSize, SHA256_DIGEST *pSha256, int *pMapCrc) = 0; - virtual bool WouldClientNameChange(int ClientID, const char *pNameRequest) = 0; - virtual bool WouldClientClanChange(int ClientID, const char *pClanRequest) = 0; - virtual void SetClientName(int ClientID, const char *pName) = 0; - virtual void SetClientClan(int ClientID, const char *pClan) = 0; - virtual void SetClientCountry(int ClientID, int Country) = 0; - virtual void SetClientScore(int ClientID, std::optional Score) = 0; - virtual void SetClientFlags(int ClientID, int Flags) = 0; + virtual bool WouldClientNameChange(int ClientId, const char *pNameRequest) = 0; + virtual bool WouldClientClanChange(int ClientId, const char *pClanRequest) = 0; + virtual void SetClientName(int ClientId, const char *pName) = 0; + virtual void SetClientClan(int ClientId, const char *pClan) = 0; + virtual void SetClientCountry(int ClientId, int Country) = 0; + virtual void SetClientScore(int ClientId, std::optional Score) = 0; + virtual void SetClientFlags(int ClientId, int Flags) = 0; - virtual int SnapNewID() = 0; - virtual void SnapFreeID(int ID) = 0; - virtual void *SnapNewItem(int Type, int ID, int Size) = 0; + virtual int SnapNewId() = 0; + virtual void SnapFreeId(int Id) = 0; + virtual void *SnapNewItem(int Type, int Id, int Size) = 0; template - T *SnapNewItem(int ID) + T *SnapNewItem(int Id) { - const int Type = protocol7::is_sixup::value ? -T::ms_MsgID : T::ms_MsgID; - return static_cast(SnapNewItem(Type, ID, sizeof(T))); + const int Type = protocol7::is_sixup::value ? -T::ms_MsgId : T::ms_MsgId; + return static_cast(SnapNewItem(Type, Id, sizeof(T))); } virtual void SnapSetStaticsize(int ItemType, int Size) = 0; @@ -230,48 +247,50 @@ class IServer : public IInterface RCON_CID_SERV = -1, RCON_CID_VOTE = -2, }; - virtual void SetRconCID(int ClientID) = 0; - virtual int GetAuthedState(int ClientID) const = 0; - virtual const char *GetAuthName(int ClientID) const = 0; - virtual void Kick(int ClientID, const char *pReason) = 0; - virtual void Ban(int ClientID, int Seconds, const char *pReason) = 0; - virtual void RedirectClient(int ClientID, int Port, bool Verbose = false) = 0; + virtual void SetRconCid(int ClientId) = 0; + virtual int GetAuthedState(int ClientId) const = 0; + virtual const char *GetAuthName(int ClientId) const = 0; + virtual void Kick(int ClientId, const char *pReason) = 0; + virtual void Ban(int ClientId, int Seconds, const char *pReason, bool VerbatimReason) = 0; + virtual void RedirectClient(int ClientId, int Port, bool Verbose = false) = 0; virtual void ChangeMap(const char *pMap) = 0; + virtual void ReloadMap() = 0; virtual void DemoRecorder_HandleAutoStart() = 0; // DDRace - virtual void SaveDemo(int ClientID, float Time) = 0; - virtual void StartRecord(int ClientID) = 0; - virtual void StopRecord(int ClientID) = 0; - virtual bool IsRecording(int ClientID) = 0; + virtual void SaveDemo(int ClientId, float Time) = 0; + virtual void StartRecord(int ClientId) = 0; + virtual void StopRecord(int ClientId) = 0; + virtual bool IsRecording(int ClientId) = 0; virtual void StopDemos() = 0; - virtual void GetClientAddr(int ClientID, NETADDR *pAddr) const = 0; + virtual void GetClientAddr(int ClientId, NETADDR *pAddr) const = 0; - virtual int *GetIdMap(int ClientID) = 0; + virtual int *GetIdMap(int ClientId) = 0; - virtual bool DnsblWhite(int ClientID) = 0; - virtual bool DnsblPending(int ClientID) = 0; - virtual bool DnsblBlack(int ClientID) = 0; - virtual const char *GetAnnouncementLine(const char *pFileName) = 0; - virtual bool ClientPrevIngame(int ClientID) = 0; - virtual const char *GetNetErrorString(int ClientID) = 0; - virtual void ResetNetErrorString(int ClientID) = 0; - virtual bool SetTimedOut(int ClientID, int OrigID) = 0; - virtual void SetTimeoutProtected(int ClientID) = 0; + virtual bool DnsblWhite(int ClientId) = 0; + virtual bool DnsblPending(int ClientId) = 0; + virtual bool DnsblBlack(int ClientId) = 0; + virtual const char *GetAnnouncementLine() = 0; + virtual void ReadAnnouncementsFile(const char *pFileName) = 0; + virtual bool ClientPrevIngame(int ClientId) = 0; + virtual const char *GetNetErrorString(int ClientId) = 0; + virtual void ResetNetErrorString(int ClientId) = 0; + virtual bool SetTimedOut(int ClientId, int OrigId) = 0; + virtual void SetTimeoutProtected(int ClientId) = 0; virtual void SetErrorShutdown(const char *pReason) = 0; virtual void ExpireServerInfo() = 0; virtual void FillAntibot(CAntibotRoundData *pData) = 0; - virtual void SendMsgRaw(int ClientID, const void *pData, int Size, int Flags) = 0; + virtual void SendMsgRaw(int ClientId, const void *pData, int Size, int Flags) = 0; virtual const char *GetMapName() const = 0; - virtual bool IsSixup(int ClientID) const = 0; + virtual bool IsSixup(int ClientId) const = 0; }; class IGameServer : public IInterface @@ -290,10 +309,10 @@ class IGameServer : public IInterface virtual void OnTick() = 0; virtual void OnPreSnap() = 0; - virtual void OnSnap(int ClientID) = 0; + virtual void OnSnap(int ClientId) = 0; virtual void OnPostSnap() = 0; - virtual void OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID) = 0; + virtual void OnMessage(int MsgId, CUnpacker *pUnpacker, int ClientId) = 0; // Called before map reload, for any data that the game wants to // persist to the next map. @@ -302,24 +321,24 @@ class IGameServer : public IInterface // // Returns whether the game should be supplied with the data when the // client connects for the next map. - virtual bool OnClientDataPersist(int ClientID, void *pData) = 0; + virtual bool OnClientDataPersist(int ClientId, void *pData) = 0; // Called when a client connects. // // If it is reconnecting to the game after a map change, the // `pPersistentData` point is nonnull and contains the data the game // previously stored. - virtual void OnClientConnected(int ClientID, void *pPersistentData) = 0; + virtual void OnClientConnected(int ClientId, void *pPersistentData) = 0; - virtual void OnClientEnter(int ClientID) = 0; - virtual void OnClientDrop(int ClientID, const char *pReason) = 0; - virtual void OnClientPrepareInput(int ClientID, void *pInput) = 0; - virtual void OnClientDirectInput(int ClientID, void *pInput) = 0; - virtual void OnClientPredictedInput(int ClientID, void *pInput) = 0; - virtual void OnClientPredictedEarlyInput(int ClientID, void *pInput) = 0; + virtual void OnClientEnter(int ClientId) = 0; + virtual void OnClientDrop(int ClientId, const char *pReason) = 0; + virtual void OnClientPrepareInput(int ClientId, void *pInput) = 0; + virtual void OnClientDirectInput(int ClientId, void *pInput) = 0; + virtual void OnClientPredictedInput(int ClientId, void *pInput) = 0; + virtual void OnClientPredictedEarlyInput(int ClientId, void *pInput) = 0; - virtual bool IsClientReady(int ClientID) const = 0; - virtual bool IsClientPlayer(int ClientID) const = 0; + virtual bool IsClientReady(int ClientId) const = 0; + virtual bool IsClientPlayer(int ClientId) const = 0; virtual int PersistentDataSize() const = 0; virtual int PersistentClientDataSize() const = 0; @@ -333,23 +352,26 @@ class IGameServer : public IInterface virtual void OnPreTickTeehistorian() = 0; - virtual void OnSetAuthed(int ClientID, int Level) = 0; - virtual bool PlayerExists(int ClientID) const = 0; + virtual void OnSetAuthed(int ClientId, int Level) = 0; + virtual bool PlayerExists(int ClientId) const = 0; virtual void TeehistorianRecordAntibot(const void *pData, int DataSize) = 0; - virtual void TeehistorianRecordPlayerJoin(int ClientID, bool Sixup) = 0; - virtual void TeehistorianRecordPlayerDrop(int ClientID, const char *pReason) = 0; - virtual void TeehistorianRecordPlayerRejoin(int ClientID) = 0; + virtual void TeehistorianRecordPlayerJoin(int ClientId, bool Sixup) = 0; + virtual void TeehistorianRecordPlayerDrop(int ClientId, const char *pReason) = 0; + virtual void TeehistorianRecordPlayerRejoin(int ClientId) = 0; + virtual void TeehistorianRecordPlayerName(int ClientId, const char *pName) = 0; + virtual void TeehistorianRecordPlayerFinish(int ClientId, int TimeTicks) = 0; + virtual void TeehistorianRecordTeamFinish(int TeamId, int TimeTicks) = 0; virtual void FillAntibot(CAntibotRoundData *pData) = 0; /** * Used to report custom player info to master servers. * - * @param aBuf Should be the json key values to add, starting with a ',' beforehand, like: ',"skin": "default", "team": 1' + * @param pJsonWriter A pointer to a CJsonStringWriter which the custom data will be added to. * @param i The client id. */ - virtual void OnUpdatePlayerServerInfo(char *aBuf, int BufSize, int ID) = 0; + virtual void OnUpdatePlayerServerInfo(CJsonStringWriter *pJSonWriter, int Id) = 0; }; extern IGameServer *CreateGameServer(); diff --git a/src/engine/server/antibot.cpp b/src/engine/server/antibot.cpp index 238e8420f5..421235e439 100644 --- a/src/engine/server/antibot.cpp +++ b/src/engine/server/antibot.cpp @@ -23,23 +23,23 @@ CAntibot::~CAntibot() if(m_Initialized) AntibotDestroy(); } -void CAntibot::Kick(int ClientID, const char *pMessage, void *pUser) +void CAntibot::Kick(int ClientId, const char *pMessage, void *pUser) { CAntibot *pAntibot = (CAntibot *)pUser; - pAntibot->Server()->Kick(ClientID, pMessage); + pAntibot->Server()->Kick(ClientId, pMessage); } void CAntibot::Log(const char *pMessage, void *pUser) { CAntibot *pAntibot = (CAntibot *)pUser; pAntibot->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "antibot", pMessage); } -void CAntibot::Report(int ClientID, const char *pMessage, void *pUser) +void CAntibot::Report(int ClientId, const char *pMessage, void *pUser) { char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "%d: %s", ClientID, pMessage); + str_format(aBuf, sizeof(aBuf), "%d: %s", ClientId, pMessage); Log(aBuf, pUser); } -void CAntibot::Send(int ClientID, const void *pData, int Size, int Flags, void *pUser) +void CAntibot::Send(int ClientId, const void *pData, int Size, int Flags, void *pUser) { CAntibot *pAntibot = (CAntibot *)pUser; @@ -52,7 +52,7 @@ void CAntibot::Send(int ClientID, const void *pData, int Size, int Flags, void * { RealFlags |= MSGFLAG_FLUSH; } - pAntibot->Server()->SendMsgRaw(ClientID, pData, Size, RealFlags); + pAntibot->Server()->SendMsgRaw(ClientId, pData, Size, RealFlags); } void CAntibot::Teehistorian(const void *pData, int Size, void *pUser) { @@ -115,50 +115,50 @@ void CAntibot::Update() } } -void CAntibot::OnPlayerInit(int ClientID) +void CAntibot::OnPlayerInit(int ClientId) { Update(); - AntibotOnPlayerInit(ClientID); + AntibotOnPlayerInit(ClientId); } -void CAntibot::OnPlayerDestroy(int ClientID) +void CAntibot::OnPlayerDestroy(int ClientId) { Update(); - AntibotOnPlayerDestroy(ClientID); + AntibotOnPlayerDestroy(ClientId); } -void CAntibot::OnSpawn(int ClientID) +void CAntibot::OnSpawn(int ClientId) { Update(); - AntibotOnSpawn(ClientID); + AntibotOnSpawn(ClientId); } -void CAntibot::OnHammerFireReloading(int ClientID) +void CAntibot::OnHammerFireReloading(int ClientId) { Update(); - AntibotOnHammerFireReloading(ClientID); + AntibotOnHammerFireReloading(ClientId); } -void CAntibot::OnHammerFire(int ClientID) +void CAntibot::OnHammerFire(int ClientId) { Update(); - AntibotOnHammerFire(ClientID); + AntibotOnHammerFire(ClientId); } -void CAntibot::OnHammerHit(int ClientID, int TargetID) +void CAntibot::OnHammerHit(int ClientId, int TargetId) { Update(); - AntibotOnHammerHit(ClientID, TargetID); + AntibotOnHammerHit(ClientId, TargetId); } -void CAntibot::OnDirectInput(int ClientID) +void CAntibot::OnDirectInput(int ClientId) { Update(); - AntibotOnDirectInput(ClientID); + AntibotOnDirectInput(ClientId); } -void CAntibot::OnCharacterTick(int ClientID) +void CAntibot::OnCharacterTick(int ClientId) { Update(); - AntibotOnCharacterTick(ClientID); + AntibotOnCharacterTick(ClientId); } -void CAntibot::OnHookAttach(int ClientID, bool Player) +void CAntibot::OnHookAttach(int ClientId, bool Player) { Update(); - AntibotOnHookAttach(ClientID, Player); + AntibotOnHookAttach(ClientId, Player); } void CAntibot::OnEngineTick() @@ -166,17 +166,17 @@ void CAntibot::OnEngineTick() Update(); AntibotOnEngineTick(); } -void CAntibot::OnEngineClientJoin(int ClientID, bool Sixup) +void CAntibot::OnEngineClientJoin(int ClientId, bool Sixup) { Update(); - AntibotOnEngineClientJoin(ClientID, Sixup); + AntibotOnEngineClientJoin(ClientId, Sixup); } -void CAntibot::OnEngineClientDrop(int ClientID, const char *pReason) +void CAntibot::OnEngineClientDrop(int ClientId, const char *pReason) { Update(); - AntibotOnEngineClientDrop(ClientID, pReason); + AntibotOnEngineClientDrop(ClientId, pReason); } -bool CAntibot::OnEngineClientMessage(int ClientID, const void *pData, int Size, int Flags) +bool CAntibot::OnEngineClientMessage(int ClientId, const void *pData, int Size, int Flags) { Update(); int AntibotFlags = 0; @@ -184,9 +184,9 @@ bool CAntibot::OnEngineClientMessage(int ClientID, const void *pData, int Size, { AntibotFlags |= ANTIBOT_MSGFLAG_NONVITAL; } - return AntibotOnEngineClientMessage(ClientID, pData, Size, AntibotFlags); + return AntibotOnEngineClientMessage(ClientId, pData, Size, AntibotFlags); } -bool CAntibot::OnEngineServerMessage(int ClientID, const void *pData, int Size, int Flags) +bool CAntibot::OnEngineServerMessage(int ClientId, const void *pData, int Size, int Flags) { Update(); int AntibotFlags = 0; @@ -194,12 +194,12 @@ bool CAntibot::OnEngineServerMessage(int ClientID, const void *pData, int Size, { AntibotFlags |= ANTIBOT_MSGFLAG_NONVITAL; } - return AntibotOnEngineServerMessage(ClientID, pData, Size, AntibotFlags); + return AntibotOnEngineServerMessage(ClientId, pData, Size, AntibotFlags); } -bool CAntibot::OnEngineSimulateClientMessage(int *pClientID, void *pBuffer, int BufferSize, int *pOutSize, int *pFlags) +bool CAntibot::OnEngineSimulateClientMessage(int *pClientId, void *pBuffer, int BufferSize, int *pOutSize, int *pFlags) { int AntibotFlags = 0; - bool Result = AntibotOnEngineSimulateClientMessage(pClientID, pBuffer, BufferSize, pOutSize, &AntibotFlags); + bool Result = AntibotOnEngineSimulateClientMessage(pClientId, pBuffer, BufferSize, pOutSize, &AntibotFlags); if(Result) { *pFlags = 0; @@ -245,22 +245,22 @@ void CAntibot::Update() { } -void CAntibot::OnPlayerInit(int ClientID) {} -void CAntibot::OnPlayerDestroy(int ClientID) {} -void CAntibot::OnSpawn(int ClientID) {} -void CAntibot::OnHammerFireReloading(int ClientID) {} -void CAntibot::OnHammerFire(int ClientID) {} -void CAntibot::OnHammerHit(int ClientID, int TargetID) {} -void CAntibot::OnDirectInput(int ClientID) {} -void CAntibot::OnCharacterTick(int ClientID) {} -void CAntibot::OnHookAttach(int ClientID, bool Player) {} +void CAntibot::OnPlayerInit(int ClientId) {} +void CAntibot::OnPlayerDestroy(int ClientId) {} +void CAntibot::OnSpawn(int ClientId) {} +void CAntibot::OnHammerFireReloading(int ClientId) {} +void CAntibot::OnHammerFire(int ClientId) {} +void CAntibot::OnHammerHit(int ClientId, int TargetId) {} +void CAntibot::OnDirectInput(int ClientId) {} +void CAntibot::OnCharacterTick(int ClientId) {} +void CAntibot::OnHookAttach(int ClientId, bool Player) {} void CAntibot::OnEngineTick() {} -void CAntibot::OnEngineClientJoin(int ClientID, bool Sixup) {} -void CAntibot::OnEngineClientDrop(int ClientID, const char *pReason) {} -bool CAntibot::OnEngineClientMessage(int ClientID, const void *pData, int Size, int Flags) { return false; } -bool CAntibot::OnEngineServerMessage(int ClientID, const void *pData, int Size, int Flags) { return false; } -bool CAntibot::OnEngineSimulateClientMessage(int *pClientID, void *pBuffer, int BufferSize, int *pOutSize, int *pFlags) { return false; } +void CAntibot::OnEngineClientJoin(int ClientId, bool Sixup) {} +void CAntibot::OnEngineClientDrop(int ClientId, const char *pReason) {} +bool CAntibot::OnEngineClientMessage(int ClientId, const void *pData, int Size, int Flags) { return false; } +bool CAntibot::OnEngineServerMessage(int ClientId, const void *pData, int Size, int Flags) { return false; } +bool CAntibot::OnEngineSimulateClientMessage(int *pClientId, void *pBuffer, int BufferSize, int *pOutSize, int *pFlags) { return false; } #endif IEngineAntibot *CreateEngineAntibot() diff --git a/src/engine/server/antibot.h b/src/engine/server/antibot.h index 00af4b0aee..a8a9becf40 100644 --- a/src/engine/server/antibot.h +++ b/src/engine/server/antibot.h @@ -19,10 +19,10 @@ class CAntibot : public IEngineAntibot bool m_Initialized; void Update(); - static void Kick(int ClientID, const char *pMessage, void *pUser); + static void Kick(int ClientId, const char *pMessage, void *pUser); static void Log(const char *pMessage, void *pUser); - static void Report(int ClientID, const char *pMessage, void *pUser); - static void Send(int ClientID, const void *pData, int Size, int Flags, void *pUser); + static void Report(int ClientId, const char *pMessage, void *pUser); + static void Send(int ClientId, const void *pData, int Size, int Flags, void *pUser); static void Teehistorian(const void *pData, int Size, void *pUser); public: @@ -33,25 +33,25 @@ class CAntibot : public IEngineAntibot void Init() override; void OnEngineTick() override; - void OnEngineClientJoin(int ClientID, bool Sixup) override; - void OnEngineClientDrop(int ClientID, const char *pReason) override; - bool OnEngineClientMessage(int ClientID, const void *pData, int Size, int Flags) override; - bool OnEngineServerMessage(int ClientID, const void *pData, int Size, int Flags) override; - bool OnEngineSimulateClientMessage(int *pClientID, void *pBuffer, int BufferSize, int *pOutSize, int *pFlags) override; + void OnEngineClientJoin(int ClientId, bool Sixup) override; + void OnEngineClientDrop(int ClientId, const char *pReason) override; + bool OnEngineClientMessage(int ClientId, const void *pData, int Size, int Flags) override; + bool OnEngineServerMessage(int ClientId, const void *pData, int Size, int Flags) override; + bool OnEngineSimulateClientMessage(int *pClientId, void *pBuffer, int BufferSize, int *pOutSize, int *pFlags) override; // Game void RoundStart(class IGameServer *pGameServer) override; void RoundEnd() override; - void OnPlayerInit(int ClientID) override; - void OnPlayerDestroy(int ClientID) override; - void OnSpawn(int ClientID) override; - void OnHammerFireReloading(int ClientID) override; - void OnHammerFire(int ClientID) override; - void OnHammerHit(int ClientID, int TargetID) override; - void OnDirectInput(int ClientID) override; - void OnCharacterTick(int ClientID) override; - void OnHookAttach(int ClientID, bool Player) override; + void OnPlayerInit(int ClientId) override; + void OnPlayerDestroy(int ClientId) override; + void OnSpawn(int ClientId) override; + void OnHammerFireReloading(int ClientId) override; + void OnHammerFire(int ClientId) override; + void OnHammerHit(int ClientId, int TargetId) override; + void OnDirectInput(int ClientId) override; + void OnCharacterTick(int ClientId) override; + void OnHookAttach(int ClientId, bool Player) override; void ConsoleCommand(const char *pCommand) override; }; diff --git a/src/engine/server/databases/connection.cpp b/src/engine/server/databases/connection.cpp index ac124baa49..3abc416581 100644 --- a/src/engine/server/databases/connection.cpp +++ b/src/engine/server/databases/connection.cpp @@ -1,7 +1,5 @@ #include "connection.h" -#include - IDbConnection::IDbConnection(const char *pPrefix) { str_copy(m_aPrefix, pPrefix); @@ -25,12 +23,12 @@ void IDbConnection::FormatCreateRace(char *aBuf, unsigned int BufferSize, bool B " cp19 FLOAT DEFAULT 0, cp20 FLOAT DEFAULT 0, cp21 FLOAT DEFAULT 0, " " cp22 FLOAT DEFAULT 0, cp23 FLOAT DEFAULT 0, cp24 FLOAT DEFAULT 0, " " cp25 FLOAT DEFAULT 0, " - " GameID VARCHAR(64), " + " GameId VARCHAR(64), " " DDNet7 BOOL DEFAULT FALSE, " " PRIMARY KEY (Map, Name, Time, Timestamp, Server)" ")", GetPrefix(), Backup ? "_backup" : "", - BinaryCollate(), MAX_NAME_LENGTH, BinaryCollate()); + BinaryCollate(), MAX_NAME_LENGTH_SQL, BinaryCollate()); } void IDbConnection::FormatCreateTeamrace(char *aBuf, unsigned int BufferSize, const char *pIdType, bool Backup) const @@ -42,12 +40,12 @@ void IDbConnection::FormatCreateTeamrace(char *aBuf, unsigned int BufferSize, co " Timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " " Time FLOAT DEFAULT 0, " " ID %s NOT NULL, " // VARBINARY(16) for MySQL and BLOB for SQLite - " GameID VARCHAR(64), " + " GameId VARCHAR(64), " " DDNet7 BOOL DEFAULT FALSE, " - " PRIMARY KEY (ID, Name)" + " PRIMARY KEY (Id, Name)" ")", GetPrefix(), Backup ? "_backup" : "", - BinaryCollate(), MAX_NAME_LENGTH, BinaryCollate(), pIdType); + BinaryCollate(), MAX_NAME_LENGTH_SQL, BinaryCollate(), pIdType); } void IDbConnection::FormatCreateMaps(char *aBuf, unsigned int BufferSize) const @@ -75,7 +73,7 @@ void IDbConnection::FormatCreateSaves(char *aBuf, unsigned int BufferSize, bool " Timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, " " Server CHAR(4), " " DDNet7 BOOL DEFAULT FALSE, " - " SaveID VARCHAR(36) DEFAULT NULL, " + " SaveId VARCHAR(36) DEFAULT NULL, " " PRIMARY KEY (Map, Code)" ")", GetPrefix(), Backup ? "_backup" : "", @@ -90,5 +88,5 @@ void IDbConnection::FormatCreatePoints(char *aBuf, unsigned int BufferSize) cons " Points INT DEFAULT 0, " " PRIMARY KEY (Name)" ")", - GetPrefix(), MAX_NAME_LENGTH, BinaryCollate()); + GetPrefix(), MAX_NAME_LENGTH_SQL, BinaryCollate()); } diff --git a/src/engine/server/databases/connection.h b/src/engine/server/databases/connection.h index a2da8e0daf..c711474747 100644 --- a/src/engine/server/databases/connection.h +++ b/src/engine/server/databases/connection.h @@ -3,8 +3,15 @@ #include "connection_pool.h" +#include #include +enum +{ + // MAX_NAME_LENGTH includes the size with \0, which is not necessary in SQL + MAX_NAME_LENGTH_SQL = MAX_NAME_LENGTH - 1, +}; + class IConsole; // can hold one PreparedStatement with Results @@ -54,6 +61,7 @@ class IDbConnection virtual void BindInt(int Idx, int Value) = 0; virtual void BindInt64(int Idx, int64_t Value) = 0; virtual void BindFloat(int Idx, float Value) = 0; + virtual void BindNull(int Idx) = 0; // Print expanded sql statement virtual void Print() = 0; diff --git a/src/engine/server/databases/connection_pool.cpp b/src/engine/server/databases/connection_pool.cpp index 2ea2e3d9e1..2d8f607d93 100644 --- a/src/engine/server/databases/connection_pool.cpp +++ b/src/engine/server/databases/connection_pool.cpp @@ -49,7 +49,7 @@ struct CSqlExecData { CDbConnectionPool::Mode m_Mode; CMysqlConfig m_Config; - } m_MySql; + } m_Mysql; struct { CDbConnectionPool::Mode m_Mode; @@ -96,7 +96,7 @@ CSqlExecData::CSqlExecData( m_pName("add sqlite server") { m_Ptr.m_Sqlite.m_Mode = m; - mem_copy(m_Ptr.m_Sqlite.m_FileName, aFileName, sizeof(m_Ptr.m_Sqlite.m_FileName)); + str_copy(m_Ptr.m_Sqlite.m_FileName, aFileName); } CSqlExecData::CSqlExecData(CDbConnectionPool::Mode m, const CMysqlConfig *pMysqlConfig) : @@ -104,8 +104,8 @@ CSqlExecData::CSqlExecData(CDbConnectionPool::Mode m, m_pThreadData(nullptr), m_pName("add mysql server") { - m_Ptr.m_MySql.m_Mode = m; - mem_copy(&m_Ptr.m_MySql.m_Config, pMysqlConfig, sizeof(m_Ptr.m_MySql.m_Config)); + m_Ptr.m_Mysql.m_Mode = m; + mem_copy(&m_Ptr.m_Mysql.m_Config, pMysqlConfig, sizeof(m_Ptr.m_Mysql.m_Config)); } CSqlExecData::CSqlExecData(IConsole *pConsole, CDbConnectionPool::Mode m) : @@ -352,8 +352,8 @@ void CWorker::ProcessQueries() break; case CSqlExecData::ADD_MYSQL: { - auto pMysql = CreateMysqlConnection(pThreadData->m_Ptr.m_MySql.m_Config); - switch(pThreadData->m_Ptr.m_MySql.m_Mode) + auto pMysql = CreateMysqlConnection(pThreadData->m_Ptr.m_Mysql.m_Config); + switch(pThreadData->m_Ptr.m_Mysql.m_Mode) { case CDbConnectionPool::Mode::READ: m_vpReadConnections.push_back(std::move(pMysql)); diff --git a/src/engine/server/databases/mysql.cpp b/src/engine/server/databases/mysql.cpp index 202fb96871..2479733f03 100644 --- a/src/engine/server/databases/mysql.cpp +++ b/src/engine/server/databases/mysql.cpp @@ -89,6 +89,7 @@ class CMysqlConnection : public IDbConnection void BindInt(int Idx, int Value) override; void BindInt64(int Idx, int64_t Value) override; void BindFloat(int Idx, float Value) override; + void BindNull(int Idx) override; void Print() override {} bool Step(bool *pEnd, char *pError, int ErrorSize) override; @@ -421,6 +422,22 @@ void CMysqlConnection::BindFloat(int Idx, float Value) pParam->error = nullptr; } +void CMysqlConnection::BindNull(int Idx) +{ + m_NewQuery = true; + Idx -= 1; + dbg_assert(0 <= Idx && Idx < (int)m_vStmtParameters.size(), "index out of bounds"); + + MYSQL_BIND *pParam = &m_vStmtParameters[Idx]; + pParam->buffer_type = MYSQL_TYPE_NULL; + pParam->buffer = nullptr; + pParam->buffer_length = 0; + pParam->length = nullptr; + pParam->is_null = nullptr; + pParam->is_unsigned = false; + pParam->error = nullptr; +} + bool CMysqlConnection::Step(bool *pEnd, char *pError, int ErrorSize) { if(m_NewQuery) diff --git a/src/engine/server/databases/sqlite.cpp b/src/engine/server/databases/sqlite.cpp index df2925f6ef..bf483dc761 100644 --- a/src/engine/server/databases/sqlite.cpp +++ b/src/engine/server/databases/sqlite.cpp @@ -37,6 +37,7 @@ class CSqliteConnection : public IDbConnection void BindInt(int Idx, int Value) override; void BindInt64(int Idx, int64_t Value) override; void BindFloat(int Idx, float Value) override; + void BindNull(int Idx) override; void Print() override; bool Step(bool *pEnd, char *pError, int ErrorSize) override; @@ -241,6 +242,13 @@ void CSqliteConnection::BindFloat(int Idx, float Value) m_Done = false; } +void CSqliteConnection::BindNull(int Idx) +{ + int Result = sqlite3_bind_null(m_pStmt, Idx); + AssertNoError(Result); + m_Done = false; +} + // Keep support for SQLite < 3.14 on older Linux distributions. MinGW does not // support __attribute__((weak)): https://sourceware.org/bugzilla/show_bug.cgi?id=9687 #if defined(__GNUC__) && !defined(__MINGW32__) diff --git a/src/engine/server/main.cpp b/src/engine/server/main.cpp index ea19875316..5904b3245c 100644 --- a/src/engine/server/main.cpp +++ b/src/engine/server/main.cpp @@ -1,6 +1,3 @@ - -#define _WIN32_WINNT 0x0501 - #include #include @@ -23,7 +20,6 @@ #include #if defined(CONF_FAMILY_WINDOWS) -#define WIN32_LEAN_AND_MEAN #include #endif @@ -47,6 +43,8 @@ void HandleSigIntTerm(int Param) int main(int argc, const char **argv) { + const int64_t MainStart = time_get(); + CCmdlineFix CmdlineFix(&argc, &argv); bool Silent = false; @@ -90,12 +88,12 @@ int main(int argc, const char **argv) if(secure_random_init() != 0) { - dbg_msg("secure", "could not initialize secure RNG"); + log_error("secure", "could not initialize secure RNG"); return -1; } if(MysqlInit() != 0) { - dbg_msg("mysql", "failed to initialize MySQL library"); + log_error("mysql", "failed to initialize MySQL library"); return -1; } @@ -110,17 +108,17 @@ int main(int argc, const char **argv) pServer->SetLoggers(pFutureFileLogger, std::move(pStdoutLogger)); IKernel *pKernel = IKernel::Create(); + pKernel->RegisterInterface(pServer); // create the components IEngine *pEngine = CreateEngine(GAME_NAME, pFutureConsoleLogger, 2 * std::thread::hardware_concurrency() + 2); - IEngineMap *pEngineMap = CreateEngineMap(); - IGameServer *pGameServer = CreateGameServer(); - IConsole *pConsole = CreateConsole(CFGFLAG_SERVER | CFGFLAG_ECON).release(); + pKernel->RegisterInterface(pEngine); + IStorage *pStorage = CreateStorage(IStorage::STORAGETYPE_SERVER, argc, argv); - IConfigManager *pConfigManager = CreateConfigManager(); - IEngineAntibot *pEngineAntibot = CreateEngineAntibot(); + pKernel->RegisterInterface(pStorage); pFutureAssertionLogger->Set(CreateAssertionLogger(pStorage, GAME_NAME)); + #if defined(CONF_EXCEPTION_HANDLING) char aBuf[IO_MAX_PATH_LENGTH]; char aBufName[IO_MAX_PATH_LENGTH]; @@ -131,26 +129,22 @@ int main(int argc, const char **argv) set_exception_handler_log_file(aBuf); #endif - { - bool RegisterFail = false; - - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pServer); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngine); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngineMap); // register as both - RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast(pEngineMap), false); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pGameServer); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pConsole); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pStorage); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pConfigManager); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngineAntibot); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast(pEngineAntibot), false); - - if(RegisterFail) - { - delete pKernel; - return -1; - } - } + IConsole *pConsole = CreateConsole(CFGFLAG_SERVER | CFGFLAG_ECON).release(); + pKernel->RegisterInterface(pConsole); + + IConfigManager *pConfigManager = CreateConfigManager(); + pKernel->RegisterInterface(pConfigManager); + + IEngineMap *pEngineMap = CreateEngineMap(); + pKernel->RegisterInterface(pEngineMap); // IEngineMap + pKernel->RegisterInterface(static_cast(pEngineMap), false); + + IEngineAntibot *pEngineAntibot = CreateEngineAntibot(); + pKernel->RegisterInterface(pEngineAntibot); // IEngineAntibot + pKernel->RegisterInterface(static_cast(pEngineAntibot), false); + + IGameServer *pGameServer = CreateGameServer(); + pKernel->RegisterInterface(pGameServer); pEngine->Init(); pConsole->Init(); @@ -173,12 +167,13 @@ int main(int argc, const char **argv) if(argc > 1) pConsole->ParseArguments(argc - 1, &argv[1]); + pConfigManager->SetReadOnly("sv_max_clients", true); pConfigManager->SetReadOnly("sv_test_cmds", true); pConfigManager->SetReadOnly("sv_rescue", true); - const int Mode = g_Config.m_Logappend ? IOFLAG_APPEND : IOFLAG_WRITE; if(g_Config.m_Logfile[0]) { + const int Mode = g_Config.m_Logappend ? IOFLAG_APPEND : IOFLAG_WRITE; IOHANDLE Logfile = pStorage->OpenFile(g_Config.m_Logfile, Mode, IStorage::TYPE_SAVE_OR_ABSOLUTE); if(Logfile) { @@ -186,14 +181,20 @@ int main(int argc, const char **argv) } else { - dbg_msg("server", "failed to open '%s' for logging", g_Config.m_Logfile); + log_error("server", "failed to open '%s' for logging", g_Config.m_Logfile); + pFutureFileLogger->Set(log_logger_noop()); } } + else + { + pFutureFileLogger->Set(log_logger_noop()); + } + auto pServerLogger = std::make_shared(pServer); pEngine->SetAdditionalLogger(pServerLogger); // run the server - dbg_msg("server", "starting..."); + log_trace("server", "initialization finished after %.2fms, starting...", (time_get() - MainStart) * 1000.0f / (float)time_freq()); int Ret = pServer->Run(); pServerLogger->OnServerDeletion(); diff --git a/src/engine/server/register.cpp b/src/engine/server/register.cpp index 34acda919b..3092ea2dc7 100644 --- a/src/engine/server/register.cpp +++ b/src/engine/server/register.cpp @@ -21,6 +21,7 @@ class CRegister : public IRegister STATUS_OK, STATUS_NEEDCHALLENGE, STATUS_NEEDINFO, + STATUS_ERROR, PROTOCOL_TW6_IPV6 = 0, PROTOCOL_TW6_IPV4, @@ -70,17 +71,19 @@ class CRegister : public IRegister int m_Index; int m_InfoSerial; std::shared_ptr m_pShared; - std::unique_ptr m_pRegister; + std::shared_ptr m_pRegister; + IHttp *m_pHttp; void Run() override; public: - CJob(int Protocol, int ServerPort, int Index, int InfoSerial, std::shared_ptr pShared, std::unique_ptr &&pRegister) : + CJob(int Protocol, int ServerPort, int Index, int InfoSerial, std::shared_ptr pShared, std::shared_ptr &&pRegister, IHttp *pHttp) : m_Protocol(Protocol), m_ServerPort(ServerPort), m_Index(Index), m_InfoSerial(InfoSerial), m_pShared(std::move(pShared)), - m_pRegister(std::move(pRegister)) + m_pRegister(std::move(pRegister)), + m_pHttp(pHttp) { } ~CJob() override = default; @@ -110,6 +113,8 @@ class CRegister : public IRegister CConfig *m_pConfig; IConsole *m_pConsole; IEngine *m_pEngine; + IHttp *m_pHttp; + // Don't start sending registers before the server has initialized // completely. bool m_GotFirstUpdateCall = false; @@ -130,7 +135,7 @@ class CRegister : public IRegister char m_aServerInfo[16384]; public: - CRegister(CConfig *pConfig, IConsole *pConsole, IEngine *pEngine, int ServerPort, unsigned SixupSecurityToken); + CRegister(CConfig *pConfig, IConsole *pConsole, IEngine *pEngine, IHttp *pHttp, int ServerPort, unsigned SixupSecurityToken); void Update() override; void OnConfigChange() override; bool OnPacket(const CNetChunk *pPacket) override; @@ -152,6 +157,10 @@ bool CRegister::StatusFromString(int *pResult, const char *pString) { *pResult = STATUS_NEEDINFO; } + else if(str_comp(pString, "error") == 0) + { + *pResult = STATUS_ERROR; + } else { *pResult = -1; @@ -298,6 +307,7 @@ void CRegister::CProtocol::SendRegister() } pRegister->LogProgress(HTTPLOG::FAILURE); pRegister->IpResolve(ProtocolToIpresolve(m_Protocol)); + pRegister->FailOnErrorStatus(false); int RequestIndex; { @@ -309,7 +319,7 @@ void CRegister::CProtocol::SendRegister() RequestIndex = m_pShared->m_NumTotalRequests; m_pShared->m_NumTotalRequests += 1; } - m_pParent->m_pEngine->AddJob(std::make_shared(m_Protocol, m_pParent->m_ServerPort, RequestIndex, InfoSerial, m_pShared, std::move(pRegister))); + m_pParent->m_pEngine->AddJob(std::make_shared(m_Protocol, m_pParent->m_ServerPort, RequestIndex, InfoSerial, m_pShared, std::move(pRegister), m_pParent->m_pHttp)); m_NewChallengeToken = false; m_PrevRegister = Now; @@ -332,7 +342,7 @@ void CRegister::CProtocol::SendDeleteIfRegistered(bool Shutdown) char aSecret[UUID_MAXSTRSIZE]; FormatUuid(m_pParent->m_Secret, aSecret, sizeof(aSecret)); - std::unique_ptr pDelete = HttpPost(m_pParent->m_pConfig->m_SvRegisterUrl, (const unsigned char *)"", 0); + std::shared_ptr pDelete = HttpPost(m_pParent->m_pConfig->m_SvRegisterUrl, (const unsigned char *)"", 0); pDelete->HeaderString("Action", "delete"); pDelete->HeaderString("Address", aAddress); pDelete->HeaderString("Secret", aSecret); @@ -348,7 +358,7 @@ void CRegister::CProtocol::SendDeleteIfRegistered(bool Shutdown) pDelete->Timeout(CTimeout{1000, 1000, 0, 0}); } log_info(ProtocolToSystem(m_Protocol), "deleting..."); - m_pParent->m_pEngine->AddJob(std::move(pDelete)); + m_pParent->m_pHttp->Run(pDelete); } CRegister::CProtocol::CProtocol(CRegister *pParent, int Protocol) : @@ -405,12 +415,12 @@ void CRegister::CProtocol::OnToken(const char *pToken) void CRegister::CProtocol::CJob::Run() { - IEngine::RunJobBlocking(m_pRegister.get()); - if(m_pRegister->State() != HTTP_DONE) + m_pHttp->Run(m_pRegister); + m_pRegister->Wait(); + if(m_pRegister->State() != EHttpState::DONE) { - // TODO: log the error response content from master // TODO: exponential backoff - log_error(ProtocolToSystem(m_Protocol), "error response from master"); + log_error(ProtocolToSystem(m_Protocol), "error sending request to master"); return; } json_value *pJson = m_pRegister->ResultJson(); @@ -434,6 +444,25 @@ void CRegister::CProtocol::CJob::Run() json_value_free(pJson); return; } + if(Status == STATUS_ERROR) + { + const json_value &Message = Json["message"]; + if(Message.type != json_string) + { + json_value_free(pJson); + log_error(ProtocolToSystem(m_Protocol), "invalid JSON error response from master"); + return; + } + log_error(ProtocolToSystem(m_Protocol), "error response from master: %d: %s", m_pRegister->StatusCode(), (const char *)Message); + json_value_free(pJson); + return; + } + if(m_pRegister->StatusCode() >= 400) + { + log_error(ProtocolToSystem(m_Protocol), "non-success status code %d from master without error code", m_pRegister->StatusCode()); + json_value_free(pJson); + return; + } { CLockScope ls(m_pShared->m_Lock); if(Status != STATUS_OK || Status != m_pShared->m_LatestResponseStatus) @@ -471,10 +500,11 @@ void CRegister::CProtocol::CJob::Run() } } -CRegister::CRegister(CConfig *pConfig, IConsole *pConsole, IEngine *pEngine, int ServerPort, unsigned SixupSecurityToken) : +CRegister::CRegister(CConfig *pConfig, IConsole *pConsole, IEngine *pEngine, IHttp *pHttp, int ServerPort, unsigned SixupSecurityToken) : m_pConfig(pConfig), m_pConsole(pConsole), m_pEngine(pEngine), + m_pHttp(pHttp), m_ServerPort(ServerPort), m_aProtocols{ CProtocol(this, PROTOCOL_TW6_IPV6), @@ -488,10 +518,8 @@ CRegister::CRegister(CConfig *pConfig, IConsole *pConsole, IEngine *pEngine, int FormatUuid(m_ChallengeSecret, m_aVerifyPacketPrefix + HEADER_LEN, sizeof(m_aVerifyPacketPrefix) - HEADER_LEN); m_aVerifyPacketPrefix[HEADER_LEN + UUID_MAXSTRSIZE - 1] = ':'; - // The DDNet code uses the `unsigned` security token in memory byte order. - unsigned char aTokenBytes[sizeof(int32_t)]; - mem_copy(aTokenBytes, &SixupSecurityToken, sizeof(aTokenBytes)); - str_format(m_aConnlessTokenHex, sizeof(m_aConnlessTokenHex), "%08x", bytes_be_to_uint(aTokenBytes)); + // The DDNet code uses the `unsigned` security token in big-endian byte order. + str_format(m_aConnlessTokenHex, sizeof(m_aConnlessTokenHex), "%08x", SixupSecurityToken); m_pConsole->Chain("sv_register", ConchainOnConfigChange, this); m_pConsole->Chain("sv_register_extra", ConchainOnConfigChange, this); @@ -749,7 +777,7 @@ void CRegister::OnShutdown() } } -IRegister *CreateRegister(CConfig *pConfig, IConsole *pConsole, IEngine *pEngine, int ServerPort, unsigned SixupSecurityToken) +IRegister *CreateRegister(CConfig *pConfig, IConsole *pConsole, IEngine *pEngine, IHttp *pHttp, int ServerPort, unsigned SixupSecurityToken) { - return new CRegister(pConfig, pConsole, pEngine, ServerPort, SixupSecurityToken); + return new CRegister(pConfig, pConsole, pEngine, pHttp, ServerPort, SixupSecurityToken); } diff --git a/src/engine/server/register.h b/src/engine/server/register.h index 85bdf7a64c..1fbfc3c567 100644 --- a/src/engine/server/register.h +++ b/src/engine/server/register.h @@ -4,6 +4,7 @@ class CConfig; class IConsole; class IEngine; +class IHttp; struct CNetChunk; class IRegister @@ -23,6 +24,6 @@ class IRegister virtual void OnShutdown() = 0; }; -IRegister *CreateRegister(CConfig *pConfig, IConsole *pConsole, IEngine *pEngine, int ServerPort, unsigned SixupSecurityToken); +IRegister *CreateRegister(CConfig *pConfig, IConsole *pConsole, IEngine *pEngine, IHttp *pHttp, int ServerPort, unsigned SixupSecurityToken); #endif diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 80cb967261..44a2f99b3e 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -47,96 +48,6 @@ extern bool IsInterrupted(); -CSnapIDPool::CSnapIDPool() -{ - Reset(); -} - -void CSnapIDPool::Reset() -{ - for(int i = 0; i < MAX_IDS; i++) - { - m_aIDs[i].m_Next = i + 1; - m_aIDs[i].m_State = ID_FREE; - } - - m_aIDs[MAX_IDS - 1].m_Next = -1; - m_FirstFree = 0; - m_FirstTimed = -1; - m_LastTimed = -1; - m_Usage = 0; - m_InUsage = 0; -} - -void CSnapIDPool::RemoveFirstTimeout() -{ - int NextTimed = m_aIDs[m_FirstTimed].m_Next; - - // add it to the free list - m_aIDs[m_FirstTimed].m_Next = m_FirstFree; - m_aIDs[m_FirstTimed].m_State = ID_FREE; - m_FirstFree = m_FirstTimed; - - // remove it from the timed list - m_FirstTimed = NextTimed; - if(m_FirstTimed == -1) - m_LastTimed = -1; - - m_Usage--; -} - -int CSnapIDPool::NewID() -{ - int64_t Now = time_get(); - - // process timed ids - while(m_FirstTimed != -1 && m_aIDs[m_FirstTimed].m_Timeout < Now) - RemoveFirstTimeout(); - - int ID = m_FirstFree; - if(ID == -1) - { - dbg_msg("server", "invalid id"); - return ID; - } - m_FirstFree = m_aIDs[m_FirstFree].m_Next; - m_aIDs[ID].m_State = ID_ALLOCATED; - m_Usage++; - m_InUsage++; - return ID; -} - -void CSnapIDPool::TimeoutIDs() -{ - // process timed ids - while(m_FirstTimed != -1) - RemoveFirstTimeout(); -} - -void CSnapIDPool::FreeID(int ID) -{ - if(ID < 0) - return; - dbg_assert((size_t)ID < std::size(m_aIDs), "id is out of range"); - dbg_assert(m_aIDs[ID].m_State == ID_ALLOCATED, "id is not allocated"); - - m_InUsage--; - m_aIDs[ID].m_State = ID_TIMED; - m_aIDs[ID].m_Timeout = time_get() + time_freq() * 5; - m_aIDs[ID].m_Next = -1; - - if(m_LastTimed != -1) - { - m_aIDs[m_LastTimed].m_Next = ID; - m_LastTimed = ID; - } - else - { - m_FirstTimed = ID; - m_LastTimed = ID; - } -} - void CServerBan::InitServerBan(IConsole *pConsole, IStorage *pStorage, CServer *pServer) { CNetBan::Init(pConsole, pStorage); @@ -150,13 +61,13 @@ void CServerBan::InitServerBan(IConsole *pConsole, IStorage *pStorage, CServer * } template -int CServerBan::BanExt(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason) +int CServerBan::BanExt(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason, bool VerbatimReason) { // validate address - if(Server()->m_RconClientID >= 0 && Server()->m_RconClientID < MAX_CLIENTS && - Server()->m_aClients[Server()->m_RconClientID].m_State != CServer::CClient::STATE_EMPTY) + if(Server()->m_RconClientId >= 0 && Server()->m_RconClientId < MAX_CLIENTS && + Server()->m_aClients[Server()->m_RconClientId].m_State != CServer::CClient::STATE_EMPTY) { - if(NetMatch(pData, Server()->m_NetServer.ClientAddr(Server()->m_RconClientID))) + if(NetMatch(pData, Server()->m_NetServer.ClientAddr(Server()->m_RconClientId))) { Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", "ban error (you can't ban yourself)"); return -1; @@ -164,7 +75,7 @@ int CServerBan::BanExt(T *pBanPool, const typename T::CDataType *pData, int Seco for(int i = 0; i < MAX_CLIENTS; ++i) { - if(i == Server()->m_RconClientID || Server()->m_aClients[i].m_State == CServer::CClient::STATE_EMPTY) + if(i == Server()->m_RconClientId || Server()->m_aClients[i].m_State == CServer::CClient::STATE_EMPTY) continue; if(Server()->m_aClients[i].m_Authed >= Server()->m_RconAuthLevel && NetMatch(pData, Server()->m_NetServer.ClientAddr(i))) @@ -174,7 +85,7 @@ int CServerBan::BanExt(T *pBanPool, const typename T::CDataType *pData, int Seco } } } - else if(Server()->m_RconClientID == IServer::RCON_CID_VOTE) + else if(Server()->m_RconClientId == IServer::RCON_CID_VOTE) { for(int i = 0; i < MAX_CLIENTS; ++i) { @@ -189,7 +100,7 @@ int CServerBan::BanExt(T *pBanPool, const typename T::CDataType *pData, int Seco } } - int Result = Ban(pBanPool, pData, Seconds, pReason); + int Result = Ban(pBanPool, pData, Seconds, pReason, VerbatimReason); if(Result != 0) return Result; @@ -212,15 +123,15 @@ int CServerBan::BanExt(T *pBanPool, const typename T::CDataType *pData, int Seco return Result; } -int CServerBan::BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason) +int CServerBan::BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason, bool VerbatimReason) { - return BanExt(&m_BanAddrPool, pAddr, Seconds, pReason); + return BanExt(&m_BanAddrPool, pAddr, Seconds, pReason, VerbatimReason); } int CServerBan::BanRange(const CNetRange *pRange, int Seconds, const char *pReason) { if(pRange->IsValid()) - return BanExt(&m_BanRangePool, pRange, Seconds, pReason); + return BanExt(&m_BanRangePool, pRange, Seconds, pReason, true); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", "ban failed (invalid range)"); return -1; @@ -236,11 +147,11 @@ void CServerBan::ConBanExt(IConsole::IResult *pResult, void *pUser) if(str_isallnum(pStr)) { - int ClientID = str_toint(pStr); - if(ClientID < 0 || ClientID >= MAX_CLIENTS || pThis->Server()->m_aClients[ClientID].m_State == CServer::CClient::STATE_EMPTY) + int ClientId = str_toint(pStr); + if(ClientId < 0 || ClientId >= MAX_CLIENTS || pThis->Server()->m_aClients[ClientId].m_State == CServer::CClient::STATE_EMPTY) pThis->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", "ban error (invalid client id)"); else - pThis->BanAddr(pThis->Server()->m_NetServer.ClientAddr(ClientID), Minutes * 60, pReason); + pThis->BanAddr(pThis->Server()->m_NetServer.ClientAddr(ClientId), Minutes * 60, pReason, false); } else ConBan(pResult, pUser); @@ -272,12 +183,12 @@ void CServerBan::ConBanRegionRange(IConsole::IResult *pResult, void *pUser) class CRconClientLogger : public ILogger { CServer *m_pServer; - int m_ClientID; + int m_ClientId; public: - CRconClientLogger(CServer *pServer, int ClientID) : + CRconClientLogger(CServer *pServer, int ClientId) : m_pServer(pServer), - m_ClientID(ClientID) + m_ClientId(ClientId) { } void Log(const CLogMessage *pMessage) override; @@ -289,7 +200,7 @@ void CRconClientLogger::Log(const CLogMessage *pMessage) { return; } - m_pServer->SendRconLogLine(m_ClientID, pMessage); + m_pServer->SendRconLogLine(m_ClientId, pMessage); } void CServer::CClient::Reset() @@ -315,7 +226,8 @@ CServer::CServer() m_pConfig = &g_Config; for(int i = 0; i < MAX_CLIENTS; i++) m_aDemoRecorder[i] = CDemoRecorder(&m_SnapshotDelta, true); - m_aDemoRecorder[MAX_CLIENTS] = CDemoRecorder(&m_SnapshotDelta, false); + m_aDemoRecorder[RECORDER_MANUAL] = CDemoRecorder(&m_SnapshotDelta, false); + m_aDemoRecorder[RECORDER_AUTO] = CDemoRecorder(&m_SnapshotDelta, false); m_pGameServer = 0; @@ -331,10 +243,11 @@ CServer::CServer() } m_MapReload = false; + m_SameMapReload = false; m_ReloadedWhenEmpty = false; m_aCurrentMap[0] = '\0'; - m_RconClientID = IServer::RCON_CID_SERV; + m_RconClientId = IServer::RCON_CID_SERV; m_RconAuthLevel = AUTHED_ADMIN; m_ServerInfoFirstRequest = 0; @@ -373,7 +286,7 @@ CServer::~CServer() delete m_pConnectionPool; } -bool CServer::IsClientNameAvailable(int ClientID, const char *pNameRequest) +bool CServer::IsClientNameAvailable(int ClientId, const char *pNameRequest) { // check for empty names if(!pNameRequest[0]) @@ -387,7 +300,7 @@ bool CServer::IsClientNameAvailable(int ClientID, const char *pNameRequest) // make sure that two clients don't have the same name for(int i = 0; i < MAX_CLIENTS; i++) { - if(i != ClientID && m_aClients[i].m_State >= CClient::STATE_READY) + if(i != ClientId && m_aClients[i].m_State >= CClient::STATE_READY) { if(str_utf8_comp_confusable(pNameRequest, m_aClients[i].m_aName) == 0) return false; @@ -397,16 +310,16 @@ bool CServer::IsClientNameAvailable(int ClientID, const char *pNameRequest) return true; } -bool CServer::SetClientNameImpl(int ClientID, const char *pNameRequest, bool Set) +bool CServer::SetClientNameImpl(int ClientId, const char *pNameRequest, bool Set) { - dbg_assert(0 <= ClientID && ClientID < MAX_CLIENTS, "invalid client id"); - if(m_aClients[ClientID].m_State < CClient::STATE_READY) + dbg_assert(0 <= ClientId && ClientId < MAX_CLIENTS, "invalid client id"); + if(m_aClients[ClientId].m_State < CClient::STATE_READY) return false; const CNameBan *pBanned = m_NameBans.IsBanned(pNameRequest); if(pBanned) { - if(m_aClients[ClientID].m_State == CClient::STATE_READY && Set) + if(m_aClients[ClientId].m_State == CClient::STATE_READY && Set) { char aBuf[256]; if(pBanned->m_aReason[0]) @@ -417,7 +330,7 @@ bool CServer::SetClientNameImpl(int ClientID, const char *pNameRequest, bool Set { str_copy(aBuf, "Kicked (your name is banned)"); } - Kick(ClientID, aBuf); + Kick(ClientId, aBuf); } return false; } @@ -430,38 +343,39 @@ bool CServer::SetClientNameImpl(int ClientID, const char *pNameRequest, bool Set char aNameTry[MAX_NAME_LENGTH]; str_copy(aNameTry, aTrimmedName); - if(!IsClientNameAvailable(ClientID, aNameTry)) + if(!IsClientNameAvailable(ClientId, aNameTry)) { // auto rename for(int i = 1;; i++) { str_format(aNameTry, sizeof(aNameTry), "(%d)%s", i, aTrimmedName); - if(IsClientNameAvailable(ClientID, aNameTry)) + if(IsClientNameAvailable(ClientId, aNameTry)) break; } } - bool Changed = str_comp(m_aClients[ClientID].m_aName, aNameTry) != 0; + bool Changed = str_comp(m_aClients[ClientId].m_aName, aNameTry) != 0; - if(Set) + if(Set && Changed) { // set the client name - str_copy(m_aClients[ClientID].m_aName, aNameTry); + str_copy(m_aClients[ClientId].m_aName, aNameTry); + GameServer()->TeehistorianRecordPlayerName(ClientId, m_aClients[ClientId].m_aName); } return Changed; } -bool CServer::SetClientClanImpl(int ClientID, const char *pClanRequest, bool Set) +bool CServer::SetClientClanImpl(int ClientId, const char *pClanRequest, bool Set) { - dbg_assert(0 <= ClientID && ClientID < MAX_CLIENTS, "invalid client id"); - if(m_aClients[ClientID].m_State < CClient::STATE_READY) + dbg_assert(0 <= ClientId && ClientId < MAX_CLIENTS, "invalid client id"); + if(m_aClients[ClientId].m_State < CClient::STATE_READY) return false; const CNameBan *pBanned = m_NameBans.IsBanned(pClanRequest); if(pBanned) { - if(m_aClients[ClientID].m_State == CClient::STATE_READY && Set) + if(m_aClients[ClientId].m_State == CClient::STATE_READY && Set) { char aBuf[256]; if(pBanned->m_aReason[0]) @@ -472,7 +386,7 @@ bool CServer::SetClientClanImpl(int ClientID, const char *pClanRequest, bool Set { str_copy(aBuf, "Kicked (your clan is banned)"); } - Kick(ClientID, aBuf); + Kick(ClientId, aBuf); } return false; } @@ -482,102 +396,102 @@ bool CServer::SetClientClanImpl(int ClientID, const char *pClanRequest, bool Set str_copy(aTrimmedClan, str_utf8_skip_whitespaces(pClanRequest)); str_utf8_trim_right(aTrimmedClan); - bool Changed = str_comp(m_aClients[ClientID].m_aClan, aTrimmedClan) != 0; + bool Changed = str_comp(m_aClients[ClientId].m_aClan, aTrimmedClan) != 0; if(Set) { // set the client clan - str_copy(m_aClients[ClientID].m_aClan, aTrimmedClan); + str_copy(m_aClients[ClientId].m_aClan, aTrimmedClan); } return Changed; } -bool CServer::WouldClientNameChange(int ClientID, const char *pNameRequest) +bool CServer::WouldClientNameChange(int ClientId, const char *pNameRequest) { - return SetClientNameImpl(ClientID, pNameRequest, false); + return SetClientNameImpl(ClientId, pNameRequest, false); } -bool CServer::WouldClientClanChange(int ClientID, const char *pClanRequest) +bool CServer::WouldClientClanChange(int ClientId, const char *pClanRequest) { - return SetClientClanImpl(ClientID, pClanRequest, false); + return SetClientClanImpl(ClientId, pClanRequest, false); } -void CServer::SetClientName(int ClientID, const char *pName) +void CServer::SetClientName(int ClientId, const char *pName) { - SetClientNameImpl(ClientID, pName, true); + SetClientNameImpl(ClientId, pName, true); } -void CServer::SetClientClan(int ClientID, const char *pClan) +void CServer::SetClientClan(int ClientId, const char *pClan) { - SetClientClanImpl(ClientID, pClan, true); + SetClientClanImpl(ClientId, pClan, true); } -void CServer::SetClientCountry(int ClientID, int Country) +void CServer::SetClientCountry(int ClientId, int Country) { - if(ClientID < 0 || ClientID >= MAX_CLIENTS || m_aClients[ClientID].m_State < CClient::STATE_READY) + if(ClientId < 0 || ClientId >= MAX_CLIENTS || m_aClients[ClientId].m_State < CClient::STATE_READY) return; - m_aClients[ClientID].m_Country = Country; + m_aClients[ClientId].m_Country = Country; } -void CServer::SetClientScore(int ClientID, std::optional Score) +void CServer::SetClientScore(int ClientId, std::optional Score) { - if(ClientID < 0 || ClientID >= MAX_CLIENTS || m_aClients[ClientID].m_State < CClient::STATE_READY) + if(ClientId < 0 || ClientId >= MAX_CLIENTS || m_aClients[ClientId].m_State < CClient::STATE_READY) return; - if(m_aClients[ClientID].m_Score != Score) + if(m_aClients[ClientId].m_Score != Score) ExpireServerInfo(); - m_aClients[ClientID].m_Score = Score; + m_aClients[ClientId].m_Score = Score; } -void CServer::SetClientFlags(int ClientID, int Flags) +void CServer::SetClientFlags(int ClientId, int Flags) { - if(ClientID < 0 || ClientID >= MAX_CLIENTS || m_aClients[ClientID].m_State < CClient::STATE_READY) + if(ClientId < 0 || ClientId >= MAX_CLIENTS || m_aClients[ClientId].m_State < CClient::STATE_READY) return; - m_aClients[ClientID].m_Flags = Flags; + m_aClients[ClientId].m_Flags = Flags; } -void CServer::Kick(int ClientID, const char *pReason) +void CServer::Kick(int ClientId, const char *pReason) { - if(ClientID < 0 || ClientID >= MAX_CLIENTS || m_aClients[ClientID].m_State == CClient::STATE_EMPTY) + if(ClientId < 0 || ClientId >= MAX_CLIENTS || m_aClients[ClientId].m_State == CClient::STATE_EMPTY) { Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "invalid client id to kick"); return; } - else if(m_RconClientID == ClientID) + else if(m_RconClientId == ClientId) { Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "you can't kick yourself"); return; } - else if(m_aClients[ClientID].m_Authed > m_RconAuthLevel) + else if(m_aClients[ClientId].m_Authed > m_RconAuthLevel) { Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "kick command denied"); return; } - m_NetServer.Drop(ClientID, pReason); + m_NetServer.Drop(ClientId, pReason); } -void CServer::Ban(int ClientID, int Seconds, const char *pReason) +void CServer::Ban(int ClientId, int Seconds, const char *pReason, bool VerbatimReason) { NETADDR Addr; - GetClientAddr(ClientID, &Addr); - m_NetServer.NetBan()->BanAddr(&Addr, Seconds, pReason); + GetClientAddr(ClientId, &Addr); + m_NetServer.NetBan()->BanAddr(&Addr, Seconds, pReason, VerbatimReason); } -void CServer::RedirectClient(int ClientID, int Port, bool Verbose) +void CServer::RedirectClient(int ClientId, int Port, bool Verbose) { - if(ClientID < 0 || ClientID >= MAX_CLIENTS) + if(ClientId < 0 || ClientId >= MAX_CLIENTS) return; char aBuf[512]; - bool SupportsRedirect = GetClientVersion(ClientID) >= VERSION_DDNET_REDIRECT; + bool SupportsRedirect = GetClientVersion(ClientId) >= VERSION_DDNET_REDIRECT; if(Verbose) { - str_format(aBuf, sizeof(aBuf), "redirecting '%s' to port %d supported=%d", ClientName(ClientID), Port, SupportsRedirect); + str_format(aBuf, sizeof(aBuf), "redirecting '%s' to port %d supported=%d", ClientName(ClientId), Port, SupportsRedirect); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "redirect", aBuf); } @@ -585,16 +499,16 @@ void CServer::RedirectClient(int ClientID, int Port, bool Verbose) { bool SamePort = Port == m_NetServer.Address().port; str_format(aBuf, sizeof(aBuf), "Redirect unsupported: please connect to port %d", Port); - Kick(ClientID, SamePort ? "Redirect unsupported: please reconnect" : aBuf); + Kick(ClientId, SamePort ? "Redirect unsupported: please reconnect" : aBuf); return; } CMsgPacker Msg(NETMSG_REDIRECT, true); Msg.AddInt(Port); - SendMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId); - m_aClients[ClientID].m_RedirectDropTime = time_get() + time_freq() * 10; - m_aClients[ClientID].m_State = CClient::STATE_REDIRECTED; + m_aClients[ClientId].m_RedirectDropTime = time_get() + time_freq() * 10; + m_aClients[ClientId].m_State = CClient::STATE_REDIRECTED; } int64_t CServer::TickStartTime(int Tick) @@ -623,8 +537,7 @@ int CServer::Init() m_CurrentGameTick = MIN_TICK; - m_AnnouncementLastLine = 0; - m_aAnnouncementFile[0] = '\0'; + m_AnnouncementLastLine = -1; mem_zero(m_aPrevStates, sizeof(m_aPrevStates)); return 0; @@ -642,19 +555,19 @@ void CServer::SendLogLine(const CLogMessage *pMessage) } } -void CServer::SetRconCID(int ClientID) +void CServer::SetRconCid(int ClientId) { - m_RconClientID = ClientID; + m_RconClientId = ClientId; } -int CServer::GetAuthedState(int ClientID) const +int CServer::GetAuthedState(int ClientId) const { - return m_aClients[ClientID].m_Authed; + return m_aClients[ClientId].m_Authed; } -const char *CServer::GetAuthName(int ClientID) const +const char *CServer::GetAuthName(int ClientId) const { - int Key = m_aClients[ClientID].m_AuthKey; + int Key = m_aClients[ClientId].m_AuthKey; if(Key == -1) { return 0; @@ -662,25 +575,25 @@ const char *CServer::GetAuthName(int ClientID) const return m_AuthManager.KeyIdent(Key); } -bool CServer::GetClientInfo(int ClientID, CClientInfo *pInfo) const +bool CServer::GetClientInfo(int ClientId, CClientInfo *pInfo) const { - dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "ClientID is not valid"); + dbg_assert(ClientId >= 0 && ClientId < MAX_CLIENTS, "ClientId is not valid"); dbg_assert(pInfo != nullptr, "pInfo cannot be null"); - if(m_aClients[ClientID].m_State == CClient::STATE_INGAME) + if(m_aClients[ClientId].m_State == CClient::STATE_INGAME) { - pInfo->m_pName = m_aClients[ClientID].m_aName; - pInfo->m_Latency = m_aClients[ClientID].m_Latency; - pInfo->m_GotDDNetVersion = m_aClients[ClientID].m_DDNetVersionSettled; - pInfo->m_DDNetVersion = m_aClients[ClientID].m_DDNetVersion >= 0 ? m_aClients[ClientID].m_DDNetVersion : VERSION_VANILLA; - if(m_aClients[ClientID].m_GotDDNetVersionPacket) + pInfo->m_pName = m_aClients[ClientId].m_aName; + pInfo->m_Latency = m_aClients[ClientId].m_Latency; + pInfo->m_GotDDNetVersion = m_aClients[ClientId].m_DDNetVersionSettled; + pInfo->m_DDNetVersion = m_aClients[ClientId].m_DDNetVersion >= 0 ? m_aClients[ClientId].m_DDNetVersion : VERSION_VANILLA; + if(m_aClients[ClientId].m_GotDDNetVersionPacket) { - pInfo->m_pConnectionID = &m_aClients[ClientID].m_ConnectionID; - pInfo->m_pDDNetVersionStr = m_aClients[ClientID].m_aDDNetVersionStr; + pInfo->m_pConnectionId = &m_aClients[ClientId].m_ConnectionId; + pInfo->m_pDDNetVersionStr = m_aClients[ClientId].m_aDDNetVersionStr; } else { - pInfo->m_pConnectionID = nullptr; + pInfo->m_pConnectionId = nullptr; pInfo->m_pDDNetVersionStr = nullptr; } return true; @@ -688,61 +601,66 @@ bool CServer::GetClientInfo(int ClientID, CClientInfo *pInfo) const return false; } -void CServer::SetClientDDNetVersion(int ClientID, int DDNetVersion) +void CServer::SetClientDDNetVersion(int ClientId, int DDNetVersion) { - dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "ClientID is not valid"); + dbg_assert(ClientId >= 0 && ClientId < MAX_CLIENTS, "ClientId is not valid"); - if(m_aClients[ClientID].m_State == CClient::STATE_INGAME) + if(m_aClients[ClientId].m_State == CClient::STATE_INGAME) { - m_aClients[ClientID].m_DDNetVersion = DDNetVersion; - m_aClients[ClientID].m_DDNetVersionSettled = true; + m_aClients[ClientId].m_DDNetVersion = DDNetVersion; + m_aClients[ClientId].m_DDNetVersionSettled = true; } } -void CServer::GetClientAddr(int ClientID, char *pAddrStr, int Size) const +void CServer::GetClientAddr(int ClientId, char *pAddrStr, int Size) const { - if(ClientID >= 0 && ClientID < MAX_CLIENTS && m_aClients[ClientID].m_State == CClient::STATE_INGAME) - net_addr_str(m_NetServer.ClientAddr(ClientID), pAddrStr, Size, false); + if(ClientId >= 0 && ClientId < MAX_CLIENTS && m_aClients[ClientId].m_State == CClient::STATE_INGAME) + net_addr_str(m_NetServer.ClientAddr(ClientId), pAddrStr, Size, false); } -const char *CServer::ClientName(int ClientID) const +const char *CServer::ClientName(int ClientId) const { - if(ClientID < 0 || ClientID >= MAX_CLIENTS || m_aClients[ClientID].m_State == CServer::CClient::STATE_EMPTY) + if(ClientId < 0 || ClientId >= MAX_CLIENTS || m_aClients[ClientId].m_State == CServer::CClient::STATE_EMPTY) return "(invalid)"; - if(m_aClients[ClientID].m_State == CServer::CClient::STATE_INGAME) - return m_aClients[ClientID].m_aName; + if(m_aClients[ClientId].m_State == CServer::CClient::STATE_INGAME) + return m_aClients[ClientId].m_aName; else return "(connecting)"; } -const char *CServer::ClientClan(int ClientID) const +const char *CServer::ClientClan(int ClientId) const { - if(ClientID < 0 || ClientID >= MAX_CLIENTS || m_aClients[ClientID].m_State == CServer::CClient::STATE_EMPTY) + if(ClientId < 0 || ClientId >= MAX_CLIENTS || m_aClients[ClientId].m_State == CServer::CClient::STATE_EMPTY) return ""; - if(m_aClients[ClientID].m_State == CServer::CClient::STATE_INGAME) - return m_aClients[ClientID].m_aClan; + if(m_aClients[ClientId].m_State == CServer::CClient::STATE_INGAME) + return m_aClients[ClientId].m_aClan; else return ""; } -int CServer::ClientCountry(int ClientID) const +int CServer::ClientCountry(int ClientId) const { - if(ClientID < 0 || ClientID >= MAX_CLIENTS || m_aClients[ClientID].m_State == CServer::CClient::STATE_EMPTY) + if(ClientId < 0 || ClientId >= MAX_CLIENTS || m_aClients[ClientId].m_State == CServer::CClient::STATE_EMPTY) return -1; - if(m_aClients[ClientID].m_State == CServer::CClient::STATE_INGAME) - return m_aClients[ClientID].m_Country; + if(m_aClients[ClientId].m_State == CServer::CClient::STATE_INGAME) + return m_aClients[ClientId].m_Country; else return -1; } -bool CServer::ClientIngame(int ClientID) const +bool CServer::ClientSlotEmpty(int ClientId) const { - return ClientID >= 0 && ClientID < MAX_CLIENTS && m_aClients[ClientID].m_State == CServer::CClient::STATE_INGAME; + return ClientId >= 0 && ClientId < MAX_CLIENTS && m_aClients[ClientId].m_State == CServer::CClient::STATE_EMPTY; } -bool CServer::ClientAuthed(int ClientID) const +bool CServer::ClientIngame(int ClientId) const { - return ClientID >= 0 && ClientID < MAX_CLIENTS && m_aClients[ClientID].m_Authed; + return ClientId >= 0 && ClientId < MAX_CLIENTS && m_aClients[ClientId].m_State == CServer::CClient::STATE_INGAME; +} + +bool CServer::ClientAuthed(int ClientId) const +{ + return ClientId >= 0 && ClientId < MAX_CLIENTS && m_aClients[ClientId].m_Authed; } int CServer::Port() const @@ -801,21 +719,21 @@ int CServer::DistinctClientCount() const return ClientCount; } -int CServer::GetClientVersion(int ClientID) const +int CServer::GetClientVersion(int ClientId) const { // Assume latest client version for server demos - if(ClientID == SERVER_DEMO_CLIENT) + if(ClientId == SERVER_DEMO_CLIENT) return DDNET_VERSION_NUMBER; CClientInfo Info; - if(GetClientInfo(ClientID, &Info)) + if(GetClientInfo(ClientId, &Info)) return Info.m_DDNetVersion; return VERSION_NONE; } static inline bool RepackMsg(const CMsgPacker *pMsg, CPacker &Packer, bool Sixup) { - int MsgId = pMsg->m_MsgID; + int MsgId = pMsg->m_MsgId; Packer.Reset(); if(Sixup && !pMsg->m_NoTranslate) @@ -864,7 +782,7 @@ static inline bool RepackMsg(const CMsgPacker *pMsg, CPacker &Packer, bool Sixup return false; } -int CServer::SendMsg(CMsgPacker *pMsg, int Flags, int ClientID) +int CServer::SendMsg(CMsgPacker *pMsg, int Flags, int ClientId) { CNetChunk Packet; mem_zero(&Packet, sizeof(CNetChunk)); @@ -873,7 +791,7 @@ int CServer::SendMsg(CMsgPacker *pMsg, int Flags, int ClientID) if(Flags & MSGFLAG_FLUSH) Packet.m_Flags |= NETSENDFLAG_FLUSH; - if(ClientID < 0) + if(ClientId < 0) { CPacker Pack6, Pack7; if(RepackMsg(pMsg, Pack6, false)) @@ -898,7 +816,7 @@ int CServer::SendMsg(CMsgPacker *pMsg, int Flags, int ClientID) CPacker *pPack = m_aClients[i].m_Sixup ? &Pack7 : &Pack6; Packet.m_pData = pPack->Data(); Packet.m_DataSize = pPack->Size(); - Packet.m_ClientID = i; + Packet.m_ClientId = i; if(Antibot()->OnEngineServerMessage(i, Packet.m_pData, Packet.m_DataSize, Flags)) { continue; @@ -911,14 +829,14 @@ int CServer::SendMsg(CMsgPacker *pMsg, int Flags, int ClientID) else { CPacker Pack; - if(RepackMsg(pMsg, Pack, m_aClients[ClientID].m_Sixup)) + if(RepackMsg(pMsg, Pack, m_aClients[ClientId].m_Sixup)) return -1; - Packet.m_ClientID = ClientID; + Packet.m_ClientId = ClientId; Packet.m_pData = Pack.Data(); Packet.m_DataSize = Pack.Size(); - if(Antibot()->OnEngineServerMessage(ClientID, Packet.m_pData, Packet.m_DataSize, Flags)) + if(Antibot()->OnEngineServerMessage(ClientId, Packet.m_pData, Packet.m_DataSize, Flags)) { return 0; } @@ -926,10 +844,12 @@ int CServer::SendMsg(CMsgPacker *pMsg, int Flags, int ClientID) // write message to demo recorders if(!(Flags & MSGFLAG_NORECORD)) { - if(m_aDemoRecorder[ClientID].IsRecording()) - m_aDemoRecorder[ClientID].RecordMessage(Pack.Data(), Pack.Size()); - if(m_aDemoRecorder[MAX_CLIENTS].IsRecording()) - m_aDemoRecorder[MAX_CLIENTS].RecordMessage(Pack.Data(), Pack.Size()); + if(m_aDemoRecorder[ClientId].IsRecording()) + m_aDemoRecorder[ClientId].RecordMessage(Pack.Data(), Pack.Size()); + if(m_aDemoRecorder[RECORDER_MANUAL].IsRecording()) + m_aDemoRecorder[RECORDER_MANUAL].RecordMessage(Pack.Data(), Pack.Size()); + if(m_aDemoRecorder[RECORDER_AUTO].IsRecording()) + m_aDemoRecorder[RECORDER_AUTO].RecordMessage(Pack.Data(), Pack.Size()); } if(!(Flags & MSGFLAG_NOSEND)) @@ -939,11 +859,11 @@ int CServer::SendMsg(CMsgPacker *pMsg, int Flags, int ClientID) return 0; } -void CServer::SendMsgRaw(int ClientID, const void *pData, int Size, int Flags) +void CServer::SendMsgRaw(int ClientId, const void *pData, int Size, int Flags) { CNetChunk Packet; mem_zero(&Packet, sizeof(CNetChunk)); - Packet.m_ClientID = ClientID; + Packet.m_ClientId = ClientId; Packet.m_pData = pData; Packet.m_DataSize = Size; Packet.m_Flags = 0; @@ -962,9 +882,9 @@ void CServer::DoSnapshot() { GameServer()->OnPreSnap(); - // create snapshot for demo recording - if(m_aDemoRecorder[MAX_CLIENTS].IsRecording()) + if(m_aDemoRecorder[RECORDER_MANUAL].IsRecording() || m_aDemoRecorder[RECORDER_AUTO].IsRecording()) { + // create snapshot for demo recording char aData[CSnapshot::MAX_SIZE]; // build snap and possibly add some messages @@ -973,7 +893,10 @@ void CServer::DoSnapshot() int SnapshotSize = m_SnapshotBuilder.Finish(aData); // write snapshot - m_aDemoRecorder[MAX_CLIENTS].RecordSnapshot(Tick(), aData, SnapshotSize); + if(m_aDemoRecorder[RECORDER_MANUAL].IsRecording()) + m_aDemoRecorder[RECORDER_MANUAL].RecordSnapshot(Tick(), aData, SnapshotSize); + if(m_aDemoRecorder[RECORDER_AUTO].IsRecording()) + m_aDemoRecorder[RECORDER_AUTO].RecordSnapshot(Tick(), aData, SnapshotSize); } // create snapshots for all clients @@ -1020,7 +943,7 @@ void CServer::DoSnapshot() int DeltaTick = -1; const CSnapshot *pDeltashot = CSnapshot::EmptySnapshot(); { - int DeltashotSize = m_aClients[i].m_Snapshots.Get(m_aClients[i].m_LastAckedSnapshot, 0, &pDeltashot, 0); + int DeltashotSize = m_aClients[i].m_Snapshots.Get(m_aClients[i].m_LastAckedSnapshot, nullptr, &pDeltashot, nullptr); if(DeltashotSize >= 0) DeltaTick = m_aClients[i].m_LastAckedSnapshot; else @@ -1088,96 +1011,96 @@ void CServer::DoSnapshot() GameServer()->OnPostSnap(); } -int CServer::ClientRejoinCallback(int ClientID, void *pUser) +int CServer::ClientRejoinCallback(int ClientId, void *pUser) { CServer *pThis = (CServer *)pUser; - pThis->m_aClients[ClientID].m_Authed = AUTHED_NO; - pThis->m_aClients[ClientID].m_AuthKey = -1; - pThis->m_aClients[ClientID].m_pRconCmdToSend = 0; - pThis->m_aClients[ClientID].m_DDNetVersion = VERSION_NONE; - pThis->m_aClients[ClientID].m_GotDDNetVersionPacket = false; - pThis->m_aClients[ClientID].m_DDNetVersionSettled = false; + pThis->m_aClients[ClientId].m_Authed = AUTHED_NO; + pThis->m_aClients[ClientId].m_AuthKey = -1; + pThis->m_aClients[ClientId].m_pRconCmdToSend = nullptr; + pThis->m_aClients[ClientId].m_DDNetVersion = VERSION_NONE; + pThis->m_aClients[ClientId].m_GotDDNetVersionPacket = false; + pThis->m_aClients[ClientId].m_DDNetVersionSettled = false; - pThis->m_aClients[ClientID].Reset(); + pThis->m_aClients[ClientId].Reset(); - pThis->GameServer()->TeehistorianRecordPlayerRejoin(ClientID); - pThis->Antibot()->OnEngineClientDrop(ClientID, "rejoin"); - pThis->Antibot()->OnEngineClientJoin(ClientID, false); + pThis->GameServer()->TeehistorianRecordPlayerRejoin(ClientId); + pThis->Antibot()->OnEngineClientDrop(ClientId, "rejoin"); + pThis->Antibot()->OnEngineClientJoin(ClientId, false); - pThis->SendMap(ClientID); + pThis->SendMap(ClientId); return 0; } -int CServer::NewClientNoAuthCallback(int ClientID, void *pUser) +int CServer::NewClientNoAuthCallback(int ClientId, void *pUser) { CServer *pThis = (CServer *)pUser; - pThis->m_aClients[ClientID].m_DnsblState = CClient::DNSBL_STATE_NONE; - - pThis->m_aClients[ClientID].m_State = CClient::STATE_CONNECTING; - pThis->m_aClients[ClientID].m_aName[0] = 0; - pThis->m_aClients[ClientID].m_aClan[0] = 0; - pThis->m_aClients[ClientID].m_Country = -1; - pThis->m_aClients[ClientID].m_Authed = AUTHED_NO; - pThis->m_aClients[ClientID].m_AuthKey = -1; - pThis->m_aClients[ClientID].m_AuthTries = 0; - pThis->m_aClients[ClientID].m_pRconCmdToSend = 0; - pThis->m_aClients[ClientID].m_ShowIps = false; - pThis->m_aClients[ClientID].m_DebugDummy = false; - pThis->m_aClients[ClientID].m_DDNetVersion = VERSION_NONE; - pThis->m_aClients[ClientID].m_GotDDNetVersionPacket = false; - pThis->m_aClients[ClientID].m_DDNetVersionSettled = false; - pThis->m_aClients[ClientID].Reset(); - - pThis->GameServer()->TeehistorianRecordPlayerJoin(ClientID, false); - pThis->Antibot()->OnEngineClientJoin(ClientID, false); - - pThis->SendCapabilities(ClientID); - pThis->SendMap(ClientID); + pThis->m_aClients[ClientId].m_DnsblState = CClient::DNSBL_STATE_NONE; + + pThis->m_aClients[ClientId].m_State = CClient::STATE_CONNECTING; + pThis->m_aClients[ClientId].m_aName[0] = 0; + pThis->m_aClients[ClientId].m_aClan[0] = 0; + pThis->m_aClients[ClientId].m_Country = -1; + pThis->m_aClients[ClientId].m_Authed = AUTHED_NO; + pThis->m_aClients[ClientId].m_AuthKey = -1; + pThis->m_aClients[ClientId].m_AuthTries = 0; + pThis->m_aClients[ClientId].m_pRconCmdToSend = nullptr; + pThis->m_aClients[ClientId].m_ShowIps = false; + pThis->m_aClients[ClientId].m_DebugDummy = false; + pThis->m_aClients[ClientId].m_DDNetVersion = VERSION_NONE; + pThis->m_aClients[ClientId].m_GotDDNetVersionPacket = false; + pThis->m_aClients[ClientId].m_DDNetVersionSettled = false; + pThis->m_aClients[ClientId].Reset(); + + pThis->GameServer()->TeehistorianRecordPlayerJoin(ClientId, false); + pThis->Antibot()->OnEngineClientJoin(ClientId, false); + + pThis->SendCapabilities(ClientId); + pThis->SendMap(ClientId); #if defined(CONF_FAMILY_UNIX) - pThis->SendConnLoggingCommand(OPEN_SESSION, pThis->m_NetServer.ClientAddr(ClientID)); + pThis->SendConnLoggingCommand(OPEN_SESSION, pThis->m_NetServer.ClientAddr(ClientId)); #endif return 0; } -int CServer::NewClientCallback(int ClientID, void *pUser, bool Sixup) +int CServer::NewClientCallback(int ClientId, void *pUser, bool Sixup) { CServer *pThis = (CServer *)pUser; - pThis->m_aClients[ClientID].m_State = CClient::STATE_PREAUTH; - pThis->m_aClients[ClientID].m_DnsblState = CClient::DNSBL_STATE_NONE; - pThis->m_aClients[ClientID].m_aName[0] = 0; - pThis->m_aClients[ClientID].m_aClan[0] = 0; - pThis->m_aClients[ClientID].m_Country = -1; - pThis->m_aClients[ClientID].m_Authed = AUTHED_NO; - pThis->m_aClients[ClientID].m_AuthKey = -1; - pThis->m_aClients[ClientID].m_AuthTries = 0; - pThis->m_aClients[ClientID].m_pRconCmdToSend = 0; - pThis->m_aClients[ClientID].m_Traffic = 0; - pThis->m_aClients[ClientID].m_TrafficSince = 0; - pThis->m_aClients[ClientID].m_ShowIps = false; - pThis->m_aClients[ClientID].m_DebugDummy = false; - pThis->m_aClients[ClientID].m_DDNetVersion = VERSION_NONE; - pThis->m_aClients[ClientID].m_GotDDNetVersionPacket = false; - pThis->m_aClients[ClientID].m_DDNetVersionSettled = false; - mem_zero(&pThis->m_aClients[ClientID].m_Addr, sizeof(NETADDR)); - pThis->m_aClients[ClientID].Reset(); - - pThis->GameServer()->TeehistorianRecordPlayerJoin(ClientID, Sixup); - pThis->Antibot()->OnEngineClientJoin(ClientID, Sixup); - - pThis->m_aClients[ClientID].m_Sixup = Sixup; + pThis->m_aClients[ClientId].m_State = CClient::STATE_PREAUTH; + pThis->m_aClients[ClientId].m_DnsblState = CClient::DNSBL_STATE_NONE; + pThis->m_aClients[ClientId].m_aName[0] = 0; + pThis->m_aClients[ClientId].m_aClan[0] = 0; + pThis->m_aClients[ClientId].m_Country = -1; + pThis->m_aClients[ClientId].m_Authed = AUTHED_NO; + pThis->m_aClients[ClientId].m_AuthKey = -1; + pThis->m_aClients[ClientId].m_AuthTries = 0; + pThis->m_aClients[ClientId].m_pRconCmdToSend = nullptr; + pThis->m_aClients[ClientId].m_Traffic = 0; + pThis->m_aClients[ClientId].m_TrafficSince = 0; + pThis->m_aClients[ClientId].m_ShowIps = false; + pThis->m_aClients[ClientId].m_DebugDummy = false; + pThis->m_aClients[ClientId].m_DDNetVersion = VERSION_NONE; + pThis->m_aClients[ClientId].m_GotDDNetVersionPacket = false; + pThis->m_aClients[ClientId].m_DDNetVersionSettled = false; + mem_zero(&pThis->m_aClients[ClientId].m_Addr, sizeof(NETADDR)); + pThis->m_aClients[ClientId].Reset(); + + pThis->GameServer()->TeehistorianRecordPlayerJoin(ClientId, Sixup); + pThis->Antibot()->OnEngineClientJoin(ClientId, Sixup); + + pThis->m_aClients[ClientId].m_Sixup = Sixup; #if defined(CONF_FAMILY_UNIX) - pThis->SendConnLoggingCommand(OPEN_SESSION, pThis->m_NetServer.ClientAddr(ClientID)); + pThis->SendConnLoggingCommand(OPEN_SESSION, pThis->m_NetServer.ClientAddr(ClientId)); #endif return 0; } -void CServer::InitDnsbl(int ClientID) +void CServer::InitDnsbl(int ClientId) { - NETADDR Addr = *m_NetServer.ClientAddr(ClientID); + NETADDR Addr = *m_NetServer.ClientAddr(ClientId); //TODO: support ipv6 if(Addr.type != NETTYPE_IPV4) @@ -1196,9 +1119,9 @@ void CServer::InitDnsbl(int ClientID) str_format(aBuf, sizeof(aBuf), "%s.%d.%d.%d.%d.%s", Config()->m_SvDnsblKey, Addr.ip[3], Addr.ip[2], Addr.ip[1], Addr.ip[0], Config()->m_SvDnsblHost); } - IEngine *pEngine = Kernel()->RequestInterface(); - pEngine->AddJob(m_aClients[ClientID].m_pDnsblLookup = std::make_shared(aBuf, NETTYPE_IPV4)); - m_aClients[ClientID].m_DnsblState = CClient::DNSBL_STATE_PENDING; + m_aClients[ClientId].m_pDnsblLookup = std::make_shared(aBuf, NETTYPE_IPV4); + Engine()->AddJob(m_aClients[ClientId].m_pDnsblLookup); + m_aClients[ClientId].m_DnsblState = CClient::DNSBL_STATE_PENDING; } #ifdef CONF_FAMILY_UNIX @@ -1218,51 +1141,51 @@ void CServer::SendConnLoggingCommand(CONN_LOGGING_CMD Cmd, const NETADDR *pAddr) } #endif -int CServer::DelClientCallback(int ClientID, const char *pReason, void *pUser) +int CServer::DelClientCallback(int ClientId, const char *pReason, void *pUser) { CServer *pThis = (CServer *)pUser; char aAddrStr[NETADDR_MAXSTRSIZE]; - net_addr_str(pThis->m_NetServer.ClientAddr(ClientID), aAddrStr, sizeof(aAddrStr), true); + net_addr_str(pThis->m_NetServer.ClientAddr(ClientId), aAddrStr, sizeof(aAddrStr), true); char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "client dropped. cid=%d addr=<{%s}> reason='%s'", ClientID, aAddrStr, pReason); + str_format(aBuf, sizeof(aBuf), "client dropped. cid=%d addr=<{%s}> reason='%s'", ClientId, aAddrStr, pReason); pThis->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "server", aBuf); // notify the mod about the drop - if(pThis->m_aClients[ClientID].m_State >= CClient::STATE_READY) - pThis->GameServer()->OnClientDrop(ClientID, pReason); - - pThis->m_aClients[ClientID].m_State = CClient::STATE_EMPTY; - pThis->m_aClients[ClientID].m_aName[0] = 0; - pThis->m_aClients[ClientID].m_aClan[0] = 0; - pThis->m_aClients[ClientID].m_Country = -1; - pThis->m_aClients[ClientID].m_Authed = AUTHED_NO; - pThis->m_aClients[ClientID].m_AuthKey = -1; - pThis->m_aClients[ClientID].m_AuthTries = 0; - pThis->m_aClients[ClientID].m_pRconCmdToSend = 0; - pThis->m_aClients[ClientID].m_Traffic = 0; - pThis->m_aClients[ClientID].m_TrafficSince = 0; - pThis->m_aClients[ClientID].m_ShowIps = false; - pThis->m_aClients[ClientID].m_DebugDummy = false; - pThis->m_aPrevStates[ClientID] = CClient::STATE_EMPTY; - pThis->m_aClients[ClientID].m_Snapshots.PurgeAll(); - pThis->m_aClients[ClientID].m_Sixup = false; - pThis->m_aClients[ClientID].m_RedirectDropTime = 0; - - pThis->GameServer()->TeehistorianRecordPlayerDrop(ClientID, pReason); - pThis->Antibot()->OnEngineClientDrop(ClientID, pReason); + if(pThis->m_aClients[ClientId].m_State >= CClient::STATE_READY) + pThis->GameServer()->OnClientDrop(ClientId, pReason); + + pThis->m_aClients[ClientId].m_State = CClient::STATE_EMPTY; + pThis->m_aClients[ClientId].m_aName[0] = 0; + pThis->m_aClients[ClientId].m_aClan[0] = 0; + pThis->m_aClients[ClientId].m_Country = -1; + pThis->m_aClients[ClientId].m_Authed = AUTHED_NO; + pThis->m_aClients[ClientId].m_AuthKey = -1; + pThis->m_aClients[ClientId].m_AuthTries = 0; + pThis->m_aClients[ClientId].m_pRconCmdToSend = nullptr; + pThis->m_aClients[ClientId].m_Traffic = 0; + pThis->m_aClients[ClientId].m_TrafficSince = 0; + pThis->m_aClients[ClientId].m_ShowIps = false; + pThis->m_aClients[ClientId].m_DebugDummy = false; + pThis->m_aPrevStates[ClientId] = CClient::STATE_EMPTY; + pThis->m_aClients[ClientId].m_Snapshots.PurgeAll(); + pThis->m_aClients[ClientId].m_Sixup = false; + pThis->m_aClients[ClientId].m_RedirectDropTime = 0; + + pThis->GameServer()->TeehistorianRecordPlayerDrop(ClientId, pReason); + pThis->Antibot()->OnEngineClientDrop(ClientId, pReason); #if defined(CONF_FAMILY_UNIX) - pThis->SendConnLoggingCommand(CLOSE_SESSION, pThis->m_NetServer.ClientAddr(ClientID)); + pThis->SendConnLoggingCommand(CLOSE_SESSION, pThis->m_NetServer.ClientAddr(ClientId)); #endif return 0; } -void CServer::SendRconType(int ClientID, bool UsernameReq) +void CServer::SendRconType(int ClientId, bool UsernameReq) { CMsgPacker Msg(NETMSG_RCONTYPE, true); Msg.AddInt(UsernameReq); - SendMsg(&Msg, MSGFLAG_VITAL, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL, ClientId); } void CServer::GetMapInfo(char *pMapName, int MapNameSize, int *pMapSize, SHA256_DIGEST *pMapSha256, int *pMapCrc) @@ -1273,17 +1196,17 @@ void CServer::GetMapInfo(char *pMapName, int MapNameSize, int *pMapSize, SHA256_ *pMapCrc = m_aCurrentMapCrc[MAP_TYPE_SIX]; } -void CServer::SendCapabilities(int ClientID) +void CServer::SendCapabilities(int ClientId) { CMsgPacker Msg(NETMSG_CAPABILITIES, true); Msg.AddInt(SERVERCAP_CURVERSION); // version Msg.AddInt(SERVERCAPFLAG_DDNET | SERVERCAPFLAG_CHATTIMEOUTCODE | SERVERCAPFLAG_ANYPLAYERFLAG | SERVERCAPFLAG_PINGEX | SERVERCAPFLAG_ALLOWDUMMY | SERVERCAPFLAG_SYNCWEAPONINPUT); // flags - SendMsg(&Msg, MSGFLAG_VITAL, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL, ClientId); } -void CServer::SendMap(int ClientID) +void CServer::SendMap(int ClientId) { - int MapType = IsSixup(ClientID) ? MAP_TYPE_SIXUP : MAP_TYPE_SIX; + int MapType = IsSixup(ClientId) ? MAP_TYPE_SIXUP : MAP_TYPE_SIX; { CMsgPacker Msg(NETMSG_MAP_DETAILS, true); Msg.AddString(GetMapName(), 0); @@ -1291,7 +1214,7 @@ void CServer::SendMap(int ClientID) Msg.AddInt(m_aCurrentMapCrc[MapType]); Msg.AddInt(m_aCurrentMapSize[MapType]); Msg.AddString("", 0); // HTTPS map download URL - SendMsg(&Msg, MSGFLAG_VITAL, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL, ClientId); } { CMsgPacker Msg(NETMSG_MAP_CHANGE, true); @@ -1304,15 +1227,15 @@ void CServer::SendMap(int ClientID) Msg.AddInt(1024 - 128); Msg.AddRaw(m_aCurrentMapSha256[MapType].data, sizeof(m_aCurrentMapSha256[MapType].data)); } - SendMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId); } - m_aClients[ClientID].m_NextMapChunk = 0; + m_aClients[ClientId].m_NextMapChunk = 0; } -void CServer::SendMapData(int ClientID, int Chunk) +void CServer::SendMapData(int ClientId, int Chunk) { - int MapType = IsSixup(ClientID) ? MAP_TYPE_SIXUP : MAP_TYPE_SIX; + int MapType = IsSixup(ClientId) ? MAP_TYPE_SIXUP : MAP_TYPE_SIX; unsigned int ChunkSize = 1024 - 128; unsigned int Offset = Chunk * ChunkSize; int Last = 0; @@ -1336,7 +1259,7 @@ void CServer::SendMapData(int ClientID, int Chunk) Msg.AddInt(ChunkSize); } Msg.AddRaw(&m_apCurrentMapData[MapType][Offset], ChunkSize); - SendMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId); if(Config()->m_Debug) { @@ -1346,20 +1269,26 @@ void CServer::SendMapData(int ClientID, int Chunk) } } -void CServer::SendConnectionReady(int ClientID) +void CServer::SendMapReload(int ClientId) +{ + CMsgPacker Msg(NETMSG_MAP_RELOAD, true); + SendMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId); +} + +void CServer::SendConnectionReady(int ClientId) { CMsgPacker Msg(NETMSG_CON_READY, true); - SendMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId); } -void CServer::SendRconLine(int ClientID, const char *pLine) +void CServer::SendRconLine(int ClientId, const char *pLine) { CMsgPacker Msg(NETMSG_RCON_LINE, true); Msg.AddString(pLine, 512); - SendMsg(&Msg, MSGFLAG_VITAL, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL, ClientId); } -void CServer::SendRconLogLine(int ClientID, const CLogMessage *pMessage) +void CServer::SendRconLogLine(int ClientId, const CLogMessage *pMessage) { const char *pLine = pMessage->m_aLine; const char *pStart = str_find(pLine, "<{"); @@ -1388,7 +1317,7 @@ void CServer::SendRconLogLine(int ClientID, const CLogMessage *pMessage) pLineWithoutIps = aLineWithoutIps; } - if(ClientID == -1) + if(ClientId == -1) { for(int i = 0; i < MAX_CLIENTS; i++) { @@ -1398,38 +1327,60 @@ void CServer::SendRconLogLine(int ClientID, const CLogMessage *pMessage) } else { - if(m_aClients[ClientID].m_State != CClient::STATE_EMPTY) - SendRconLine(ClientID, m_aClients[ClientID].m_ShowIps ? pLine : pLineWithoutIps); + if(m_aClients[ClientId].m_State != CClient::STATE_EMPTY) + SendRconLine(ClientId, m_aClients[ClientId].m_ShowIps ? pLine : pLineWithoutIps); } } -void CServer::SendRconCmdAdd(const IConsole::CCommandInfo *pCommandInfo, int ClientID) +void CServer::SendRconCmdAdd(const IConsole::CCommandInfo *pCommandInfo, int ClientId) { CMsgPacker Msg(NETMSG_RCON_CMD_ADD, true); Msg.AddString(pCommandInfo->m_pName, IConsole::TEMPCMD_NAME_LENGTH); Msg.AddString(pCommandInfo->m_pHelp, IConsole::TEMPCMD_HELP_LENGTH); Msg.AddString(pCommandInfo->m_pParams, IConsole::TEMPCMD_PARAMS_LENGTH); - SendMsg(&Msg, MSGFLAG_VITAL, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL, ClientId); } -void CServer::SendRconCmdRem(const IConsole::CCommandInfo *pCommandInfo, int ClientID) +void CServer::SendRconCmdRem(const IConsole::CCommandInfo *pCommandInfo, int ClientId) { CMsgPacker Msg(NETMSG_RCON_CMD_REM, true); Msg.AddString(pCommandInfo->m_pName, 256); - SendMsg(&Msg, MSGFLAG_VITAL, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL, ClientId); +} + +int CServer::GetConsoleAccessLevel(int ClientId) +{ + return m_aClients[ClientId].m_Authed == AUTHED_ADMIN ? IConsole::ACCESS_LEVEL_ADMIN : m_aClients[ClientId].m_Authed == AUTHED_MOD ? IConsole::ACCESS_LEVEL_MOD : IConsole::ACCESS_LEVEL_HELPER; +} + +int CServer::NumRconCommands(int ClientId) +{ + int Num = 0; + int ConsoleAccessLevel = GetConsoleAccessLevel(ClientId); + for(const IConsole::CCommandInfo *pCmd = Console()->FirstCommandInfo(ConsoleAccessLevel, CFGFLAG_SERVER); + pCmd; pCmd = pCmd->NextCommandInfo(ConsoleAccessLevel, CFGFLAG_SERVER)) + { + Num++; + } + return Num; } void CServer::UpdateClientRconCommands() { - int ClientID = Tick() % MAX_CLIENTS; + int ClientId = Tick() % MAX_CLIENTS; - if(m_aClients[ClientID].m_State != CClient::STATE_EMPTY && m_aClients[ClientID].m_Authed) + if(m_aClients[ClientId].m_State != CClient::STATE_EMPTY && m_aClients[ClientId].m_Authed) { - int ConsoleAccessLevel = m_aClients[ClientID].m_Authed == AUTHED_ADMIN ? IConsole::ACCESS_LEVEL_ADMIN : m_aClients[ClientID].m_Authed == AUTHED_MOD ? IConsole::ACCESS_LEVEL_MOD : IConsole::ACCESS_LEVEL_HELPER; - for(int i = 0; i < MAX_RCONCMD_SEND && m_aClients[ClientID].m_pRconCmdToSend; ++i) + int ConsoleAccessLevel = GetConsoleAccessLevel(ClientId); + for(int i = 0; i < MAX_RCONCMD_SEND && m_aClients[ClientId].m_pRconCmdToSend; ++i) { - SendRconCmdAdd(m_aClients[ClientID].m_pRconCmdToSend, ClientID); - m_aClients[ClientID].m_pRconCmdToSend = m_aClients[ClientID].m_pRconCmdToSend->NextCommandInfo(ConsoleAccessLevel, CFGFLAG_SERVER); + SendRconCmdAdd(m_aClients[ClientId].m_pRconCmdToSend, ClientId); + m_aClients[ClientId].m_pRconCmdToSend = m_aClients[ClientId].m_pRconCmdToSend->NextCommandInfo(ConsoleAccessLevel, CFGFLAG_SERVER); + if(m_aClients[ClientId].m_pRconCmdToSend == nullptr) + { + CMsgPacker Msg(NETMSG_RCON_CMD_GROUP_END, true); + SendMsg(&Msg, MSGFLAG_VITAL, ClientId); + } } } } @@ -1451,13 +1402,13 @@ static inline int MsgFromSixup(int Msg, bool System) return Msg; } -bool CServer::CheckReservedSlotAuth(int ClientID, const char *pPassword) +bool CServer::CheckReservedSlotAuth(int ClientId, const char *pPassword) { char aBuf[256]; if(Config()->m_SvReservedSlotsPass[0] && !str_comp(Config()->m_SvReservedSlotsPass, pPassword)) { - str_format(aBuf, sizeof(aBuf), "cid=%d joining reserved slot with reserved pass", ClientID); + str_format(aBuf, sizeof(aBuf), "cid=%d joining reserved slot with reserved pass", ClientId); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); return true; } @@ -1474,7 +1425,7 @@ bool CServer::CheckReservedSlotAuth(int ClientID, const char *pPassword) int Slot = m_AuthManager.FindKey(aName); if(m_AuthManager.CheckKey(Slot, pInnerPassword + 1) && m_AuthManager.KeyLevel(Slot) >= Config()->m_SvReservedSlotsAuthLevel) { - str_format(aBuf, sizeof(aBuf), "cid=%d joining reserved slot with key=%s", ClientID, m_AuthManager.KeyIdent(Slot)); + str_format(aBuf, sizeof(aBuf), "cid=%d joining reserved slot with key=%s", ClientId, m_AuthManager.KeyIdent(Slot)); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); return true; } @@ -1485,7 +1436,7 @@ bool CServer::CheckReservedSlotAuth(int ClientID, const char *pPassword) void CServer::ProcessClientPacket(CNetChunk *pPacket) { - int ClientID = pPacket->m_ClientID; + int ClientId = pPacket->m_ClientId; CUnpacker Unpacker; Unpacker.Reset(pPacket->m_pData, pPacket->m_DataSize); CMsgPacker Packer(NETMSG_EX, true); @@ -1495,13 +1446,13 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) bool Sys; CUuid Uuid; - int Result = UnpackMessageID(&Msg, &Sys, &Uuid, &Unpacker, &Packer); + int Result = UnpackMessageId(&Msg, &Sys, &Uuid, &Unpacker, &Packer); if(Result == UNPACKMESSAGE_ERROR) { return; } - if(m_aClients[ClientID].m_Sixup && (Msg = MsgFromSixup(Msg, Sys)) < 0) + if(m_aClients[ClientId].m_Sixup && (Msg = MsgFromSixup(Msg, Sys)) < 0) { return; } @@ -1509,25 +1460,25 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) if(Config()->m_SvNetlimit && Msg != NETMSG_REQUEST_MAP_DATA) { int64_t Now = time_get(); - int64_t Diff = Now - m_aClients[ClientID].m_TrafficSince; + int64_t Diff = Now - m_aClients[ClientId].m_TrafficSince; double Alpha = Config()->m_SvNetlimitAlpha / 100.0; double Limit = (double)(Config()->m_SvNetlimit * 1024) / time_freq(); - if(m_aClients[ClientID].m_Traffic > Limit) + if(m_aClients[ClientId].m_Traffic > Limit) { - m_NetServer.NetBan()->BanAddr(&pPacket->m_Address, 600, "Stressing network"); + m_NetServer.NetBan()->BanAddr(&pPacket->m_Address, 600, "Stressing network", false); return; } if(Diff > 100) { - m_aClients[ClientID].m_Traffic = (Alpha * ((double)pPacket->m_DataSize / Diff)) + (1.0 - Alpha) * m_aClients[ClientID].m_Traffic; - m_aClients[ClientID].m_TrafficSince = Now; + m_aClients[ClientId].m_Traffic = (Alpha * ((double)pPacket->m_DataSize / Diff)) + (1.0 - Alpha) * m_aClients[ClientId].m_Traffic; + m_aClients[ClientId].m_TrafficSince = Now; } } if(Result == UNPACKMESSAGE_ANSWER) { - SendMsg(&Packer, MSGFLAG_VITAL, ClientID); + SendMsg(&Packer, MSGFLAG_VITAL, ClientId); } if(Sys) @@ -1535,29 +1486,29 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) // system message if(Msg == NETMSG_CLIENTVER) { - if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && m_aClients[ClientID].m_State == CClient::STATE_PREAUTH) + if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && m_aClients[ClientId].m_State == CClient::STATE_PREAUTH) { - CUuid *pConnectionID = (CUuid *)Unpacker.GetRaw(sizeof(*pConnectionID)); + CUuid *pConnectionId = (CUuid *)Unpacker.GetRaw(sizeof(*pConnectionId)); int DDNetVersion = Unpacker.GetInt(); const char *pDDNetVersionStr = Unpacker.GetString(CUnpacker::SANITIZE_CC); - if(Unpacker.Error() || !str_utf8_check(pDDNetVersionStr) || DDNetVersion < 0) + if(Unpacker.Error() || DDNetVersion < 0) { return; } - m_aClients[ClientID].m_ConnectionID = *pConnectionID; - m_aClients[ClientID].m_DDNetVersion = DDNetVersion; - str_copy(m_aClients[ClientID].m_aDDNetVersionStr, pDDNetVersionStr); - m_aClients[ClientID].m_DDNetVersionSettled = true; - m_aClients[ClientID].m_GotDDNetVersionPacket = true; - m_aClients[ClientID].m_State = CClient::STATE_AUTH; + m_aClients[ClientId].m_ConnectionId = *pConnectionId; + m_aClients[ClientId].m_DDNetVersion = DDNetVersion; + str_copy(m_aClients[ClientId].m_aDDNetVersionStr, pDDNetVersionStr); + m_aClients[ClientId].m_DDNetVersionSettled = true; + m_aClients[ClientId].m_GotDDNetVersionPacket = true; + m_aClients[ClientId].m_State = CClient::STATE_AUTH; } } else if(Msg == NETMSG_INFO) { - if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && (m_aClients[ClientID].m_State == CClient::STATE_PREAUTH || m_aClients[ClientID].m_State == CClient::STATE_AUTH)) + if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && (m_aClients[ClientId].m_State == CClient::STATE_PREAUTH || m_aClients[ClientId].m_State == CClient::STATE_AUTH)) { const char *pVersion = Unpacker.GetString(CUnpacker::SANITIZE_CC); - if(!str_utf8_check(pVersion)) + if(Unpacker.Error()) { return; } @@ -1566,53 +1517,57 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) // wrong version char aReason[256]; str_format(aReason, sizeof(aReason), "Wrong version. Server is running '%s' and client '%s'", GameServer()->NetVersion(), pVersion); - m_NetServer.Drop(ClientID, aReason); + m_NetServer.Drop(ClientId, aReason); return; } const char *pPassword = Unpacker.GetString(CUnpacker::SANITIZE_CC); - if(!str_utf8_check(pPassword)) + if(Unpacker.Error()) { return; } if(Config()->m_Password[0] != 0 && str_comp(Config()->m_Password, pPassword) != 0) { // wrong password - m_NetServer.Drop(ClientID, "Wrong password"); + m_NetServer.Drop(ClientId, "Wrong password"); return; } // reserved slot - if(ClientID >= Config()->m_SvMaxClients - Config()->m_SvReservedSlots && !CheckReservedSlotAuth(ClientID, pPassword)) + if(ClientId >= MaxClients() - Config()->m_SvReservedSlots && !CheckReservedSlotAuth(ClientId, pPassword)) { - m_NetServer.Drop(ClientID, "This server is full"); + m_NetServer.Drop(ClientId, "This server is full"); return; } - m_aClients[ClientID].m_State = CClient::STATE_CONNECTING; - SendRconType(ClientID, m_AuthManager.NumNonDefaultKeys() > 0); - SendCapabilities(ClientID); - SendMap(ClientID); + m_aClients[ClientId].m_State = CClient::STATE_CONNECTING; + SendRconType(ClientId, m_AuthManager.NumNonDefaultKeys() > 0); + SendCapabilities(ClientId); + SendMap(ClientId); } } else if(Msg == NETMSG_REQUEST_MAP_DATA) { - if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) == 0 || m_aClients[ClientID].m_State < CClient::STATE_CONNECTING) + if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) == 0 || m_aClients[ClientId].m_State < CClient::STATE_CONNECTING) return; - if(m_aClients[ClientID].m_Sixup) + if(m_aClients[ClientId].m_Sixup) { for(int i = 0; i < Config()->m_SvMapWindow; i++) { - SendMapData(ClientID, m_aClients[ClientID].m_NextMapChunk++); + SendMapData(ClientId, m_aClients[ClientId].m_NextMapChunk++); } return; } int Chunk = Unpacker.GetInt(); - if(Chunk != m_aClients[ClientID].m_NextMapChunk || !Config()->m_SvFastDownload) + if(Unpacker.Error()) + { + return; + } + if(Chunk != m_aClients[ClientId].m_NextMapChunk || !Config()->m_SvFastDownload) { - SendMapData(ClientID, Chunk); + SendMapData(ClientId, Chunk); return; } @@ -1620,91 +1575,92 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) { for(int i = 0; i < Config()->m_SvMapWindow; i++) { - SendMapData(ClientID, i); + SendMapData(ClientId, i); } } - SendMapData(ClientID, Config()->m_SvMapWindow + m_aClients[ClientID].m_NextMapChunk); - m_aClients[ClientID].m_NextMapChunk++; + SendMapData(ClientId, Config()->m_SvMapWindow + m_aClients[ClientId].m_NextMapChunk); + m_aClients[ClientId].m_NextMapChunk++; } else if(Msg == NETMSG_READY) { - if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && (m_aClients[ClientID].m_State == CClient::STATE_CONNECTING)) + if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && (m_aClients[ClientId].m_State == CClient::STATE_CONNECTING)) { char aAddrStr[NETADDR_MAXSTRSIZE]; - net_addr_str(m_NetServer.ClientAddr(ClientID), aAddrStr, sizeof(aAddrStr), true); + net_addr_str(m_NetServer.ClientAddr(ClientId), aAddrStr, sizeof(aAddrStr), true); char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "player is ready. ClientID=%d addr=<{%s}> secure=%s", ClientID, aAddrStr, m_NetServer.HasSecurityToken(ClientID) ? "yes" : "no"); + str_format(aBuf, sizeof(aBuf), "player is ready. ClientId=%d addr=<{%s}> secure=%s", ClientId, aAddrStr, m_NetServer.HasSecurityToken(ClientId) ? "yes" : "no"); Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "server", aBuf); void *pPersistentData = 0; - if(m_aClients[ClientID].m_HasPersistentData) + if(m_aClients[ClientId].m_HasPersistentData) { - pPersistentData = m_aClients[ClientID].m_pPersistentData; - m_aClients[ClientID].m_HasPersistentData = false; + pPersistentData = m_aClients[ClientId].m_pPersistentData; + m_aClients[ClientId].m_HasPersistentData = false; } - m_aClients[ClientID].m_State = CClient::STATE_READY; - GameServer()->OnClientConnected(ClientID, pPersistentData); + m_aClients[ClientId].m_State = CClient::STATE_READY; + GameServer()->OnClientConnected(ClientId, pPersistentData); } - SendConnectionReady(ClientID); + SendConnectionReady(ClientId); } else if(Msg == NETMSG_ENTERGAME) { - if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && m_aClients[ClientID].m_State == CClient::STATE_READY && GameServer()->IsClientReady(ClientID)) + if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && m_aClients[ClientId].m_State == CClient::STATE_READY && GameServer()->IsClientReady(ClientId)) { char aAddrStr[NETADDR_MAXSTRSIZE]; - net_addr_str(m_NetServer.ClientAddr(ClientID), aAddrStr, sizeof(aAddrStr), true); + net_addr_str(m_NetServer.ClientAddr(ClientId), aAddrStr, sizeof(aAddrStr), true); char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "player has entered the game. ClientID=%d addr=<{%s}> sixup=%d", ClientID, aAddrStr, IsSixup(ClientID)); + str_format(aBuf, sizeof(aBuf), "player has entered the game. ClientId=%d addr=<{%s}> sixup=%d", ClientId, aAddrStr, IsSixup(ClientId)); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); - m_aClients[ClientID].m_State = CClient::STATE_INGAME; - if(!IsSixup(ClientID)) + m_aClients[ClientId].m_State = CClient::STATE_INGAME; + if(!IsSixup(ClientId)) { - SendServerInfo(m_NetServer.ClientAddr(ClientID), -1, SERVERINFO_EXTENDED, false); + SendServerInfo(m_NetServer.ClientAddr(ClientId), -1, SERVERINFO_EXTENDED, false); } else { CMsgPacker Msgp(protocol7::NETMSG_SERVERINFO, true, true); GetServerInfoSixup(&Msgp, -1, false); - SendMsg(&Msgp, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientID); + SendMsg(&Msgp, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId); } - GameServer()->OnClientEnter(ClientID); + GameServer()->OnClientEnter(ClientId); } } else if(Msg == NETMSG_INPUT) { - m_aClients[ClientID].m_LastAckedSnapshot = Unpacker.GetInt(); + const int LastAckedSnapshot = Unpacker.GetInt(); int IntendedTick = Unpacker.GetInt(); int Size = Unpacker.GetInt(); - - // check for errors if(Unpacker.Error() || Size / 4 > MAX_INPUT_SIZE || IntendedTick < MIN_TICK || IntendedTick >= MAX_TICK) + { return; + } - if(m_aClients[ClientID].m_LastAckedSnapshot > 0) - m_aClients[ClientID].m_SnapRate = CClient::SNAPRATE_FULL; + m_aClients[ClientId].m_LastAckedSnapshot = LastAckedSnapshot; + if(m_aClients[ClientId].m_LastAckedSnapshot > 0) + m_aClients[ClientId].m_SnapRate = CClient::SNAPRATE_FULL; int64_t TagTime; - if(m_aClients[ClientID].m_Snapshots.Get(m_aClients[ClientID].m_LastAckedSnapshot, &TagTime, 0, 0) >= 0) - m_aClients[ClientID].m_Latency = (int)(((time_get() - TagTime) * 1000) / time_freq()); + if(m_aClients[ClientId].m_Snapshots.Get(m_aClients[ClientId].m_LastAckedSnapshot, &TagTime, nullptr, nullptr) >= 0) + m_aClients[ClientId].m_Latency = (int)(((time_get() - TagTime) * 1000) / time_freq()); // add message to report the input timing // skip packets that are old - if(IntendedTick > m_aClients[ClientID].m_LastInputTick) + if(IntendedTick > m_aClients[ClientId].m_LastInputTick) { const int TimeLeft = (TickStartTime(IntendedTick) - time_get()) / (time_freq() / 1000); CMsgPacker Msgp(NETMSG_INPUTTIMING, true); Msgp.AddInt(IntendedTick); Msgp.AddInt(TimeLeft); - SendMsg(&Msgp, 0, ClientID); + SendMsg(&Msgp, 0, ClientId); } - m_aClients[ClientID].m_LastInputTick = IntendedTick; + m_aClients[ClientId].m_LastInputTick = IntendedTick; - CClient::CInput *pInput = &m_aClients[ClientID].m_aInputs[m_aClients[ClientID].m_CurrentInput]; + CClient::CInput *pInput = &m_aClients[ClientId].m_aInputs[m_aClients[ClientId].m_CurrentInput]; if(IntendedTick <= Tick()) IntendedTick = Tick() + 1; @@ -1712,174 +1668,195 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) pInput->m_GameTick = IntendedTick; for(int i = 0; i < Size / 4; i++) + { pInput->m_aData[i] = Unpacker.GetInt(); + } + if(Unpacker.Error()) + { + return; + } - GameServer()->OnClientPrepareInput(ClientID, pInput->m_aData); - mem_copy(m_aClients[ClientID].m_LatestInput.m_aData, pInput->m_aData, MAX_INPUT_SIZE * sizeof(int)); + GameServer()->OnClientPrepareInput(ClientId, pInput->m_aData); + mem_copy(m_aClients[ClientId].m_LatestInput.m_aData, pInput->m_aData, MAX_INPUT_SIZE * sizeof(int)); - m_aClients[ClientID].m_CurrentInput++; - m_aClients[ClientID].m_CurrentInput %= 200; + m_aClients[ClientId].m_CurrentInput++; + m_aClients[ClientId].m_CurrentInput %= 200; // call the mod with the fresh input data - if(m_aClients[ClientID].m_State == CClient::STATE_INGAME) - GameServer()->OnClientDirectInput(ClientID, m_aClients[ClientID].m_LatestInput.m_aData); + if(m_aClients[ClientId].m_State == CClient::STATE_INGAME) + GameServer()->OnClientDirectInput(ClientId, m_aClients[ClientId].m_LatestInput.m_aData); } else if(Msg == NETMSG_RCON_CMD) { const char *pCmd = Unpacker.GetString(); - if(!str_utf8_check(pCmd)) + if(Unpacker.Error()) { return; } - if(Unpacker.Error() == 0 && !str_comp(pCmd, "crashmeplx")) + if(!str_comp(pCmd, "crashmeplx")) { - int Version = m_aClients[ClientID].m_DDNetVersion; - if(GameServer()->PlayerExists(ClientID) && Version < VERSION_DDNET_OLD) + int Version = m_aClients[ClientId].m_DDNetVersion; + if(GameServer()->PlayerExists(ClientId) && Version < VERSION_DDNET_OLD) { - m_aClients[ClientID].m_DDNetVersion = VERSION_DDNET_OLD; + m_aClients[ClientId].m_DDNetVersion = VERSION_DDNET_OLD; } } - else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Unpacker.Error() == 0 && m_aClients[ClientID].m_Authed) + else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && m_aClients[ClientId].m_Authed) { - if(GameServer()->PlayerExists(ClientID)) + if(GameServer()->PlayerExists(ClientId)) { char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "ClientID=%d rcon='%s'", ClientID, pCmd); + str_format(aBuf, sizeof(aBuf), "ClientId=%d rcon='%s'", ClientId, pCmd); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); - m_RconClientID = ClientID; - m_RconAuthLevel = m_aClients[ClientID].m_Authed; - Console()->SetAccessLevel(m_aClients[ClientID].m_Authed == AUTHED_ADMIN ? IConsole::ACCESS_LEVEL_ADMIN : m_aClients[ClientID].m_Authed == AUTHED_MOD ? IConsole::ACCESS_LEVEL_MOD : m_aClients[ClientID].m_Authed == AUTHED_HELPER ? IConsole::ACCESS_LEVEL_HELPER : IConsole::ACCESS_LEVEL_USER); + m_RconClientId = ClientId; + m_RconAuthLevel = m_aClients[ClientId].m_Authed; + Console()->SetAccessLevel(m_aClients[ClientId].m_Authed == AUTHED_ADMIN ? IConsole::ACCESS_LEVEL_ADMIN : m_aClients[ClientId].m_Authed == AUTHED_MOD ? IConsole::ACCESS_LEVEL_MOD : m_aClients[ClientId].m_Authed == AUTHED_HELPER ? IConsole::ACCESS_LEVEL_HELPER : IConsole::ACCESS_LEVEL_USER); { - CRconClientLogger Logger(this, ClientID); + CRconClientLogger Logger(this, ClientId); CLogScope Scope(&Logger); - Console()->ExecuteLineFlag(pCmd, CFGFLAG_SERVER, ClientID); + Console()->ExecuteLineFlag(pCmd, CFGFLAG_SERVER, ClientId); } Console()->SetAccessLevel(IConsole::ACCESS_LEVEL_ADMIN); - m_RconClientID = IServer::RCON_CID_SERV; + m_RconClientId = IServer::RCON_CID_SERV; m_RconAuthLevel = AUTHED_ADMIN; } } } else if(Msg == NETMSG_RCON_AUTH) { + if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) == 0) + { + return; + } const char *pName = ""; - if(!IsSixup(ClientID)) + if(!IsSixup(ClientId)) + { pName = Unpacker.GetString(CUnpacker::SANITIZE_CC); // login name, now used + } const char *pPw = Unpacker.GetString(CUnpacker::SANITIZE_CC); - if(!str_utf8_check(pPw) || !str_utf8_check(pName)) + if(Unpacker.Error()) { return; } - if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Unpacker.Error() == 0) - { - int AuthLevel = -1; - int KeySlot = -1; + int AuthLevel = -1; + int KeySlot = -1; - if(!pName[0]) - { - if(m_AuthManager.CheckKey((KeySlot = m_AuthManager.DefaultKey(AUTHED_ADMIN)), pPw)) - AuthLevel = AUTHED_ADMIN; - else if(m_AuthManager.CheckKey((KeySlot = m_AuthManager.DefaultKey(AUTHED_MOD)), pPw)) - AuthLevel = AUTHED_MOD; - else if(m_AuthManager.CheckKey((KeySlot = m_AuthManager.DefaultKey(AUTHED_HELPER)), pPw)) - AuthLevel = AUTHED_HELPER; - } - else - { - KeySlot = m_AuthManager.FindKey(pName); - if(m_AuthManager.CheckKey(KeySlot, pPw)) - AuthLevel = m_AuthManager.KeyLevel(KeySlot); - } + if(!pName[0]) + { + if(m_AuthManager.CheckKey((KeySlot = m_AuthManager.DefaultKey(AUTHED_ADMIN)), pPw)) + AuthLevel = AUTHED_ADMIN; + else if(m_AuthManager.CheckKey((KeySlot = m_AuthManager.DefaultKey(AUTHED_MOD)), pPw)) + AuthLevel = AUTHED_MOD; + else if(m_AuthManager.CheckKey((KeySlot = m_AuthManager.DefaultKey(AUTHED_HELPER)), pPw)) + AuthLevel = AUTHED_HELPER; + } + else + { + KeySlot = m_AuthManager.FindKey(pName); + if(m_AuthManager.CheckKey(KeySlot, pPw)) + AuthLevel = m_AuthManager.KeyLevel(KeySlot); + } - if(AuthLevel != -1) + if(AuthLevel != -1) + { + if(m_aClients[ClientId].m_Authed != AuthLevel) { - if(m_aClients[ClientID].m_Authed != AuthLevel) + if(!IsSixup(ClientId)) { - if(!IsSixup(ClientID)) - { - CMsgPacker Msgp(NETMSG_RCON_AUTH_STATUS, true); - Msgp.AddInt(1); //authed - Msgp.AddInt(1); //cmdlist - SendMsg(&Msgp, MSGFLAG_VITAL, ClientID); - } - else - { - CMsgPacker Msgp(protocol7::NETMSG_RCON_AUTH_ON, true, true); - SendMsg(&Msgp, MSGFLAG_VITAL, ClientID); - } - - m_aClients[ClientID].m_Authed = AuthLevel; // Keeping m_Authed around is unwise... - m_aClients[ClientID].m_AuthKey = KeySlot; - int SendRconCmds = IsSixup(ClientID) ? true : Unpacker.GetInt(); - if(Unpacker.Error() == 0 && SendRconCmds) - // AUTHED_ADMIN - AuthLevel gets the proper IConsole::ACCESS_LEVEL_ - m_aClients[ClientID].m_pRconCmdToSend = Console()->FirstCommandInfo(AUTHED_ADMIN - AuthLevel, CFGFLAG_SERVER); + CMsgPacker Msgp(NETMSG_RCON_AUTH_STATUS, true); + Msgp.AddInt(1); //authed + Msgp.AddInt(1); //cmdlist + SendMsg(&Msgp, MSGFLAG_VITAL, ClientId); + } + else + { + CMsgPacker Msgp(protocol7::NETMSG_RCON_AUTH_ON, true, true); + SendMsg(&Msgp, MSGFLAG_VITAL, ClientId); + } - char aBuf[256]; - const char *pIdent = m_AuthManager.KeyIdent(KeySlot); - switch(AuthLevel) - { - case AUTHED_ADMIN: - { - SendRconLine(ClientID, "Admin authentication successful. Full remote console access granted."); - str_format(aBuf, sizeof(aBuf), "ClientID=%d authed with key=%s (admin)", ClientID, pIdent); - break; - } - case AUTHED_MOD: - { - SendRconLine(ClientID, "Moderator authentication successful. Limited remote console access granted."); - str_format(aBuf, sizeof(aBuf), "ClientID=%d authed with key=%s (moderator)", ClientID, pIdent); - break; - } - case AUTHED_HELPER: + m_aClients[ClientId].m_Authed = AuthLevel; // Keeping m_Authed around is unwise... + m_aClients[ClientId].m_AuthKey = KeySlot; + int SendRconCmds = IsSixup(ClientId) ? true : Unpacker.GetInt(); + if(!Unpacker.Error() && SendRconCmds) + { + // AUTHED_ADMIN - AuthLevel gets the proper IConsole::ACCESS_LEVEL_ + m_aClients[ClientId].m_pRconCmdToSend = Console()->FirstCommandInfo(AUTHED_ADMIN - AuthLevel, CFGFLAG_SERVER); + CMsgPacker MsgStart(NETMSG_RCON_CMD_GROUP_START, true); + MsgStart.AddInt(NumRconCommands(ClientId)); + SendMsg(&MsgStart, MSGFLAG_VITAL, ClientId); + if(m_aClients[ClientId].m_pRconCmdToSend == nullptr) { - SendRconLine(ClientID, "Helper authentication successful. Limited remote console access granted."); - str_format(aBuf, sizeof(aBuf), "ClientID=%d authed with key=%s (helper)", ClientID, pIdent); - break; - } + CMsgPacker MsgEnd(NETMSG_RCON_CMD_GROUP_END, true); + SendMsg(&MsgEnd, MSGFLAG_VITAL, ClientId); } - Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); + } - // DDRace - GameServer()->OnSetAuthed(ClientID, AuthLevel); + char aBuf[256]; + const char *pIdent = m_AuthManager.KeyIdent(KeySlot); + switch(AuthLevel) + { + case AUTHED_ADMIN: + { + SendRconLine(ClientId, "Admin authentication successful. Full remote console access granted."); + str_format(aBuf, sizeof(aBuf), "ClientId=%d authed with key=%s (admin)", ClientId, pIdent); + break; } - } - else if(Config()->m_SvRconMaxTries) - { - m_aClients[ClientID].m_AuthTries++; - char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "Wrong password %d/%d.", m_aClients[ClientID].m_AuthTries, Config()->m_SvRconMaxTries); - SendRconLine(ClientID, aBuf); - if(m_aClients[ClientID].m_AuthTries >= Config()->m_SvRconMaxTries) + case AUTHED_MOD: { - if(!Config()->m_SvRconBantime) - m_NetServer.Drop(ClientID, "Too many remote console authentication tries"); - else - m_ServerBan.BanAddr(m_NetServer.ClientAddr(ClientID), Config()->m_SvRconBantime * 60, "Too many remote console authentication tries"); + SendRconLine(ClientId, "Moderator authentication successful. Limited remote console access granted."); + str_format(aBuf, sizeof(aBuf), "ClientId=%d authed with key=%s (moderator)", ClientId, pIdent); + break; + } + case AUTHED_HELPER: + { + SendRconLine(ClientId, "Helper authentication successful. Limited remote console access granted."); + str_format(aBuf, sizeof(aBuf), "ClientId=%d authed with key=%s (helper)", ClientId, pIdent); + break; } + } + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); + + // DDRace + GameServer()->OnSetAuthed(ClientId, AuthLevel); } - else + } + else if(Config()->m_SvRconMaxTries) + { + m_aClients[ClientId].m_AuthTries++; + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "Wrong password %d/%d.", m_aClients[ClientId].m_AuthTries, Config()->m_SvRconMaxTries); + SendRconLine(ClientId, aBuf); + if(m_aClients[ClientId].m_AuthTries >= Config()->m_SvRconMaxTries) { - SendRconLine(ClientID, "Wrong password."); + if(!Config()->m_SvRconBantime) + m_NetServer.Drop(ClientId, "Too many remote console authentication tries"); + else + m_ServerBan.BanAddr(m_NetServer.ClientAddr(ClientId), Config()->m_SvRconBantime * 60, "Too many remote console authentication tries", false); } } + else + { + SendRconLine(ClientId, "Wrong password."); + } } else if(Msg == NETMSG_PING) { CMsgPacker Msgp(NETMSG_PING_REPLY, true); - SendMsg(&Msgp, MSGFLAG_FLUSH, ClientID); + int Vital = (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 ? MSGFLAG_VITAL : 0; + SendMsg(&Msgp, MSGFLAG_FLUSH | Vital, ClientId); } else if(Msg == NETMSG_PINGEX) { - CUuid *pID = (CUuid *)Unpacker.GetRaw(sizeof(*pID)); + CUuid *pId = (CUuid *)Unpacker.GetRaw(sizeof(*pId)); if(Unpacker.Error()) { return; } CMsgPacker Msgp(NETMSG_PONGEX, true); - Msgp.AddRaw(pID, sizeof(*pID)); - SendMsg(&Msgp, MSGFLAG_FLUSH, ClientID); + Msgp.AddRaw(pId, sizeof(*pId)); + int Vital = (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 ? MSGFLAG_VITAL : 0; + SendMsg(&Msgp, MSGFLAG_FLUSH | Vital, ClientId); } else { @@ -1890,17 +1867,16 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) str_hex(aBuf, sizeof(aBuf), pPacket->m_pData, minimum(pPacket->m_DataSize, MaxDumpedDataSize)); char aBufMsg[256]; - str_format(aBufMsg, sizeof(aBufMsg), "strange message ClientID=%d msg=%d data_size=%d", ClientID, Msg, pPacket->m_DataSize); + str_format(aBufMsg, sizeof(aBufMsg), "strange message ClientId=%d msg=%d data_size=%d", ClientId, Msg, pPacket->m_DataSize); Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "server", aBufMsg); Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "server", aBuf); } } } - else + else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && m_aClients[ClientId].m_State >= CClient::STATE_READY) { // game message - if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && m_aClients[ClientID].m_State >= CClient::STATE_READY) - GameServer()->OnMessage(Msg, &Unpacker, ClientID); + GameServer()->OnMessage(Msg, &Unpacker, ClientId); } } @@ -1993,7 +1969,7 @@ void CServer::CacheServerInfo(CCache *pCache, int Type, bool SendClients) #define ADD_INT(p, x) \ do \ { \ - str_from_int(x, aBuf); \ + str_format(aBuf, sizeof(aBuf), "%d", x); \ (p).AddString(aBuf, 0); \ } while(0) @@ -2254,12 +2230,12 @@ void CServer::SendServerInfo(const NETADDR *pAddr, int Token, int Type, bool Sen #define ADD_INT(p, x) \ do \ { \ - str_from_int(x, aBuf); \ + str_format(aBuf, sizeof(aBuf), "%d", x); \ (p).AddString(aBuf, 0); \ } while(0) CNetChunk Packet; - Packet.m_ClientID = -1; + Packet.m_ClientId = -1; Packet.m_Address = *pAddr; Packet.m_Flags = NETSENDFLAG_CONNLESS; @@ -2316,7 +2292,11 @@ void CServer::FillAntibot(CAntibotRoundData *pData) for(int i = 0; i < MAX_CLIENTS; i++) { CAntibotPlayerData *pPlayer = &pData->m_aPlayers[i]; - net_addr_str(m_NetServer.ClientAddr(i), pPlayer->m_aAddress, sizeof(pPlayer->m_aAddress), true); + // No need for expensive str_copy since we don't truncate and the string is + // ASCII anyway + static_assert(std::size((CAntibotPlayerData{}).m_aAddress) >= NETADDR_MAXSTRSIZE); + static_assert(sizeof(*(CNetServer{}).ClientAddrString(i)) == NETADDR_MAXSTRSIZE); + mem_copy(pPlayer->m_aAddress, m_NetServer.ClientAddrString(i), NETADDR_MAXSTRSIZE); } } @@ -2342,76 +2322,81 @@ void CServer::UpdateRegisterServerInfo() int MaxPlayers = maximum(m_NetServer.MaxClients() - maximum(g_Config.m_SvSpectatorSlots, g_Config.m_SvReservedSlots), PlayerCount); int MaxClients = maximum(m_NetServer.MaxClients() - g_Config.m_SvReservedSlots, ClientCount); - char aName[256]; - char aGameType[32]; - char aMapName[64]; - char aVersion[64]; char aMapSha256[SHA256_MAXSTRSIZE]; sha256_str(m_aCurrentMapSha256[MAP_TYPE_SIX], aMapSha256, sizeof(aMapSha256)); - char aInfo[16384]; - str_format(aInfo, sizeof(aInfo), - "{" - "\"max_clients\":%d," - "\"max_players\":%d," - "\"passworded\":%s," - "\"game_type\":\"%s\"," - "\"name\":\"%s\"," - "\"map\":{" - "\"name\":\"%s\"," - "\"sha256\":\"%s\"," - "\"size\":%d" - "}," - "\"version\":\"%s\"," - "\"client_score_kind\":\"time\"," - "\"clients\":[", - MaxClients, - MaxPlayers, - JsonBool(g_Config.m_Password[0]), - EscapeJson(aGameType, sizeof(aGameType), GameServer()->GameType()), - EscapeJson(aName, sizeof(aName), g_Config.m_SvName), - EscapeJson(aMapName, sizeof(aMapName), m_aCurrentMap), - aMapSha256, - m_aCurrentMapSize[MAP_TYPE_SIX], - EscapeJson(aVersion, sizeof(aVersion), GameServer()->Version())); - - bool FirstPlayer = true; + CJsonStringWriter JsonWriter; + + JsonWriter.BeginObject(); + JsonWriter.WriteAttribute("max_clients"); + JsonWriter.WriteIntValue(MaxClients); + + JsonWriter.WriteAttribute("max_players"); + JsonWriter.WriteIntValue(MaxPlayers); + + JsonWriter.WriteAttribute("passworded"); + JsonWriter.WriteBoolValue(g_Config.m_Password[0]); + + JsonWriter.WriteAttribute("game_type"); + JsonWriter.WriteStrValue(GameServer()->GameType()); + + JsonWriter.WriteAttribute("name"); + JsonWriter.WriteStrValue(g_Config.m_SvName); + + JsonWriter.WriteAttribute("map"); + JsonWriter.BeginObject(); + JsonWriter.WriteAttribute("name"); + JsonWriter.WriteStrValue(GetMapName()); + JsonWriter.WriteAttribute("sha256"); + JsonWriter.WriteStrValue(aMapSha256); + JsonWriter.WriteAttribute("size"); + JsonWriter.WriteIntValue(m_aCurrentMapSize[MAP_TYPE_SIX]); + JsonWriter.EndObject(); + + JsonWriter.WriteAttribute("version"); + JsonWriter.WriteStrValue(GameServer()->Version()); + + JsonWriter.WriteAttribute("client_score_kind"); + JsonWriter.WriteStrValue("time"); // "points" or "time" + + JsonWriter.WriteAttribute("requires_login"); + JsonWriter.WriteBoolValue(false); + + JsonWriter.WriteAttribute("clients"); + JsonWriter.BeginArray(); + for(int i = 0; i < MAX_CLIENTS; i++) { if(m_aClients[i].IncludedInServerInfo()) { - char aCName[32]; - char aCClan[32]; + JsonWriter.BeginObject(); + + JsonWriter.WriteAttribute("name"); + JsonWriter.WriteStrValue(ClientName(i)); + + JsonWriter.WriteAttribute("clan"); + JsonWriter.WriteStrValue(ClientClan(i)); + + JsonWriter.WriteAttribute("country"); + JsonWriter.WriteIntValue(m_aClients[i].m_Country); + + JsonWriter.WriteAttribute("score"); + JsonWriter.WriteIntValue(m_aClients[i].m_Score.value_or(-9999)); - char aExtraPlayerInfo[512]; - GameServer()->OnUpdatePlayerServerInfo(aExtraPlayerInfo, sizeof(aExtraPlayerInfo), i); + JsonWriter.WriteAttribute("is_player"); + JsonWriter.WriteBoolValue(GameServer()->IsClientPlayer(i)); - char aClientInfo[1024]; - str_format(aClientInfo, sizeof(aClientInfo), - "%s{" - "\"name\":\"%s\"," - "\"clan\":\"%s\"," - "\"country\":%d," - "\"score\":%d," - "\"is_player\":%s" - "%s" - "}", - !FirstPlayer ? "," : "", - EscapeJson(aCName, sizeof(aCName), ClientName(i)), - EscapeJson(aCClan, sizeof(aCClan), ClientClan(i)), - m_aClients[i].m_Country, - m_aClients[i].m_Score.value_or(-9999), - JsonBool(GameServer()->IsClientPlayer(i)), - aExtraPlayerInfo); - str_append(aInfo, aClientInfo); - FirstPlayer = false; + GameServer()->OnUpdatePlayerServerInfo(&JsonWriter, i); + + JsonWriter.EndObject(); } } - str_append(aInfo, "]}"); + JsonWriter.EndArray(); + JsonWriter.EndObject(); - m_pRegister->OnNewInfo(aInfo); + m_pRegister->OnNewInfo(JsonWriter.GetOutputString().c_str()); } void CServer::UpdateServerInfo(bool Resend) @@ -2459,9 +2444,10 @@ void CServer::PumpNetwork(bool PacketWaiting) if(PacketWaiting) { // process packets + ResponseToken = NET_SECURITY_TOKEN_UNKNOWN; while(m_NetServer.Recv(&Packet, &ResponseToken)) { - if(Packet.m_ClientID == -1) + if(Packet.m_ClientId == -1) { if(ResponseToken == NET_SECURITY_TOKEN_UNKNOWN && m_pRegister->OnPacket(&Packet)) continue; @@ -2491,14 +2477,15 @@ void CServer::PumpNetwork(bool PacketWaiting) Unpacker.Reset((unsigned char *)Packet.m_pData + sizeof(SERVERBROWSE_GETINFO), Packet.m_DataSize - sizeof(SERVERBROWSE_GETINFO)); int SrvBrwsToken = Unpacker.GetInt(); if(Unpacker.Error()) + { continue; + } CPacker Packer; - CNetChunk Response; - GetServerInfoSixup(&Packer, SrvBrwsToken, RateLimitServerInfoConnless()); - Response.m_ClientID = -1; + CNetChunk Response; + Response.m_ClientId = -1; Response.m_Address = Packet.m_Address; Response.m_Flags = NETSENDFLAG_CONNLESS; Response.m_pData = Packer.Data(); @@ -2515,7 +2502,7 @@ void CServer::PumpNetwork(bool PacketWaiting) } else { - if(m_aClients[Packet.m_ClientID].m_State == CClient::STATE_REDIRECTED) + if(m_aClients[Packet.m_ClientId].m_State == CClient::STATE_REDIRECTED) continue; int GameFlags = 0; @@ -2523,7 +2510,7 @@ void CServer::PumpNetwork(bool PacketWaiting) { GameFlags |= MSGFLAG_VITAL; } - if(Antibot()->OnEngineClientMessage(Packet.m_ClientID, Packet.m_pData, Packet.m_DataSize, GameFlags)) + if(Antibot()->OnEngineClientMessage(Packet.m_ClientId, Packet.m_pData, Packet.m_DataSize, GameFlags)) { continue; } @@ -2537,7 +2524,7 @@ void CServer::PumpNetwork(bool PacketWaiting) int Flags; mem_zero(&Packet, sizeof(Packet)); Packet.m_pData = aBuffer; - while(Antibot()->OnEngineSimulateClientMessage(&Packet.m_ClientID, aBuffer, sizeof(aBuffer), &Packet.m_DataSize, &Flags)) + while(Antibot()->OnEngineSimulateClientMessage(&Packet.m_ClientId, aBuffer, sizeof(aBuffer), &Packet.m_DataSize, &Flags)) { Packet.m_Flags = 0; if(Flags & MSGFLAG_VITAL) @@ -2570,9 +2557,15 @@ void CServer::ChangeMap(const char *pMap) m_MapReload = str_comp(Config()->m_SvMap, m_aCurrentMap) != 0; } +void CServer::ReloadMap() +{ + m_SameMapReload = true; +} + int CServer::LoadMap(const char *pMapName) { m_MapReload = false; + m_SameMapReload = false; char aBuf[IO_MAX_PATH_LENGTH]; str_format(aBuf, sizeof(aBuf), "maps/%s.map", pMapName); @@ -2582,7 +2575,7 @@ int CServer::LoadMap(const char *pMapName) return 0; // reinit snapshot ids - m_IDPool.TimeoutIDs(); + m_IdPool.TimeoutIds(); // get the crc of the map m_aCurrentMapSha256[MAP_TYPE_SIX] = m_pMap->Sha256(); @@ -2615,9 +2608,8 @@ int CServer::LoadMap(const char *pMapName) { m_pRegister->OnConfigChange(); } - str_format(aBufMsg, sizeof(aBufMsg), "couldn't load map %s", aBuf); - Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "sixup", aBufMsg); - Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "sixup", "disabling 0.7 compatibility"); + log_error("sixup", "couldn't load map %s", aBuf); + log_info("sixup", "disabling 0.7 compatibility"); } else { @@ -2649,32 +2641,33 @@ void CServer::UpdateDebugDummies(bool ForceDisconnect) if(m_PreviousDebugDummies == g_Config.m_DbgDummies && !ForceDisconnect) return; + g_Config.m_DbgDummies = clamp(g_Config.m_DbgDummies, 0, MaxClients()); for(int DummyIndex = 0; DummyIndex < maximum(m_PreviousDebugDummies, g_Config.m_DbgDummies); ++DummyIndex) { const bool AddDummy = !ForceDisconnect && DummyIndex < g_Config.m_DbgDummies; - const int ClientID = MAX_CLIENTS - DummyIndex - 1; - if(AddDummy && m_aClients[ClientID].m_State == CClient::STATE_EMPTY) + const int ClientId = MaxClients() - DummyIndex - 1; + if(AddDummy && m_aClients[ClientId].m_State == CClient::STATE_EMPTY) { - NewClientCallback(ClientID, this, false); - m_aClients[ClientID].m_DebugDummy = true; - GameServer()->OnClientConnected(ClientID, nullptr); - m_aClients[ClientID].m_State = CClient::STATE_INGAME; - str_format(m_aClients[ClientID].m_aName, sizeof(m_aClients[ClientID].m_aName), "Debug dummy %d", DummyIndex + 1); - GameServer()->OnClientEnter(ClientID); + NewClientCallback(ClientId, this, false); + m_aClients[ClientId].m_DebugDummy = true; + GameServer()->OnClientConnected(ClientId, nullptr); + m_aClients[ClientId].m_State = CClient::STATE_INGAME; + str_format(m_aClients[ClientId].m_aName, sizeof(m_aClients[ClientId].m_aName), "Debug dummy %d", DummyIndex + 1); + GameServer()->OnClientEnter(ClientId); } - else if(!AddDummy && m_aClients[ClientID].m_DebugDummy) + else if(!AddDummy && m_aClients[ClientId].m_DebugDummy) { - DelClientCallback(ClientID, "Dropping debug dummy", this); + DelClientCallback(ClientId, "Dropping debug dummy", this); } - if(AddDummy && m_aClients[ClientID].m_DebugDummy) + if(AddDummy && m_aClients[ClientId].m_DebugDummy) { CNetObj_PlayerInput Input = {0}; - Input.m_Direction = (ClientID & 1) ? -1 : 1; - m_aClients[ClientID].m_aInputs[0].m_GameTick = Tick() + 1; - mem_copy(m_aClients[ClientID].m_aInputs[0].m_aData, &Input, minimum(sizeof(Input), sizeof(m_aClients[ClientID].m_aInputs[0].m_aData))); - m_aClients[ClientID].m_LatestInput = m_aClients[ClientID].m_aInputs[0]; - m_aClients[ClientID].m_CurrentInput = 0; + Input.m_Direction = (ClientId & 1) ? -1 : 1; + m_aClients[ClientId].m_aInputs[0].m_GameTick = Tick() + 1; + mem_copy(m_aClients[ClientId].m_aInputs[0].m_aData, &Input, minimum(sizeof(Input), sizeof(m_aClients[ClientId].m_aInputs[0].m_aData))); + m_aClients[ClientId].m_LatestInput = m_aClients[ClientId].m_aInputs[0]; + m_aClients[ClientId].m_CurrentInput = 0; } } @@ -2707,7 +2700,7 @@ int CServer::Run() // load map if(!LoadMap(Config()->m_SvMap)) { - dbg_msg("server", "failed to load map. mapname='%s'", Config()->m_SvMap); + log_error("server", "failed to load map. mapname='%s'", Config()->m_SvMap); return -1; } @@ -2716,7 +2709,7 @@ int CServer::Run() char aFullPath[IO_MAX_PATH_LENGTH]; Storage()->GetCompletePath(IStorage::TYPE_SAVE_OR_ABSOLUTE, Config()->m_SvSqliteFile, aFullPath, sizeof(aFullPath)); - if(Config()->m_SvUseSQL) + if(Config()->m_SvUseSql) { DbPool()->RegisterSqliteDatabase(CDbConnectionPool::WRITE_BACKUP, aFullPath); } @@ -2735,30 +2728,36 @@ int CServer::Run() } else if(net_host_lookup(g_Config.m_Bindaddr, &BindAddr, NETTYPE_ALL) != 0) { - dbg_msg("server", "The configured bindaddr '%s' cannot be resolved", g_Config.m_Bindaddr); + log_error("server", "The configured bindaddr '%s' cannot be resolved", g_Config.m_Bindaddr); return -1; } BindAddr.type = Config()->m_SvIpv4Only ? NETTYPE_IPV4 : NETTYPE_ALL; int Port = Config()->m_SvPort; - for(BindAddr.port = Port != 0 ? Port : 8303; !m_NetServer.Open(BindAddr, &m_ServerBan, Config()->m_SvMaxClients, Config()->m_SvMaxClientsPerIP); BindAddr.port++) + for(BindAddr.port = Port != 0 ? Port : 8303; !m_NetServer.Open(BindAddr, &m_ServerBan, Config()->m_SvMaxClients, Config()->m_SvMaxClientsPerIp); BindAddr.port++) { if(Port != 0 || BindAddr.port >= 8310) { - dbg_msg("server", "couldn't open socket. port %d might already be in use", BindAddr.port); + log_error("server", "couldn't open socket. port %d might already be in use", BindAddr.port); return -1; } } if(Port == 0) - dbg_msg("server", "using port %d", BindAddr.port); + log_info("server", "using port %d", BindAddr.port); #if defined(CONF_UPNP) m_UPnP.Open(BindAddr); #endif - IEngine *pEngine = Kernel()->RequestInterface(); - m_pRegister = CreateRegister(&g_Config, m_pConsole, pEngine, this->Port(), m_NetServer.GetGlobalToken()); + if(!m_Http.Init(std::chrono::seconds{2})) + { + log_error("server", "Failed to initialize the HTTP client."); + return -1; + } + + m_pEngine = Kernel()->RequestInterface(); + m_pRegister = CreateRegister(&g_Config, m_pConsole, m_pEngine, &m_Http, this->Port(), m_NetServer.GetGlobalToken()); m_NetServer.SetCallbacks(NewClientCallback, NewClientNoAuthCallback, ClientRejoinCallback, DelClientCallback, this); @@ -2783,15 +2782,17 @@ int CServer::Run() Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); } + ReadAnnouncementsFile(g_Config.m_SvAnnouncementFileName); + // process pending commands m_pConsole->StoreCommands(false); m_pRegister->OnConfigChange(); if(m_AuthManager.IsGenerated()) { - dbg_msg("server", "+-------------------------+"); - dbg_msg("server", "| rcon password: '%s' |", Config()->m_SvRconPassword); - dbg_msg("server", "+-------------------------+"); + log_info("server", "+-------------------------+"); + log_info("server", "| rcon password: '%s' |", Config()->m_SvRconPassword); + log_info("server", "+-------------------------+"); } // start game @@ -2813,8 +2814,9 @@ int CServer::Run() int NewTicks = 0; // load new map - if(m_MapReload || m_CurrentGameTick >= MAX_TICK) // force reload to make sure the ticks stay within a valid range + if(m_MapReload || m_SameMapReload || m_CurrentGameTick >= MAX_TICK) // force reload to make sure the ticks stay within a valid range { + const bool SameMapReload = m_SameMapReload; // load map if(LoadMap(Config()->m_SvMap)) { @@ -2834,16 +2836,19 @@ int CServer::Run() #endif GameServer()->OnShutdown(m_pPersistentData); - for(int ClientID = 0; ClientID < MAX_CLIENTS; ClientID++) + for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) { - if(m_aClients[ClientID].m_State <= CClient::STATE_AUTH) + if(m_aClients[ClientId].m_State <= CClient::STATE_AUTH) continue; - SendMap(ClientID); - bool HasPersistentData = m_aClients[ClientID].m_HasPersistentData; - m_aClients[ClientID].Reset(); - m_aClients[ClientID].m_HasPersistentData = HasPersistentData; - m_aClients[ClientID].m_State = CClient::STATE_CONNECTING; + if(SameMapReload) + SendMapReload(ClientId); + + SendMap(ClientId); + bool HasPersistentData = m_aClients[ClientId].m_HasPersistentData; + m_aClients[ClientId].Reset(); + m_aClients[ClientId].m_HasPersistentData = HasPersistentData; + m_aClients[ClientId].m_State = CClient::STATE_CONNECTING; } m_GameStartTime = time_get(); @@ -2856,15 +2861,15 @@ int CServer::Run() break; } UpdateServerInfo(true); - for(int ClientID = 0; ClientID < MAX_CLIENTS; ClientID++) + for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) { - if(m_aClients[ClientID].m_State != CClient::STATE_CONNECTING) + if(m_aClients[ClientId].m_State != CClient::STATE_CONNECTING) continue; // When doing a map change, a new Teehistorian file is created. For players that are already // on the server, no PlayerJoin event is produced in Teehistorian from the network engine. // Record PlayerJoin events here to record the Sixup version and player join event. - GameServer()->TeehistorianRecordPlayerJoin(ClientID, m_aClients[ClientID].m_Sixup); + GameServer()->TeehistorianRecordPlayerJoin(ClientId, m_aClients[ClientId].m_Sixup); } } else @@ -2875,48 +2880,6 @@ int CServer::Run() } } - // handle dnsbl - if(Config()->m_SvDnsbl) - { - for(int ClientID = 0; ClientID < MAX_CLIENTS; ClientID++) - { - if(m_aClients[ClientID].m_State == CClient::STATE_EMPTY) - continue; - - if(m_aClients[ClientID].m_DnsblState == CClient::DNSBL_STATE_NONE) - { - // initiate dnsbl lookup - InitDnsbl(ClientID); - } - else if(m_aClients[ClientID].m_DnsblState == CClient::DNSBL_STATE_PENDING && - m_aClients[ClientID].m_pDnsblLookup->Status() == IJob::STATE_DONE) - { - if(m_aClients[ClientID].m_pDnsblLookup->Result() != 0) - { - // entry not found -> whitelisted - m_aClients[ClientID].m_DnsblState = CClient::DNSBL_STATE_WHITELISTED; - } - else - { - // entry found -> blacklisted - m_aClients[ClientID].m_DnsblState = CClient::DNSBL_STATE_BLACKLISTED; - - // console output - char aAddrStr[NETADDR_MAXSTRSIZE]; - net_addr_str(m_NetServer.ClientAddr(ClientID), aAddrStr, sizeof(aAddrStr), true); - - str_format(aBuf, sizeof(aBuf), "ClientID=%d addr=<{%s}> secure=%s blacklisted", ClientID, aAddrStr, m_NetServer.HasSecurityToken(ClientID) ? "yes" : "no"); - - Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "dnsbl", aBuf); - } - } - - if(m_aClients[ClientID].m_DnsblState == CClient::DNSBL_STATE_BLACKLISTED && - Config()->m_SvDnsblBan) - m_NetServer.NetBan()->BanAddr(m_NetServer.ClientAddr(ClientID), 60 * 10, "VPN detected, try connecting without. Contact admin if mistaken"); - } - } - while(t > TickStartTime(m_CurrentGameTick + 1)) { GameServer()->OnPreTickTeehistorian(); @@ -2980,27 +2943,81 @@ int CServer::Run() UpdateClientRconCommands(); m_Fifo.Update(); - } - // master server stuff - m_pRegister->Update(); + // master server stuff + m_pRegister->Update(); - if(m_ServerInfoNeedsUpdate) - UpdateServerInfo(); + if(m_ServerInfoNeedsUpdate) + UpdateServerInfo(); - Antibot()->OnEngineTick(); + Antibot()->OnEngineTick(); + + // handle dnsbl + if(Config()->m_SvDnsbl) + { + for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) + { + if(m_aClients[ClientId].m_State == CClient::STATE_EMPTY) + continue; + + if(m_aClients[ClientId].m_DnsblState == CClient::DNSBL_STATE_NONE) + { + // initiate dnsbl lookup + InitDnsbl(ClientId); + } + else if(m_aClients[ClientId].m_DnsblState == CClient::DNSBL_STATE_PENDING && + m_aClients[ClientId].m_pDnsblLookup->State() == IJob::STATE_DONE) + { + if(m_aClients[ClientId].m_pDnsblLookup->Result() != 0) + { + // entry not found -> whitelisted + m_aClients[ClientId].m_DnsblState = CClient::DNSBL_STATE_WHITELISTED; + + char aAddrStr[NETADDR_MAXSTRSIZE]; + net_addr_str(m_NetServer.ClientAddr(ClientId), aAddrStr, sizeof(aAddrStr), true); + + str_format(aBuf, sizeof(aBuf), "ClientId=%d addr=<{%s}> secure=%s whitelisted", ClientId, aAddrStr, m_NetServer.HasSecurityToken(ClientId) ? "yes" : "no"); + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "dnsbl", aBuf); + } + else + { + // entry found -> blacklisted + m_aClients[ClientId].m_DnsblState = CClient::DNSBL_STATE_BLACKLISTED; + + // console output + char aAddrStr[NETADDR_MAXSTRSIZE]; + net_addr_str(m_NetServer.ClientAddr(ClientId), aAddrStr, sizeof(aAddrStr), true); + + str_format(aBuf, sizeof(aBuf), "ClientId=%d addr=<{%s}> secure=%s blacklisted", ClientId, aAddrStr, m_NetServer.HasSecurityToken(ClientId) ? "yes" : "no"); + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "dnsbl", aBuf); + + if(Config()->m_SvDnsblBan) + { + m_NetServer.NetBan()->BanAddr(m_NetServer.ClientAddr(ClientId), 60, Config()->m_SvDnsblBanReason, true); + } + } + } + } + } + for(int i = 0; i < MAX_CLIENTS; ++i) + { + if(m_aClients[i].m_State == CClient::STATE_REDIRECTED) + { + if(time_get() > m_aClients[i].m_RedirectDropTime) + { + m_NetServer.Drop(i, "redirected"); + } + } + } + } if(!NonActive) PumpNetwork(PacketWaiting); NonActive = true; - - for(int i = 0; i < MAX_CLIENTS; ++i) + for(const auto &Client : m_aClients) { - if(m_aClients[i].m_State == CClient::STATE_REDIRECTED) - if(time_get() > m_aClients[i].m_RedirectDropTime) - m_NetServer.Drop(i, "redirected"); - if(m_aClients[i].m_State != CClient::STATE_EMPTY) + if(Client.m_State != CClient::STATE_EMPTY) { NonActive = false; break; @@ -3049,7 +3066,7 @@ int CServer::Run() if(ErrorShutdown()) { - dbg_msg("server", "shutdown from game server (%s)", m_aErrorShutdownReason); + log_info("server", "shutdown from game server (%s)", m_aErrorShutdownReason); pDisconnectReason = m_aErrorShutdownReason; } // disconnect all clients on shutdown @@ -3059,23 +3076,20 @@ int CServer::Run() m_NetServer.Drop(i, pDisconnectReason); } + m_pRegister->OnShutdown(); m_Econ.Shutdown(); - m_Fifo.Shutdown(); + Engine()->ShutdownJobs(); GameServer()->OnShutdown(nullptr); m_pMap->Unload(); - DbPool()->OnShutdown(); #if defined(CONF_UPNP) m_UPnP.Shutdown(); #endif - m_NetServer.Close(); - m_pRegister->OnShutdown(); - return ErrorShutdown(); } @@ -3377,122 +3391,148 @@ void CServer::DemoRecorder_HandleAutoStart() { if(Config()->m_SvAutoDemoRecord) { - m_aDemoRecorder[MAX_CLIENTS].Stop(); + m_aDemoRecorder[RECORDER_AUTO].Stop(IDemoRecorder::EStopMode::KEEP_FILE); + + char aTimestamp[20]; + str_timestamp(aTimestamp, sizeof(aTimestamp)); char aFilename[IO_MAX_PATH_LENGTH]; - char aDate[20]; - str_timestamp(aDate, sizeof(aDate)); - str_format(aFilename, sizeof(aFilename), "demos/%s_%s.demo", "auto/autorecord", aDate); - m_aDemoRecorder[MAX_CLIENTS].Start(Storage(), m_pConsole, aFilename, GameServer()->NetVersion(), m_aCurrentMap, m_aCurrentMapSha256[MAP_TYPE_SIX], m_aCurrentMapCrc[MAP_TYPE_SIX], "server", m_aCurrentMapSize[MAP_TYPE_SIX], m_apCurrentMapData[MAP_TYPE_SIX]); + str_format(aFilename, sizeof(aFilename), "demos/auto/server/%s_%s.demo", m_aCurrentMap, aTimestamp); + m_aDemoRecorder[RECORDER_AUTO].Start( + Storage(), + m_pConsole, + aFilename, + GameServer()->NetVersion(), + m_aCurrentMap, + m_aCurrentMapSha256[MAP_TYPE_SIX], + m_aCurrentMapCrc[MAP_TYPE_SIX], + "server", + m_aCurrentMapSize[MAP_TYPE_SIX], + m_apCurrentMapData[MAP_TYPE_SIX], + nullptr, + nullptr, + nullptr); + if(Config()->m_SvAutoDemoMax) { // clean up auto recorded demos CFileCollection AutoDemos; - AutoDemos.Init(Storage(), "demos/server", "autorecord", ".demo", Config()->m_SvAutoDemoMax); + AutoDemos.Init(Storage(), "demos/auto/server", "", ".demo", Config()->m_SvAutoDemoMax); } } } -void CServer::SaveDemo(int ClientID, float Time) +void CServer::SaveDemo(int ClientId, float Time) { - if(IsRecording(ClientID)) + if(IsRecording(ClientId)) { - m_aDemoRecorder[ClientID].Stop(); - - // rename the demo - char aOldFilename[IO_MAX_PATH_LENGTH]; char aNewFilename[IO_MAX_PATH_LENGTH]; - str_format(aOldFilename, sizeof(aOldFilename), "demos/%s_%d_%d_tmp.demo", m_aCurrentMap, m_NetServer.Address().port, ClientID); - str_format(aNewFilename, sizeof(aNewFilename), "demos/%s_%s_%05.2f.demo", m_aCurrentMap, m_aClients[ClientID].m_aName, Time); - Storage()->RenameFile(aOldFilename, aNewFilename, IStorage::TYPE_SAVE); + str_format(aNewFilename, sizeof(aNewFilename), "demos/%s_%s_%05.2f.demo", m_aCurrentMap, m_aClients[ClientId].m_aName, Time); + m_aDemoRecorder[ClientId].Stop(IDemoRecorder::EStopMode::KEEP_FILE, aNewFilename); } } -void CServer::StartRecord(int ClientID) +void CServer::StartRecord(int ClientId) { if(Config()->m_SvPlayerDemoRecord) { char aFilename[IO_MAX_PATH_LENGTH]; - str_format(aFilename, sizeof(aFilename), "demos/%s_%d_%d_tmp.demo", m_aCurrentMap, m_NetServer.Address().port, ClientID); - m_aDemoRecorder[ClientID].Start(Storage(), Console(), aFilename, GameServer()->NetVersion(), m_aCurrentMap, m_aCurrentMapSha256[MAP_TYPE_SIX], m_aCurrentMapCrc[MAP_TYPE_SIX], "server", m_aCurrentMapSize[MAP_TYPE_SIX], m_apCurrentMapData[MAP_TYPE_SIX]); + str_format(aFilename, sizeof(aFilename), "demos/%s_%d_%d_tmp.demo", m_aCurrentMap, m_NetServer.Address().port, ClientId); + m_aDemoRecorder[ClientId].Start( + Storage(), + Console(), + aFilename, + GameServer()->NetVersion(), + m_aCurrentMap, + m_aCurrentMapSha256[MAP_TYPE_SIX], + m_aCurrentMapCrc[MAP_TYPE_SIX], + "server", + m_aCurrentMapSize[MAP_TYPE_SIX], + m_apCurrentMapData[MAP_TYPE_SIX], + nullptr, + nullptr, + nullptr); } } -void CServer::StopRecord(int ClientID) +void CServer::StopRecord(int ClientId) { - if(IsRecording(ClientID)) + if(IsRecording(ClientId)) { - m_aDemoRecorder[ClientID].Stop(); - - char aFilename[IO_MAX_PATH_LENGTH]; - str_format(aFilename, sizeof(aFilename), "demos/%s_%d_%d_tmp.demo", m_aCurrentMap, m_NetServer.Address().port, ClientID); - Storage()->RemoveFile(aFilename, IStorage::TYPE_SAVE); + m_aDemoRecorder[ClientId].Stop(IDemoRecorder::EStopMode::REMOVE_FILE); } } -bool CServer::IsRecording(int ClientID) +bool CServer::IsRecording(int ClientId) { - return m_aDemoRecorder[ClientID].IsRecording(); + return m_aDemoRecorder[ClientId].IsRecording(); } void CServer::StopDemos() { - for(int i = 0; i < MAX_CLIENTS + 1; i++) + for(int i = 0; i < NUM_RECORDERS; i++) { if(!m_aDemoRecorder[i].IsRecording()) continue; - m_aDemoRecorder[i].Stop(); - - // remove tmp demos - if(i < MAX_CLIENTS) - { - char aPath[256]; - str_format(aPath, sizeof(aPath), "demos/%s_%d_%d_tmp.demo", m_aCurrentMap, m_NetServer.Address().port, i); - Storage()->RemoveFile(aPath, IStorage::TYPE_SAVE); - } + m_aDemoRecorder[i].Stop(i < MAX_CLIENTS ? IDemoRecorder::EStopMode::REMOVE_FILE : IDemoRecorder::EStopMode::KEEP_FILE); } } void CServer::ConRecord(IConsole::IResult *pResult, void *pUser) { CServer *pServer = (CServer *)pUser; - char aFilename[IO_MAX_PATH_LENGTH]; - if(pServer->IsRecording(MAX_CLIENTS)) + if(pServer->IsRecording(RECORDER_MANUAL)) { pServer->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Demo recorder already recording"); return; } + char aFilename[IO_MAX_PATH_LENGTH]; if(pResult->NumArguments()) + { str_format(aFilename, sizeof(aFilename), "demos/%s.demo", pResult->GetString(0)); + } else { - char aDate[20]; - str_timestamp(aDate, sizeof(aDate)); - str_format(aFilename, sizeof(aFilename), "demos/demo_%s.demo", aDate); - } - pServer->m_aDemoRecorder[MAX_CLIENTS].Start(pServer->Storage(), pServer->Console(), aFilename, pServer->GameServer()->NetVersion(), pServer->m_aCurrentMap, pServer->m_aCurrentMapSha256[MAP_TYPE_SIX], pServer->m_aCurrentMapCrc[MAP_TYPE_SIX], "server", pServer->m_aCurrentMapSize[MAP_TYPE_SIX], pServer->m_apCurrentMapData[MAP_TYPE_SIX]); + char aTimestamp[20]; + str_timestamp(aTimestamp, sizeof(aTimestamp)); + str_format(aFilename, sizeof(aFilename), "demos/demo_%s.demo", aTimestamp); + } + pServer->m_aDemoRecorder[RECORDER_MANUAL].Start( + pServer->Storage(), + pServer->Console(), + aFilename, + pServer->GameServer()->NetVersion(), + pServer->m_aCurrentMap, + pServer->m_aCurrentMapSha256[MAP_TYPE_SIX], + pServer->m_aCurrentMapCrc[MAP_TYPE_SIX], + "server", + pServer->m_aCurrentMapSize[MAP_TYPE_SIX], + pServer->m_apCurrentMapData[MAP_TYPE_SIX], + nullptr, + nullptr, + nullptr); } void CServer::ConStopRecord(IConsole::IResult *pResult, void *pUser) { - ((CServer *)pUser)->m_aDemoRecorder[MAX_CLIENTS].Stop(); + ((CServer *)pUser)->m_aDemoRecorder[RECORDER_MANUAL].Stop(IDemoRecorder::EStopMode::KEEP_FILE); } void CServer::ConMapReload(IConsole::IResult *pResult, void *pUser) { - ((CServer *)pUser)->m_MapReload = true; + ((CServer *)pUser)->ReloadMap(); } void CServer::ConLogout(IConsole::IResult *pResult, void *pUser) { CServer *pServer = (CServer *)pUser; - if(pServer->m_RconClientID >= 0 && pServer->m_RconClientID < MAX_CLIENTS && - pServer->m_aClients[pServer->m_RconClientID].m_State != CServer::CClient::STATE_EMPTY) + if(pServer->m_RconClientId >= 0 && pServer->m_RconClientId < MAX_CLIENTS && + pServer->m_aClients[pServer->m_RconClientId].m_State != CServer::CClient::STATE_EMPTY) { - pServer->LogoutClient(pServer->m_RconClientID, ""); + pServer->LogoutClient(pServer->m_RconClientId, ""); } } @@ -3500,19 +3540,19 @@ void CServer::ConShowIps(IConsole::IResult *pResult, void *pUser) { CServer *pServer = (CServer *)pUser; - if(pServer->m_RconClientID >= 0 && pServer->m_RconClientID < MAX_CLIENTS && - pServer->m_aClients[pServer->m_RconClientID].m_State != CServer::CClient::STATE_EMPTY) + if(pServer->m_RconClientId >= 0 && pServer->m_RconClientId < MAX_CLIENTS && + pServer->m_aClients[pServer->m_RconClientId].m_State != CServer::CClient::STATE_EMPTY) { if(pResult->NumArguments()) { - pServer->m_aClients[pServer->m_RconClientID].m_ShowIps = pResult->GetInteger(0); + pServer->m_aClients[pServer->m_RconClientId].m_ShowIps = pResult->GetInteger(0); } else { char aStr[9]; - str_format(aStr, sizeof(aStr), "Value: %d", pServer->m_aClients[pServer->m_RconClientID].m_ShowIps); + str_format(aStr, sizeof(aStr), "Value: %d", pServer->m_aClients[pServer->m_RconClientId].m_ShowIps); char aBuf[32]; - pServer->SendRconLine(pServer->m_RconClientID, pServer->Console()->Format(aBuf, sizeof(aBuf), "server", aStr)); + pServer->SendRconLine(pServer->m_RconClientId, pServer->Console()->Format(aBuf, sizeof(aBuf), "server", aStr)); } } } @@ -3527,7 +3567,7 @@ void CServer::ConAddSqlServer(IConsole::IResult *pResult, void *pUserData) return; } - if(!pSelf->Config()->m_SvUseSQL) + if(!pSelf->Config()->m_SvUseSql) return; if(pResult->NumArguments() != 7 && pResult->NumArguments() != 8) @@ -3601,7 +3641,7 @@ void CServer::ConchainMaxclientsperipUpdate(IConsole::IResult *pResult, void *pU { pfnCallback(pResult, pCallbackUserData); if(pResult->NumArguments()) - ((CServer *)pUserData)->m_NetServer.SetMaxClientsPerIP(pResult->GetInteger(0)); + ((CServer *)pUserData)->m_NetServer.SetMaxClientsPerIp(pResult->GetInteger(0)); } void CServer::ConchainCommandAccessUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) @@ -3635,41 +3675,41 @@ void CServer::ConchainCommandAccessUpdate(IConsole::IResult *pResult, void *pUse pfnCallback(pResult, pCallbackUserData); } -void CServer::LogoutClient(int ClientID, const char *pReason) +void CServer::LogoutClient(int ClientId, const char *pReason) { - if(!IsSixup(ClientID)) + if(!IsSixup(ClientId)) { CMsgPacker Msg(NETMSG_RCON_AUTH_STATUS, true); Msg.AddInt(0); //authed Msg.AddInt(0); //cmdlist - SendMsg(&Msg, MSGFLAG_VITAL, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL, ClientId); } else { CMsgPacker Msg(protocol7::NETMSG_RCON_AUTH_OFF, true, true); - SendMsg(&Msg, MSGFLAG_VITAL, ClientID); + SendMsg(&Msg, MSGFLAG_VITAL, ClientId); } - m_aClients[ClientID].m_AuthTries = 0; - m_aClients[ClientID].m_pRconCmdToSend = 0; + m_aClients[ClientId].m_AuthTries = 0; + m_aClients[ClientId].m_pRconCmdToSend = nullptr; char aBuf[64]; if(*pReason) { str_format(aBuf, sizeof(aBuf), "Logged out by %s.", pReason); - SendRconLine(ClientID, aBuf); - str_format(aBuf, sizeof(aBuf), "ClientID=%d with key=%s logged out by %s", ClientID, m_AuthManager.KeyIdent(m_aClients[ClientID].m_AuthKey), pReason); + SendRconLine(ClientId, aBuf); + str_format(aBuf, sizeof(aBuf), "ClientId=%d with key=%s logged out by %s", ClientId, m_AuthManager.KeyIdent(m_aClients[ClientId].m_AuthKey), pReason); } else { - SendRconLine(ClientID, "Logout successful."); - str_format(aBuf, sizeof(aBuf), "ClientID=%d with key=%s logged out", ClientID, m_AuthManager.KeyIdent(m_aClients[ClientID].m_AuthKey)); + SendRconLine(ClientId, "Logout successful."); + str_format(aBuf, sizeof(aBuf), "ClientId=%d with key=%s logged out", ClientId, m_AuthManager.KeyIdent(m_aClients[ClientId].m_AuthKey)); } - m_aClients[ClientID].m_Authed = AUTHED_NO; - m_aClients[ClientID].m_AuthKey = -1; + m_aClients[ClientId].m_Authed = AUTHED_NO; + m_aClients[ClientId].m_AuthKey = -1; - GameServer()->OnSetAuthed(ClientID, AUTHED_NO); + GameServer()->OnSetAuthed(ClientId, AUTHED_NO); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); } @@ -3770,6 +3810,17 @@ void CServer::ConchainStdoutOutputLevel(IConsole::IResult *pResult, void *pUserD } } +void CServer::ConchainAnnouncementFileName(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) +{ + CServer *pSelf = (CServer *)pUserData; + bool Changed = pResult->NumArguments() && str_comp(pResult->GetString(0), g_Config.m_SvAnnouncementFileName); + pfnCallback(pResult, pCallbackUserData); + if(Changed) + { + pSelf->ReadAnnouncementsFile(g_Config.m_SvAnnouncementFileName); + } +} + #if defined(CONF_FAMILY_UNIX) void CServer::ConchainConnLoggingServerChange(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) { @@ -3806,7 +3857,7 @@ void CServer::RegisterCommands() m_pStorage = Kernel()->RequestInterface(); m_pAntibot = Kernel()->RequestInterface(); - HttpInit(m_pStorage); + Kernel()->RegisterInterface(static_cast(&m_Http), false); // register console commands Console()->Register("kick", "i[id] ?r[reason]", CFGFLAG_SERVER, ConKick, this, "Kick player with specified id for any reason"); @@ -3848,6 +3899,8 @@ void CServer::RegisterCommands() Console()->Chain("loglevel", ConchainLoglevel, this); Console()->Chain("stdout_output_level", ConchainStdoutOutputLevel, this); + Console()->Chain("sv_announcement_filename", ConchainAnnouncementFileName, this); + #if defined(CONF_FAMILY_UNIX) Console()->Chain("sv_conn_logging_server", ConchainConnLoggingServerChange, this); #endif @@ -3858,20 +3911,20 @@ void CServer::RegisterCommands() m_pGameServer->OnConsoleInit(); } -int CServer::SnapNewID() +int CServer::SnapNewId() { - return m_IDPool.NewID(); + return m_IdPool.NewId(); } -void CServer::SnapFreeID(int ID) +void CServer::SnapFreeId(int Id) { - m_IDPool.FreeID(ID); + m_IdPool.FreeId(Id); } -void *CServer::SnapNewItem(int Type, int ID, int Size) +void *CServer::SnapNewItem(int Type, int Id, int Size) { - dbg_assert(ID >= -1 && ID <= 0xffff, "incorrect id"); - return ID < 0 ? 0 : m_SnapshotBuilder.NewItem(Type, ID, Size); + dbg_assert(Id >= -1 && Id <= 0xffff, "incorrect id"); + return Id < 0 ? 0 : m_SnapshotBuilder.NewItem(Type, Id, Size); } void CServer::SnapSetStaticsize(int ItemType, int Size) @@ -3883,35 +3936,38 @@ CServer *CreateServer() { return new CServer(); } // DDRace -void CServer::GetClientAddr(int ClientID, NETADDR *pAddr) const +void CServer::GetClientAddr(int ClientId, NETADDR *pAddr) const { - if(ClientID >= 0 && ClientID < MAX_CLIENTS && m_aClients[ClientID].m_State == CClient::STATE_INGAME) + if(ClientId >= 0 && ClientId < MAX_CLIENTS && m_aClients[ClientId].m_State == CClient::STATE_INGAME) { - *pAddr = *m_NetServer.ClientAddr(ClientID); + *pAddr = *m_NetServer.ClientAddr(ClientId); } } -const char *CServer::GetAnnouncementLine(const char *pFileName) +void CServer::ReadAnnouncementsFile(const char *pFileName) { - if(str_comp(pFileName, m_aAnnouncementFile) != 0) - { - str_copy(m_aAnnouncementFile, pFileName); - m_vAnnouncements.clear(); + m_vAnnouncements.clear(); - IOHANDLE File = m_pStorage->OpenFile(pFileName, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL); - if(!File) - return 0; - - char *pLine; - CLineReader Reader; - Reader.Init(File); - while((pLine = Reader.Get())) - if(str_length(pLine) && pLine[0] != '#') - m_vAnnouncements.emplace_back(pLine); + if(pFileName[0] == '\0') + return; - io_close(File); + CLineReader LineReader; + if(!LineReader.OpenFile(m_pStorage->OpenFile(pFileName, IOFLAG_READ, IStorage::TYPE_ALL))) + { + dbg_msg("announcements", "failed to open '%s'", pFileName); + return; } + while(const char *pLine = LineReader.Get()) + { + if(str_length(pLine) && pLine[0] != '#') + { + m_vAnnouncements.emplace_back(pLine); + } + } +} +const char *CServer::GetAnnouncementLine() +{ if(m_vAnnouncements.empty()) { return 0; @@ -3920,7 +3976,7 @@ const char *CServer::GetAnnouncementLine(const char *pFileName) { m_AnnouncementLastLine = 0; } - else if(!Config()->m_SvAnnouncementRandom) + else if(!g_Config.m_SvAnnouncementRandom) { if(++m_AnnouncementLastLine >= m_vAnnouncements.size()) m_AnnouncementLastLine %= m_vAnnouncements.size(); @@ -3939,29 +3995,29 @@ const char *CServer::GetAnnouncementLine(const char *pFileName) return m_vAnnouncements[m_AnnouncementLastLine].c_str(); } -int *CServer::GetIdMap(int ClientID) +int *CServer::GetIdMap(int ClientId) { - return m_aIdMap + VANILLA_MAX_CLIENTS * ClientID; + return m_aIdMap + VANILLA_MAX_CLIENTS * ClientId; } -bool CServer::SetTimedOut(int ClientID, int OrigID) +bool CServer::SetTimedOut(int ClientId, int OrigId) { - if(!m_NetServer.SetTimedOut(ClientID, OrigID)) + if(!m_NetServer.SetTimedOut(ClientId, OrigId)) { return false; } - m_aClients[ClientID].m_Sixup = m_aClients[OrigID].m_Sixup; + m_aClients[ClientId].m_Sixup = m_aClients[OrigId].m_Sixup; - if(m_aClients[OrigID].m_Authed != AUTHED_NO) + if(m_aClients[OrigId].m_Authed != AUTHED_NO) { - LogoutClient(ClientID, "Timeout Protection"); + LogoutClient(ClientId, "Timeout Protection"); } - DelClientCallback(OrigID, "Timeout Protection used", this); - m_aClients[ClientID].m_Authed = AUTHED_NO; - m_aClients[ClientID].m_Flags = m_aClients[OrigID].m_Flags; - m_aClients[ClientID].m_DDNetVersion = m_aClients[OrigID].m_DDNetVersion; - m_aClients[ClientID].m_GotDDNetVersionPacket = m_aClients[OrigID].m_GotDDNetVersionPacket; - m_aClients[ClientID].m_DDNetVersionSettled = m_aClients[OrigID].m_DDNetVersionSettled; + DelClientCallback(OrigId, "Timeout Protection used", this); + m_aClients[ClientId].m_Authed = AUTHED_NO; + m_aClients[ClientId].m_Flags = m_aClients[OrigId].m_Flags; + m_aClients[ClientId].m_DDNetVersion = m_aClients[OrigId].m_DDNetVersion; + m_aClients[ClientId].m_GotDDNetVersionPacket = m_aClients[OrigId].m_GotDDNetVersionPacket; + m_aClients[ClientId].m_DDNetVersionSettled = m_aClients[OrigId].m_DDNetVersionSettled; return true; } diff --git a/src/engine/server/server.h b/src/engine/server/server.h index a5dcb34ae1..7a54efe36d 100644 --- a/src/engine/server/server.h +++ b/src/engine/server/server.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include "antibot.h" #include "authmanager.h" #include "name_ban.h" +#include "snap_id_pool.h" #if defined(CONF_UPNP) #include "upnp.h" @@ -35,63 +37,23 @@ class CHostLookup; class CLogMessage; class CMsgPacker; class CPacker; +class IEngine; class IEngineMap; class ILogger; -class CSnapIDPool -{ - enum - { - MAX_IDS = 32 * 1024, - }; - - // State of a Snap ID - enum - { - ID_FREE = 0, - ID_ALLOCATED = 1, - ID_TIMED = 2, - }; - - class CID - { - public: - short m_Next; - short m_State; // 0 = free, 1 = allocated, 2 = timed - int m_Timeout; - }; - - CID m_aIDs[MAX_IDS]; - - int m_FirstFree; - int m_FirstTimed; - int m_LastTimed; - int m_Usage; - int m_InUsage; - -public: - CSnapIDPool(); - - void Reset(); - void RemoveFirstTimeout(); - int NewID(); - void TimeoutIDs(); - void FreeID(int ID); -}; - class CServerBan : public CNetBan { class CServer *m_pServer; template - int BanExt(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason); + int BanExt(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason, bool VerbatimReason); public: class CServer *Server() const { return m_pServer; } void InitServerBan(class IConsole *pConsole, class IStorage *pStorage, class CServer *pServer); - int BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason) override; + int BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason, bool VerbatimReason) override; int BanRange(const CNetRange *pRange, int Seconds, const char *pReason) override; static void ConBanExt(class IConsole::IResult *pResult, void *pUser); @@ -109,6 +71,7 @@ class CServer : public IServer class IStorage *m_pStorage; class IEngineAntibot *m_pAntibot; class IRegister *m_pRegister; + IEngine *m_pEngine; #if defined(CONF_UPNP) CUPnP m_UPnP; @@ -135,6 +98,7 @@ class CServer : public IServer class IStorage *Storage() { return m_pStorage; } class IEngineAntibot *Antibot() { return m_pAntibot; } class CDbConnectionPool *DbPool() { return m_pConnectionPool; } + IEngine *Engine() { return m_pEngine; } enum { @@ -213,7 +177,7 @@ class CServer : public IServer bool m_DDNetVersionSettled; int m_DDNetVersion; char m_aDDNetVersionStr[64]; - CUuid m_ConnectionID; + CUuid m_ConnectionId; int64_t m_RedirectDropTime; // DNSBL @@ -233,11 +197,12 @@ class CServer : public IServer CSnapshotDelta m_SnapshotDelta; CSnapshotBuilder m_SnapshotBuilder; - CSnapIDPool m_IDPool; + CSnapIdPool m_IdPool; CNetServer m_NetServer; CEcon m_Econ; CFifo m_Fifo; CServerBan m_ServerBan; + CHttp m_Http; IEngineMap *m_pMap; @@ -254,8 +219,9 @@ class CServer : public IServer int m_RunServer; bool m_MapReload; + bool m_SameMapReload; bool m_ReloadedWhenEmpty; - int m_RconClientID; + int m_RconClientId; int m_RconAuthLevel; int m_PrintCBIndex; char m_aShutdownReason[128]; @@ -268,13 +234,20 @@ class CServer : public IServer NUM_MAP_TYPES }; + enum + { + RECORDER_MANUAL = MAX_CLIENTS, + RECORDER_AUTO = MAX_CLIENTS + 1, + NUM_RECORDERS = MAX_CLIENTS + 2, + }; + char m_aCurrentMap[IO_MAX_PATH_LENGTH]; SHA256_DIGEST m_aCurrentMapSha256[NUM_MAP_TYPES]; unsigned m_aCurrentMapCrc[NUM_MAP_TYPES]; unsigned char *m_apCurrentMapData[NUM_MAP_TYPES]; unsigned int m_aCurrentMapSize[NUM_MAP_TYPES]; - CDemoRecorder m_aDemoRecorder[MAX_CLIENTS + 1]; + CDemoRecorder m_aDemoRecorder[NUM_RECORDERS]; CAuthManager m_AuthManager; int64_t m_ServerInfoFirstRequest; @@ -286,7 +259,6 @@ class CServer : public IServer size_t m_AnnouncementLastLine; std::vector m_vAnnouncements; - char m_aAnnouncementFile[IO_MAX_PATH_LENGTH]; std::shared_ptr m_pFileLogger = nullptr; std::shared_ptr m_pStdoutLogger = nullptr; @@ -294,21 +266,21 @@ class CServer : public IServer CServer(); ~CServer(); - bool IsClientNameAvailable(int ClientID, const char *pNameRequest); - bool SetClientNameImpl(int ClientID, const char *pNameRequest, bool Set); - bool SetClientClanImpl(int ClientID, const char *pClanRequest, bool Set); + bool IsClientNameAvailable(int ClientId, const char *pNameRequest); + bool SetClientNameImpl(int ClientId, const char *pNameRequest, bool Set); + bool SetClientClanImpl(int ClientId, const char *pClanRequest, bool Set); - bool WouldClientNameChange(int ClientID, const char *pNameRequest) override; - bool WouldClientClanChange(int ClientID, const char *pClanRequest) override; - void SetClientName(int ClientID, const char *pName) override; - void SetClientClan(int ClientID, const char *pClan) override; - void SetClientCountry(int ClientID, int Country) override; - void SetClientScore(int ClientID, std::optional Score) override; - void SetClientFlags(int ClientID, int Flags) override; + bool WouldClientNameChange(int ClientId, const char *pNameRequest) override; + bool WouldClientClanChange(int ClientId, const char *pClanRequest) override; + void SetClientName(int ClientId, const char *pName) override; + void SetClientClan(int ClientId, const char *pClan) override; + void SetClientCountry(int ClientId, int Country) override; + void SetClientScore(int ClientId, std::optional Score) override; + void SetClientFlags(int ClientId, int Flags) override; - void Kick(int ClientID, const char *pReason) override; - void Ban(int ClientID, int Seconds, const char *pReason) override; - void RedirectClient(int ClientID, int Port, bool Verbose = false) override; + void Kick(int ClientId, const char *pReason) override; + void Ban(int ClientId, int Seconds, const char *pReason, bool VerbatimReason) override; + void RedirectClient(int ClientId, int Port, bool Verbose = false) override; void DemoRecorder_HandleAutoStart() override; @@ -319,48 +291,52 @@ class CServer : public IServer int Init(); void SendLogLine(const CLogMessage *pMessage); - void SetRconCID(int ClientID) override; - int GetAuthedState(int ClientID) const override; - const char *GetAuthName(int ClientID) const override; + void SetRconCid(int ClientId) override; + int GetAuthedState(int ClientId) const override; + const char *GetAuthName(int ClientId) const override; void GetMapInfo(char *pMapName, int MapNameSize, int *pMapSize, SHA256_DIGEST *pMapSha256, int *pMapCrc) override; - bool GetClientInfo(int ClientID, CClientInfo *pInfo) const override; - void SetClientDDNetVersion(int ClientID, int DDNetVersion) override; - void GetClientAddr(int ClientID, char *pAddrStr, int Size) const override; - const char *ClientName(int ClientID) const override; - const char *ClientClan(int ClientID) const override; - int ClientCountry(int ClientID) const override; - bool ClientIngame(int ClientID) const override; - bool ClientAuthed(int ClientID) const override; + bool GetClientInfo(int ClientId, CClientInfo *pInfo) const override; + void SetClientDDNetVersion(int ClientId, int DDNetVersion) override; + void GetClientAddr(int ClientId, char *pAddrStr, int Size) const override; + const char *ClientName(int ClientId) const override; + const char *ClientClan(int ClientId) const override; + int ClientCountry(int ClientId) const override; + bool ClientSlotEmpty(int ClientId) const override; + bool ClientIngame(int ClientId) const override; + bool ClientAuthed(int ClientId) const override; int Port() const override; int MaxClients() const override; int ClientCount() const override; int DistinctClientCount() const override; - int GetClientVersion(int ClientID) const override; - int SendMsg(CMsgPacker *pMsg, int Flags, int ClientID) override; + int GetClientVersion(int ClientId) const override; + int SendMsg(CMsgPacker *pMsg, int Flags, int ClientId) override; void DoSnapshot(); - static int NewClientCallback(int ClientID, void *pUser, bool Sixup); - static int NewClientNoAuthCallback(int ClientID, void *pUser); - static int DelClientCallback(int ClientID, const char *pReason, void *pUser); - - static int ClientRejoinCallback(int ClientID, void *pUser); - - void SendRconType(int ClientID, bool UsernameReq); - void SendCapabilities(int ClientID); - void SendMap(int ClientID); - void SendMapData(int ClientID, int Chunk); - void SendConnectionReady(int ClientID); - void SendRconLine(int ClientID, const char *pLine); - // Accepts -1 as ClientID to mean "all clients with at least auth level admin" - void SendRconLogLine(int ClientID, const CLogMessage *pMessage); - - void SendRconCmdAdd(const IConsole::CCommandInfo *pCommandInfo, int ClientID); - void SendRconCmdRem(const IConsole::CCommandInfo *pCommandInfo, int ClientID); + static int NewClientCallback(int ClientId, void *pUser, bool Sixup); + static int NewClientNoAuthCallback(int ClientId, void *pUser); + static int DelClientCallback(int ClientId, const char *pReason, void *pUser); + + static int ClientRejoinCallback(int ClientId, void *pUser); + + void SendRconType(int ClientId, bool UsernameReq); + void SendCapabilities(int ClientId); + void SendMap(int ClientId); + void SendMapData(int ClientId, int Chunk); + void SendMapReload(int ClientId); + void SendConnectionReady(int ClientId); + void SendRconLine(int ClientId, const char *pLine); + // Accepts -1 as ClientId to mean "all clients with at least auth level admin" + void SendRconLogLine(int ClientId, const CLogMessage *pMessage); + + void SendRconCmdAdd(const IConsole::CCommandInfo *pCommandInfo, int ClientId); + void SendRconCmdRem(const IConsole::CCommandInfo *pCommandInfo, int ClientId); + int GetConsoleAccessLevel(int ClientId); + int NumRconCommands(int ClientId); void UpdateClientRconCommands(); - bool CheckReservedSlotAuth(int ClientID, const char *pPassword); + bool CheckReservedSlotAuth(int ClientId, const char *pPassword); void ProcessClientPacket(CNetChunk *pPacket); class CCache @@ -404,12 +380,13 @@ class CServer : public IServer void ChangeMap(const char *pMap) override; const char *GetMapName() const override; + void ReloadMap() override; int LoadMap(const char *pMapName); - void SaveDemo(int ClientID, float Time) override; - void StartRecord(int ClientID) override; - void StopRecord(int ClientID) override; - bool IsRecording(int ClientID) override; + void SaveDemo(int ClientId, float Time) override; + void StartRecord(int ClientId) override; + void StopRecord(int ClientId) override; + bool IsRecording(int ClientId) override; void StopDemos() override; int Run(); @@ -438,7 +415,7 @@ class CServer : public IServer static void ConchainMaxclientsperipUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainCommandAccessUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); - void LogoutClient(int ClientID, const char *pReason); + void LogoutClient(int ClientId, const char *pReason); void LogoutKey(int Key, const char *pReason); void ConchainRconPasswordChangeGeneric(int Level, const char *pCurrent, IConsole::IResult *pResult); @@ -449,6 +426,7 @@ class CServer : public IServer static void ConchainSixupUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainLoglevel(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainStdoutOutputLevel(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); + static void ConchainAnnouncementFileName(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); #if defined(CONF_FAMILY_UNIX) static void ConchainConnLoggingServerChange(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); @@ -456,47 +434,48 @@ class CServer : public IServer void RegisterCommands(); - int SnapNewID() override; - void SnapFreeID(int ID) override; - void *SnapNewItem(int Type, int ID, int Size) override; + int SnapNewId() override; + void SnapFreeId(int Id) override; + void *SnapNewItem(int Type, int Id, int Size) override; void SnapSetStaticsize(int ItemType, int Size) override; // DDRace - void GetClientAddr(int ClientID, NETADDR *pAddr) const override; + void GetClientAddr(int ClientId, NETADDR *pAddr) const override; int m_aPrevStates[MAX_CLIENTS]; - const char *GetAnnouncementLine(const char *pFileName) override; + const char *GetAnnouncementLine() override; + void ReadAnnouncementsFile(const char *pFileName) override; - int *GetIdMap(int ClientID) override; + int *GetIdMap(int ClientId) override; - void InitDnsbl(int ClientID); - bool DnsblWhite(int ClientID) override + void InitDnsbl(int ClientId); + bool DnsblWhite(int ClientId) override { - return m_aClients[ClientID].m_DnsblState == CClient::DNSBL_STATE_NONE || - m_aClients[ClientID].m_DnsblState == CClient::DNSBL_STATE_WHITELISTED; + return m_aClients[ClientId].m_DnsblState == CClient::DNSBL_STATE_NONE || + m_aClients[ClientId].m_DnsblState == CClient::DNSBL_STATE_WHITELISTED; } - bool DnsblPending(int ClientID) override + bool DnsblPending(int ClientId) override { - return m_aClients[ClientID].m_DnsblState == CClient::DNSBL_STATE_PENDING; + return m_aClients[ClientId].m_DnsblState == CClient::DNSBL_STATE_PENDING; } - bool DnsblBlack(int ClientID) override + bool DnsblBlack(int ClientId) override { - return m_aClients[ClientID].m_DnsblState == CClient::DNSBL_STATE_BLACKLISTED; + return m_aClients[ClientId].m_DnsblState == CClient::DNSBL_STATE_BLACKLISTED; } void AuthRemoveKey(int KeySlot); - bool ClientPrevIngame(int ClientID) override { return m_aPrevStates[ClientID] == CClient::STATE_INGAME; } - const char *GetNetErrorString(int ClientID) override { return m_NetServer.ErrorString(ClientID); } - void ResetNetErrorString(int ClientID) override { m_NetServer.ResetErrorString(ClientID); } - bool SetTimedOut(int ClientID, int OrigID) override; - void SetTimeoutProtected(int ClientID) override { m_NetServer.SetTimeoutProtected(ClientID); } + bool ClientPrevIngame(int ClientId) override { return m_aPrevStates[ClientId] == CClient::STATE_INGAME; } + const char *GetNetErrorString(int ClientId) override { return m_NetServer.ErrorString(ClientId); } + void ResetNetErrorString(int ClientId) override { m_NetServer.ResetErrorString(ClientId); } + bool SetTimedOut(int ClientId, int OrigId) override; + void SetTimeoutProtected(int ClientId) override { m_NetServer.SetTimeoutProtected(ClientId); } - void SendMsgRaw(int ClientID, const void *pData, int Size, int Flags) override; + void SendMsgRaw(int ClientId, const void *pData, int Size, int Flags) override; bool ErrorShutdown() const { return m_aErrorShutdownReason[0] != 0; } void SetErrorShutdown(const char *pReason) override; - bool IsSixup(int ClientID) const override { return ClientID != SERVER_DEMO_CLIENT && m_aClients[ClientID].m_Sixup; } + bool IsSixup(int ClientId) const override { return ClientId != SERVER_DEMO_CLIENT && m_aClients[ClientId].m_Sixup; } void SetLoggers(std::shared_ptr &&pFileLogger, std::shared_ptr &&pStdoutLogger); diff --git a/src/engine/server/snap_id_pool.cpp b/src/engine/server/snap_id_pool.cpp new file mode 100644 index 0000000000..516a807040 --- /dev/null +++ b/src/engine/server/snap_id_pool.cpp @@ -0,0 +1,96 @@ +/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ +/* If you are missing that file, acquire a complete release at teeworlds.com. */ + +#include "snap_id_pool.h" + +#include + +CSnapIdPool::CSnapIdPool() +{ + Reset(); +} + +void CSnapIdPool::Reset() +{ + for(int i = 0; i < MAX_IDS; i++) + { + m_aIds[i].m_Next = i + 1; + m_aIds[i].m_State = ID_FREE; + } + + m_aIds[MAX_IDS - 1].m_Next = -1; + m_FirstFree = 0; + m_FirstTimed = -1; + m_LastTimed = -1; + m_Usage = 0; + m_InUsage = 0; +} + +void CSnapIdPool::RemoveFirstTimeout() +{ + int NextTimed = m_aIds[m_FirstTimed].m_Next; + + // add it to the free list + m_aIds[m_FirstTimed].m_Next = m_FirstFree; + m_aIds[m_FirstTimed].m_State = ID_FREE; + m_FirstFree = m_FirstTimed; + + // remove it from the timed list + m_FirstTimed = NextTimed; + if(m_FirstTimed == -1) + m_LastTimed = -1; + + m_Usage--; +} + +int CSnapIdPool::NewId() +{ + int64_t Now = time_get(); + + // process timed ids + while(m_FirstTimed != -1 && m_aIds[m_FirstTimed].m_Timeout < Now) + RemoveFirstTimeout(); + + int Id = m_FirstFree; + if(Id == -1) + { + dbg_msg("server", "invalid id"); + return Id; + } + m_FirstFree = m_aIds[m_FirstFree].m_Next; + m_aIds[Id].m_State = ID_ALLOCATED; + m_Usage++; + m_InUsage++; + return Id; +} + +void CSnapIdPool::TimeoutIds() +{ + // process timed ids + while(m_FirstTimed != -1) + RemoveFirstTimeout(); +} + +void CSnapIdPool::FreeId(int Id) +{ + if(Id < 0) + return; + dbg_assert((size_t)Id < std::size(m_aIds), "id is out of range"); + dbg_assert(m_aIds[Id].m_State == ID_ALLOCATED, "id is not allocated"); + + m_InUsage--; + m_aIds[Id].m_State = ID_TIMED; + m_aIds[Id].m_Timeout = time_get() + time_freq() * 5; + m_aIds[Id].m_Next = -1; + + if(m_LastTimed != -1) + { + m_aIds[m_LastTimed].m_Next = Id; + m_LastTimed = Id; + } + else + { + m_FirstTimed = Id; + m_LastTimed = Id; + } +} diff --git a/src/engine/server/snap_id_pool.h b/src/engine/server/snap_id_pool.h new file mode 100644 index 0000000000..e6a3327998 --- /dev/null +++ b/src/engine/server/snap_id_pool.h @@ -0,0 +1,48 @@ +/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ +/* If you are missing that file, acquire a complete release at teeworlds.com. */ + +#ifndef ENGINE_SERVER_SNAP_ID_POOL_H +#define ENGINE_SERVER_SNAP_ID_POOL_H + +class CSnapIdPool +{ + enum + { + MAX_IDS = 32 * 1024, + }; + + // State of a Snap ID + enum + { + ID_FREE = 0, + ID_ALLOCATED = 1, + ID_TIMED = 2, + }; + + class CID + { + public: + short m_Next; + short m_State; // 0 = free, 1 = allocated, 2 = timed + int m_Timeout; + }; + + CID m_aIds[MAX_IDS]; + + int m_FirstFree; + int m_FirstTimed; + int m_LastTimed; + int m_Usage; + int m_InUsage; + +public: + CSnapIdPool(); + + void Reset(); + void RemoveFirstTimeout(); + int NewId(); + void TimeoutIds(); + void FreeId(int Id); +}; + +#endif diff --git a/src/engine/server/upnp.cpp b/src/engine/server/upnp.cpp index e6639c07d2..2f0ebe0a07 100644 --- a/src/engine/server/upnp.cpp +++ b/src/engine/server/upnp.cpp @@ -25,14 +25,20 @@ void CUPnP::Open(NETADDR Address) m_pUPnPDevice = upnpDiscover(2000, NULL, NULL, 0, 0, 2, &Error); +#if MINIUPNPC_API_VERSION > 17 + char aWanAddr[64]; + int Status = UPNP_GetValidIGD(m_pUPnPDevice, m_pUPnPUrls, m_pUPnPData, aLanAddr, sizeof(aLanAddr), aWanAddr, sizeof(aWanAddr)); + dbg_msg("upnp", "status=%d, lan_addr=%s, wan_addr=%s", Status, aLanAddr, aWanAddr); +#else int Status = UPNP_GetValidIGD(m_pUPnPDevice, m_pUPnPUrls, m_pUPnPData, aLanAddr, sizeof(aLanAddr)); dbg_msg("upnp", "status=%d, lan_addr=%s", Status, aLanAddr); +#endif if(Status == 1) { m_Enabled = true; dbg_msg("upnp", "found valid IGD: %s", m_pUPnPUrls->controlURL); - str_from_int(m_Addr.port, aPort); + str_format(aPort, sizeof(aPort), "%d", m_Addr.port); Error = UPNP_AddPortMapping(m_pUPnPUrls->controlURL, m_pUPnPData->first.servicetype, aPort, aPort, aLanAddr, "DDNet Server " GAME_RELEASE_VERSION, @@ -55,7 +61,7 @@ void CUPnP::Shutdown() if(m_Enabled) { char aPort[6]; - str_from_int(m_Addr.port, aPort); + str_format(aPort, sizeof(aPort), "%d", m_Addr.port); int Error = UPNP_DeletePortMapping(m_pUPnPUrls->controlURL, m_pUPnPData->first.servicetype, aPort, "UDP", NULL); if(Error != 0) diff --git a/src/engine/serverbrowser.h b/src/engine/serverbrowser.h index 015f5171a3..0b3f58eac5 100644 --- a/src/engine/serverbrowser.h +++ b/src/engine/serverbrowser.h @@ -117,10 +117,10 @@ class CServerInfo char m_aAddress[MAX_SERVER_ADDRESSES * NETADDR_MAXSTRSIZE]; CClient m_aClients[SERVERINFO_MAX_CLIENTS]; int m_NumFilteredPlayers; + bool m_RequiresLogin; static int EstimateLatency(int Loc1, int Loc2); static bool ParseLocation(int *pResult, const char *pString); - void InfoToString(char *pBuffer, int BufferSize) const; }; class CCommunityCountryServer @@ -231,6 +231,8 @@ class CCommunity const SHA256_DIGEST &IconSha256() const { return m_IconSha256; } const std::vector &Countries() const { return m_vCountries; } const std::vector &Types() const { return m_vTypes; } + bool HasCountry(const char *pCountryName) const; + bool HasType(const char *pTypeName) const; bool HasRanks() const { return m_HasFinishes; } CServerInfo::ERankState HasRank(const char *pMap) const; }; @@ -245,6 +247,18 @@ class IFilterList virtual bool Filtered(const char *pElement) const = 0; }; +class ICommunityCache +{ +public: + virtual void Update(bool Force) = 0; + virtual const std::vector &SelectedCommunities() const = 0; + virtual const std::vector &SelectableCountries() const = 0; + virtual const std::vector &SelectableTypes() const = 0; + virtual bool AnyRanksAvailable() const = 0; + virtual bool CountriesTypesFilterAvailable() const = 0; + virtual const char *CountryTypeFilterKey() const = 0; +}; + class IServerBrowser : public IInterface { MACRO_INTERFACE("serverbrowser") @@ -255,6 +269,7 @@ class IServerBrowser : public IInterface SORT_MAP - Sort by map SORT_GAMETYPE - Sort by game type. DM, TDM etc. SORT_NUMPLAYERS - Sort after how many players there are on the server. + SORT_NUMFRIENDS - Sort after how many friends there are on the server. */ enum { @@ -263,6 +278,7 @@ class IServerBrowser : public IInterface SORT_MAP, SORT_GAMETYPE, SORT_NUMPLAYERS, + SORT_NUMFRIENDS, QUICK_SERVERNAME = 1, QUICK_PLAYER = 2, @@ -271,16 +287,43 @@ class IServerBrowser : public IInterface TYPE_INTERNET = 0, TYPE_LAN, TYPE_FAVORITES, + TYPE_FAVORITE_COMMUNITY_1, + TYPE_FAVORITE_COMMUNITY_2, + TYPE_FAVORITE_COMMUNITY_3, + TYPE_FAVORITE_COMMUNITY_4, + TYPE_FAVORITE_COMMUNITY_5, NUM_TYPES, + LAN_PORT_BEGIN = 8303, + LAN_PORT_END = 8310, + }; + + class CServerEntry + { + public: + int64_t m_RequestTime; + bool m_RequestIgnoreInfo; + int m_GotInfo; + CServerInfo m_Info; + + CServerEntry *m_pPrevReq; // request list + CServerEntry *m_pNextReq; }; static constexpr const char *COMMUNITY_DDNET = "ddnet"; static constexpr const char *COMMUNITY_NONE = "none"; + static constexpr const char *COMMUNITY_COUNTRY_NONE = "none"; + static constexpr const char *COMMUNITY_TYPE_NONE = "None"; + /** + * Special community value for country/type filters that + * affect all communities. + */ + static constexpr const char *COMMUNITY_ALL = "all"; + static constexpr const char *SEARCH_EXCLUDE_TOKEN = ";"; - virtual void Refresh(int Type) = 0; + virtual void Refresh(int Type, bool Force = false) = 0; virtual bool IsGettingServerlist() const = 0; virtual bool IsRefreshing() const = 0; virtual int LoadingProgression() const = 0; @@ -297,16 +340,26 @@ class IServerBrowser : public IInterface virtual const std::vector &Communities() const = 0; virtual const CCommunity *Community(const char *pCommunityId) const = 0; virtual std::vector SelectedCommunities() const = 0; - virtual int64_t DDNetInfoUpdateTime() const = 0; + virtual std::vector FavoriteCommunities() const = 0; + virtual std::vector CurrentCommunities() const = 0; + virtual unsigned CurrentCommunitiesHash() const = 0; + + virtual bool DDNetInfoAvailable() const = 0; + virtual SHA256_DIGEST DDNetInfoSha256() const = 0; + virtual ICommunityCache &CommunityCache() = 0; + virtual const ICommunityCache &CommunityCache() const = 0; + virtual IFilterList &FavoriteCommunitiesFilter() = 0; virtual IFilterList &CommunitiesFilter() = 0; virtual IFilterList &CountriesFilter() = 0; virtual IFilterList &TypesFilter() = 0; + virtual const IFilterList &FavoriteCommunitiesFilter() const = 0; virtual const IFilterList &CommunitiesFilter() const = 0; virtual const IFilterList &CountriesFilter() const = 0; virtual const IFilterList &TypesFilter() const = 0; virtual void CleanFilters() = 0; + virtual CServerEntry *Find(const NETADDR &Addr) = 0; virtual int GetCurrentType() = 0; virtual const char *GetTutorialServer() = 0; }; diff --git a/src/engine/shared/Cargo.toml b/src/engine/shared/Cargo.toml index b939c23d16..690081ea0f 100644 --- a/src/engine/shared/Cargo.toml +++ b/src/engine/shared/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ddnet-engine-shared" version = "0.0.1" -edition = "2018" +edition = "2021" publish = false license = "Zlib" diff --git a/src/engine/shared/config.cpp b/src/engine/shared/config.cpp index 3c0f61e23b..28f0e30e84 100644 --- a/src/engine/shared/config.cpp +++ b/src/engine/shared/config.cpp @@ -11,361 +11,267 @@ CConfig g_Config; +// ----------------------- Config Variables + static void EscapeParam(char *pDst, const char *pSrc, int Size) { str_escape(&pDst, pSrc, pDst + Size); } -struct SConfigVariable +void SConfigVariable::ExecuteLine(const char *pLine) const { - enum EVariableType - { - VAR_INT, - VAR_COLOR, - VAR_STRING, - }; - IConsole *m_pConsole; - const char *m_pScriptName; - EVariableType m_Type; - int m_Flags; - const char *m_pHelp; - // Note that this only applies to the console command and the SetValue function, - // but the underlying config variable can still be modified programatically. - bool m_ReadOnly = false; - - SConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp) : - m_pConsole(pConsole), - m_pScriptName(pScriptName), - m_Type(Type), - m_Flags(Flags), - m_pHelp(pHelp) - { - } - - virtual ~SConfigVariable() = default; - - virtual void Register() = 0; - virtual bool IsDefault() const = 0; - virtual void Serialize(char *pOut, size_t Size) const = 0; - virtual void ResetToDefault() = 0; - virtual void ResetToOld() = 0; + m_pConsole->ExecuteLine(pLine, (m_Flags & CFGFLAG_GAME) != 0 ? IConsole::CLIENT_ID_GAME : -1); +} -protected: - void ExecuteLine(const char *pLine) const - { - m_pConsole->ExecuteLine(pLine, (m_Flags & CFGFLAG_GAME) != 0 ? IConsole::CLIENT_ID_GAME : -1); - } +bool SConfigVariable::CheckReadOnly() const +{ + if(!m_ReadOnly) + return false; + char aBuf[IConsole::CMDLINE_LENGTH + 64]; + str_format(aBuf, sizeof(aBuf), "The config variable '%s' cannot be changed right now.", m_pScriptName); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); + return true; +} - bool CheckReadOnly() const - { - if(!m_ReadOnly) - return false; - char aBuf[IConsole::CMDLINE_LENGTH + 64]; - str_format(aBuf, sizeof(aBuf), "The config variable '%s' cannot be changed right now.", m_pScriptName); - m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - return true; - } -}; - -struct SIntConfigVariable : public SConfigVariable -{ - int *m_pVariable; - int m_Default; - int m_Min; - int m_Max; - int m_OldValue; - - SIntConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, int *pVariable, int Default, int Min, int Max) : - SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), - m_pVariable(pVariable), - m_Default(Default), - m_Min(Min), - m_Max(Max), - m_OldValue(Default) - { - *m_pVariable = m_Default; - } +// ----- - ~SIntConfigVariable() override = default; +void SIntConfigVariable::CommandCallback(IConsole::IResult *pResult, void *pUserData) +{ + SIntConfigVariable *pData = static_cast(pUserData); - static void CommandCallback(IConsole::IResult *pResult, void *pUserData) + if(pResult->NumArguments()) { - SIntConfigVariable *pData = static_cast(pUserData); - - if(pResult->NumArguments()) - { - if(pData->CheckReadOnly()) - return; - - int Value = pResult->GetInteger(0); + if(pData->CheckReadOnly()) + return; - // do clamping - if(pData->m_Min != pData->m_Max) - { - if(Value < pData->m_Min) - Value = pData->m_Min; - if(pData->m_Max != 0 && Value > pData->m_Max) - Value = pData->m_Max; - } + int Value = pResult->GetInteger(0); - *pData->m_pVariable = Value; - if(pResult->m_ClientID != IConsole::CLIENT_ID_GAME) - pData->m_OldValue = Value; - } - else + // do clamping + if(pData->m_Min != pData->m_Max) { - char aBuf[32]; - str_format(aBuf, sizeof(aBuf), "Value: %d", *pData->m_pVariable); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); + if(Value < pData->m_Min) + Value = pData->m_Min; + if(pData->m_Max != 0 && Value > pData->m_Max) + Value = pData->m_Max; } - } - void Register() override - { - m_pConsole->Register(m_pScriptName, "?i", m_Flags, CommandCallback, this, m_pHelp); + *pData->m_pVariable = Value; + if(pResult->m_ClientId != IConsole::CLIENT_ID_GAME) + pData->m_OldValue = Value; } - - bool IsDefault() const override + else { - return *m_pVariable == m_Default; + char aBuf[32]; + str_format(aBuf, sizeof(aBuf), "Value: %d", *pData->m_pVariable); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); } +} - void Serialize(char *pOut, size_t Size, int Value) const - { - str_format(pOut, Size, "%s %i", m_pScriptName, Value); - } +void SIntConfigVariable::Register() +{ + m_pConsole->Register(m_pScriptName, "?i", m_Flags, CommandCallback, this, m_pHelp); +} - void Serialize(char *pOut, size_t Size) const override - { - Serialize(pOut, Size, *m_pVariable); - } +bool SIntConfigVariable::IsDefault() const +{ + return *m_pVariable == m_Default; +} - void SetValue(int Value) - { - if(CheckReadOnly()) - return; - char aBuf[IConsole::CMDLINE_LENGTH]; - Serialize(aBuf, sizeof(aBuf), Value); - ExecuteLine(aBuf); - } +void SIntConfigVariable::Serialize(char *pOut, size_t Size, int Value) const +{ + str_format(pOut, Size, "%s %i", m_pScriptName, Value); +} - void ResetToDefault() override - { - SetValue(m_Default); - } +void SIntConfigVariable::Serialize(char *pOut, size_t Size) const +{ + Serialize(pOut, Size, *m_pVariable); +} - void ResetToOld() override - { - *m_pVariable = m_OldValue; - } -}; - -struct SColorConfigVariable : public SConfigVariable -{ - unsigned *m_pVariable; - unsigned m_Default; - bool m_Light; - bool m_Alpha; - unsigned m_OldValue; - - SColorConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, unsigned *pVariable, unsigned Default) : - SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), - m_pVariable(pVariable), - m_Default(Default), - m_Light(Flags & CFGFLAG_COLLIGHT), - m_Alpha(Flags & CFGFLAG_COLALPHA), - m_OldValue(Default) - { - *m_pVariable = m_Default; - } +void SIntConfigVariable::SetValue(int Value) +{ + if(CheckReadOnly()) + return; + char aBuf[IConsole::CMDLINE_LENGTH]; + Serialize(aBuf, sizeof(aBuf), Value); + ExecuteLine(aBuf); +} + +void SIntConfigVariable::ResetToDefault() +{ + SetValue(m_Default); +} + +void SIntConfigVariable::ResetToOld() +{ + *m_pVariable = m_OldValue; +} - ~SColorConfigVariable() override = default; +// ----- - static void CommandCallback(IConsole::IResult *pResult, void *pUserData) +void SColorConfigVariable::CommandCallback(IConsole::IResult *pResult, void *pUserData) +{ + SColorConfigVariable *pData = static_cast(pUserData); + char aBuf[IConsole::CMDLINE_LENGTH + 64]; + if(pResult->NumArguments()) { - SColorConfigVariable *pData = static_cast(pUserData); + if(pData->CheckReadOnly()) + return; - if(pResult->NumArguments()) + const auto Color = pResult->GetColor(0, pData->m_DarkestLighting); + if(Color) { - if(pData->CheckReadOnly()) - return; - - const ColorHSLA Color = pResult->GetColor(0, pData->m_Light); - const unsigned Value = Color.Pack(pData->m_Light ? 0.5f : 0.0f, pData->m_Alpha); + const unsigned Value = Color->Pack(pData->m_DarkestLighting, pData->m_Alpha); *pData->m_pVariable = Value; - if(pResult->m_ClientID != IConsole::CLIENT_ID_GAME) + if(pResult->m_ClientId != IConsole::CLIENT_ID_GAME) pData->m_OldValue = Value; } else { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "Value: %u", *pData->m_pVariable); + str_format(aBuf, sizeof(aBuf), "%s is not a valid color.", pResult->GetString(0)); pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - - ColorHSLA Hsla = ColorHSLA(*pData->m_pVariable, true); - if(pData->m_Light) - Hsla = Hsla.UnclampLighting(); - str_format(aBuf, sizeof(aBuf), "H: %d°, S: %d%%, L: %d%%", round_truncate(Hsla.h * 360), round_truncate(Hsla.s * 100), round_truncate(Hsla.l * 100)); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - - const ColorRGBA Rgba = color_cast(Hsla); - str_format(aBuf, sizeof(aBuf), "R: %d, G: %d, B: %d, #%06X", round_truncate(Rgba.r * 255), round_truncate(Rgba.g * 255), round_truncate(Rgba.b * 255), Rgba.Pack(false)); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - - if(pData->m_Alpha) - { - str_format(aBuf, sizeof(aBuf), "A: %d%%", round_truncate(Hsla.a * 100)); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - } } } - - void Register() override + else { - m_pConsole->Register(m_pScriptName, "?i", m_Flags, CommandCallback, this, m_pHelp); - } + str_format(aBuf, sizeof(aBuf), "Value: %u", *pData->m_pVariable); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - bool IsDefault() const override - { - return *m_pVariable == m_Default; - } + const ColorHSLA Hsla = ColorHSLA(*pData->m_pVariable, true).UnclampLighting(pData->m_DarkestLighting); + str_format(aBuf, sizeof(aBuf), "H: %d°, S: %d%%, L: %d%%", round_to_int(Hsla.h * 360), round_to_int(Hsla.s * 100), round_to_int(Hsla.l * 100)); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - void Serialize(char *pOut, size_t Size, unsigned Value) const - { - str_format(pOut, Size, "%s %u", m_pScriptName, Value); - } + const ColorRGBA Rgba = color_cast(Hsla); + str_format(aBuf, sizeof(aBuf), "R: %d, G: %d, B: %d, #%06X", round_to_int(Rgba.r * 255), round_to_int(Rgba.g * 255), round_to_int(Rgba.b * 255), Rgba.Pack(false)); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - void Serialize(char *pOut, size_t Size) const override - { - Serialize(pOut, Size, *m_pVariable); + if(pData->m_Alpha) + { + str_format(aBuf, sizeof(aBuf), "A: %d%%", round_to_int(Hsla.a * 100)); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); + } } +} - void SetValue(unsigned Value) - { - if(CheckReadOnly()) - return; - char aBuf[IConsole::CMDLINE_LENGTH]; - Serialize(aBuf, sizeof(aBuf), Value); - ExecuteLine(aBuf); - } +void SColorConfigVariable::Register() +{ + m_pConsole->Register(m_pScriptName, "?i", m_Flags, CommandCallback, this, m_pHelp); +} - void ResetToDefault() override - { - SetValue(m_Default); - } +bool SColorConfigVariable::IsDefault() const +{ + return *m_pVariable == m_Default; +} - void ResetToOld() override - { - *m_pVariable = m_OldValue; - } -}; +void SColorConfigVariable::Serialize(char *pOut, size_t Size, unsigned Value) const +{ + str_format(pOut, Size, "%s %u", m_pScriptName, Value); +} -struct SStringConfigVariable : public SConfigVariable +void SColorConfigVariable::Serialize(char *pOut, size_t Size) const { - char *m_pStr; - const char *m_pDefault; - size_t m_MaxSize; - char *m_pOldValue; + Serialize(pOut, Size, *m_pVariable); +} - SStringConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, char *pStr, const char *pDefault, size_t MaxSize, char *pOldValue) : - SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), - m_pStr(pStr), - m_pDefault(pDefault), - m_MaxSize(MaxSize), - m_pOldValue(pOldValue) - { - str_copy(m_pStr, m_pDefault, m_MaxSize); - str_copy(m_pOldValue, m_pDefault, m_MaxSize); - } +void SColorConfigVariable::SetValue(unsigned Value) +{ + if(CheckReadOnly()) + return; + char aBuf[IConsole::CMDLINE_LENGTH]; + Serialize(aBuf, sizeof(aBuf), Value); + ExecuteLine(aBuf); +} - ~SStringConfigVariable() override = default; +void SColorConfigVariable::ResetToDefault() +{ + SetValue(m_Default); +} - static void CommandCallback(IConsole::IResult *pResult, void *pUserData) - { - SStringConfigVariable *pData = static_cast(pUserData); +void SColorConfigVariable::ResetToOld() +{ + *m_pVariable = m_OldValue; +} - if(pResult->NumArguments()) - { - if(pData->CheckReadOnly()) - return; +// ----- - const char *pString = pResult->GetString(0); - if(!str_utf8_check(pString)) - { - char aTemp[4]; - size_t Length = 0; - while(*pString) - { - size_t Size = str_utf8_encode(aTemp, static_cast(*pString++)); - if(Length + Size < pData->m_MaxSize) - { - mem_copy(pData->m_pStr + Length, aTemp, Size); - Length += Size; - } - else - break; - } - pData->m_pStr[Length] = '\0'; - } - else - str_copy(pData->m_pStr, pString, pData->m_MaxSize); +SStringConfigVariable::SStringConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, char *pStr, const char *pDefault, size_t MaxSize, char *pOldValue) : + SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), + m_pStr(pStr), + m_pDefault(pDefault), + m_MaxSize(MaxSize), + m_pOldValue(pOldValue) +{ + str_copy(m_pStr, m_pDefault, m_MaxSize); + str_copy(m_pOldValue, m_pDefault, m_MaxSize); +} - if(pResult->m_ClientID != IConsole::CLIENT_ID_GAME) - str_copy(pData->m_pOldValue, pData->m_pStr, pData->m_MaxSize); - } - else - { - char aBuf[1024]; - str_format(aBuf, sizeof(aBuf), "Value: %s", pData->m_pStr); - pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); - } - } +void SStringConfigVariable::CommandCallback(IConsole::IResult *pResult, void *pUserData) +{ + SStringConfigVariable *pData = static_cast(pUserData); - void Register() override + if(pResult->NumArguments()) { - m_pConsole->Register(m_pScriptName, "?r", m_Flags, CommandCallback, this, m_pHelp); - } + if(pData->CheckReadOnly()) + return; - bool IsDefault() const override - { - return str_comp(m_pStr, m_pDefault) == 0; - } + const char *pString = pResult->GetString(0); + str_copy(pData->m_pStr, pString, pData->m_MaxSize); - void Serialize(char *pOut, size_t Size, const char *pValue) const - { - str_copy(pOut, m_pScriptName, Size); - str_append(pOut, " \"", Size); - const int OutLen = str_length(pOut); - EscapeParam(pOut + OutLen, pValue, Size - OutLen - 1); // -1 to ensure space for final quote - str_append(pOut, "\"", Size); + if(pResult->m_ClientId != IConsole::CLIENT_ID_GAME) + str_copy(pData->m_pOldValue, pData->m_pStr, pData->m_MaxSize); } - - void Serialize(char *pOut, size_t Size) const override + else { - Serialize(pOut, Size, m_pStr); + char aBuf[1024]; + str_format(aBuf, sizeof(aBuf), "Value: %s", pData->m_pStr); + pData->m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "config", aBuf); } +} - void SetValue(const char *pValue) - { - if(CheckReadOnly()) - return; - char aBuf[2048]; - Serialize(aBuf, sizeof(aBuf), pValue); - ExecuteLine(aBuf); - } +void SStringConfigVariable::Register() +{ + m_pConsole->Register(m_pScriptName, "?r", m_Flags, CommandCallback, this, m_pHelp); +} - void ResetToDefault() override - { - SetValue(m_pDefault); - } +bool SStringConfigVariable::IsDefault() const +{ + return str_comp(m_pStr, m_pDefault) == 0; +} - void ResetToOld() override - { - str_copy(m_pStr, m_pOldValue, m_MaxSize); - } -}; +void SStringConfigVariable::Serialize(char *pOut, size_t Size, const char *pValue) const +{ + str_copy(pOut, m_pScriptName, Size); + str_append(pOut, " \"", Size); + const int OutLen = str_length(pOut); + EscapeParam(pOut + OutLen, pValue, Size - OutLen - 1); // -1 to ensure space for final quote + str_append(pOut, "\"", Size); +} + +void SStringConfigVariable::Serialize(char *pOut, size_t Size) const +{ + Serialize(pOut, Size, m_pStr); +} + +void SStringConfigVariable::SetValue(const char *pValue) +{ + if(CheckReadOnly()) + return; + char aBuf[2048]; + Serialize(aBuf, sizeof(aBuf), pValue); + ExecuteLine(aBuf); +} + +void SStringConfigVariable::ResetToDefault() +{ + SetValue(m_pDefault); +} +void SStringConfigVariable::ResetToOld() +{ + str_copy(m_pStr, m_pOldValue, m_MaxSize); +} + +// ----------------------- Config Manager CConfigManager::CConfigManager() { m_pConsole = nullptr; @@ -388,7 +294,7 @@ void CConfigManager::Init() #define MACRO_CONFIG_INT(Name, ScriptName, Def, Min, Max, Flags, Desc) \ { \ - const char *pHelp = Min == Max ? Desc " (default: " #Def ")" : Max == 0 ? Desc " (default: " #Def ", min: " #Min ")" : Desc " (default: " #Def ", min: " #Min ", max: " #Max ")"; \ + const char *pHelp = Min == Max ? Desc " (default: " #Def ")" : (Max == 0 ? Desc " (default: " #Def ", min: " #Min ")" : Desc " (default: " #Def ", min: " #Min ", max: " #Max ")"); \ AddVariable(m_ConfigHeap.Allocate(m_pConsole, #ScriptName, SConfigVariable::VAR_INT, Flags, pHelp, &g_Config.m_##Name, Def, Min, Max)); \ } @@ -496,19 +402,27 @@ bool CConfigManager::Save() WriteLine(pCommand); } + if(m_Failed) + { + log_error("config", "ERROR: writing to %s failed", aConfigFileTmp); + } + if(io_sync(m_ConfigFile) != 0) { m_Failed = true; + log_error("config", "ERROR: synchronizing %s failed", aConfigFileTmp); } if(io_close(m_ConfigFile) != 0) + { m_Failed = true; + log_error("config", "ERROR: closing %s failed", aConfigFileTmp); + } m_ConfigFile = 0; if(m_Failed) { - log_error("config", "ERROR: writing to %s failed", aConfigFileTmp); return false; } @@ -518,6 +432,7 @@ bool CConfigManager::Save() return false; } + log_info("config", "saved to " CONFIG_FILE); return true; } @@ -541,6 +456,20 @@ void CConfigManager::StoreUnknownCommand(const char *pCommand) m_vpUnknownCommands.push_back(m_ConfigHeap.StoreString(pCommand)); } +void CConfigManager::PossibleConfigVariables(const char *pStr, int FlagMask, POSSIBLECFGFUNC pfnCallback, void *pUserData) +{ + for(const SConfigVariable *pVariable : m_vpAllVariables) + { + if(pVariable->m_Flags & FlagMask) + { + if(str_find_nocase(pVariable->m_pScriptName, pStr)) + { + pfnCallback(pVariable, pUserData); + } + } + } +} + void CConfigManager::Con_Reset(IConsole::IResult *pResult, void *pUserData) { static_cast(pUserData)->Reset(pResult->GetString(0)); @@ -563,19 +492,21 @@ void CConfigManager::Con_Toggle(IConsole::IResult *pResult, void *pUserData) if(pVariable->m_Type == SConfigVariable::VAR_INT) { SIntConfigVariable *pIntVariable = static_cast(pVariable); - pIntVariable->SetValue(*pIntVariable->m_pVariable == pResult->GetInteger(1) ? pResult->GetInteger(2) : pResult->GetInteger(1)); + const bool EqualToFirst = *pIntVariable->m_pVariable == pResult->GetInteger(1); + pIntVariable->SetValue(pResult->GetInteger(EqualToFirst ? 2 : 1)); } else if(pVariable->m_Type == SConfigVariable::VAR_COLOR) { SColorConfigVariable *pColorVariable = static_cast(pVariable); - const float Darkest = pColorVariable->m_Light ? 0.5f : 0.0f; - const ColorHSLA Value = *pColorVariable->m_pVariable == pResult->GetColor(1, pColorVariable->m_Light).Pack(Darkest, pColorVariable->m_Alpha) ? pResult->GetColor(2, pColorVariable->m_Light) : pResult->GetColor(1, pColorVariable->m_Light); - pColorVariable->SetValue(Value.Pack(Darkest, pColorVariable->m_Alpha)); + const bool EqualToFirst = *pColorVariable->m_pVariable == pResult->GetColor(1, pColorVariable->m_DarkestLighting).value_or(ColorHSLA(0, 0, 0)).Pack(pColorVariable->m_DarkestLighting, pColorVariable->m_Alpha); + const std::optional Value = pResult->GetColor(EqualToFirst ? 2 : 1, pColorVariable->m_DarkestLighting); + pColorVariable->SetValue(Value.value_or(ColorHSLA(0, 0, 0)).Pack(pColorVariable->m_DarkestLighting, pColorVariable->m_Alpha)); } else if(pVariable->m_Type == SConfigVariable::VAR_STRING) { SStringConfigVariable *pStringVariable = static_cast(pVariable); - pStringVariable->SetValue(str_comp(pStringVariable->m_pStr, pResult->GetString(1)) == 0 ? pResult->GetString(2) : pResult->GetString(1)); + const bool EqualToFirst = str_comp(pStringVariable->m_pStr, pResult->GetString(1)) == 0; + pStringVariable->SetValue(pResult->GetString(EqualToFirst ? 2 : 1)); } return; } diff --git a/src/engine/shared/config.h b/src/engine/shared/config.h index de3c54e47d..431ca02728 100644 --- a/src/engine/shared/config.h +++ b/src/engine/shared/config.h @@ -18,7 +18,7 @@ #define AUTOEXEC_FILE "autoexec.cfg" #define AUTOEXEC_CLIENT_FILE "autoexec_client.cfg" #define AUTOEXEC_SERVER_FILE "autoexec_server.cfg" -#define TCONFIG_FILE "settings_sta.cfg" + class CConfig { public: @@ -32,7 +32,6 @@ class CConfig static constexpr const char *ms_p##Name = Def; \ char m_##Name[Len]; // Flawfinder: ignore #include "config_variables.h" - #undef MACRO_CONFIG_INT #undef MACRO_CONFIG_COL #undef MACRO_CONFIG_STR @@ -55,8 +54,142 @@ enum CFGFLAG_GAME = 1 << 8, CFGFLAG_NONTEEHISTORIC = 1 << 9, CFGFLAG_COLLIGHT = 1 << 10, - CFGFLAG_COLALPHA = 1 << 11, - CFGFLAG_INSENSITIVE = 1 << 12, + CFGFLAG_COLLIGHT7 = 1 << 11, + CFGFLAG_COLALPHA = 1 << 12, + CFGFLAG_INSENSITIVE = 1 << 13, + CMDFLAG_PRACTICE = 1 << 14, +}; + +struct SConfigVariable +{ + enum EVariableType + { + VAR_INT, + VAR_COLOR, + VAR_STRING, + }; + IConsole *m_pConsole; + const char *m_pScriptName; + EVariableType m_Type; + int m_Flags; + const char *m_pHelp; + // Note that this only applies to the console command and the SetValue function, + // but the underlying config variable can still be modified programatically. + bool m_ReadOnly = false; + + SConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp) : + m_pConsole(pConsole), + m_pScriptName(pScriptName), + m_Type(Type), + m_Flags(Flags), + m_pHelp(pHelp) + { + } + + virtual ~SConfigVariable() = default; + + virtual void Register() = 0; + virtual bool IsDefault() const = 0; + virtual void Serialize(char *pOut, size_t Size) const = 0; + virtual void ResetToDefault() = 0; + virtual void ResetToOld() = 0; + +protected: + void ExecuteLine(const char *pLine) const; + bool CheckReadOnly() const; +}; + +struct SIntConfigVariable : public SConfigVariable +{ + int *m_pVariable; + int m_Default; + int m_Min; + int m_Max; + int m_OldValue; + + SIntConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, int *pVariable, int Default, int Min, int Max) : + SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), + m_pVariable(pVariable), + m_Default(Default), + m_Min(Min), + m_Max(Max), + m_OldValue(Default) + { + *m_pVariable = m_Default; + } + + ~SIntConfigVariable() override = default; + + static void CommandCallback(IConsole::IResult *pResult, void *pUserData); + void Register() override; + bool IsDefault() const override; + void Serialize(char *pOut, size_t Size, int Value) const; + void Serialize(char *pOut, size_t Size) const override; + void SetValue(int Value); + void ResetToDefault() override; + void ResetToOld() override; +}; + +struct SColorConfigVariable : public SConfigVariable +{ + unsigned *m_pVariable; + unsigned m_Default; + float m_DarkestLighting; + bool m_Alpha; + unsigned m_OldValue; + + SColorConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, unsigned *pVariable, unsigned Default) : + SConfigVariable(pConsole, pScriptName, Type, Flags, pHelp), + m_pVariable(pVariable), + m_Default(Default), + m_Alpha(Flags & CFGFLAG_COLALPHA), + m_OldValue(Default) + { + *m_pVariable = m_Default; + if(Flags & CFGFLAG_COLLIGHT) + { + m_DarkestLighting = ColorHSLA::DARKEST_LGT; + } + else if(Flags & CFGFLAG_COLLIGHT7) + { + m_DarkestLighting = ColorHSLA::DARKEST_LGT7; + } + else + { + m_DarkestLighting = 0.0f; + } + } + + ~SColorConfigVariable() override = default; + + static void CommandCallback(IConsole::IResult *pResult, void *pUserData); + void Register() override; + bool IsDefault() const override; + void Serialize(char *pOut, size_t Size, unsigned Value) const; + void Serialize(char *pOut, size_t Size) const override; + void SetValue(unsigned Value); + void ResetToDefault() override; + void ResetToOld() override; +}; + +struct SStringConfigVariable : public SConfigVariable +{ + char *m_pStr; + const char *m_pDefault; + size_t m_MaxSize; + char *m_pOldValue; + + SStringConfigVariable(IConsole *pConsole, const char *pScriptName, EVariableType Type, int Flags, const char *pHelp, char *pStr, const char *pDefault, size_t MaxSize, char *pOldValue); + ~SStringConfigVariable() override = default; + + static void CommandCallback(IConsole::IResult *pResult, void *pUserData); + void Register() override; + bool IsDefault() const override; + void Serialize(char *pOut, size_t Size, const char *pValue) const; + void Serialize(char *pOut, size_t Size) const override; + void SetValue(const char *pValue); + void ResetToDefault() override; + void ResetToOld() override; }; class CConfigManager : public IConfigManager @@ -80,8 +213,8 @@ class CConfigManager : public IConfigManager }; std::vector m_vCallbacks; - std::vector m_vpAllVariables; - std::vector m_vpGameVariables; + std::vector m_vpAllVariables; + std::vector m_vpGameVariables; std::vector m_vpUnknownCommands; CHeap m_ConfigHeap; @@ -104,6 +237,8 @@ class CConfigManager : public IConfigManager void WriteLine(const char *pLine) override; void StoreUnknownCommand(const char *pCommand) override; + + void PossibleConfigVariables(const char *pStr, int FlagMask, POSSIBLECFGFUNC pfnCallback, void *pUserData) override; }; #endif diff --git a/src/engine/shared/config.rs b/src/engine/shared/config.rs index ca42303ee6..968f18025e 100644 --- a/src/engine/shared/config.rs +++ b/src/engine/shared/config.rs @@ -45,13 +45,21 @@ pub const CFGFLAG_NONTEEHISTORIC: i32 = 1 << 9; /// Has no effect on other commands or config variables. pub const CFGFLAG_COLLIGHT: i32 = 1 << 10; +/// Color config variable that can only have lightness (61/255) to 1.0. +/// +/// This is achieved by dividing the lightness channel by and adding (61/255), i.e. +/// remapping all the colors. +/// +/// Has no effect on other commands or config variables. +pub const CFGFLAG_COLLIGHT7: i32 = 1 << 11; + /// Color config variable that includes an alpha (opacity) value. /// /// Has no effect on other commands or config variables. -pub const CFGFLAG_COLALPHA: i32 = 1 << 11; +pub const CFGFLAG_COLALPHA: i32 = 1 << 12; /// Config variable with insensitive data that can be included in client /// integrity checks. /// /// This should only be set on config variables the server could observe anyway. -pub const CFGFLAG_INSENSITIVE: i32 = 1 << 12; +pub const CFGFLAG_INSENSITIVE: i32 = 1 << 13; diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index b73e4b9287..45859dec3c 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -30,7 +30,7 @@ MACRO_CONFIG_INT(ClNameplatesTeamcolors, cl_nameplates_teamcolors, 1, 0, 1, CFGF MACRO_CONFIG_INT(ClNameplatesSize, cl_nameplates_size, 50, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Size of the name plates from 0 to 100%") MACRO_CONFIG_INT(ClNameplatesClan, cl_nameplates_clan, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show clan in name plates") MACRO_CONFIG_INT(ClNameplatesClanSize, cl_nameplates_clan_size, 30, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Size of the clan plates from 0 to 100%") -MACRO_CONFIG_INT(ClNameplatesIDs, cl_nameplates_ids, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show IDs in name plates") +MACRO_CONFIG_INT(ClNameplatesIds, cl_nameplates_ids, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show IDs in name plates") MACRO_CONFIG_INT(ClNameplatesOwn, cl_nameplates_own, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show own name plate (useful for demo recording)") MACRO_CONFIG_INT(ClNameplatesFriendMark, cl_nameplates_friendmark, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show friend mark (♥) in name plates") MACRO_CONFIG_INT(ClNameplatesStrong, cl_nameplates_strong, 0, 0, 2, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show strong/weak in name plates (0 - off, 1 - icons, 2 - icons + numbers)") @@ -49,6 +49,7 @@ MACRO_CONFIG_INT(ClShowhud, cl_showhud, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, MACRO_CONFIG_INT(ClShowhudHealthAmmo, cl_showhud_healthammo, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame HUD (Health + Ammo)") MACRO_CONFIG_INT(ClShowhudScore, cl_showhud_score, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame HUD (Score)") MACRO_CONFIG_INT(ClShowhudTimer, cl_showhud_timer, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame HUD (Timer)") +MACRO_CONFIG_INT(ClShowhudTimeCpDiff, cl_showhud_time_cp_diff, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame HUD (Time Checkpoint Difference)") MACRO_CONFIG_INT(ClShowhudDummyActions, cl_showhud_dummy_actions, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame HUD (Dummy Actions)") MACRO_CONFIG_INT(ClShowhudPlayerPosition, cl_showhud_player_position, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame HUD (Player Position)") MACRO_CONFIG_INT(ClShowhudPlayerSpeed, cl_showhud_player_speed, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame HUD (Player Speed)") @@ -62,6 +63,7 @@ MACRO_CONFIG_INT(ClShowNotifications, cl_shownotifications, 1, 0, 1, CFGFLAG_CLI MACRO_CONFIG_INT(ClShowEmotes, cl_showemotes, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show tee emotes") MACRO_CONFIG_INT(ClShowChat, cl_showchat, 1, 0, 2, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show chat (2 to always show large chat area)") MACRO_CONFIG_INT(ClShowChatFriends, cl_show_chat_friends, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show only chat messages from friends") +MACRO_CONFIG_INT(ClShowChatTeamMembersOnly, cl_show_chat_team_members_only, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show only chat messages from team members") MACRO_CONFIG_INT(ClShowChatSystem, cl_show_chat_system, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show chat messages from the server") MACRO_CONFIG_INT(ClShowKillMessages, cl_showkillmessages, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show kill messages") MACRO_CONFIG_INT(ClShowFinishMessages, cl_show_finish_messages, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show finish messages") @@ -71,6 +73,7 @@ MACRO_CONFIG_INT(ClShowfps, cl_showfps, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, MACRO_CONFIG_INT(ClShowpred, cl_showpred, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ingame prediction time in milliseconds") MACRO_CONFIG_INT(ClEyeWheel, cl_eye_wheel, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show eye wheel along together with emotes") MACRO_CONFIG_INT(ClEyeDuration, cl_eye_duration, 999999, 1, 999999, CFGFLAG_CLIENT | CFGFLAG_SAVE, "How long the eyes emotes last") +MACRO_CONFIG_INT(ClFreezeStars, cl_freeze_stars, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show old star particles for frozen tees") MACRO_CONFIG_INT(ClAirjumpindicator, cl_airjumpindicator, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show the air jump indicator") MACRO_CONFIG_INT(ClThreadsoundloading, cl_threadsoundloading, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Load sound files threaded") @@ -95,6 +98,9 @@ MACRO_CONFIG_INT(ClDyncamStabilizing, cl_dyncam_stabilizing, 0, 0, 100, CFGFLAG_ MACRO_CONFIG_INT(ClMultiViewSensitivity, cl_multiview_sensitivity, 100, 0, 200, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "Set how fast the camera will move to the desired location (higher = faster)") MACRO_CONFIG_INT(ClMultiViewZoomSmoothness, cl_multiview_zoom_smoothness, 1300, 50, 5000, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "Set the smoothness of the multi-view zoom (in ms, higher = slower)") +MACRO_CONFIG_INT(ClSpectatorMouseclicks, cl_spectator_mouseclicks, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enables left-click to toggle between spectating the closest player and free-view") +MACRO_CONFIG_INT(ClSmoothSpectatingTime, cl_smooth_spectating_time, 300, 0, 5000, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Time of smooth camera switch animation when spectating in ms (0 for off)") + MACRO_CONFIG_INT(EdAutosaveInterval, ed_autosave_interval, 10, 0, 240, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interval in minutes at which a copy of the current editor map is automatically saved to the 'auto' folder (0 for off)") MACRO_CONFIG_INT(EdAutosaveMax, ed_autosave_max, 10, 0, 1000, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Maximum number of autosaves that are kept per map name (0 = no limit)") MACRO_CONFIG_INT(EdSmoothZoomTime, ed_smooth_zoom_time, 250, 0, 5000, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Time of smooth zoom animation in the editor in ms (0 for off)") @@ -103,6 +109,8 @@ MACRO_CONFIG_INT(EdZoomTarget, ed_zoom_target, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG MACRO_CONFIG_INT(EdShowkeys, ed_showkeys, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show pressed keys") MACRO_CONFIG_INT(EdAlignQuads, ed_align_quads, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable/disable quad alignment. When enabled, red lines appear to show how quad/points are aligned and snapped to other quads/points when moving them") MACRO_CONFIG_INT(EdShowQuadsRect, ed_show_quads_rect, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show the bounds of the selected quad. In case of multiple quads, it shows the bounds of the englobing rect. Can be helpful when aligning a group of quads") +MACRO_CONFIG_INT(EdAutoMapReload, ed_auto_map_reload, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Run 'hot_reload' on the local server while rcon authed on map save") +MACRO_CONFIG_INT(EdLayerSelector, ed_layer_selector, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Ctrl+right click tiles to select their layers in the editor") MACRO_CONFIG_INT(ClShowWelcome, cl_show_welcome, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show welcome message indicating the first launch of the client") MACRO_CONFIG_INT(ClMotdTime, cl_motd_time, 10, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "How long to show the server message of the day") @@ -134,7 +142,49 @@ MACRO_CONFIG_INT(ClPlayerDefaultEyes, player_default_eyes, 0, 0, 5, CFGFLAG_CLIE MACRO_CONFIG_STR(ClSkinPrefix, cl_skin_prefix, 12, "", CFGFLAG_CLIENT | CFGFLAG_SAVE, "Replace the skins by skins with this prefix (e.g. kitty, santa)") MACRO_CONFIG_INT(ClFatSkins, cl_fat_skins, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable fat skins") -MACRO_CONFIG_INT(UiPage, ui_page, 6, 6, 10, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interface page") +MACRO_CONFIG_COL(ClPlayer7ColorBody, player7_color_body, 0x1B6F74, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_INSENSITIVE, "Player body color") +MACRO_CONFIG_COL(ClPlayer7ColorFeet, player7_color_feet, 0x1C873E, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_INSENSITIVE, "Player feet color") + +MACRO_CONFIG_COL(ClPlayer7ColorMarking, player7_color_marking, 0xFF0000FF, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_COLALPHA | CFGFLAG_INSENSITIVE, "Player marking color") +MACRO_CONFIG_COL(ClPlayer7ColorDecoration, player7_color_decoration, 0x1B6F74, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_INSENSITIVE, "Player decoration color") +MACRO_CONFIG_COL(ClPlayer7ColorHands, player7_color_hands, 0x1B759E, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_INSENSITIVE, "Player hands color") +MACRO_CONFIG_COL(ClPlayer7ColorEyes, player7_color_eyes, 0x0000FF, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_INSENSITIVE, "Player eyes color") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorBody, player7_use_custom_color_body, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for body") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorMarking, player7_use_custom_color_marking, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for marking") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorDecoration, player7_use_custom_color_decoration, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for decoration") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorHands, player7_use_custom_color_hands, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for hands") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorFeet, player7_use_custom_color_feet, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for feet") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorEyes, player7_use_custom_color_eyes, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for eyes") +MACRO_CONFIG_STR(ClPlayer7Skin, player7_skin, protocol7::MAX_SKIN_ARRAY_SIZE, "default", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin") +MACRO_CONFIG_STR(ClPlayer7SkinBody, player7_skin_body, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin body") +MACRO_CONFIG_STR(ClPlayer7SkinMarking, player7_skin_marking, protocol7::MAX_SKIN_ARRAY_SIZE, "", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin marking") +MACRO_CONFIG_STR(ClPlayer7SkinDecoration, player7_skin_decoration, protocol7::MAX_SKIN_ARRAY_SIZE, "", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin decoration") +MACRO_CONFIG_STR(ClPlayer7SkinHands, player7_skin_hands, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin hands") +MACRO_CONFIG_STR(ClPlayer7SkinFeet, player7_skin_feet, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin feet") +MACRO_CONFIG_STR(ClPlayer7SkinEyes, player7_skin_eyes, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin eyes") + +MACRO_CONFIG_COL(ClDummy7ColorBody, dummy7_color_body, 0x1B6F74, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_INSENSITIVE, "Dummy body color") +MACRO_CONFIG_COL(ClDummy7ColorFeet, dummy7_color_feet, 0x1C873E, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_INSENSITIVE, "Dummy feet color") + +MACRO_CONFIG_COL(ClDummy7ColorMarking, dummy7_color_marking, 0xFF0000FF, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_COLALPHA | CFGFLAG_INSENSITIVE, "Dummy marking color") +MACRO_CONFIG_COL(ClDummy7ColorDecoration, dummy7_color_decoration, 0x1B6F74, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_INSENSITIVE, "Dummy decoration color") +MACRO_CONFIG_COL(ClDummy7ColorHands, dummy7_color_hands, 0x1B759E, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_INSENSITIVE, "Dummy hands color") +MACRO_CONFIG_COL(ClDummy7ColorEyes, dummy7_color_eyes, 0x0000FF, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT7 | CFGFLAG_INSENSITIVE, "Dummy eyes color") +MACRO_CONFIG_INT(ClDummy7UseCustomColorBody, dummy7_use_custom_color_body, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for body") +MACRO_CONFIG_INT(ClDummy7UseCustomColorMarking, dummy7_use_custom_color_marking, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for marking") +MACRO_CONFIG_INT(ClDummy7UseCustomColorDecoration, dummy7_use_custom_color_decoration, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for decoration") +MACRO_CONFIG_INT(ClDummy7UseCustomColorHands, dummy7_use_custom_color_hands, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for hands") +MACRO_CONFIG_INT(ClDummy7UseCustomColorFeet, dummy7_use_custom_color_feet, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for feet") +MACRO_CONFIG_INT(ClDummy7UseCustomColorEyes, dummy7_use_custom_color_eyes, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for eyes") +MACRO_CONFIG_STR(ClDummy7Skin, dummy7_skin, protocol7::MAX_SKIN_ARRAY_SIZE, "default", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin") +MACRO_CONFIG_STR(ClDummy7SkinBody, dummy7_skin_body, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin body") +MACRO_CONFIG_STR(ClDummy7SkinMarking, dummy7_skin_marking, protocol7::MAX_SKIN_ARRAY_SIZE, "", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin marking") +MACRO_CONFIG_STR(ClDummy7SkinDecoration, dummy7_skin_decoration, protocol7::MAX_SKIN_ARRAY_SIZE, "", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin decoration") +MACRO_CONFIG_STR(ClDummy7SkinHands, dummy7_skin_hands, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin hands") +MACRO_CONFIG_STR(ClDummy7SkinFeet, dummy7_skin_feet, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin feet") +MACRO_CONFIG_STR(ClDummy7SkinEyes, dummy7_skin_eyes, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin eyes") + +MACRO_CONFIG_INT(UiPage, ui_page, 6, 6, 13, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interface page") MACRO_CONFIG_INT(UiSettingsPage, ui_settings_page, 0, 0, 9, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interface settings page") MACRO_CONFIG_INT(UiToolboxPage, ui_toolbox_page, 0, 0, 2, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Toolbox page") MACRO_CONFIG_STR(UiServerAddress, ui_server_address, 1024, "localhost:8303", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Interface server address") @@ -160,15 +210,15 @@ MACRO_CONFIG_INT(ClDummyUseCustomColor, dummy_use_custom_color, 0, 0, 1, CFGFLAG MACRO_CONFIG_COL(ClDummyColorBody, dummy_color_body, 65408, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Dummy body color") MACRO_CONFIG_COL(ClDummyColorFeet, dummy_color_feet, 65408, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Dummy feet color") MACRO_CONFIG_STR(ClDummySkin, dummy_skin, 24, "default", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin") -MACRO_CONFIG_INT(ClDummyDefaultEyes, dummy_default_eyes, 0, 0, 5, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Dummy eyes when joining server. 0 = normal, 1 = pain, 2 = happy, 3 = surprise, 4 = angry, 5 = blink") -MACRO_CONFIG_INT(ClDummy, cl_dummy, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "0 - player / 1 - dummy") +MACRO_CONFIG_INT(ClDummyDefaultEyes, dummy_default_eyes, 0, 0, 5, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Dummy eyes when joining server (0 = normal, 1 = pain, 2 = happy, 3 = surprise, 4 = angry, 5 = blink)") +MACRO_CONFIG_INT(ClDummy, cl_dummy, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "Whether you control your player (0) or your dummy (1)") MACRO_CONFIG_INT(ClDummyHammer, cl_dummy_hammer, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "Whether dummy is hammering for a hammerfly") -MACRO_CONFIG_INT(ClDummyResetOnSwitch, cl_dummy_resetonswitch, 0, 0, 2, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Whether dummy or player should stop pressing keys when you switch. 0 = off, 1 = dummy, 2 = player") +MACRO_CONFIG_INT(ClDummyResetOnSwitch, cl_dummy_resetonswitch, 0, 0, 2, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Whether dummy or player should stop pressing keys when you switch (0 = off, 1 = dummy, 2 = player)") MACRO_CONFIG_INT(ClDummyRestoreWeapon, cl_dummy_restore_weapon, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Whether dummy should switch to last weapon after hammerfly") MACRO_CONFIG_INT(ClDummyCopyMoves, cl_dummy_copy_moves, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "Whether dummy should copy your moves") // more controllable dummy command -MACRO_CONFIG_INT(ClDummyControl, cl_dummy_control, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "Whether can you control dummy at the same time (cl_dummy_jump, cl_dummy_fire, cl_dummy_hook)") +MACRO_CONFIG_INT(ClDummyControl, cl_dummy_control, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "Whether you can control dummy at the same time (cl_dummy_jump, cl_dummy_fire, cl_dummy_hook)") MACRO_CONFIG_INT(ClDummyJump, cl_dummy_jump, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "Whether dummy is jumping (requires cl_dummy_control 1)") MACRO_CONFIG_INT(ClDummyFire, cl_dummy_fire, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "Whether dummy is firing (requires cl_dummy_control 1)") MACRO_CONFIG_INT(ClDummyHook, cl_dummy_hook, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_INSENSITIVE, "Whether dummy is hooking (requires cl_dummy_control 1)") @@ -182,7 +232,7 @@ MACRO_CONFIG_INT(SvWarmup, sv_warmup, 0, 0, 0, CFGFLAG_SERVER, "Number of second MACRO_CONFIG_STR(SvMotd, sv_motd, 900, "", CFGFLAG_SERVER, "Message of the day to display for the clients") MACRO_CONFIG_STR(SvGametype, sv_gametype, 32, "ddnet", CFGFLAG_SERVER, "Game type (ddnet, mod)") MACRO_CONFIG_INT(SvTournamentMode, sv_tournament_mode, 0, 0, 1, CFGFLAG_SERVER, "Tournament mode. When enabled, players joins the server as spectator") -MACRO_CONFIG_INT(SvSpamprotection, sv_spamprotection, 1, 0, 1, CFGFLAG_SERVER, "Spam protection") +MACRO_CONFIG_INT(SvSpamprotection, sv_spamprotection, 1, 0, 1, CFGFLAG_SERVER, "Spam protection for: team change, chat, skin change, emotes and votes") MACRO_CONFIG_INT(SvSpectatorSlots, sv_spectator_slots, 0, 0, MAX_CLIENTS, CFGFLAG_SERVER, "Number of slots to reserve for spectators") MACRO_CONFIG_INT(SvInactiveKickTime, sv_inactivekick_time, 0, 0, 1000, CFGFLAG_SERVER, "How many minutes to wait before taking care of inactive players") @@ -285,10 +335,8 @@ MACRO_CONFIG_INT(BrFilterGametypeStrict, br_filter_gametype_strict, 0, 0, 1, CFG MACRO_CONFIG_INT(BrFilterConnectingPlayers, br_filter_connecting_players, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter connecting players") MACRO_CONFIG_STR(BrFilterServerAddress, br_filter_serveraddress, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Server address to filter") MACRO_CONFIG_INT(BrFilterUnfinishedMap, br_filter_unfinished_map, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Show only servers with unfinished maps") +MACRO_CONFIG_INT(BrFilterLogin, br_filter_login, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out servers that require login") -MACRO_CONFIG_STR(BrFilterExcludeCommunities, br_filter_exclude_communities, 512, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out servers by community") -MACRO_CONFIG_STR(BrFilterExcludeCountries, br_filter_exclude_countries, 512, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out communities' servers by country") -MACRO_CONFIG_STR(BrFilterExcludeTypes, br_filter_exclude_types, 512, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Filter out communities' servers by gametype") MACRO_CONFIG_INT(BrIndicateFinished, br_indicate_finished, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Show whether you have finished a DDNet map (transmits your player name to info.ddnet.org/info)") MACRO_CONFIG_STR(BrLocation, br_location, 16, "auto", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Override location for ping estimation, available: auto, af, as, as:cn, eu, na, oc, sa (Automatic, Africa, Asia, China, Europe, North America, Oceania/Australia, South America") MACRO_CONFIG_STR(BrCachedBestServerinfoUrl, br_cached_best_serverinfo_url, 256, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Do not set this variable, instead create a ddnet-serverlist-urls.cfg next to settings_ddnet.cfg to specify all possible serverlist URLs") @@ -301,6 +349,9 @@ MACRO_CONFIG_INT(BrDemoSort, br_demo_sort, 0, 0, 2, CFGFLAG_SAVE | CFGFLAG_CLIEN MACRO_CONFIG_INT(BrDemoSortOrder, br_demo_sort_order, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Sorting order in demo browser") MACRO_CONFIG_INT(BrDemoFetchInfo, br_demo_fetch_info, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Whether to auto fetch demo infos on refresh") +MACRO_CONFIG_INT(GhSort, gh_sort, 1, 0, 2, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Sorting column in ghost list") +MACRO_CONFIG_INT(GhSortOrder, gh_sort_order, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Sorting order in ghost list") + MACRO_CONFIG_INT(SndBufferSize, snd_buffer_size, 512, 128, 32768, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Sound buffer size (may cause delay if large)") MACRO_CONFIG_INT(SndRate, snd_rate, 48000, 5512, 384000, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Sound mixing rate") MACRO_CONFIG_INT(SndEnable, snd_enable, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Sound enable") @@ -372,11 +423,11 @@ MACRO_CONFIG_INT(ClContactPort, cl_contact_port, 0, 0, 65535, CFGFLAG_SAVE | CFG MACRO_CONFIG_STR(SvName, sv_name, 128, "unnamed server", CFGFLAG_SERVER, "Server name") MACRO_CONFIG_STR(Bindaddr, bindaddr, 128, "", CFGFLAG_CLIENT | CFGFLAG_SERVER | CFGFLAG_MASTER, "Address to bind the client/server to") MACRO_CONFIG_INT(SvIpv4Only, sv_ipv4only, 0, 0, 1, CFGFLAG_SERVER, "Whether to bind only to ipv4, otherwise bind to all available interfaces") -MACRO_CONFIG_INT(SvPort, sv_port, 0, 0, 0, CFGFLAG_SERVER, "Port to use for the server (Only ports 8303-8310 work in LAN server browser, 0 to automatically find a free port in 8303-8310)") +MACRO_CONFIG_INT(SvPort, sv_port, 0, 0, 65535, CFGFLAG_SERVER, "Port to use for the server (Only ports 8303-8310 work in LAN server browser, 0 to automatically find a free port in 8303-8310)") MACRO_CONFIG_STR(SvHostname, sv_hostname, 128, "", CFGFLAG_SERVER, "Server hostname (0.7 only)") MACRO_CONFIG_STR(SvMap, sv_map, 128, "Sunny Side Up", CFGFLAG_SERVER, "Map to use on the server") MACRO_CONFIG_INT(SvMaxClients, sv_max_clients, MAX_CLIENTS, 1, MAX_CLIENTS, CFGFLAG_SERVER, "Maximum number of clients that are allowed on a server") -MACRO_CONFIG_INT(SvMaxClientsPerIP, sv_max_clients_per_ip, 4, 1, MAX_CLIENTS, CFGFLAG_SERVER, "Maximum number of clients with the same IP that can connect to the server") +MACRO_CONFIG_INT(SvMaxClientsPerIp, sv_max_clients_per_ip, 4, 1, MAX_CLIENTS, CFGFLAG_SERVER, "Maximum number of clients with the same IP that can connect to the server") MACRO_CONFIG_INT(SvHighBandwidth, sv_high_bandwidth, 0, 0, 1, CFGFLAG_SERVER, "Use high bandwidth mode. Doubles the bandwidth required for the server. LAN use only") MACRO_CONFIG_STR(SvRegister, sv_register, 16, "1", CFGFLAG_SERVER, "Register server with master server for public listing, can also accept a comma-separated list of protocols to register on, like 'ipv4,ipv6'") MACRO_CONFIG_STR(SvRegisterExtra, sv_register_extra, 256, "", CFGFLAG_SERVER, "Extra headers to send to the register endpoint, comma separated 'Header: Value' pairs") @@ -395,6 +446,7 @@ MACRO_CONFIG_STR(SvDnsblHost, sv_dnsbl_host, 128, "", CFGFLAG_SERVER, "Hostname MACRO_CONFIG_STR(SvDnsblKey, sv_dnsbl_key, 128, "", CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, "Optional Authentication Key for the specified DNSBL provider") MACRO_CONFIG_INT(SvDnsblVote, sv_dnsbl_vote, 0, 0, 1, CFGFLAG_SERVER, "Block votes by blacklisted addresses") MACRO_CONFIG_INT(SvDnsblBan, sv_dnsbl_ban, 0, 0, 1, CFGFLAG_SERVER, "Automatically ban blacklisted addresses") +MACRO_CONFIG_STR(SvDnsblBanReason, sv_dnsbl_ban_reason, 128, "VPN detected, try connecting without. Contact admin if mistaken", CFGFLAG_SERVER, "Ban reason for 'sv_dnsbl_ban'") MACRO_CONFIG_INT(SvDnsblChat, sv_dnsbl_chat, 0, 0, 1, CFGFLAG_SERVER, "Don't allow chat from blacklisted addresses") MACRO_CONFIG_INT(SvRconVote, sv_rcon_vote, 0, 0, 1, CFGFLAG_SERVER, "Only allow authed clients to call votes") @@ -406,8 +458,8 @@ MACRO_CONFIG_INT(SvSixup, sv_sixup, 1, 0, 1, CFGFLAG_SERVER, "Enable sixup conne MACRO_CONFIG_INT(SvSkillLevel, sv_skill_level, 1, SERVERINFO_LEVEL_MIN, SERVERINFO_LEVEL_MAX, CFGFLAG_SERVER, "Difficulty level for Teeworlds 0.7 (0: Casual, 1: Normal, 2: Competitive)") MACRO_CONFIG_STR(EcBindaddr, ec_bindaddr, 128, "localhost", CFGFLAG_ECON, "Address to bind the external console to. Anything but 'localhost' is dangerous") -MACRO_CONFIG_INT(EcPort, ec_port, 0, 0, 0, CFGFLAG_ECON, "Port to use for the external console") -MACRO_CONFIG_STR(EcPassword, ec_password, 128, "", CFGFLAG_ECON, "External console password") +MACRO_CONFIG_INT(EcPort, ec_port, 0, 0, 65535, CFGFLAG_ECON, "Port to use for the external console") +MACRO_CONFIG_STR(EcPassword, ec_password, 128, "", CFGFLAG_ECON, "External console password. This option is required to be set for econ to be enabled.") MACRO_CONFIG_INT(EcBantime, ec_bantime, 0, 0, 1440, CFGFLAG_ECON, "The time a client gets banned if econ authentication fails. 0 just closes the connection") MACRO_CONFIG_INT(EcAuthTimeout, ec_auth_timeout, 30, 1, 120, CFGFLAG_ECON, "Time in seconds before the the econ authentication times out") MACRO_CONFIG_INT(EcOutputLevel, ec_output_level, 0, -3, 2, CFGFLAG_ECON, "Adjusts the amount of information in the external console (-3 = none, -2 = error only, -1 = warn, 0 = info, 1 = debug, 2 = trace)") @@ -424,7 +476,7 @@ MACRO_CONFIG_STR(DbgStressServer, dbg_stress_server, 32, "localhost", CFGFLAG_CL MACRO_CONFIG_INT(HttpAllowInsecure, http_allow_insecure, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SERVER, "Allow insecure HTTP protocol in addition to the secure HTTPS one. Mostly useful for testing.") // DDRace -MACRO_CONFIG_STR(SvWelcome, sv_welcome, 64, "", CFGFLAG_SERVER, "Message that will be displayed to players who join the server") +MACRO_CONFIG_STR(SvWelcome, sv_welcome, 256, "", CFGFLAG_SERVER, "Message that will be displayed to players who join the server") MACRO_CONFIG_INT(SvReservedSlots, sv_reserved_slots, 0, 0, MAX_CLIENTS, CFGFLAG_SERVER, "The number of slots that are reserved for special players") MACRO_CONFIG_STR(SvReservedSlotsPass, sv_reserved_slots_pass, 256, "", CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, "The password that is required to use a reserved slot") MACRO_CONFIG_INT(SvReservedSlotsAuthLevel, sv_reserved_slots_auth_level, 1, 1, 4, CFGFLAG_SERVER, "Minimum rcon auth level needed to use a reserved slot. 4 = rcon auth disabled") @@ -452,13 +504,13 @@ MACRO_CONFIG_INT(SvEyeEmoteChangeDelay, sv_eye_emote_change_delay, 1, 0, 9999, C MACRO_CONFIG_INT(SvChatDelay, sv_chat_delay, 1, 0, 9999, CFGFLAG_SERVER, "The time in seconds between chat messages") MACRO_CONFIG_INT(SvTeamChangeDelay, sv_team_change_delay, 3, 0, 9999, CFGFLAG_SERVER, "The time in seconds between team changes (spectator/in game)") MACRO_CONFIG_INT(SvInfoChangeDelay, sv_info_change_delay, 5, 0, 9999, CFGFLAG_SERVER, "The time in seconds between info changes (name/skin/color), to avoid ranbow mod set this to a very high time") -MACRO_CONFIG_INT(SvVoteTime, sv_vote_time, 25, 1, 9999, CFGFLAG_SERVER, "The time in seconds a vote lasts") +MACRO_CONFIG_INT(SvVoteTime, sv_vote_time, 25, 1, 60, CFGFLAG_SERVER, "The time in seconds a vote lasts") MACRO_CONFIG_INT(SvVoteMapTimeDelay, sv_vote_map_delay, 0, 0, 9999, CFGFLAG_SERVER, "The minimum time in seconds between map votes") MACRO_CONFIG_INT(SvVoteDelay, sv_vote_delay, 3, 0, 9999, CFGFLAG_SERVER, "The time in seconds between any vote") MACRO_CONFIG_INT(SvVoteKickDelay, sv_vote_kick_delay, 0, 0, 9999, CFGFLAG_SERVER, "The minimum time in seconds between kick votes") -MACRO_CONFIG_INT(SvVoteYesPercentage, sv_vote_yes_percentage, 50, 1, 100, CFGFLAG_SERVER, "The percent of people that need to agree or deny for the vote to succeed/fail") -MACRO_CONFIG_INT(SvVoteMajority, sv_vote_majority, 0, 0, 1, CFGFLAG_SERVER, "Whether No. of Yes is compared to No. of No votes or to number of total Players ( Default is 0 Y compare N)") -MACRO_CONFIG_INT(SvVoteMaxTotal, sv_vote_max_total, 0, 0, MAX_CLIENTS, CFGFLAG_SERVER, "How many people can participate in a vote at max (0 = no limit by default)") +MACRO_CONFIG_INT(SvVoteYesPercentage, sv_vote_yes_percentage, 50, 1, 99, CFGFLAG_SERVER, "More than this percentage of players need to agree for a vote to succeed") +MACRO_CONFIG_INT(SvVoteMajority, sv_vote_majority, 0, 0, 1, CFGFLAG_SERVER, "Whether non-voting players are considered as votes for \"no\" (0) or are ignored (1)") +MACRO_CONFIG_INT(SvVoteMaxTotal, sv_vote_max_total, 0, 0, MAX_CLIENTS, CFGFLAG_SERVER, "How many players can participate in a vote at max (0 = no limit)") MACRO_CONFIG_INT(SvVoteVetoTime, sv_vote_veto_time, 20, 0, 1000, CFGFLAG_SERVER, "Minutes of time on a server until a player can veto map change votes (0 = disabled)") MACRO_CONFIG_INT(SvKillDelay, sv_kill_delay, 1, 0, 9999, CFGFLAG_SERVER, "The minimum time in seconds between kills") @@ -474,7 +526,8 @@ MACRO_CONFIG_INT(SvSaveSwapGamesDelay, sv_saveswapgames_delay, 30, 0, 10000, CFG MACRO_CONFIG_INT(SvSaveSwapGamesPenalty, sv_saveswapgames_penalty, 60, 0, 10000, CFGFLAG_SERVER, "Penalty in seconds for saving or swapping position") MACRO_CONFIG_INT(SvSwapTimeout, sv_swap_timeout, 180, 0, 10000, CFGFLAG_SERVER, "Timeout in seconds before option to swap expires") MACRO_CONFIG_INT(SvSwap, sv_swap, 1, 0, 1, CFGFLAG_SERVER, "Enable /swap") -MACRO_CONFIG_INT(SvUseSQL, sv_use_sql, 0, 0, 1, CFGFLAG_SERVER, "Enables MySQL backend instead of SQLite backend (sv_sqlite_file is still used as fallback write server when no MySQL server is reachable)") +MACRO_CONFIG_INT(SvTeam0Mode, sv_team0mode, 1, 0, 1, CFGFLAG_SERVER, "Enables /team0mode") +MACRO_CONFIG_INT(SvUseSql, sv_use_sql, 0, 0, 1, CFGFLAG_SERVER, "Enables MySQL backend instead of SQLite backend (sv_sqlite_file is still used as fallback write server when no MySQL server is reachable)") MACRO_CONFIG_INT(SvSqlQueriesDelay, sv_sql_queries_delay, 1, 0, 20, CFGFLAG_SERVER, "Delay in seconds between SQL queries of a single player") MACRO_CONFIG_STR(SvSqliteFile, sv_sqlite_file, 64, "ddnet-server.sqlite", CFGFLAG_SERVER, "File to store ranks in case sv_use_sql is turned off or used as backup sql server") @@ -499,8 +552,8 @@ MACRO_CONFIG_INT(SvMinTeamSize, sv_min_team_size, 2, 1, MAX_CLIENTS, CFGFLAG_SER MACRO_CONFIG_INT(SvMaxTeamSize, sv_max_team_size, MAX_CLIENTS, 1, MAX_CLIENTS, CFGFLAG_SERVER | CFGFLAG_GAME, "Maximum team size") MACRO_CONFIG_INT(SvMapVote, sv_map_vote, 1, 0, 1, CFGFLAG_SERVER, "Whether to allow /map") -MACRO_CONFIG_STR(SvAnnouncementFileName, sv_announcement_filename, 24, "announcement.txt", CFGFLAG_SERVER, "file which will have the announcement, each one at a line") -MACRO_CONFIG_INT(SvAnnouncementInterval, sv_announcement_interval, 300, 1, 9999, CFGFLAG_SERVER, "time(minutes) in which the announcement will be displayed from the announcement file") +MACRO_CONFIG_STR(SvAnnouncementFileName, sv_announcement_filename, IO_MAX_PATH_LENGTH, "announcement.txt", CFGFLAG_SERVER, "File which contains the announcements, one on each line") +MACRO_CONFIG_INT(SvAnnouncementInterval, sv_announcement_interval, 120, 1, 9999, CFGFLAG_SERVER, "The time (minutes) for how often an announcement will be displayed from the announcement file") MACRO_CONFIG_INT(SvAnnouncementRandom, sv_announcement_random, 1, 0, 1, CFGFLAG_SERVER, "Whether announcements are sequential or random") MACRO_CONFIG_INT(SvOldLaser, sv_old_laser, 0, 0, 1, CFGFLAG_SERVER | CFGFLAG_GAME, "Whether lasers can hit you if you shot them and that they pull you towards the bounce origin (0 for all new maps) or lasers can't hit you if you shot them, and they pull others towards the shooter") @@ -533,14 +586,13 @@ MACRO_CONFIG_COL(ClMessageFriendColor, cl_message_friend_color, 65425, CFGFLAG_C MACRO_CONFIG_INT(ConnTimeout, conn_timeout, 100, 5, 1000, CFGFLAG_SAVE | CFGFLAG_CLIENT | CFGFLAG_SERVER, "Network timeout") MACRO_CONFIG_INT(ConnTimeoutProtection, conn_timeout_protection, 1000, 5, 10000, CFGFLAG_SERVER, "Network timeout protection") -MACRO_CONFIG_INT(ClShowIDs, cl_show_ids, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Whether to show client ids in scoreboard") +MACRO_CONFIG_INT(ClShowIds, cl_show_ids, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Whether to show client IDs in scoreboard, chat and spectator menu") MACRO_CONFIG_INT(ClScoreboardOnDeath, cl_scoreboard_on_death, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Whether to show scoreboard after death or not") MACRO_CONFIG_INT(ClAutoRaceRecord, cl_auto_race_record, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Save the best demo of each race") MACRO_CONFIG_INT(ClReplays, cl_replays, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable/disable replays") MACRO_CONFIG_INT(ClReplayLength, cl_replay_length, 30, 10, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Set the default length of the replays") MACRO_CONFIG_INT(ClRaceRecordServerControl, cl_race_record_server_control, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Let the server start the race recorder") MACRO_CONFIG_INT(ClDemoName, cl_demo_name, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Save the player name within the demo") -MACRO_CONFIG_INT(ClDemoAssumeRace, cl_demo_assume_race, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Assume that demos are race demos") MACRO_CONFIG_INT(ClRaceGhost, cl_race_ghost, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable ghost") MACRO_CONFIG_INT(ClRaceGhostServerControl, cl_race_ghost_server_control, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Let the server start the ghost") MACRO_CONFIG_INT(ClRaceShowGhost, cl_race_show_ghost, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show ghost") @@ -549,7 +601,6 @@ MACRO_CONFIG_INT(ClRaceGhostStrictMap, cl_race_ghost_strict_map, 0, 0, 1, CFGFLA MACRO_CONFIG_INT(ClRaceGhostSaveBest, cl_race_ghost_save_best, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Save only ghosts that are better than the previous record.") MACRO_CONFIG_INT(ClRaceGhostAlpha, cl_race_ghost_alpha, 40, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Visbility of ghosts (alpha value, 0 invisible, 100 fully visible)") -MACRO_CONFIG_INT(ClDDRaceScoreBoard, cl_ddrace_scoreboard, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Enable DDRace Scoreboard") MACRO_CONFIG_INT(SvResetPickups, sv_reset_pickups, 0, 0, 1, CFGFLAG_SERVER | CFGFLAG_GAME, "Whether the weapons are reset on passing the start tile or not") MACRO_CONFIG_INT(ClShowOthers, cl_show_others, 0, 0, 2, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show players in other teams (2 to show own team only)") MACRO_CONFIG_INT(ClShowOthersAlpha, cl_show_others_alpha, 40, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show players in other teams (alpha value, 0 invisible, 100 fully visible)") @@ -557,7 +608,7 @@ MACRO_CONFIG_INT(ClOverlayEntities, cl_overlay_entities, 0, 0, 100, CFGFLAG_CLIE MACRO_CONFIG_INT(ClShowQuads, cl_showquads, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show quads (only interesting for mappers, or if your system has extremely bad performance)") MACRO_CONFIG_COL(ClBackgroundColor, cl_background_color, 128, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Background color") // 0 0 128 MACRO_CONFIG_COL(ClBackgroundEntitiesColor, cl_background_entities_color, 128, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Background (entities) color") // 0 0 128 -MACRO_CONFIG_STR(ClBackgroundEntities, cl_background_entities, 100, "", CFGFLAG_CLIENT | CFGFLAG_SAVE, "Background (entities)") +MACRO_CONFIG_STR(ClBackgroundEntities, cl_background_entities, IO_MAX_PATH_LENGTH, "", CFGFLAG_CLIENT | CFGFLAG_SAVE, "Background (entities)") MACRO_CONFIG_STR(ClRunOnJoin, cl_run_on_join, 100, "", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Command to run when joining a server") // menu background map @@ -587,7 +638,7 @@ MACRO_CONFIG_INT(SvDefaultTimerType, sv_default_timer_type, 0, 0, 3, CFGFLAG_SER // these might need some fine tuning MACRO_CONFIG_INT(SvChatInitialDelay, sv_chat_initial_delay, 0, 0, 360, CFGFLAG_SERVER, "The time in seconds before the first message can be sent") MACRO_CONFIG_INT(SvChatPenalty, sv_chat_penalty, 250, 50, 1000, CFGFLAG_SERVER, "chat score will be increased by this on every message, and decremented by 1 on every tick.") -MACRO_CONFIG_INT(SvChatThreshold, sv_chat_threshold, 1000, 50, 10000, CFGFLAG_SERVER, "if chats core exceeds this, the player will be muted for sv_spam_mute_duration seconds") +MACRO_CONFIG_INT(SvChatThreshold, sv_chat_threshold, 1000, 50, 10000, CFGFLAG_SERVER, "if chats score exceeds this, the player will be muted for sv_spam_mute_duration seconds") MACRO_CONFIG_INT(SvSpamMuteDuration, sv_spam_mute_duration, 60, 0, 3600, CFGFLAG_SERVER, "how many seconds to mute, if player triggers mute on spam. 0 = off") MACRO_CONFIG_INT(SvShutdownWhenEmpty, sv_shutdown_when_empty, 0, 0, 1, CFGFLAG_SERVER, "Shutdown server as soon as no one is on it anymore") @@ -622,9 +673,11 @@ MACRO_CONFIG_COL(ClHookCollColorNoColl, cl_hook_coll_color_no_coll, 65407, CFGFL MACRO_CONFIG_COL(ClHookCollColorHookableColl, cl_hook_coll_color_hookable_coll, 6401973, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Specifies the color of a hookline that hits hookable tiles.") MACRO_CONFIG_COL(ClHookCollColorTeeColl, cl_hook_coll_color_tee_coll, 2817919, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Specifies the color of a hookline that hits tees.") -MACRO_CONFIG_INT(ClChatTeamColors, cl_chat_teamcolors, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show names in chat in team colors") +MACRO_CONFIG_INT(ClChatTeamColors, cl_chat_teamcolors, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show names in chat in team colors") MACRO_CONFIG_INT(ClChatReset, cl_chat_reset, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Reset chat when pressing escape") MACRO_CONFIG_INT(ClChatOld, cl_chat_old, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Old chat style: No tee, no background") +MACRO_CONFIG_INT(ClChatFontSize, cl_chat_size, 60, 10, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Chat font size") +MACRO_CONFIG_INT(ClChatWidth, cl_chat_width, 200, 140, 400, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Chat width") MACRO_CONFIG_INT(ClShowDirection, cl_show_direction, 1, 0, 3, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Show key presses (1 = other players', 2 = also your own, 3 = only your own") MACRO_CONFIG_INT(ClOldGunPosition, cl_old_gun_position, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Tees hold gun a bit higher like in TW 0.6.1 and older") @@ -665,91 +718,20 @@ MACRO_CONFIG_INT(Gfx3DTextureAnalysisRan, gfx_3d_texture_analysis_ran, 0, 0, 1, MACRO_CONFIG_STR(Gfx3DTextureAnalysisRenderer, gfx_3d_texture_analysis_renderer, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The renderer on which the analysis was performed") MACRO_CONFIG_STR(Gfx3DTextureAnalysisVersion, gfx_3d_texture_analysis_version, 128, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The version on which the analysis was performed") -MACRO_CONFIG_STR(GfxGPUName, gfx_gpu_name, 256, "auto", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The GPU's name, which will be selected by the backend. (if supported by the backend)") -#if !defined(CONF_ARCH_IA32) && !defined(CONF_PLATFORM_MACOS) +MACRO_CONFIG_STR(GfxGpuName, gfx_gpu_name, 256, "auto", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The GPU's name, which will be selected by the backend. (if supported by the backend)") +#if defined(CONF_PLATFORM_ANDROID) +MACRO_CONFIG_STR(GfxBackend, gfx_backend, 256, "GLES", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The backend to use (e.g. GLES or Vulkan)") +#elif !defined(CONF_ARCH_IA32) && !defined(CONF_PLATFORM_MACOS) MACRO_CONFIG_STR(GfxBackend, gfx_backend, 256, "Vulkan", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The backend to use (e.g. OpenGL or Vulkan)") #else MACRO_CONFIG_STR(GfxBackend, gfx_backend, 256, "OpenGL", CFGFLAG_SAVE | CFGFLAG_CLIENT, "The backend to use (e.g. OpenGL or Vulkan)") #endif -//sta MACRO_CONFIG_INT(GfxRenderThreadCount, gfx_render_thread_count, 3, 0, 0, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Number of threads the backend can use for rendering. (note: the value can be ignored by the backend)") MACRO_CONFIG_INT(GfxDriverIsBlocked, gfx_driver_is_blocked, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "If 1, the current driver is in a blocked error state.") MACRO_CONFIG_INT(ClVideoRecorderFPS, cl_video_recorder_fps, 60, 1, 1000, CFGFLAG_SAVE | CFGFLAG_CLIENT, "At which FPS the videorecorder should record demos.") + /* * Add config variables for mods below this comment to avoid merge conflicts. */ - -MACRO_CONFIG_INT(ClShowFrozenText, sc_frozen_tees_text, 0, 0, 2, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show how many tees in your team are currently frozen. (0 - off, 1 - show alive, 2 - show frozen)") -MACRO_CONFIG_INT(ClShowFrozenHud, sc_frozen_tees_hud, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show frozen tee HUD") -MACRO_CONFIG_INT(ClShowFrozenHudSkins, sc_frozen_tees_hud_skins, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Use ninja skin, or darkened skin for frozen tees on hud") - -MACRO_CONFIG_INT(ClFrozenHudTeeSize, sc_frozen_tees_size, 15, 8, 20, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Size of tees in frozen tee hud. (Default : 15)") -MACRO_CONFIG_INT(ClFrozenMaxRows, sc_frozen_tees_max_rows, 1, 1, 6, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Maximum number of rows in frozen tee HUD display") -MACRO_CONFIG_INT(ClFrozenHudTeamOnly, sc_frozen_tees_only_inteam, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Only render frozen tee HUD display while in team") - -MACRO_CONFIG_COL(ScPlayerOwnColor, sc_player_own_color, 6684927, CFGFLAG_CLIENT | CFGFLAG_SAVE, "You'r color in TAB list") -MACRO_CONFIG_COL(ScLocalConsoleColor, sc_local_console_color, 51, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Local console color") -MACRO_CONFIG_COL(ScConsoleBarColor, sc_console_bar_color, 8883654, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Console bar color") -MACRO_CONFIG_COL(ScRemoteConsoleColor, sc_remote_console_color, 21837, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Remote console color") -MACRO_CONFIG_INT(ClLocalConsolaAlpha, cl_local_console_alpha, 90, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Opacity of local console") -MACRO_CONFIG_INT(ClRemoteConsolaAlpha, cl_remote_console_alpha, 90, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Opacity of remote console") - -MACRO_CONFIG_COL(ScBlacklistPColor, sc_blacklist_p_color, 65457, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Blacklist players color") -MACRO_CONFIG_COL(ScFriendColor, sc_friend_color, 7995321, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Friends color") -MACRO_CONFIG_COL(ScFrozenTeeColor, sc_frozen_tee_color, 172, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Frozen tee color") - -MACRO_CONFIG_INT(ClApplyProfileSkin, sc_passet_skin, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Apply skin in profiles") -MACRO_CONFIG_INT(ClApplyProfileName, sc_passet_name, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Apply name in profiles") -MACRO_CONFIG_INT(ClApplyProfileClan, sc_passet_clan, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Apply clan in profiles") -MACRO_CONFIG_INT(ClApplyProfileFlag, sc_passet_flag, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Apply flag in profiles") -MACRO_CONFIG_INT(ClApplyProfileColors, sc_passet_colors, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Apply colors in profiles") -MACRO_CONFIG_INT(ClApplyProfileEmote, sc_passet_emote, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Apply emote in profiles") -//autolog -MACRO_CONFIG_INT(ClAutoVerify, sc_auto_verify, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Auto verify") -//chat size -MACRO_CONFIG_INT(ClChatSize, sc_chat_size, 100, 10, 200, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Chat sizer") -//Chat pos -MACRO_CONFIG_INT(ClChatPos, sc_chat_pos, 1, 1, 3, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Chat Position") -//Anim -MACRO_CONFIG_INT(ClAnimFeetSpeed, cl_anim_feet_speed, 100, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Speed of animation for feet") -MACRO_CONFIG_INT(ClAnimHammerSpeed, cl_anim_hammer_speed, 100, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Speed of animation for hammer") -MACRO_CONFIG_INT(ClAnimNinjaSpeed, cl_anim_ninja_speed, 100, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Speed of animation for ninja") -MACRO_CONFIG_INT(ClAnimGunsSpeed, cl_anim_guns_speed, 100, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Speed of animation for guns(shotgun, gun, grenade, laser)") -//nameplates rendr. -MACRO_CONFIG_INT(ScShowFriendsNameplatesColor, sc_show_friends_nameplates_color, 0, 1, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Friend's Nameplates Color") -MACRO_CONFIG_INT(ScShowBlacklistNameplaterColor, sc_show_blacklist_nameplates_color, 0, 1, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Foe's Nameplates Color") -MACRO_CONFIG_INT(ScShowFrozenNameplaterColor, sc_show_frozen_nameplates_color, 0, 1, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Frozen tee Nameplates Color") - -MACRO_CONFIG_INT(ClChatFontSize, cl_chat_size, 60, 10, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Chat font size") -MACRO_CONFIG_INT(ClChatWidth, cl_chat_width, 200, 140, 400, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Chat width") - -//Outline Variables -MACRO_CONFIG_INT(ClOutline, sc_outline, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Draws outlines") -MACRO_CONFIG_INT(ClOutlineEntities, sc_outline_in_entities, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Only show outlines in entities") -MACRO_CONFIG_INT(ClOutlineFreeze, sc_outline_freeze, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Draws outline around freeze and deep") -MACRO_CONFIG_INT(ClOutlineUnFreeze, sc_outline_unfreeze, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Draws outline around unfreeze and undeep") -MACRO_CONFIG_INT(ClOutlineTele, sc_outline_tele, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Draws outline around teleporters") -MACRO_CONFIG_INT(ClOutlineSolid, sc_outline_solid, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Draws outline around hook and unhook") -MACRO_CONFIG_INT(ClOutlineWidth, sc_outline_width, 5, 1, 16, CFGFLAG_CLIENT | CFGFLAG_SAVE, "(1-16) Width of freeze outline") -MACRO_CONFIG_INT(ClOutlineAlpha, sc_outline_alpha, 50, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "(0-100) Outline alpha") -MACRO_CONFIG_INT(ClOutlineAlphaSolid, sc_outline_alpha_solid, 100, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "(0-100) Outline solids alpha") -MACRO_CONFIG_COL(ClOutlineColorSolid, sc_outline_color_solid, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Solid outline color") //0 0 0 -MACRO_CONFIG_COL(ClOutlineColorFreeze, sc_outline_color_freeze, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Freeze outline color") //0 0 0 -MACRO_CONFIG_COL(ClOutlineColorTele, sc_outline_color_tele, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Tele outline color") //0 0 0 -MACRO_CONFIG_COL(ClOutlineColorUnfreeze, sc_outline_color_unfreeze, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Unfreeze outline color") //0 0 0 - -MACRO_CONFIG_INT(ClRainbow, sc_rainbow, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Rainbow urself") -MACRO_CONFIG_INT(ClRainbowSpeed, sc_rainbow_speed, 20, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Rainbow speed") -MACRO_CONFIG_INT(ClRainbowEveryone, sc_rainbow_all, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Rainbow all") -MACRO_CONFIG_INT(ClShowStateChangeCountdown, sc_stars, 1, 1, 2, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Rainbow all") - -//tee skin name -MACRO_CONFIG_INT(ClShowSkinName, sc_nameplates_skin_name, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Display name skin in nameplates") -MACRO_CONFIG_INT(ClConsoleSimple, sc_console_simple, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Display name skin in nameplates") -MACRO_CONFIG_INT(ClConsoleBarSimple, cl_consolebar_simple, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Display name skin in nameplates") -// Old ninja mode -MACRO_CONFIG_INT(ClOldFreezeMode, cl_old_freeze_mode, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "OldNinjaMode") -MACRO_CONFIG_INT(ClFrozenWeapon, cl_frozen_weapon, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Give frozen tees their weapon") -MACRO_CONFIG_INT(ClRainbowHook, cl_rainbow_hook, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "rainbow hook lmfao") \ No newline at end of file diff --git a/src/engine/shared/console.cpp b/src/engine/shared/console.cpp index e20e2dae28..c7e0fb4d24 100644 --- a/src/engine/shared/console.cpp +++ b/src/engine/shared/console.cpp @@ -40,22 +40,26 @@ float CConsole::CResult::GetFloat(unsigned Index) const return str_tofloat(m_apArgs[Index]); } -ColorHSLA CConsole::CResult::GetColor(unsigned Index, bool Light) const +std::optional CConsole::CResult::GetColor(unsigned Index, float DarkestLighting) const { if(Index >= m_NumArgs) - return ColorHSLA(0, 0, 0); + return std::nullopt; const char *pStr = m_apArgs[Index]; if(str_isallnum(pStr) || ((pStr[0] == '-' || pStr[0] == '+') && str_isallnum(pStr + 1))) // Teeworlds Color (Packed HSL) { - const ColorHSLA Hsla = ColorHSLA(str_toulong_base(pStr, 10), true); - if(Light) - return Hsla.UnclampLighting(); - return Hsla; + unsigned long Value = str_toulong_base(pStr, 10); + if(Value == std::numeric_limits::max()) + return std::nullopt; + return ColorHSLA(Value, true).UnclampLighting(DarkestLighting); } else if(*pStr == '$') // Hex RGB/RGBA { - return color_cast(color_parse(pStr + 1).value_or(ColorRGBA(0.0f, 0.0f, 0.0f, 1.0f))); + auto ParsedColor = color_parse(pStr + 1); + if(ParsedColor) + return color_cast(ParsedColor.value()); + else + return std::nullopt; } else if(!str_comp_nocase(pStr, "red")) return ColorHSLA(0.0f / 6.0f, 1, .5f); @@ -76,7 +80,7 @@ ColorHSLA CConsole::CResult::GetColor(unsigned Index, bool Light) const else if(!str_comp_nocase(pStr, "black")) return ColorHSLA(0, 0, 0); - return ColorHSLA(0, 0, 0); + return std::nullopt; } const IConsole::CCommandInfo *CConsole::CCommand::NextCommandInfo(int AccessLevel, int FlagMask) const @@ -129,12 +133,12 @@ int CConsole::ParseStart(CResult *pResult, const char *pString, int Length) return 0; } -int CConsole::ParseArgs(CResult *pResult, const char *pFormat) +int CConsole::ParseArgs(CResult *pResult, const char *pFormat, bool IsColor) { char Command = *pFormat; char *pStr; int Optional = 0; - int Error = 0; + int Error = PARSEARGS_OK; pResult->ResetVictim(); @@ -155,7 +159,7 @@ int CConsole::ParseArgs(CResult *pResult, const char *pFormat) { if(!Optional) { - Error = 1; + Error = PARSEARGS_MISSING_VALUE; break; } @@ -191,7 +195,7 @@ int CConsole::ParseArgs(CResult *pResult, const char *pFormat) pStr++; // skip due to escape } else if(pStr[0] == 0) - return 1; // return error + return PARSEARGS_MISSING_VALUE; // return error *pDst = *pStr; pDst++; @@ -215,13 +219,7 @@ int CConsole::ParseArgs(CResult *pResult, const char *pFormat) if(Command == 'r') // rest of the string break; - else if(Command == 'v') // validate victim - pStr = str_skip_to_whitespace(pStr); - else if(Command == 'i') // validate int - pStr = str_skip_to_whitespace(pStr); - else if(Command == 'f') // validate float - pStr = str_skip_to_whitespace(pStr); - else if(Command == 's') // validate string + else if(Command == 'v' || Command == 'i' || Command == 'f' || Command == 's') pStr = str_skip_to_whitespace(pStr); if(pStr[0] != 0) // check for end of string @@ -230,6 +228,32 @@ int CConsole::ParseArgs(CResult *pResult, const char *pFormat) pStr++; } + // validate args + if(Command == 'i') + { + // don't validate colors here + if(!IsColor) + { + int Value; + if(!str_toint(pResult->GetString(pResult->NumArguments() - 1), &Value) || + Value == std::numeric_limits::max() || Value == std::numeric_limits::min()) + { + Error = PARSEARGS_INVALID_INTEGER; + break; + } + } + } + else if(Command == 'f') + { + float Value; + if(!str_tofloat(pResult->GetString(pResult->NumArguments() - 1), &Value) || + Value == std::numeric_limits::max() || Value == std::numeric_limits::min()) + { + Error = PARSEARGS_INVALID_FLOAT; + break; + } + } + if(pVictim) { pResult->SetVictim(pVictim); @@ -314,7 +338,7 @@ void CConsole::Print(int Level, const char *pFrom, const char *pStr, ColorRGBA P { LEVEL LogLevel = IConsole::ToLogLevel(Level); // if console colors are not enabled or if the color is pure white, use default terminal color - if(g_Config.m_ConsoleEnableColors && mem_comp(&PrintColor, &gs_ConsoleDefaultColor, sizeof(ColorRGBA)) != 0) + if(g_Config.m_ConsoleEnableColors && PrintColor != gs_ConsoleDefaultColor) { log_log_color(LogLevel, ColorToLogColor(PrintColor), pFrom, "%s", pStr); } @@ -402,7 +426,7 @@ bool CConsole::LineIsValid(const char *pStr) return true; } -void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientID, bool InterpretSemicolons) +void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientId, bool InterpretSemicolons) { const char *pWithoutPrefix = str_startswith(pStr, "mc;"); if(pWithoutPrefix) @@ -413,7 +437,7 @@ void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientID, bo while(pStr && *pStr) { CResult Result; - Result.m_ClientID = ClientID; + Result.m_ClientId = ClientId; const char *pEnd = pStr; const char *pNextPart = 0; int InString = 0; @@ -448,14 +472,14 @@ void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientID, bo return; CCommand *pCommand; - if(ClientID == IConsole::CLIENT_ID_GAME) + if(ClientId == IConsole::CLIENT_ID_GAME) pCommand = FindCommand(Result.m_pCommand, m_FlagMask | CFGFLAG_GAME); else pCommand = FindCommand(Result.m_pCommand, m_FlagMask); if(pCommand) { - if(ClientID == IConsole::CLIENT_ID_GAME && !(pCommand->m_Flags & CFGFLAG_GAME)) + if(ClientId == IConsole::CLIENT_ID_GAME && !(pCommand->m_Flags & CFGFLAG_GAME)) { if(Stroke) { @@ -464,7 +488,7 @@ void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientID, bo Print(OUTPUT_LEVEL_STANDARD, "console", aBuf); } } - else if(ClientID == IConsole::CLIENT_ID_NO_GAME && pCommand->m_Flags & CFGFLAG_GAME) + else if(ClientId == IConsole::CLIENT_ID_NO_GAME && pCommand->m_Flags & CFGFLAG_GAME) { if(Stroke) { @@ -487,10 +511,15 @@ void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientID, bo if(Stroke || IsStrokeCommand) { - if(ParseArgs(&Result, pCommand->m_pParams)) + if(int Error = ParseArgs(&Result, pCommand->m_pParams, pCommand->m_pfnCallback == &SColorConfigVariable::CommandCallback)) { - char aBuf[TEMPCMD_NAME_LENGTH + TEMPCMD_PARAMS_LENGTH + 32]; - str_format(aBuf, sizeof(aBuf), "Invalid arguments. Usage: %s %s", pCommand->m_pName, pCommand->m_pParams); + char aBuf[CMDLINE_LENGTH + 64]; + if(Error == PARSEARGS_INVALID_INTEGER) + str_format(aBuf, sizeof(aBuf), "%s is not a valid integer.", Result.GetString(Result.NumArguments() - 1)); + else if(Error == PARSEARGS_INVALID_FLOAT) + str_format(aBuf, sizeof(aBuf), "%s is not a valid decimal number.", Result.GetString(Result.NumArguments() - 1)); + else + str_format(aBuf, sizeof(aBuf), "Invalid arguments. Usage: %s %s", pCommand->m_pName, pCommand->m_pParams); Print(OUTPUT_LEVEL_STANDARD, "chatresp", aBuf); } else if(m_StoreCommands && pCommand->m_Flags & CFGFLAG_STORE) @@ -502,15 +531,18 @@ void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientID, bo else { if(pCommand->m_Flags & CMDFLAG_TEST && !g_Config.m_SvTestingCommands) + { + Print(OUTPUT_LEVEL_STANDARD, "console", "Test commands aren't allowed, enable them with 'sv_test_cmds 1' in your initial config."); return; + } if(m_pfnTeeHistorianCommandCallback && !(pCommand->m_Flags & CFGFLAG_NONTEEHISTORIC)) { - m_pfnTeeHistorianCommandCallback(ClientID, m_FlagMask, pCommand->m_pName, &Result, m_pTeeHistorianCommandUserdata); + m_pfnTeeHistorianCommandCallback(ClientId, m_FlagMask, pCommand->m_pName, &Result, m_pTeeHistorianCommandUserdata); } if(Result.GetVictim() == CResult::VICTIM_ME) - Result.SetVictim(ClientID); + Result.SetVictim(ClientId); if(Result.HasVictim() && Result.GetVictim() == CResult::VICTIM_ALL) { @@ -544,7 +576,10 @@ void CConsole::ExecuteLineStroked(int Stroke, const char *pStr, int ClientID, bo if(!m_pfnUnknownCommandCallback(pStr, m_pUnknownCommandUserdata)) { char aBuf[CMDLINE_LENGTH + 32]; - str_format(aBuf, sizeof(aBuf), "No such command: %s.", Result.m_pCommand); + if(m_FlagMask & CFGFLAG_CHAT) + str_format(aBuf, sizeof(aBuf), "No such command: %s. Use /cmdlist for a list of all commands.", Result.m_pCommand); + else + str_format(aBuf, sizeof(aBuf), "No such command: %s.", Result.m_pCommand); Print(OUTPUT_LEVEL_STANDARD, "chatresp", aBuf); } } @@ -584,21 +619,21 @@ CConsole::CCommand *CConsole::FindCommand(const char *pName, int FlagMask) return 0x0; } -void CConsole::ExecuteLine(const char *pStr, int ClientID, bool InterpretSemicolons) +void CConsole::ExecuteLine(const char *pStr, int ClientId, bool InterpretSemicolons) { - CConsole::ExecuteLineStroked(1, pStr, ClientID, InterpretSemicolons); // press it - CConsole::ExecuteLineStroked(0, pStr, ClientID, InterpretSemicolons); // then release it + CConsole::ExecuteLineStroked(1, pStr, ClientId, InterpretSemicolons); // press it + CConsole::ExecuteLineStroked(0, pStr, ClientId, InterpretSemicolons); // then release it } -void CConsole::ExecuteLineFlag(const char *pStr, int FlagMask, int ClientID, bool InterpretSemicolons) +void CConsole::ExecuteLineFlag(const char *pStr, int FlagMask, int ClientId, bool InterpretSemicolons) { int Temp = m_FlagMask; m_FlagMask = FlagMask; - ExecuteLine(pStr, ClientID, InterpretSemicolons); + ExecuteLine(pStr, ClientId, InterpretSemicolons); m_FlagMask = Temp; } -bool CConsole::ExecuteFile(const char *pFilename, int ClientID, bool LogFailure, int StorageType) +bool CConsole::ExecuteFile(const char *pFilename, int ClientId, bool LogFailure, int StorageType) { // make sure that this isn't being executed already for(CExecFile *pCur = m_pFirstExec; pCur; pCur = pCur->m_pPrev) @@ -616,23 +651,19 @@ bool CConsole::ExecuteFile(const char *pFilename, int ClientID, bool LogFailure, m_pFirstExec = &ThisFile; // exec the file - IOHANDLE File = m_pStorage->OpenFile(pFilename, IOFLAG_READ | IOFLAG_SKIP_BOM, StorageType); - + CLineReader LineReader; bool Success = false; char aBuf[32 + IO_MAX_PATH_LENGTH]; - if(File) + if(LineReader.OpenFile(m_pStorage->OpenFile(pFilename, IOFLAG_READ, StorageType))) { str_format(aBuf, sizeof(aBuf), "executing '%s'", pFilename); Print(IConsole::OUTPUT_LEVEL_STANDARD, "console", aBuf); - CLineReader Reader; - Reader.Init(File); - - char *pLine; - while((pLine = Reader.Get())) - ExecuteLine(pLine, ClientID); + while(const char *pLine = LineReader.Get()) + { + ExecuteLine(pLine, ClientId); + } - io_close(File); Success = true; } else if(LogFailure) @@ -727,10 +758,10 @@ void CConsole::ConUserCommandStatus(IResult *pResult, void *pUser) CResult Result; Result.m_pCommand = "access_status"; char aBuf[4]; - str_from_int((int)IConsole::ACCESS_LEVEL_USER, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", (int)IConsole::ACCESS_LEVEL_USER); Result.AddArgument(aBuf); - pConsole->ConCommandStatus(&Result, pConsole); + CConsole::ConCommandStatus(&Result, pConsole); } void CConsole::TraverseChain(FCommandCallback *ppfnCallback, void **ppUserData) diff --git a/src/engine/shared/console.h b/src/engine/shared/console.h index 339b521ce6..9a40dd3d15 100644 --- a/src/engine/shared/console.h +++ b/src/engine/shared/console.h @@ -61,7 +61,7 @@ class CConsole : public IConsole static void ConCommandAccess(IResult *pResult, void *pUser); static void ConCommandStatus(IConsole::IResult *pResult, void *pUser); - void ExecuteLineStroked(int Stroke, const char *pStr, int ClientID = -1, bool InterpretSemicolons = true) override; + void ExecuteLineStroked(int Stroke, const char *pStr, int ClientId = -1, bool InterpretSemicolons = true) override; FTeeHistorianCommandCallback m_pfnTeeHistorianCommandCallback; void *m_pTeeHistorianCommandUserdata; @@ -115,7 +115,7 @@ class CConsole : public IConsole const char *GetString(unsigned Index) const override; int GetInteger(unsigned Index) const override; float GetFloat(unsigned Index) const override; - ColorHSLA GetColor(unsigned Index, bool Light) const override; + std::optional GetColor(unsigned Index, float DarkestLighting) const override; void RemoveArgument(unsigned Index) override { @@ -144,7 +144,16 @@ class CConsole : public IConsole }; int ParseStart(CResult *pResult, const char *pString, int Length); - int ParseArgs(CResult *pResult, const char *pFormat); + + enum + { + PARSEARGS_OK = 0, + PARSEARGS_MISSING_VALUE, + PARSEARGS_INVALID_INTEGER, + PARSEARGS_INVALID_FLOAT, + }; + + int ParseArgs(CResult *pResult, const char *pFormat, bool IsColor = false); /* this function will set pFormat to the next parameter (i,s,r,v,?) it contains and @@ -207,9 +216,9 @@ class CConsole : public IConsole void StoreCommands(bool Store) override; bool LineIsValid(const char *pStr) override; - void ExecuteLine(const char *pStr, int ClientID = -1, bool InterpretSemicolons = true) override; - void ExecuteLineFlag(const char *pStr, int FlagMask, int ClientID = -1, bool InterpretSemicolons = true) override; - bool ExecuteFile(const char *pFilename, int ClientID = -1, bool LogFailure = false, int StorageType = IStorage::TYPE_ALL) override; + void ExecuteLine(const char *pStr, int ClientId = -1, bool InterpretSemicolons = true) override; + void ExecuteLineFlag(const char *pStr, int FlagMask, int ClientId = -1, bool InterpretSemicolons = true) override; + bool ExecuteFile(const char *pFilename, int ClientId = -1, bool LogFailure = false, int StorageType = IStorage::TYPE_ALL) override; char *Format(char *pBuf, int Size, const char *pFrom, const char *pStr) override; void Print(int Level, const char *pFrom, const char *pStr, ColorRGBA PrintColor = gs_ConsoleDefaultColor) const override; diff --git a/src/engine/shared/datafile.cpp b/src/engine/shared/datafile.cpp index 7714b4e77f..abcccedec4 100644 --- a/src/engine/shared/datafile.cpp +++ b/src/engine/shared/datafile.cpp @@ -14,6 +14,8 @@ #include #include +#include + static const int DEBUG = 0; enum @@ -51,13 +53,13 @@ struct CDatafileItemType struct CDatafileItem { - int m_TypeAndID; + int m_TypeAndId; int m_Size; }; struct CDatafileHeader { - char m_aID[4]; + char m_aId[4]; int m_Version; int m_Size; int m_Swaplen; @@ -70,7 +72,7 @@ struct CDatafileHeader constexpr size_t SizeOffset() { // The size of these members is not included in m_Size and m_Swaplen - return sizeof(m_aID) + sizeof(m_Version) + sizeof(m_Size) + sizeof(m_Swaplen); + return sizeof(m_aId) + sizeof(m_Version) + sizeof(m_Size) + sizeof(m_Swaplen); } }; @@ -142,11 +144,11 @@ bool CDataFileReader::Open(class IStorage *pStorage, const char *pFilename, int dbg_msg("datafile", "couldn't load header"); return false; } - if(Header.m_aID[0] != 'A' || Header.m_aID[1] != 'T' || Header.m_aID[2] != 'A' || Header.m_aID[3] != 'D') + if(Header.m_aId[0] != 'A' || Header.m_aId[1] != 'T' || Header.m_aId[2] != 'A' || Header.m_aId[3] != 'D') { - if(Header.m_aID[0] != 'D' || Header.m_aID[1] != 'A' || Header.m_aID[2] != 'T' || Header.m_aID[3] != 'A') + if(Header.m_aId[0] != 'D' || Header.m_aId[1] != 'A' || Header.m_aId[2] != 'T' || Header.m_aId[3] != 'A') { - dbg_msg("datafile", "wrong signature. %x %x %x %x", Header.m_aID[0], Header.m_aID[1], Header.m_aID[2], Header.m_aID[3]); + dbg_msg("datafile", "wrong signature. %x %x %x %x", Header.m_aId[0], Header.m_aId[1], Header.m_aId[2], Header.m_aId[3]); return false; } } @@ -450,20 +452,27 @@ int CDataFileReader::GetItemSize(int Index) const return m_pDataFile->m_Info.m_pItemOffsets[Index + 1] - m_pDataFile->m_Info.m_pItemOffsets[Index] - sizeof(CDatafileItem); } -int CDataFileReader::GetExternalItemType(int InternalType) +int CDataFileReader::GetExternalItemType(int InternalType, CUuid *pUuid) { if(InternalType <= OFFSET_UUID_TYPE || InternalType == ITEMTYPE_EX) { + if(pUuid) + *pUuid = UUID_ZEROED; return InternalType; } int TypeIndex = FindItemIndex(ITEMTYPE_EX, InternalType); if(TypeIndex < 0 || GetItemSize(TypeIndex) < (int)sizeof(CItemEx)) { + if(pUuid) + *pUuid = UUID_ZEROED; return InternalType; } const CItemEx *pItemEx = (const CItemEx *)GetItem(TypeIndex); + CUuid Uuid = pItemEx->ToUuid(); + if(pUuid) + *pUuid = Uuid; // Propagate UUID_UNKNOWN, it doesn't hurt. - return g_UuidManager.LookupUuid(pItemEx->ToUuid()); + return g_UuidManager.LookupUuid(Uuid); } int CDataFileReader::GetInternalItemType(int ExternalType) @@ -481,35 +490,39 @@ int CDataFileReader::GetInternalItemType(int ExternalType) { continue; } - int ID; - if(Uuid == ((const CItemEx *)GetItem(i, nullptr, &ID))->ToUuid()) + int Id; + if(Uuid == ((const CItemEx *)GetItem(i, nullptr, &Id))->ToUuid()) { - return ID; + return Id; } } return -1; } -void *CDataFileReader::GetItem(int Index, int *pType, int *pID) +void *CDataFileReader::GetItem(int Index, int *pType, int *pId, CUuid *pUuid) { if(!m_pDataFile) { if(pType) *pType = 0; - if(pID) - *pID = 0; + if(pId) + *pId = 0; + if(pUuid) + *pUuid = UUID_ZEROED; return nullptr; } CDatafileItem *pItem = (CDatafileItem *)(m_pDataFile->m_Info.m_pItemStart + m_pDataFile->m_Info.m_pItemOffsets[Index]); + + // remove sign extension + const int Type = GetExternalItemType((pItem->m_TypeAndId >> 16) & 0xffff, pUuid); if(pType) { - // remove sign extension - *pType = GetExternalItemType((pItem->m_TypeAndID >> 16) & 0xffff); + *pType = Type; } - if(pID) + if(pId) { - *pID = pItem->m_TypeAndID & 0xffff; + *pId = pItem->m_TypeAndId & 0xffff; } return (void *)(pItem + 1); } @@ -534,7 +547,7 @@ void CDataFileReader::GetType(int Type, int *pStart, int *pNum) } } -int CDataFileReader::FindItemIndex(int Type, int ID) +int CDataFileReader::FindItemIndex(int Type, int Id) { if(!m_pDataFile) { @@ -545,9 +558,9 @@ int CDataFileReader::FindItemIndex(int Type, int ID) GetType(Type, &Start, &Num); for(int i = 0; i < Num; i++) { - int ItemID; - GetItem(Start + i, nullptr, &ItemID); - if(ID == ItemID) + int ItemId; + GetItem(Start + i, nullptr, &ItemId); + if(Id == ItemId) { return Start + i; } @@ -555,9 +568,9 @@ int CDataFileReader::FindItemIndex(int Type, int ID) return -1; } -void *CDataFileReader::FindItem(int Type, int ID) +void *CDataFileReader::FindItem(int Type, int Id) { - int Index = FindItemIndex(Type, ID); + int Index = FindItemIndex(Type, Id); if(Index < 0) { return nullptr; @@ -643,42 +656,58 @@ int CDataFileWriter::GetTypeFromIndex(int Index) const return ITEMTYPE_EX - Index - 1; } -int CDataFileWriter::GetExtendedItemTypeIndex(int Type) +int CDataFileWriter::GetExtendedItemTypeIndex(int Type, const CUuid *pUuid) { int Index = 0; - for(int ExtendedItemType : m_vExtendedItemTypes) + if(Type == -1) + { + // Unknown type, search for UUID + for(const auto &ExtendedItemType : m_vExtendedItemTypes) + { + if(ExtendedItemType.m_Uuid == *pUuid) + return Index; + ++Index; + } + } + else { - if(ExtendedItemType == Type) - return Index; - ++Index; + for(const auto &ExtendedItemType : m_vExtendedItemTypes) + { + if(ExtendedItemType.m_Type == Type) + return Index; + ++Index; + } } // Type not found, add it. - m_vExtendedItemTypes.push_back(Type); + CExtendedItemType ExtendedType; + ExtendedType.m_Type = Type; + ExtendedType.m_Uuid = Type == -1 ? *pUuid : g_UuidManager.GetUuid(Type); + m_vExtendedItemTypes.push_back(ExtendedType); - CItemEx ExtendedType = CItemEx::FromUuid(g_UuidManager.GetUuid(Type)); - AddItem(ITEMTYPE_EX, GetTypeFromIndex(Index), sizeof(ExtendedType), &ExtendedType); + CItemEx ItemEx = CItemEx::FromUuid(ExtendedType.m_Uuid); + AddItem(ITEMTYPE_EX, GetTypeFromIndex(Index), sizeof(ItemEx), &ItemEx); return Index; } -int CDataFileWriter::AddItem(int Type, int ID, size_t Size, const void *pData) +int CDataFileWriter::AddItem(int Type, int Id, size_t Size, const void *pData, const CUuid *pUuid) { - dbg_assert((Type >= 0 && Type < MAX_ITEM_TYPES) || Type >= OFFSET_UUID, "Invalid type"); - dbg_assert(ID >= 0 && ID <= ITEMTYPE_EX, "Invalid ID"); + dbg_assert((Type >= 0 && Type < MAX_ITEM_TYPES) || Type >= OFFSET_UUID || (Type == -1 && pUuid != nullptr), "Invalid type"); + dbg_assert(Id >= 0 && Id <= ITEMTYPE_EX, "Invalid ID"); dbg_assert(Size == 0 || pData != nullptr, "Data missing"); // Items without data are allowed dbg_assert(Size <= (size_t)std::numeric_limits::max(), "Data too large"); dbg_assert(Size % sizeof(int) == 0, "Invalid data boundary"); - if(Type >= OFFSET_UUID) + if(Type == -1 || Type >= OFFSET_UUID) { - Type = GetTypeFromIndex(GetExtendedItemTypeIndex(Type)); + Type = GetTypeFromIndex(GetExtendedItemTypeIndex(Type, pUuid)); } const int NumItems = m_vItems.size(); m_vItems.emplace_back(); CItemInfo &Info = m_vItems.back(); Info.m_Type = Type; - Info.m_ID = ID; + Info.m_Id = Id; Info.m_Size = Size; // copy data @@ -705,7 +734,7 @@ int CDataFileWriter::AddItem(int Type, int ID, size_t Size, const void *pData) return NumItems; } -int CDataFileWriter::AddData(size_t Size, const void *pData, int CompressionLevel) +int CDataFileWriter::AddData(size_t Size, const void *pData, ECompressionLevel CompressionLevel) { dbg_assert(Size > 0 && pData != nullptr, "Data missing"); dbg_assert(Size <= (size_t)std::numeric_limits::max(), "Data too large"); @@ -749,6 +778,20 @@ int CDataFileWriter::AddDataString(const char *pStr) return AddData(str_length(pStr) + 1, pStr); } +static int CompressionLevelToZlib(CDataFileWriter::ECompressionLevel CompressionLevel) +{ + switch(CompressionLevel) + { + case CDataFileWriter::COMPRESSION_DEFAULT: + return Z_DEFAULT_COMPRESSION; + case CDataFileWriter::COMPRESSION_BEST: + return Z_BEST_COMPRESSION; + default: + dbg_assert(false, "CompressionLevel invalid"); + dbg_break(); + } +} + void CDataFileWriter::Finish() { dbg_assert((bool)m_File, "File not open"); @@ -759,7 +802,7 @@ void CDataFileWriter::Finish() { unsigned long CompressedSize = compressBound(DataInfo.m_UncompressedSize); DataInfo.m_pCompressedData = malloc(CompressedSize); - const int Result = compress2((Bytef *)DataInfo.m_pCompressedData, &CompressedSize, (Bytef *)DataInfo.m_pUncompressedData, DataInfo.m_UncompressedSize, DataInfo.m_CompressionLevel); + const int Result = compress2((Bytef *)DataInfo.m_pCompressedData, &CompressedSize, (Bytef *)DataInfo.m_pUncompressedData, DataInfo.m_UncompressedSize, CompressionLevelToZlib(DataInfo.m_CompressionLevel)); DataInfo.m_CompressedSize = CompressedSize; free(DataInfo.m_pUncompressedData); DataInfo.m_pUncompressedData = nullptr; @@ -808,10 +851,10 @@ void CDataFileWriter::Finish() // Construct and write header { CDatafileHeader Header; - Header.m_aID[0] = 'D'; - Header.m_aID[1] = 'A'; - Header.m_aID[2] = 'T'; - Header.m_aID[3] = 'A'; + Header.m_aId[0] = 'D'; + Header.m_aId[1] = 'A'; + Header.m_aId[2] = 'T'; + Header.m_aId[3] = 'A'; Header.m_Version = 4; Header.m_Size = FileSize - Header.SizeOffset(); Header.m_Swaplen = SwapSize - Header.SizeOffset(); @@ -904,11 +947,11 @@ void CDataFileWriter::Finish() for(int ItemIndex = m_aItemTypes[Type].m_First; ItemIndex != -1; ItemIndex = m_vItems[ItemIndex].m_Next) { CDatafileItem Item; - Item.m_TypeAndID = (Type << 16) | m_vItems[ItemIndex].m_ID; + Item.m_TypeAndId = (Type << 16) | m_vItems[ItemIndex].m_Id; Item.m_Size = m_vItems[ItemIndex].m_Size; if(DEBUG) - dbg_msg("datafile", "writing item. Type=%x ItemIndex=%d ID=%d Size=%d", Type, ItemIndex, m_vItems[ItemIndex].m_ID, m_vItems[ItemIndex].m_Size); + dbg_msg("datafile", "writing item. Type=%x ItemIndex=%d Id=%d Size=%d", Type, ItemIndex, m_vItems[ItemIndex].m_Id, m_vItems[ItemIndex].m_Size); #if defined(CONF_ARCH_ENDIAN_BIG) swap_endian(&Item, sizeof(int), sizeof(Item) / sizeof(int)); diff --git a/src/engine/shared/datafile.h b/src/engine/shared/datafile.h index db77ca41e4..f86b7c4d47 100644 --- a/src/engine/shared/datafile.h +++ b/src/engine/shared/datafile.h @@ -8,11 +8,11 @@ #include #include +#include "uuid_manager.h" + #include #include -#include - enum { ITEMTYPE_EX = 0xffff, @@ -25,7 +25,7 @@ class CDataFileReader void *GetDataImpl(int Index, bool Swap); int GetFileDataSize(int Index) const; - int GetExternalItemType(int InternalType); + int GetExternalItemType(int InternalType, CUuid *pUuid); int GetInternalItemType(int ExternalType); public: @@ -54,10 +54,10 @@ class CDataFileReader int NumData() const; int GetItemSize(int Index) const; - void *GetItem(int Index, int *pType = nullptr, int *pID = nullptr); + void *GetItem(int Index, int *pType = nullptr, int *pId = nullptr, CUuid *pUuid = nullptr); void GetType(int Type, int *pStart, int *pNum); - int FindItemIndex(int Type, int ID); - void *FindItem(int Type, int ID); + int FindItemIndex(int Type, int Id); + void *FindItem(int Type, int Id); int NumItems() const; SHA256_DIGEST Sha256() const; @@ -68,19 +68,27 @@ class CDataFileReader // write access class CDataFileWriter { +public: + enum ECompressionLevel + { + COMPRESSION_DEFAULT, + COMPRESSION_BEST, + }; + +private: struct CDataInfo { void *m_pUncompressedData; int m_UncompressedSize; void *m_pCompressedData; int m_CompressedSize; - int m_CompressionLevel; + ECompressionLevel m_CompressionLevel; }; struct CItemInfo { int m_Type; - int m_ID; + int m_Id; int m_Size; int m_Next; int m_Prev; @@ -94,6 +102,12 @@ class CDataFileWriter int m_Last; }; + struct CExtendedItemType + { + int m_Type; + CUuid m_Uuid; + }; + enum { MAX_ITEM_TYPES = 0x10000, @@ -103,10 +117,10 @@ class CDataFileWriter std::array m_aItemTypes; std::vector m_vItems; std::vector m_vDatas; - std::vector m_vExtendedItemTypes; + std::vector m_vExtendedItemTypes; int GetTypeFromIndex(int Index) const; - int GetExtendedItemTypeIndex(int Type); + int GetExtendedItemTypeIndex(int Type, const CUuid *pUuid); public: CDataFileWriter(); @@ -122,8 +136,8 @@ class CDataFileWriter ~CDataFileWriter(); bool Open(class IStorage *pStorage, const char *pFilename, int StorageType = IStorage::TYPE_SAVE); - int AddItem(int Type, int ID, size_t Size, const void *pData); - int AddData(size_t Size, const void *pData, int CompressionLevel = Z_DEFAULT_COMPRESSION); + int AddItem(int Type, int Id, size_t Size, const void *pData, const CUuid *pUuid = nullptr); + int AddData(size_t Size, const void *pData, ECompressionLevel CompressionLevel = COMPRESSION_DEFAULT); int AddDataSwapped(size_t Size, const void *pData); int AddDataString(const char *pStr); void Finish(); diff --git a/src/engine/shared/demo.cpp b/src/engine/shared/demo.cpp index 2eefe2b824..161e0d6cc1 100644 --- a/src/engine/shared/demo.cpp +++ b/src/engine/shared/demo.cpp @@ -62,18 +62,15 @@ int CDemoRecorder::Start(class IStorage *pStorage, class IConsole *pConsole, con { dbg_assert(m_File == 0, "Demo recorder already recording"); - m_pfnFilter = pfnFilter; - m_pUser = pUser; - - m_pMapData = pMapData; m_pConsole = pConsole; + m_pStorage = pStorage; IOHANDLE DemoFile = pStorage->OpenFile(pFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE); if(!DemoFile) { if(m_pConsole) { - char aBuf[256]; + char aBuf[64 + IO_MAX_PATH_LENGTH]; str_format(aBuf, sizeof(aBuf), "Unable to open '%s' for recording", pFilename); m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor); } @@ -186,6 +183,10 @@ int CDemoRecorder::Start(class IStorage *pStorage, class IConsole *pConsole, con str_format(aBuf, sizeof(aBuf), "Recording to '%s'", pFilename); m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor); } + + m_pfnFilter = pfnFilter; + m_pUser = pUser; + m_File = DemoFile; str_copy(m_aCurrentFilename, pFilename); @@ -315,6 +316,8 @@ void CDemoRecorder::RecordSnapshot(int Tick, const void *pData, int Size) // create delta char aDeltaData[CSnapshot::MAX_SIZE + sizeof(int)]; + m_pSnapshotDelta->SetStaticsize(protocol7::NETEVENTTYPE_SOUNDWORLD, true); + m_pSnapshotDelta->SetStaticsize(protocol7::NETEVENTTYPE_DAMAGE, true); const int DeltaSize = m_pSnapshotDelta->CreateDelta((CSnapshot *)m_aLastSnapshotData, (CSnapshot *)pData, &aDeltaData); if(DeltaSize) { @@ -337,33 +340,68 @@ void CDemoRecorder::RecordMessage(const void *pData, int Size) Write(CHUNKTYPE_MESSAGE, pData, Size); } -int CDemoRecorder::Stop() +int CDemoRecorder::Stop(IDemoRecorder::EStopMode Mode, const char *pTargetFilename) { if(!m_File) return -1; - // add the demo length to the header - io_seek(m_File, gs_LengthOffset, IOSEEK_START); - unsigned char aLength[sizeof(int32_t)]; - uint_to_bytes_be(aLength, Length()); - io_write(m_File, aLength, sizeof(aLength)); - - // add the timeline markers to the header - io_seek(m_File, gs_NumMarkersOffset, IOSEEK_START); - unsigned char aNumMarkers[sizeof(int32_t)]; - uint_to_bytes_be(aNumMarkers, m_NumTimelineMarkers); - io_write(m_File, aNumMarkers, sizeof(aNumMarkers)); - for(int i = 0; i < m_NumTimelineMarkers; i++) + if(Mode == IDemoRecorder::EStopMode::KEEP_FILE) { - unsigned char aMarker[sizeof(int32_t)]; - uint_to_bytes_be(aMarker, m_aTimelineMarkers[i]); - io_write(m_File, aMarker, sizeof(aMarker)); + // add the demo length to the header + io_seek(m_File, gs_LengthOffset, IOSEEK_START); + unsigned char aLength[sizeof(int32_t)]; + uint_to_bytes_be(aLength, Length()); + io_write(m_File, aLength, sizeof(aLength)); + + // add the timeline markers to the header + io_seek(m_File, gs_NumMarkersOffset, IOSEEK_START); + unsigned char aNumMarkers[sizeof(int32_t)]; + uint_to_bytes_be(aNumMarkers, m_NumTimelineMarkers); + io_write(m_File, aNumMarkers, sizeof(aNumMarkers)); + for(int i = 0; i < m_NumTimelineMarkers; i++) + { + unsigned char aMarker[sizeof(int32_t)]; + uint_to_bytes_be(aMarker, m_aTimelineMarkers[i]); + io_write(m_File, aMarker, sizeof(aMarker)); + } } io_close(m_File); m_File = 0; + + if(Mode == IDemoRecorder::EStopMode::REMOVE_FILE) + { + if(!m_pStorage->RemoveFile(m_aCurrentFilename, IStorage::TYPE_SAVE)) + { + if(m_pConsole) + { + char aBuf[64 + IO_MAX_PATH_LENGTH]; + str_format(aBuf, sizeof(aBuf), "Could not remove demo file '%s'.", m_aCurrentFilename); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor); + } + return -1; + } + } + else if(pTargetFilename[0] != '\0') + { + if(!m_pStorage->RenameFile(m_aCurrentFilename, pTargetFilename, IStorage::TYPE_SAVE)) + { + if(m_pConsole) + { + char aBuf[64 + 2 * IO_MAX_PATH_LENGTH]; + str_format(aBuf, sizeof(aBuf), "Could not move demo file '%s' to '%s'.", m_aCurrentFilename, pTargetFilename); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor); + } + return -1; + } + } + if(m_pConsole) - m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Stopped recording", gs_DemoPrintColor); + { + char aBuf[64 + IO_MAX_PATH_LENGTH]; + str_format(aBuf, sizeof(aBuf), "Stopped recording to '%s'", m_aCurrentFilename); + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", aBuf, gs_DemoPrintColor); + } return 0; } @@ -379,20 +417,34 @@ void CDemoRecorder::AddDemoMarker(int Tick) { dbg_assert(Tick >= 0, "invalid marker tick"); if(m_NumTimelineMarkers >= MAX_TIMELINE_MARKERS) + { + if(m_pConsole) + { + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Too many timeline markers", gs_DemoPrintColor); + } return; + } // not more than 1 marker in a second if(m_NumTimelineMarkers > 0) { - int Diff = Tick - m_aTimelineMarkers[m_NumTimelineMarkers - 1]; + const int Diff = Tick - m_aTimelineMarkers[m_NumTimelineMarkers - 1]; if(Diff < (float)SERVER_TICK_SPEED) + { + if(m_pConsole) + { + m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Previous timeline marker too close", gs_DemoPrintColor); + } return; + } } m_aTimelineMarkers[m_NumTimelineMarkers++] = Tick; if(m_pConsole) + { m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demo_recorder", "Added timeline marker", gs_DemoPrintColor); + } } CDemoPlayer::CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo, TUpdateIntraTimesFunc &&UpdateIntraTimesFunc) @@ -443,15 +495,15 @@ CDemoPlayer::EReadChunkHeaderResult CDemoPlayer::ReadChunkHeader(int *pType, int if(Chunk & CHUNKTYPEFLAG_TICKMARKER) { // decode tick marker - int Tickdelta_legacy = Chunk & CHUNKMASK_TICK_LEGACY; // compatibility + int TickdeltaLegacy = Chunk & CHUNKMASK_TICK_LEGACY; // compatibility *pType = Chunk & (CHUNKTYPEFLAG_TICKMARKER | CHUNKTICKFLAG_KEYFRAME); int NewTick; - if(m_Info.m_Header.m_Version < gs_VersionTickCompression && Tickdelta_legacy != 0) + if(m_Info.m_Header.m_Version < gs_VersionTickCompression && TickdeltaLegacy != 0) { if(*pTick < 0) // initial tick not initialized before a tick delta return CHUNKHEADER_ERROR; - NewTick = *pTick + Tickdelta_legacy; + NewTick = *pTick + TickdeltaLegacy; } else if(Chunk & CHUNKTICKFLAG_TICK_COMPRESSED) { @@ -615,7 +667,7 @@ void CDemoPlayer::DoTick() break; } - DataSize = CVariableInt::Decompress(m_aDecompressedSnapshotData, DataSize, m_aCurrentSnapshotData, sizeof(m_aCurrentSnapshotData)); + DataSize = CVariableInt::Decompress(m_aDecompressedSnapshotData, DataSize, m_aChunkData, sizeof(m_aChunkData)); if(DataSize < 0) { Stop("Error during intpack decompression"); @@ -626,8 +678,8 @@ void CDemoPlayer::DoTick() if(ChunkType == CHUNKTYPE_DELTA) { // process delta snapshot - CSnapshot *pNewsnap = (CSnapshot *)m_aDeltaSnapshotData; - DataSize = m_pSnapshotDelta->UnpackDelta((CSnapshot *)m_aLastSnapshotData, pNewsnap, m_aCurrentSnapshotData, DataSize); + CSnapshot *pNewsnap = (CSnapshot *)m_aSnapshot; + DataSize = m_pSnapshotDelta->UnpackDelta((CSnapshot *)m_aLastSnapshotData, pNewsnap, m_aChunkData, DataSize, IsSixup()); if(DataSize < 0) { @@ -650,17 +702,17 @@ void CDemoPlayer::DoTick() else { if(m_pListener) - m_pListener->OnDemoPlayerSnapshot(m_aDeltaSnapshotData, DataSize); + m_pListener->OnDemoPlayerSnapshot(m_aSnapshot, DataSize); m_LastSnapshotDataSize = DataSize; - mem_copy(m_aLastSnapshotData, m_aDeltaSnapshotData, DataSize); + mem_copy(m_aLastSnapshotData, m_aSnapshot, DataSize); GotSnapshot = true; } } else if(ChunkType == CHUNKTYPE_SNAPSHOT) { // process full snapshot - CSnapshot *pSnap = (CSnapshot *)m_aCurrentSnapshotData; + CSnapshot *pSnap = (CSnapshot *)m_aChunkData; if(!pSnap->IsValid(DataSize)) { if(m_pConsole) @@ -675,9 +727,9 @@ void CDemoPlayer::DoTick() GotSnapshot = true; m_LastSnapshotDataSize = DataSize; - mem_copy(m_aLastSnapshotData, m_aCurrentSnapshotData, DataSize); + mem_copy(m_aLastSnapshotData, m_aChunkData, DataSize); if(m_pListener) - m_pListener->OnDemoPlayerSnapshot(m_aCurrentSnapshotData, DataSize); + m_pListener->OnDemoPlayerSnapshot(m_aChunkData, DataSize); } } else @@ -698,7 +750,7 @@ void CDemoPlayer::DoTick() else if(ChunkType == CHUNKTYPE_MESSAGE) { if(m_pListener) - m_pListener->OnDemoPlayerMessage(m_aCurrentSnapshotData, DataSize); + m_pListener->OnDemoPlayerMessage(m_aChunkData, DataSize); } } } @@ -753,6 +805,7 @@ int CDemoPlayer::Load(class IStorage *pStorage, class IConsole *pConsole, const str_copy(m_aFilename, ""); return -1; } + m_Sixup = str_startswith(m_Info.m_Header.m_aNetversion, "0.7"); // save byte offset of map for later use m_MapOffset = io_tell(m_File); @@ -1167,19 +1220,18 @@ class CDemoRecordingListener : public CDemoPlayer::IListener } }; -void CDemoEditor::Init(const char *pNetVersion, class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage) +void CDemoEditor::Init(class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage) { - m_pNetVersion = pNetVersion; m_pSnapshotDelta = pSnapshotDelta; m_pConsole = pConsole; m_pStorage = pStorage; } -void CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser) +bool CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser) { CDemoPlayer DemoPlayer(m_pSnapshotDelta, false); if(DemoPlayer.Load(m_pStorage, m_pConsole, pDemo, IStorage::TYPE_ALL_OR_ABSOLUTE) == -1) - return; + return false; const CMapInfo *pMapInfo = DemoPlayer.GetMapInfo(); const CDemoPlayer::CPlaybackInfo *pInfo = DemoPlayer.Info(); @@ -1193,12 +1245,12 @@ void CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int CDemoRecorder DemoRecorder(m_pSnapshotDelta); unsigned char *pMapData = DemoPlayer.GetMapData(m_pStorage); - const int Result = DemoRecorder.Start(m_pStorage, m_pConsole, pDst, m_pNetVersion, pMapInfo->m_aName, Sha256, pMapInfo->m_Crc, pInfo->m_Header.m_aType, pMapInfo->m_Size, pMapData, nullptr, pfnFilter, pUser) == -1; + const int Result = DemoRecorder.Start(m_pStorage, m_pConsole, pDst, pInfo->m_Header.m_aNetversion, pMapInfo->m_aName, Sha256, pMapInfo->m_Crc, pInfo->m_Header.m_aType, pMapInfo->m_Size, pMapData, nullptr, pfnFilter, pUser) == -1; free(pMapData); if(Result != 0) { DemoPlayer.Stop(); - return; + return false; } CDemoRecordingListener Listener; @@ -1229,5 +1281,6 @@ void CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int } DemoPlayer.Stop(); - DemoRecorder.Stop(); + DemoRecorder.Stop(IDemoRecorder::EStopMode::KEEP_FILE); + return true; } diff --git a/src/engine/shared/demo.h b/src/engine/shared/demo.h index 0544f41786..0230e60239 100644 --- a/src/engine/shared/demo.h +++ b/src/engine/shared/demo.h @@ -18,17 +18,21 @@ typedef std::function TUpdateIntraTimesFunc; class CDemoRecorder : public IDemoRecorder { class IConsole *m_pConsole; + class IStorage *m_pStorage; + IOHANDLE m_File; char m_aCurrentFilename[IO_MAX_PATH_LENGTH]; int m_LastTickMarker; int m_LastKeyFrame; int m_FirstTick; + unsigned char m_aLastSnapshotData[CSnapshot::MAX_SIZE]; class CSnapshotDelta *m_pSnapshotDelta; + int m_NumTimelineMarkers; int m_aTimelineMarkers[MAX_TIMELINE_MARKERS]; + bool m_NoMapData; - unsigned char *m_pMapData; DEMOFUNC_FILTER m_pfnFilter; void *m_pUser; @@ -41,8 +45,8 @@ class CDemoRecorder : public IDemoRecorder CDemoRecorder() {} ~CDemoRecorder() override; - int Start(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, const char *pNetversion, const char *pMap, const SHA256_DIGEST &Sha256, unsigned MapCrc, const char *pType, unsigned MapSize, unsigned char *pMapData, IOHANDLE MapFile = nullptr, DEMOFUNC_FILTER pfnFilter = nullptr, void *pUser = nullptr); - int Stop() override; + int Start(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, const char *pNetversion, const char *pMap, const SHA256_DIGEST &Sha256, unsigned MapCrc, const char *pType, unsigned MapSize, unsigned char *pMapData, IOHANDLE MapFile, DEMOFUNC_FILTER pfnFilter, void *pUser); + int Stop(IDemoRecorder::EStopMode Mode, const char *pTargetFilename = "") override; void AddDemoMarker(); void AddDemoMarker(int Tick); @@ -51,8 +55,7 @@ class CDemoRecorder : public IDemoRecorder void RecordMessage(const void *pData, int Size); bool IsRecording() const override { return m_File != nullptr; } - char *GetCurrentFilename() override { return m_aCurrentFilename; } - void ClearCurrentFilename() { m_aCurrentFilename[0] = '\0'; } + const char *CurrentFilename() const override { return m_aCurrentFilename; } int Length() const override { return (m_LastTickMarker - m_FirstTick) / SERVER_TICK_SPEED; } }; @@ -115,8 +118,13 @@ class CDemoPlayer : public IDemoPlayer CPlaybackInfo m_Info; unsigned char m_aCompressedSnapshotData[CSnapshot::MAX_SIZE]; unsigned char m_aDecompressedSnapshotData[CSnapshot::MAX_SIZE]; - unsigned char m_aCurrentSnapshotData[CSnapshot::MAX_SIZE]; - unsigned char m_aDeltaSnapshotData[CSnapshot::MAX_SIZE]; + + // Depending on the chunk header + // this is either a full CSnapshot or a CSnapshotDelta. + unsigned char m_aChunkData[CSnapshot::MAX_SIZE]; + // Storage for the full snapshot + // where the delta gets unpacked into. + unsigned char m_aSnapshot[CSnapshot::MAX_SIZE]; unsigned char m_aLastSnapshotData[CSnapshot::MAX_SIZE]; int m_LastSnapshotDataSize; class CSnapshotDelta *m_pSnapshotDelta; @@ -137,6 +145,7 @@ class CDemoPlayer : public IDemoPlayer bool ScanFile(); int64_t Time(); + bool m_Sixup; public: CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo); @@ -164,10 +173,11 @@ class CDemoPlayer : public IDemoPlayer const CInfo *BaseInfo() const override { return &m_Info.m_Info; } void GetDemoName(char *pBuffer, size_t BufferSize) const override; bool GetDemoInfo(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, int StorageType, CDemoHeader *pDemoHeader, CTimelineMarkers *pTimelineMarkers, CMapInfo *pMapInfo, IOHANDLE *pFile = nullptr, char *pErrorMessage = nullptr, size_t ErrorMessageSize = 0) const override; - const char *Filename() { return m_aFilename; } - const char *ErrorMessage() { return m_aErrorMessage; } + const char *Filename() const { return m_aFilename; } + const char *ErrorMessage() const override { return m_aErrorMessage; } int Update(bool RealTime = true); + bool IsSixup() const { return m_Sixup; } const CPlaybackInfo *Info() const { return &m_Info; } bool IsPlaying() const override { return m_File != nullptr; } @@ -179,11 +189,10 @@ class CDemoEditor : public IDemoEditor IConsole *m_pConsole; IStorage *m_pStorage; class CSnapshotDelta *m_pSnapshotDelta; - const char *m_pNetVersion; public: - virtual void Init(const char *pNetVersion, class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage); - void Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser) override; + virtual void Init(class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage); + bool Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser) override; }; #endif diff --git a/src/engine/shared/econ.cpp b/src/engine/shared/econ.cpp index 7a00362cfd..52305395ab 100644 --- a/src/engine/shared/econ.cpp +++ b/src/engine/shared/econ.cpp @@ -9,35 +9,35 @@ CEcon::CEcon() : { } -int CEcon::NewClientCallback(int ClientID, void *pUser) +int CEcon::NewClientCallback(int ClientId, void *pUser) { CEcon *pThis = (CEcon *)pUser; char aAddrStr[NETADDR_MAXSTRSIZE]; - net_addr_str(pThis->m_NetConsole.ClientAddr(ClientID), aAddrStr, sizeof(aAddrStr), true); + net_addr_str(pThis->m_NetConsole.ClientAddr(ClientId), aAddrStr, sizeof(aAddrStr), true); char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "client accepted. cid=%d addr=%s'", ClientID, aAddrStr); + str_format(aBuf, sizeof(aBuf), "client accepted. cid=%d addr=%s'", ClientId, aAddrStr); pThis->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "econ", aBuf); - pThis->m_aClients[ClientID].m_State = CClient::STATE_CONNECTED; - pThis->m_aClients[ClientID].m_TimeConnected = time_get(); - pThis->m_aClients[ClientID].m_AuthTries = 0; + pThis->m_aClients[ClientId].m_State = CClient::STATE_CONNECTED; + pThis->m_aClients[ClientId].m_TimeConnected = time_get(); + pThis->m_aClients[ClientId].m_AuthTries = 0; - pThis->m_NetConsole.Send(ClientID, "Enter password:"); + pThis->m_NetConsole.Send(ClientId, "Enter password:"); return 0; } -int CEcon::DelClientCallback(int ClientID, const char *pReason, void *pUser) +int CEcon::DelClientCallback(int ClientId, const char *pReason, void *pUser) { CEcon *pThis = (CEcon *)pUser; char aAddrStr[NETADDR_MAXSTRSIZE]; - net_addr_str(pThis->m_NetConsole.ClientAddr(ClientID), aAddrStr, sizeof(aAddrStr), true); + net_addr_str(pThis->m_NetConsole.ClientAddr(ClientId), aAddrStr, sizeof(aAddrStr), true); char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "client dropped. cid=%d addr=%s reason='%s'", ClientID, aAddrStr, pReason); + str_format(aBuf, sizeof(aBuf), "client dropped. cid=%d addr=%s reason='%s'", ClientId, aAddrStr, pReason); pThis->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "econ", aBuf); - pThis->m_aClients[ClientID].m_State = CClient::STATE_EMPTY; + pThis->m_aClients[ClientId].m_State = CClient::STATE_EMPTY; return 0; } @@ -45,8 +45,8 @@ void CEcon::ConLogout(IConsole::IResult *pResult, void *pUserData) { CEcon *pThis = static_cast(pUserData); - if(pThis->m_UserClientID >= 0 && pThis->m_UserClientID < NET_MAX_CONSOLE_CLIENTS && pThis->m_aClients[pThis->m_UserClientID].m_State != CClient::STATE_EMPTY) - pThis->m_NetConsole.Drop(pThis->m_UserClientID, "Logout"); + if(pThis->m_UserClientId >= 0 && pThis->m_UserClientId < NET_MAX_CONSOLE_CLIENTS && pThis->m_aClients[pThis->m_UserClientId].m_State != CClient::STATE_EMPTY) + pThis->m_NetConsole.Drop(pThis->m_UserClientId, "Logout"); } void CEcon::Init(CConfig *pConfig, IConsole *pConsole, CNetBan *pNetBan) @@ -58,24 +58,30 @@ void CEcon::Init(CConfig *pConfig, IConsole *pConsole, CNetBan *pNetBan) Client.m_State = CClient::STATE_EMPTY; m_Ready = false; - m_UserClientID = -1; + m_UserClientId = -1; - if(g_Config.m_EcPort == 0 || g_Config.m_EcPassword[0] == 0) + if(g_Config.m_EcPort == 0) return; + if(g_Config.m_EcPassword[0] == 0) + { + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "econ", "ec_password is required to be set for econ to be enabled."); + return; + } + NETADDR BindAddr; - if(g_Config.m_EcBindaddr[0] == '\0') + if(g_Config.m_EcBindaddr[0] && net_host_lookup(g_Config.m_EcBindaddr, &BindAddr, NETTYPE_ALL) == 0) { - mem_zero(&BindAddr, sizeof(BindAddr)); + // got bindaddr + BindAddr.port = g_Config.m_EcPort; } - else if(net_host_lookup(g_Config.m_EcBindaddr, &BindAddr, NETTYPE_ALL) != 0) + else { char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "The configured bindaddr '%s' cannot be resolved.", g_Config.m_Bindaddr); + str_format(aBuf, sizeof(aBuf), "The configured bindaddr '%s' cannot be resolved.", g_Config.m_EcBindaddr); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "econ", aBuf); + return; } - BindAddr.type = NETTYPE_ALL; - BindAddr.port = g_Config.m_EcPort; if(m_NetConsole.Open(BindAddr, pNetBan)) { @@ -98,44 +104,44 @@ void CEcon::Update() m_NetConsole.Update(); char aBuf[NET_MAX_PACKETSIZE]; - int ClientID; + int ClientId; - while(m_NetConsole.Recv(aBuf, (int)(sizeof(aBuf)) - 1, &ClientID)) + while(m_NetConsole.Recv(aBuf, (int)(sizeof(aBuf)) - 1, &ClientId)) { - dbg_assert(m_aClients[ClientID].m_State != CClient::STATE_EMPTY, "got message from empty slot"); - if(m_aClients[ClientID].m_State == CClient::STATE_CONNECTED) + dbg_assert(m_aClients[ClientId].m_State != CClient::STATE_EMPTY, "got message from empty slot"); + if(m_aClients[ClientId].m_State == CClient::STATE_CONNECTED) { if(str_comp(aBuf, g_Config.m_EcPassword) == 0) { - m_aClients[ClientID].m_State = CClient::STATE_AUTHED; - m_NetConsole.Send(ClientID, "Authentication successful. External console access granted."); + m_aClients[ClientId].m_State = CClient::STATE_AUTHED; + m_NetConsole.Send(ClientId, "Authentication successful. External console access granted."); - str_format(aBuf, sizeof(aBuf), "cid=%d authed", ClientID); + str_format(aBuf, sizeof(aBuf), "cid=%d authed", ClientId); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "econ", aBuf); } else { - m_aClients[ClientID].m_AuthTries++; + m_aClients[ClientId].m_AuthTries++; char aMsg[128]; - str_format(aMsg, sizeof(aMsg), "Wrong password %d/%d.", m_aClients[ClientID].m_AuthTries, MAX_AUTH_TRIES); - m_NetConsole.Send(ClientID, aMsg); - if(m_aClients[ClientID].m_AuthTries >= MAX_AUTH_TRIES) + str_format(aMsg, sizeof(aMsg), "Wrong password %d/%d.", m_aClients[ClientId].m_AuthTries, MAX_AUTH_TRIES); + m_NetConsole.Send(ClientId, aMsg); + if(m_aClients[ClientId].m_AuthTries >= MAX_AUTH_TRIES) { if(!g_Config.m_EcBantime) - m_NetConsole.Drop(ClientID, "Too many authentication tries"); + m_NetConsole.Drop(ClientId, "Too many authentication tries"); else - m_NetConsole.NetBan()->BanAddr(m_NetConsole.ClientAddr(ClientID), g_Config.m_EcBantime * 60, "Too many authentication tries"); + m_NetConsole.NetBan()->BanAddr(m_NetConsole.ClientAddr(ClientId), g_Config.m_EcBantime * 60, "Too many authentication tries", false); } } } - else if(m_aClients[ClientID].m_State == CClient::STATE_AUTHED) + else if(m_aClients[ClientId].m_State == CClient::STATE_AUTHED) { char aFormatted[256]; - str_format(aFormatted, sizeof(aFormatted), "cid=%d cmd='%s'", ClientID, aBuf); + str_format(aFormatted, sizeof(aFormatted), "cid=%d cmd='%s'", ClientId, aBuf); Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "server", aFormatted); - m_UserClientID = ClientID; + m_UserClientId = ClientId; Console()->ExecuteLine(aBuf); - m_UserClientID = -1; + m_UserClientId = -1; } } @@ -147,12 +153,12 @@ void CEcon::Update() } } -void CEcon::Send(int ClientID, const char *pLine) +void CEcon::Send(int ClientId, const char *pLine) { if(!m_Ready) return; - if(ClientID == -1) + if(ClientId == -1) { for(int i = 0; i < NET_MAX_CONSOLE_CLIENTS; i++) { @@ -160,8 +166,8 @@ void CEcon::Send(int ClientID, const char *pLine) m_NetConsole.Send(i, pLine); } } - else if(ClientID >= 0 && ClientID < NET_MAX_CONSOLE_CLIENTS && m_aClients[ClientID].m_State == CClient::STATE_AUTHED) - m_NetConsole.Send(ClientID, pLine); + else if(ClientId >= 0 && ClientId < NET_MAX_CONSOLE_CLIENTS && m_aClients[ClientId].m_State == CClient::STATE_AUTHED) + m_NetConsole.Send(ClientId, pLine); } void CEcon::Shutdown() diff --git a/src/engine/shared/econ.h b/src/engine/shared/econ.h index c7cc60f526..52c0cd6a8e 100644 --- a/src/engine/shared/econ.h +++ b/src/engine/shared/econ.h @@ -38,13 +38,13 @@ class CEcon bool m_Ready; int m_PrintCBIndex; - int m_UserClientID; + int m_UserClientId; static void SendLineCB(const char *pLine, void *pUserData, ColorRGBA PrintColor = {1, 1, 1, 1}); static void ConLogout(IConsole::IResult *pResult, void *pUserData); - static int NewClientCallback(int ClientID, void *pUser); - static int DelClientCallback(int ClientID, const char *pReason, void *pUser); + static int NewClientCallback(int ClientId, void *pUser); + static int DelClientCallback(int ClientId, const char *pReason, void *pUser); public: CEcon(); @@ -52,7 +52,7 @@ class CEcon void Init(CConfig *pConfig, IConsole *pConsole, CNetBan *pNetBan); void Update(); - void Send(int ClientID, const char *pLine); + void Send(int ClientId, const char *pLine); void Shutdown(); }; diff --git a/src/engine/shared/engine.cpp b/src/engine/shared/engine.cpp index 9f0469e98d..411eae5ab9 100644 --- a/src/engine/shared/engine.cpp +++ b/src/engine/shared/engine.cpp @@ -7,20 +7,22 @@ #include #include #include +#include #include #include class CEngine : public IEngine { -public: IConsole *m_pConsole; IStorage *m_pStorage; - bool m_Logging; + bool m_Logging; std::shared_ptr m_pFutureLogger; char m_aAppName[256]; + CJobPool m_JobPool; + static void Con_DbgLognetwork(IConsole::IResult *pResult, void *pUserData) { CEngine *pEngine = static_cast(pUserData); @@ -43,6 +45,7 @@ class CEngine : public IEngine } } +public: CEngine(bool Test, const char *pAppname, std::shared_ptr pFutureLogger, int Jobs) : m_pFutureLogger(std::move(pFutureLogger)) { @@ -70,7 +73,6 @@ class CEngine : public IEngine ~CEngine() override { - m_JobPool.Destroy(); CNetBase::CloseLog(); } @@ -92,16 +94,16 @@ class CEngine : public IEngine m_JobPool.Add(std::move(pJob)); } + void ShutdownJobs() override + { + m_JobPool.Shutdown(); + } + void SetAdditionalLogger(std::shared_ptr &&pLogger) override { m_pFutureLogger->Set(pLogger); } }; -void IEngine::RunJobBlocking(IJob *pJob) -{ - CJobPool::RunBlocking(pJob); -} - IEngine *CreateEngine(const char *pAppname, std::shared_ptr pFutureLogger, int Jobs) { return new CEngine(false, pAppname, std::move(pFutureLogger), Jobs); } IEngine *CreateTestEngine(const char *pAppname, int Jobs) { return new CEngine(true, pAppname, nullptr, Jobs); } diff --git a/src/engine/shared/fifo.cpp b/src/engine/shared/fifo.cpp index af2b49f09a..2595d88866 100644 --- a/src/engine/shared/fifo.cpp +++ b/src/engine/shared/fifo.cpp @@ -9,7 +9,7 @@ #include #include -void CFifo::Init(IConsole *pConsole, char *pFifoFile, int Flag) +void CFifo::Init(IConsole *pConsole, const char *pFifoFile, int Flag) { m_File = -1; @@ -70,19 +70,23 @@ void CFifo::Update() if(aBuf[i] != '\n') continue; aBuf[i] = '\0'; - m_pConsole->ExecuteLineFlag(pCur, m_Flag, -1); + if(str_utf8_check(pCur)) + { + m_pConsole->ExecuteLineFlag(pCur, m_Flag, -1); + } pCur = aBuf + i + 1; } - if(pCur < aBuf + Length) // missed the last line + if(pCur < aBuf + Length && str_utf8_check(pCur)) // missed the last line + { m_pConsole->ExecuteLineFlag(pCur, m_Flag, -1); + } } #elif defined(CONF_FAMILY_WINDOWS) -#define WIN32_LEAN_AND_MEAN #include -void CFifo::Init(IConsole *pConsole, char *pFifoFile, int Flag) +void CFifo::Init(IConsole *pConsole, const char *pFifoFile, int Flag) { m_pConsole = pConsole; if(pFifoFile[0] == '\0') @@ -188,11 +192,16 @@ void CFifo::Update() if(pBuf[i] != '\n') continue; pBuf[i] = '\0'; - m_pConsole->ExecuteLineFlag(pCur, m_Flag, -1); + if(str_utf8_check(pCur)) + { + m_pConsole->ExecuteLineFlag(pCur, m_Flag, -1); + } pCur = pBuf + i + 1; } - if(pCur < pBuf + Length) // missed the last line + if(pCur < pBuf + Length && str_utf8_check(pCur)) // missed the last line + { m_pConsole->ExecuteLineFlag(pCur, m_Flag, -1); + } free(pBuf); } diff --git a/src/engine/shared/fifo.h b/src/engine/shared/fifo.h index ad431b9bba..910b87e910 100644 --- a/src/engine/shared/fifo.h +++ b/src/engine/shared/fifo.h @@ -16,7 +16,7 @@ class CFifo #endif public: - void Init(IConsole *pConsole, char *pFifoFile, int Flag); + void Init(IConsole *pConsole, const char *pFifoFile, int Flag); void Update(); void Shutdown(); }; diff --git a/src/engine/shared/filecollection.cpp b/src/engine/shared/filecollection.cpp index 0dab1c258f..252e6e1efb 100644 --- a/src/engine/shared/filecollection.cpp +++ b/src/engine/shared/filecollection.cpp @@ -1,135 +1,16 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include #include -#include #include #include "filecollection.h" -bool CFileCollection::IsFilenameValid(const char *pFilename) -{ - if(m_aFileDesc[0] == '\0') - { - int FilenameLength = str_length(pFilename); - if(m_FileExtLength + TIMESTAMP_LENGTH > FilenameLength) - { - return false; - } - - pFilename += FilenameLength - m_FileExtLength - TIMESTAMP_LENGTH; - } - else - { - if(str_length(pFilename) != m_FileDescLength + TIMESTAMP_LENGTH + m_FileExtLength || - str_comp_num(pFilename, m_aFileDesc, m_FileDescLength) || - str_comp(pFilename + m_FileDescLength + TIMESTAMP_LENGTH, m_aFileExt)) - return false; - - pFilename += m_FileDescLength; - } - - return pFilename[0] == '_' && - pFilename[1] >= '0' && pFilename[1] <= '9' && - pFilename[2] >= '0' && pFilename[2] <= '9' && - pFilename[3] >= '0' && pFilename[3] <= '9' && - pFilename[4] >= '0' && pFilename[4] <= '9' && - pFilename[5] == '-' && - pFilename[6] >= '0' && pFilename[6] <= '9' && - pFilename[7] >= '0' && pFilename[7] <= '9' && - pFilename[8] == '-' && - pFilename[9] >= '0' && pFilename[9] <= '9' && - pFilename[10] >= '0' && pFilename[10] <= '9' && - pFilename[11] == '_' && - pFilename[12] >= '0' && pFilename[12] <= '9' && - pFilename[13] >= '0' && pFilename[13] <= '9' && - pFilename[14] == '-' && - pFilename[15] >= '0' && pFilename[15] <= '9' && - pFilename[16] >= '0' && pFilename[16] <= '9' && - pFilename[17] == '-' && - pFilename[18] >= '0' && pFilename[18] <= '9' && - pFilename[19] >= '0' && pFilename[19] <= '9'; -} - -int64_t CFileCollection::ExtractTimestamp(const char *pTimestring) -{ - int64_t Timestamp = pTimestring[0] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[1] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[2] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[3] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[5] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[6] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[8] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[9] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[11] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[12] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[14] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[15] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[17] - '0'; - Timestamp <<= 4; - Timestamp += pTimestring[18] - '0'; - - return Timestamp; -} - -void CFileCollection::BuildTimestring(int64_t Timestamp, char *pTimestring) -{ - pTimestring[19] = 0; - pTimestring[18] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[17] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[16] = '-'; - pTimestring[15] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[14] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[13] = '-'; - pTimestring[12] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[11] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[10] = '_'; - pTimestring[9] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[8] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[7] = '-'; - pTimestring[6] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[5] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[4] = '-'; - pTimestring[3] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[2] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[1] = (Timestamp & 0xF) + '0'; - Timestamp >>= 4; - pTimestring[0] = (Timestamp & 0xF) + '0'; -} - void CFileCollection::Init(IStorage *pStorage, const char *pPath, const char *pFileDesc, const char *pFileExt, int MaxEntries) { - mem_zero(m_aTimestamps, sizeof(m_aTimestamps)); - m_NumTimestamps = 0; - m_Remove = -1; - // MAX_ENTRIES - 1 to make sure that we can insert one entry into the sorted - // list and then remove the oldest one - m_MaxEntries = clamp(MaxEntries, 1, static_cast(MAX_ENTRIES) - 1); + m_vFileEntries.clear(); str_copy(m_aFileDesc, pFileDesc); m_FileDescLength = str_length(m_aFileDesc); str_copy(m_aFileExt, pFileExt); @@ -138,136 +19,84 @@ void CFileCollection::Init(IStorage *pStorage, const char *pPath, const char *pF m_pStorage = pStorage; m_pStorage->ListDirectory(IStorage::TYPE_SAVE, m_aPath, FilelistCallback, this); -} + std::sort(m_vFileEntries.begin(), m_vFileEntries.end(), [](const CFileEntry &Lhs, const CFileEntry &Rhs) { return Lhs.m_Timestamp < Rhs.m_Timestamp; }); -void CFileCollection::AddEntry(int64_t Timestamp) -{ - if(m_NumTimestamps == 0) - { - // empty list - m_aTimestamps[m_NumTimestamps++] = Timestamp; - } - else + int FilesDeleted = 0; + for(auto FileEntry : m_vFileEntries) { - // add entry to the sorted list - if(Timestamp < m_aTimestamps[0]) - { - // first entry - if(m_NumTimestamps <= m_MaxEntries) - { - mem_move(m_aTimestamps + 1, m_aTimestamps, m_NumTimestamps * sizeof(int64_t)); - m_aTimestamps[0] = Timestamp; - ++m_NumTimestamps; - } - } - else if(Timestamp >= m_aTimestamps[m_NumTimestamps - 1]) + if((int)m_vFileEntries.size() - FilesDeleted <= MaxEntries) + break; + + char aBuf[IO_MAX_PATH_LENGTH]; + if(m_aFileDesc[0] == '\0') { - // last entry - if(m_NumTimestamps > m_MaxEntries) - { - mem_move(m_aTimestamps, m_aTimestamps + 1, (m_NumTimestamps - 1) * sizeof(int64_t)); - m_aTimestamps[m_NumTimestamps - 1] = Timestamp; - } - else - m_aTimestamps[m_NumTimestamps++] = Timestamp; + str_format(aBuf, sizeof(aBuf), "%s/%s", m_aPath, FileEntry.m_aFilename); } else { - // middle entry - int Left = 0, Right = m_NumTimestamps - 1; - while(Right - Left > 1) - { - int Mid = (Left + Right) / 2; - if(m_aTimestamps[Mid] > Timestamp) - Right = Mid; - else - Left = Mid; - } - - if(m_NumTimestamps > m_MaxEntries) - { - mem_move(m_aTimestamps, m_aTimestamps + 1, (Right - 1) * sizeof(int64_t)); - m_aTimestamps[Right - 1] = Timestamp; - } - else - { - mem_move(m_aTimestamps + Right + 1, m_aTimestamps + Right, (m_NumTimestamps - Right) * sizeof(int64_t)); - m_aTimestamps[Right] = Timestamp; - ++m_NumTimestamps; - } + char aTimestring[TIMESTAMP_LENGTH]; + str_timestamp_ex(FileEntry.m_Timestamp, aTimestring, sizeof(aBuf), FORMAT_NOSPACE); + str_format(aBuf, sizeof(aBuf), "%s/%s_%s%s", m_aPath, m_aFileDesc, aTimestring, m_aFileExt); } - // remove old file only after we inserted the new entry, otherwise we can't - // know which one is the oldest - if(m_NumTimestamps > m_MaxEntries) - { - if(m_aFileDesc[0] == '\0') // consider an empty file desc as a wild card - { - m_Remove = m_aTimestamps[0]; - m_pStorage->ListDirectory(IStorage::TYPE_SAVE, m_aPath, RemoveCallback, this); - } - else - { - char aBuf[IO_MAX_PATH_LENGTH]; - char aTimestring[TIMESTAMP_LENGTH]; - BuildTimestring(m_aTimestamps[0], aTimestring); - - str_format(aBuf, sizeof(aBuf), "%s/%s_%s%s", m_aPath, m_aFileDesc, aTimestring, m_aFileExt); - m_pStorage->RemoveFile(aBuf, IStorage::TYPE_SAVE); - } - } + m_pStorage->RemoveFile(aBuf, IStorage::TYPE_SAVE); + FilesDeleted++; } } -int64_t CFileCollection::GetTimestamp(const char *pFilename) +bool CFileCollection::ExtractTimestamp(const char *pTimestring, time_t *pTimestamp) +{ + // Discard anything after timestamp length from pTimestring (most likely the extension) + char aStrippedTimestring[TIMESTAMP_LENGTH]; + str_copy(aStrippedTimestring, pTimestring); + return timestamp_from_str(aStrippedTimestring, FORMAT_NOSPACE, pTimestamp); +} + +bool CFileCollection::ParseFilename(const char *pFilename, time_t *pTimestamp) { + // Check if filename is valid + if(!str_endswith(pFilename, m_aFileExt)) + return false; + + const char *pTimestring = pFilename; + if(m_aFileDesc[0] == '\0') { int FilenameLength = str_length(pFilename); - return ExtractTimestamp(pFilename + FilenameLength - m_FileExtLength - TIMESTAMP_LENGTH + 1); + if(m_FileExtLength + TIMESTAMP_LENGTH > FilenameLength) + { + return false; + } + + pTimestring += FilenameLength - m_FileExtLength - TIMESTAMP_LENGTH + 1; } else { - return ExtractTimestamp(pFilename + m_FileDescLength + 1); - } -} - -int CFileCollection::FilelistCallback(const char *pFilename, int IsDir, int StorageType, void *pUser) -{ - CFileCollection *pThis = static_cast(pUser); - - // check for valid file name format - if(IsDir || !pThis->IsFilenameValid(pFilename)) - return 0; + if(str_length(pFilename) != m_FileDescLength + TIMESTAMP_LENGTH + m_FileExtLength || + !str_startswith(pFilename, m_aFileDesc)) + return false; - // extract the timestamp - int64_t Timestamp = pThis->GetTimestamp(pFilename); + pTimestring += m_FileDescLength + 1; + } - // add the entry - pThis->AddEntry(Timestamp); + // Extract timestamp + if(!ExtractTimestamp(pTimestring, pTimestamp)) + return false; - return 0; + return true; } -int CFileCollection::RemoveCallback(const char *pFilename, int IsDir, int StorageType, void *pUser) +int CFileCollection::FilelistCallback(const char *pFilename, int IsDir, int StorageType, void *pUser) { CFileCollection *pThis = static_cast(pUser); - // check for valid file name format - if(IsDir || !pThis->IsFilenameValid(pFilename)) + // Try to parse filename and extract timestamp + time_t Timestamp; + if(IsDir || !pThis->ParseFilename(pFilename, &Timestamp)) return 0; - // extract the timestamp - int64_t Timestamp = pThis->GetTimestamp(pFilename); - - if(Timestamp == pThis->m_Remove) - { - char aBuf[IO_MAX_PATH_LENGTH]; - str_format(aBuf, sizeof(aBuf), "%s/%s", pThis->m_aPath, pFilename); - pThis->m_pStorage->RemoveFile(aBuf, IStorage::TYPE_SAVE); - pThis->m_Remove = -1; - return 1; - } + // Add the entry + pThis->m_vFileEntries.emplace_back(Timestamp, pFilename); return 0; } diff --git a/src/engine/shared/filecollection.h b/src/engine/shared/filecollection.h index 9a4b561c65..6fb066a55b 100644 --- a/src/engine/shared/filecollection.h +++ b/src/engine/shared/filecollection.h @@ -3,9 +3,11 @@ #ifndef ENGINE_SHARED_FILECOLLECTION_H #define ENGINE_SHARED_FILECOLLECTION_H +#include #include #include +#include class IStorage; @@ -13,32 +15,35 @@ class CFileCollection { enum { - MAX_ENTRIES = 1001, TIMESTAMP_LENGTH = 20, // _YYYY-MM-DD_HH-MM-SS }; - int64_t m_aTimestamps[MAX_ENTRIES]; - int m_NumTimestamps; - int m_MaxEntries; + struct CFileEntry + { + time_t m_Timestamp; + char m_aFilename[IO_MAX_PATH_LENGTH]; + CFileEntry(int64_t Timestamp, const char *pFilename) + { + m_Timestamp = Timestamp; + str_copy(m_aFilename, pFilename); + } + }; + + std::vector m_vFileEntries; char m_aFileDesc[128]; int m_FileDescLength; char m_aFileExt[32]; int m_FileExtLength; char m_aPath[IO_MAX_PATH_LENGTH]; IStorage *m_pStorage; - int64_t m_Remove; // Timestamp we want to remove - bool IsFilenameValid(const char *pFilename); - int64_t ExtractTimestamp(const char *pTimestring); - void BuildTimestring(int64_t Timestamp, char *pTimestring); - int64_t GetTimestamp(const char *pFilename); + bool ExtractTimestamp(const char *pTimestring, time_t *pTimestamp); + bool ParseFilename(const char *pFilename, time_t *pTimestamp); public: void Init(IStorage *pStorage, const char *pPath, const char *pFileDesc, const char *pFileExt, int MaxEntries); - void AddEntry(int64_t Timestamp); static int FilelistCallback(const char *pFilename, int IsDir, int StorageType, void *pUser); - static int RemoveCallback(const char *pFilename, int IsDir, int StorageType, void *pUser); }; #endif diff --git a/src/engine/shared/host_lookup.cpp b/src/engine/shared/host_lookup.cpp index ea79b97f0b..a6efe45deb 100644 --- a/src/engine/shared/host_lookup.cpp +++ b/src/engine/shared/host_lookup.cpp @@ -11,6 +11,7 @@ CHostLookup::CHostLookup(const char *pHostname, int Nettype) { str_copy(m_aHostname, pHostname); m_Nettype = Nettype; + Abortable(true); } void CHostLookup::Run() diff --git a/src/engine/shared/http.cpp b/src/engine/shared/http.cpp index 259ff8d044..c69da07dcd 100644 --- a/src/engine/shared/http.cpp +++ b/src/engine/shared/http.cpp @@ -8,41 +8,19 @@ #include #include +#include +#include + #if !defined(CONF_FAMILY_WINDOWS) #include #endif -#define WIN32_LEAN_AND_MEAN #include -// TODO: Non-global pls? -static CURLSH *gs_pShare; -static CLock gs_aLocks[CURL_LOCK_DATA_LAST + 1]; -static bool gs_Initialized = false; - -static int GetLockIndex(int Data) -{ - if(!(0 <= Data && Data < CURL_LOCK_DATA_LAST)) - { - Data = CURL_LOCK_DATA_LAST; - } - return Data; -} - -static void CurlLock(CURL *pHandle, curl_lock_data Data, curl_lock_access Access, void *pUser) ACQUIRE(gs_aLocks[GetLockIndex(Data)]) -{ - (void)pHandle; - (void)Access; - (void)pUser; - gs_aLocks[GetLockIndex(Data)].lock(); -} - -static void CurlUnlock(CURL *pHandle, curl_lock_data Data, void *pUser) RELEASE(gs_aLocks[GetLockIndex(Data)]) -{ - (void)pHandle; - (void)pUser; - gs_aLocks[GetLockIndex(Data)].unlock(); -} +// There is a stray constant on Windows/MSVC... +#ifdef ERROR +#undef ERROR +#endif int CurlDebug(CURL *pHandle, curl_infotype Type, char *pData, size_t DataSize, void *pUser) { @@ -71,39 +49,6 @@ int CurlDebug(CURL *pHandle, curl_infotype Type, char *pData, size_t DataSize, v return 0; } -bool HttpInit(IStorage *pStorage) -{ - if(curl_global_init(CURL_GLOBAL_DEFAULT)) - { - return true; - } - gs_pShare = curl_share_init(); - if(!gs_pShare) - { - return true; - } - // print curl version - { - curl_version_info_data *pVersion = curl_version_info(CURLVERSION_NOW); - dbg_msg("http", "libcurl version %s (compiled = " LIBCURL_VERSION ")", pVersion->version); - } - - curl_share_setopt(gs_pShare, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); - curl_share_setopt(gs_pShare, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); - curl_share_setopt(gs_pShare, CURLSHOPT_LOCKFUNC, CurlLock); - curl_share_setopt(gs_pShare, CURLSHOPT_UNLOCKFUNC, CurlUnlock); - -#if !defined(CONF_FAMILY_WINDOWS) - // As a multithreaded application we have to tell curl to not install signal - // handlers and instead ignore SIGPIPE from OpenSSL ourselves. - signal(SIGPIPE, SIG_IGN); -#endif - - gs_Initialized = true; - - return false; -} - void EscapeUrl(char *pBuf, int Size, const char *pStr) { char *pEsc = curl_easy_escape(0, pStr, 0); @@ -121,44 +66,15 @@ bool HttpHasIpresolveBug() CHttpRequest::CHttpRequest(const char *pUrl) { str_copy(m_aUrl, pUrl); - sha256_init(&m_ActualSha256); + sha256_init(&m_ActualSha256Ctx); } CHttpRequest::~CHttpRequest() { - m_ResponseLength = 0; - if(!m_WriteToFile) - { - m_BufferSize = 0; - free(m_pBuffer); - m_pBuffer = nullptr; - } + dbg_assert(m_File == nullptr, "HTTP request file was not closed"); + free(m_pBuffer); curl_slist_free_all((curl_slist *)m_pHeaders); - m_pHeaders = nullptr; - if(m_pBody) - { - m_BodyLength = 0; - free(m_pBody); - m_pBody = nullptr; - } -} - -void CHttpRequest::Run() -{ - dbg_assert(gs_Initialized, "must initialize HTTP before running HTTP requests"); - int FinalState; - if(!BeforeInit()) - { - FinalState = HTTP_ERROR; - } - else - { - CURL *pHandle = curl_easy_init(); - FinalState = RunImpl(pHandle); - curl_easy_cleanup(pHandle); - } - - m_State = OnCompletion(FinalState); + free(m_pBody); } bool CHttpRequest::BeforeInit() @@ -167,98 +83,102 @@ bool CHttpRequest::BeforeInit() { if(fs_makedir_rec_for(m_aDestAbsolute) < 0) { - dbg_msg("http", "i/o error, cannot create folder for: %s", m_aDest); + log_error("http", "i/o error, cannot create folder for: %s", m_aDest); return false; } m_File = io_open(m_aDestAbsolute, IOFLAG_WRITE); if(!m_File) { - dbg_msg("http", "i/o error, cannot open file: %s", m_aDest); + log_error("http", "i/o error, cannot open file: %s", m_aDest); return false; } } return true; } -int CHttpRequest::RunImpl(CURL *pUser) +bool CHttpRequest::ConfigureHandle(void *pHandle) { - CURL *pHandle = (CURL *)pUser; - if(!pHandle) + CURL *pH = (CURL *)pHandle; + if(!BeforeInit()) { - return HTTP_ERROR; + return false; } if(g_Config.m_DbgCurl) { - curl_easy_setopt(pHandle, CURLOPT_VERBOSE, 1L); - curl_easy_setopt(pHandle, CURLOPT_DEBUGFUNCTION, CurlDebug); + curl_easy_setopt(pH, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(pH, CURLOPT_DEBUGFUNCTION, CurlDebug); } long Protocols = CURLPROTO_HTTPS; if(g_Config.m_HttpAllowInsecure) { Protocols |= CURLPROTO_HTTP; } - char aErr[CURL_ERROR_SIZE]; - curl_easy_setopt(pHandle, CURLOPT_ERRORBUFFER, aErr); - curl_easy_setopt(pHandle, CURLOPT_CONNECTTIMEOUT_MS, m_Timeout.ConnectTimeoutMs); - curl_easy_setopt(pHandle, CURLOPT_TIMEOUT_MS, m_Timeout.TimeoutMs); - curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_LIMIT, m_Timeout.LowSpeedLimit); - curl_easy_setopt(pHandle, CURLOPT_LOW_SPEED_TIME, m_Timeout.LowSpeedTime); + curl_easy_setopt(pH, CURLOPT_ERRORBUFFER, m_aErr); + + curl_easy_setopt(pH, CURLOPT_CONNECTTIMEOUT_MS, m_Timeout.ConnectTimeoutMs); + curl_easy_setopt(pH, CURLOPT_TIMEOUT_MS, m_Timeout.TimeoutMs); + curl_easy_setopt(pH, CURLOPT_LOW_SPEED_LIMIT, m_Timeout.LowSpeedLimit); + curl_easy_setopt(pH, CURLOPT_LOW_SPEED_TIME, m_Timeout.LowSpeedTime); if(m_MaxResponseSize >= 0) { - curl_easy_setopt(pHandle, CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)m_MaxResponseSize); + curl_easy_setopt(pH, CURLOPT_MAXFILESIZE_LARGE, (curl_off_t)m_MaxResponseSize); } - curl_easy_setopt(pHandle, CURLOPT_SHARE, gs_pShare); // ‘CURLOPT_PROTOCOLS’ is deprecated: since 7.85.0. Use CURLOPT_PROTOCOLS_STR // Wait until all platforms have 7.85.0 #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif - curl_easy_setopt(pHandle, CURLOPT_PROTOCOLS, Protocols); + curl_easy_setopt(pH, CURLOPT_PROTOCOLS, Protocols); #ifdef __GNUC__ #pragma GCC diagnostic pop #endif - curl_easy_setopt(pHandle, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(pHandle, CURLOPT_MAXREDIRS, 4L); - curl_easy_setopt(pHandle, CURLOPT_FAILONERROR, 1L); - curl_easy_setopt(pHandle, CURLOPT_URL, m_aUrl); - curl_easy_setopt(pHandle, CURLOPT_NOSIGNAL, 1L); - curl_easy_setopt(pHandle, CURLOPT_USERAGENT, GAME_NAME " " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")"); - curl_easy_setopt(pHandle, CURLOPT_ACCEPT_ENCODING, ""); // Use any compression algorithm supported by libcurl. - - curl_easy_setopt(pHandle, CURLOPT_WRITEDATA, this); - curl_easy_setopt(pHandle, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(pHandle, CURLOPT_NOPROGRESS, 0L); - curl_easy_setopt(pHandle, CURLOPT_PROGRESSDATA, this); + curl_easy_setopt(pH, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(pH, CURLOPT_MAXREDIRS, 4L); + if(m_FailOnErrorStatus) + { + curl_easy_setopt(pH, CURLOPT_FAILONERROR, 1L); + } + curl_easy_setopt(pH, CURLOPT_URL, m_aUrl); + curl_easy_setopt(pH, CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(pH, CURLOPT_USERAGENT, GAME_NAME " " GAME_RELEASE_VERSION " (" CONF_PLATFORM_STRING "; " CONF_ARCH_STRING ")"); + curl_easy_setopt(pH, CURLOPT_ACCEPT_ENCODING, ""); // Use any compression algorithm supported by libcurl. + + curl_easy_setopt(pH, CURLOPT_HEADERDATA, this); + curl_easy_setopt(pH, CURLOPT_HEADERFUNCTION, HeaderCallback); + curl_easy_setopt(pH, CURLOPT_WRITEDATA, this); + curl_easy_setopt(pH, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(pH, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(pH, CURLOPT_PROGRESSDATA, this); // ‘CURLOPT_PROGRESSFUNCTION’ is deprecated: since 7.32.0. Use CURLOPT_XFERINFOFUNCTION // See problems with curl_off_t type in header file in https://github.com/ddnet/ddnet/pull/6185/ #ifdef __GNUC__ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #endif - curl_easy_setopt(pHandle, CURLOPT_PROGRESSFUNCTION, ProgressCallback); + curl_easy_setopt(pH, CURLOPT_PROGRESSFUNCTION, ProgressCallback); #ifdef __GNUC__ #pragma GCC diagnostic pop #endif - curl_easy_setopt(pHandle, CURLOPT_IPRESOLVE, m_IpResolve == IPRESOLVE::V4 ? CURL_IPRESOLVE_V4 : m_IpResolve == IPRESOLVE::V6 ? CURL_IPRESOLVE_V6 : CURL_IPRESOLVE_WHATEVER); + curl_easy_setopt(pH, CURLOPT_IPRESOLVE, m_IpResolve == IPRESOLVE::V4 ? CURL_IPRESOLVE_V4 : m_IpResolve == IPRESOLVE::V6 ? CURL_IPRESOLVE_V6 : CURL_IPRESOLVE_WHATEVER); if(g_Config.m_Bindaddr[0] != '\0') { - curl_easy_setopt(pHandle, CURLOPT_INTERFACE, g_Config.m_Bindaddr); + curl_easy_setopt(pH, CURLOPT_INTERFACE, g_Config.m_Bindaddr); } if(curl_version_info(CURLVERSION_NOW)->version_num < 0x074400) { // Causes crashes, see https://github.com/ddnet/ddnet/issues/4342. // No longer a problem in curl 7.68 and above, and 0x44 = 68. - curl_easy_setopt(pHandle, CURLOPT_FORBID_REUSE, 1L); + curl_easy_setopt(pH, CURLOPT_FORBID_REUSE, 1L); } #ifdef CONF_PLATFORM_ANDROID - curl_easy_setopt(pHandle, CURLOPT_CAINFO, "data/cacert.pem"); + curl_easy_setopt(pH, CURLOPT_CAINFO, "data/cacert.pem"); #endif switch(m_Type) @@ -266,7 +186,7 @@ int CHttpRequest::RunImpl(CURL *pUser) case REQUEST::GET: break; case REQUEST::HEAD: - curl_easy_setopt(pHandle, CURLOPT_NOBODY, 1L); + curl_easy_setopt(pH, CURLOPT_NOBODY, 1L); break; case REQUEST::POST: case REQUEST::POST_JSON: @@ -278,29 +198,60 @@ int CHttpRequest::RunImpl(CURL *pUser) { Header("Content-Type:"); } - curl_easy_setopt(pHandle, CURLOPT_POSTFIELDS, m_pBody); - curl_easy_setopt(pHandle, CURLOPT_POSTFIELDSIZE, m_BodyLength); + curl_easy_setopt(pH, CURLOPT_POSTFIELDS, m_pBody); + curl_easy_setopt(pH, CURLOPT_POSTFIELDSIZE, m_BodyLength); break; } - curl_easy_setopt(pHandle, CURLOPT_HTTPHEADER, m_pHeaders); + curl_easy_setopt(pH, CURLOPT_HTTPHEADER, m_pHeaders); + + return true; +} + +size_t CHttpRequest::OnHeader(char *pHeader, size_t HeaderSize) +{ + // `pHeader` is NOT null-terminated. + // `pHeader` has a trailing newline. + + if(HeaderSize <= 1) + { + m_HeadersEnded = true; + return HeaderSize; + } + if(m_HeadersEnded) + { + // redirect, clear old headers + m_HeadersEnded = false; + m_ResultDate = {}; + m_ResultLastModified = {}; + } + + static const char DATE[] = "Date: "; + static const char LAST_MODIFIED[] = "Last-Modified: "; - if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::ALL) - dbg_msg("http", "fetching %s", m_aUrl); - m_State = HTTP_RUNNING; - int Ret = curl_easy_perform(pHandle); - if(Ret != CURLE_OK) + // Trailing newline and null termination evens out. + if(HeaderSize - 1 >= sizeof(DATE) - 1 && str_startswith_nocase(pHeader, DATE)) { - if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::FAILURE) - dbg_msg("http", "%s failed. libcurl error (%d): %s", m_aUrl, (int)Ret, aErr); - return (Ret == CURLE_ABORTED_BY_CALLBACK) ? HTTP_ABORTED : HTTP_ERROR; + char aValue[128]; + str_truncate(aValue, sizeof(aValue), pHeader + (sizeof(DATE) - 1), HeaderSize - (sizeof(DATE) - 1) - 1); + int64_t Value = curl_getdate(aValue, nullptr); + if(Value != -1) + { + m_ResultDate = Value; + } } - else + if(HeaderSize - 1 >= sizeof(LAST_MODIFIED) - 1 && str_startswith_nocase(pHeader, LAST_MODIFIED)) { - if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::ALL) - dbg_msg("http", "task done %s", m_aUrl); - return HTTP_DONE; + char aValue[128]; + str_truncate(aValue, sizeof(aValue), pHeader + (sizeof(LAST_MODIFIED) - 1), HeaderSize - (sizeof(LAST_MODIFIED) - 1) - 1); + int64_t Value = curl_getdate(aValue, nullptr); + if(Value != -1) + { + m_ResultLastModified = Value; + } } + + return HeaderSize; } size_t CHttpRequest::OnData(char *pData, size_t DataSize) @@ -312,7 +263,7 @@ size_t CHttpRequest::OnData(char *pData, size_t DataSize) return 0; } - sha256_update(&m_ActualSha256, pData, DataSize); + sha256_update(&m_ActualSha256Ctx, pData, DataSize); if(!m_WriteToFile) { @@ -341,6 +292,12 @@ size_t CHttpRequest::OnData(char *pData, size_t DataSize) } } +size_t CHttpRequest::HeaderCallback(char *pData, size_t Size, size_t Number, void *pUser) +{ + dbg_assert(Size == 1, "invalid size parameter passed to header callback"); + return ((CHttpRequest *)pUser)->OnHeader(pData, Number); +} + size_t CHttpRequest::WriteCallback(char *pData, size_t Size, size_t Number, void *pUser) { return ((CHttpRequest *)pUser)->OnData(pData, Size * Number); @@ -351,30 +308,54 @@ int CHttpRequest::ProgressCallback(void *pUser, double DlTotal, double DlCurr, d CHttpRequest *pTask = (CHttpRequest *)pUser; pTask->m_Current.store(DlCurr, std::memory_order_relaxed); pTask->m_Size.store(DlTotal, std::memory_order_relaxed); - pTask->m_Progress.store((100 * DlCurr) / (DlTotal ? DlTotal : 1), std::memory_order_relaxed); + pTask->m_Progress.store(DlTotal == 0.0 ? 0 : (100 * DlCurr) / DlTotal, std::memory_order_relaxed); pTask->OnProgress(); return pTask->m_Abort ? -1 : 0; } -int CHttpRequest::OnCompletion(int State) +void CHttpRequest::OnCompletionInternal(void *pHandle, unsigned int Result) { - if(m_Abort) - State = HTTP_ABORTED; + if(pHandle) + { + CURL *pH = (CURL *)pHandle; + long StatusCode; + curl_easy_getinfo(pH, CURLINFO_RESPONSE_CODE, &StatusCode); + m_StatusCode = StatusCode; + } + + EHttpState State; + const CURLcode Code = static_cast(Result); + if(Code != CURLE_OK) + { + if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::FAILURE) + { + log_error("http", "%s failed. libcurl error (%u): %s", m_aUrl, Code, m_aErr); + } + State = (Code == CURLE_ABORTED_BY_CALLBACK) ? EHttpState::ABORTED : EHttpState::ERROR; + } + else + { + if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::ALL) + { + log_info("http", "task done: %s", m_aUrl); + } + State = EHttpState::DONE; + } - if(State == HTTP_DONE && m_ExpectedSha256 != SHA256_ZEROED) + if(State == EHttpState::DONE) { - const SHA256_DIGEST ActualSha256 = sha256_finish(&m_ActualSha256); - if(ActualSha256 != m_ExpectedSha256) + m_ActualSha256 = sha256_finish(&m_ActualSha256Ctx); + if(m_ExpectedSha256 != SHA256_ZEROED && m_ActualSha256 != m_ExpectedSha256) { if(g_Config.m_DbgCurl || m_LogProgress >= HTTPLOG::FAILURE) { char aActualSha256[SHA256_MAXSTRSIZE]; - sha256_str(ActualSha256, aActualSha256, sizeof(aActualSha256)); + sha256_str(m_ActualSha256, aActualSha256, sizeof(aActualSha256)); char aExpectedSha256[SHA256_MAXSTRSIZE]; sha256_str(m_ExpectedSha256, aExpectedSha256, sizeof(aExpectedSha256)); - dbg_msg("http", "SHA256 mismatch: got=%s, expected=%s, url=%s", aActualSha256, aExpectedSha256, m_aUrl); + log_error("http", "SHA256 mismatch: got=%s, expected=%s, url=%s", aActualSha256, aExpectedSha256, m_aUrl); } - State = HTTP_ERROR; + State = EHttpState::ERROR; } } @@ -382,16 +363,22 @@ int CHttpRequest::OnCompletion(int State) { if(m_File && io_close(m_File) != 0) { - dbg_msg("http", "i/o error, cannot close file: %s", m_aDest); - State = HTTP_ERROR; + log_error("http", "i/o error, cannot close file: %s", m_aDest); + State = EHttpState::ERROR; } + m_File = nullptr; - if(State == HTTP_ERROR || State == HTTP_ABORTED) + if(State == EHttpState::ERROR || State == EHttpState::ABORTED) { fs_remove(m_aDestAbsolute); } } - return State; + + // The globally visible state must be updated after OnCompletion has finished, + // or other threads may try to access the result of a completed HTTP request, + // before the result has been initialized/updated in OnCompletion. + OnCompletion(State); + m_State = State; } void CHttpRequest::WriteToFile(IStorage *pStorage, const char *pDest, int StorageType) @@ -413,14 +400,26 @@ void CHttpRequest::Header(const char *pNameColonValue) m_pHeaders = curl_slist_append((curl_slist *)m_pHeaders, pNameColonValue); } -void CHttpRequest::Result(unsigned char **ppResult, size_t *pResultLength) const +void CHttpRequest::Wait() { - if(m_WriteToFile || State() != HTTP_DONE) + using namespace std::chrono_literals; + + // This is so uncommon that polling just might work + for(;;) { - *ppResult = nullptr; - *pResultLength = 0; - return; + EHttpState State = m_State.load(std::memory_order_seq_cst); + if(State != EHttpState::QUEUED && State != EHttpState::RUNNING) + { + return; + } + std::this_thread::sleep_for(10ms); } +} + +void CHttpRequest::Result(unsigned char **ppResult, size_t *pResultLength) const +{ + dbg_assert(State() == EHttpState::DONE, "Request not done"); + dbg_assert(!m_WriteToFile, "Result not usable together with WriteToFile"); *ppResult = m_pBuffer; *pResultLength = m_ResponseLength; } @@ -430,9 +429,262 @@ json_value *CHttpRequest::ResultJson() const unsigned char *pResult; size_t ResultLength; Result(&pResult, &ResultLength); - if(!pResult) + return json_parse((char *)pResult, ResultLength); +} + +const SHA256_DIGEST &CHttpRequest::ResultSha256() const +{ + dbg_assert(State() == EHttpState::DONE, "Request not done"); + return m_ActualSha256; +} + +int CHttpRequest::StatusCode() const +{ + dbg_assert(State() == EHttpState::DONE, "Request not done"); + return m_StatusCode; +} + +std::optional CHttpRequest::ResultAgeSeconds() const +{ + dbg_assert(State() == EHttpState::DONE, "Request not done"); + if(!m_ResultDate || !m_ResultLastModified) { - return nullptr; + return {}; } - return json_parse((char *)pResult, ResultLength); + return *m_ResultDate - *m_ResultLastModified; +} + +std::optional CHttpRequest::ResultLastModified() const +{ + dbg_assert(State() == EHttpState::DONE, "Request not done"); + return m_ResultLastModified; +} + +bool CHttp::Init(std::chrono::milliseconds ShutdownDelay) +{ + m_ShutdownDelay = ShutdownDelay; + +#if !defined(CONF_FAMILY_WINDOWS) + // As a multithreaded application we have to tell curl to not install signal + // handlers and instead ignore SIGPIPE from OpenSSL ourselves. + signal(SIGPIPE, SIG_IGN); +#endif + m_pThread = thread_init(CHttp::ThreadMain, this, "http"); + + std::unique_lock Lock(m_Lock); + m_Cv.wait(Lock, [this]() { return m_State != CHttp::UNINITIALIZED; }); + if(m_State != CHttp::RUNNING) + { + return false; + } + + return true; +} + +void CHttp::ThreadMain(void *pUser) +{ + CHttp *pHttp = static_cast(pUser); + pHttp->RunLoop(); +} + +void CHttp::RunLoop() +{ + std::unique_lock Lock(m_Lock); + if(curl_global_init(CURL_GLOBAL_DEFAULT)) + { + log_error("http", "curl_global_init failed"); + m_State = CHttp::ERROR; + m_Cv.notify_all(); + return; + } + + m_pMultiH = curl_multi_init(); + if(!m_pMultiH) + { + log_error("http", "curl_multi_init failed"); + m_State = CHttp::ERROR; + m_Cv.notify_all(); + return; + } + + // print curl version + { + curl_version_info_data *pVersion = curl_version_info(CURLVERSION_NOW); + log_info("http", "libcurl version %s (compiled = " LIBCURL_VERSION ")", pVersion->version); + } + + m_State = CHttp::RUNNING; + m_Cv.notify_all(); + Lock.unlock(); + + while(m_State == CHttp::RUNNING) + { + static int s_NextTimeout = std::numeric_limits::max(); + int Events = 0; + const CURLMcode PollCode = curl_multi_poll(m_pMultiH, nullptr, 0, s_NextTimeout, &Events); + + // We may have been woken up for a shutdown + if(m_Shutdown) + { + auto Now = std::chrono::steady_clock::now(); + if(!m_ShutdownTime.has_value()) + { + m_ShutdownTime = Now + m_ShutdownDelay; + s_NextTimeout = m_ShutdownDelay.count(); + } + else if(m_ShutdownTime < Now || m_RunningRequests.empty()) + { + break; + } + } + + if(PollCode != CURLM_OK) + { + Lock.lock(); + log_error("http", "curl_multi_poll failed: %s", curl_multi_strerror(PollCode)); + m_State = CHttp::ERROR; + break; + } + + const CURLMcode PerformCode = curl_multi_perform(m_pMultiH, &Events); + if(PerformCode != CURLM_OK) + { + Lock.lock(); + log_error("http", "curl_multi_perform failed: %s", curl_multi_strerror(PerformCode)); + m_State = CHttp::ERROR; + break; + } + + struct CURLMsg *pMsg; + while((pMsg = curl_multi_info_read(m_pMultiH, &Events))) + { + if(pMsg->msg == CURLMSG_DONE) + { + auto RequestIt = m_RunningRequests.find(pMsg->easy_handle); + dbg_assert(RequestIt != m_RunningRequests.end(), "Running handle not added to map"); + auto pRequest = std::move(RequestIt->second); + m_RunningRequests.erase(RequestIt); + + pRequest->OnCompletionInternal(pMsg->easy_handle, pMsg->data.result); + curl_multi_remove_handle(m_pMultiH, pMsg->easy_handle); + curl_easy_cleanup(pMsg->easy_handle); + } + } + + decltype(m_PendingRequests) NewRequests = {}; + Lock.lock(); + std::swap(m_PendingRequests, NewRequests); + Lock.unlock(); + + while(!NewRequests.empty()) + { + auto &pRequest = NewRequests.front(); + if(g_Config.m_DbgCurl) + log_debug("http", "task: %s %s", CHttpRequest::GetRequestType(pRequest->m_Type), pRequest->m_aUrl); + + CURL *pEH = curl_easy_init(); + if(!pEH) + { + log_error("http", "curl_easy_init failed"); + goto error_init; + } + + if(!pRequest->ConfigureHandle(pEH)) + { + curl_easy_cleanup(pEH); + str_copy(pRequest->m_aErr, "Failed to initialize request"); + pRequest->OnCompletionInternal(nullptr, CURLE_ABORTED_BY_CALLBACK); + NewRequests.pop_front(); + continue; + } + + if(curl_multi_add_handle(m_pMultiH, pEH) != CURLM_OK) + { + log_error("http", "curl_multi_add_handle failed"); + goto error_configure; + } + + m_RunningRequests.emplace(pEH, std::move(pRequest)); + NewRequests.pop_front(); + continue; + + error_configure: + curl_easy_cleanup(pEH); + error_init: + Lock.lock(); + m_State = CHttp::ERROR; + break; + } + + // Only happens if m_State == ERROR, thus we already hold the lock + if(!NewRequests.empty()) + { + m_PendingRequests.insert(m_PendingRequests.end(), std::make_move_iterator(NewRequests.begin()), std::make_move_iterator(NewRequests.end())); + break; + } + } + + if(!Lock.owns_lock()) + Lock.lock(); + + bool Cleanup = m_State != CHttp::ERROR; + for(auto &pRequest : m_PendingRequests) + { + str_copy(pRequest->m_aErr, "Shutting down"); + pRequest->OnCompletionInternal(nullptr, CURLE_ABORTED_BY_CALLBACK); + } + + for(auto &ReqPair : m_RunningRequests) + { + auto &[pHandle, pRequest] = ReqPair; + + str_copy(pRequest->m_aErr, "Shutting down"); + pRequest->OnCompletionInternal(pHandle, CURLE_ABORTED_BY_CALLBACK); + + if(Cleanup) + { + curl_multi_remove_handle(m_pMultiH, pHandle); + curl_easy_cleanup(pHandle); + } + } + + if(Cleanup) + { + curl_multi_cleanup(m_pMultiH); + curl_global_cleanup(); + } +} + +void CHttp::Run(std::shared_ptr pRequest) +{ + std::shared_ptr pRequestImpl = std::static_pointer_cast(pRequest); + std::unique_lock Lock(m_Lock); + if(m_Shutdown || m_State == CHttp::ERROR) + { + str_copy(pRequestImpl->m_aErr, "Shutting down"); + pRequestImpl->OnCompletionInternal(nullptr, CURLE_ABORTED_BY_CALLBACK); + return; + } + m_Cv.wait(Lock, [this]() { return m_State != CHttp::UNINITIALIZED; }); + m_PendingRequests.emplace_back(pRequestImpl); + curl_multi_wakeup(m_pMultiH); +} + +void CHttp::Shutdown() +{ + std::unique_lock Lock(m_Lock); + if(m_Shutdown || m_State != CHttp::RUNNING) + return; + + m_Shutdown = true; + curl_multi_wakeup(m_pMultiH); +} + +CHttp::~CHttp() +{ + if(!m_pThread) + return; + + Shutdown(); + thread_wait(m_pThread); } diff --git a/src/engine/shared/http.h b/src/engine/shared/http.h index eb90fcaf11..86e53fac9f 100644 --- a/src/engine/shared/http.h +++ b/src/engine/shared/http.h @@ -7,17 +7,24 @@ #include #include +#include +#include +#include +#include +#include + +#include typedef struct _json_value json_value; class IStorage; -enum +enum class EHttpState { - HTTP_ERROR = -1, - HTTP_QUEUED, - HTTP_RUNNING, - HTTP_DONE, - HTTP_ABORTED, + ERROR = -1, + QUEUED, + RUNNING, + DONE, + ABORTED, }; enum class HTTPLOG @@ -42,8 +49,10 @@ struct CTimeout long LowSpeedTime; }; -class CHttpRequest : public IJob +class CHttpRequest : public IHttpRequest { + friend class CHttp; + enum class REQUEST { GET = 0, @@ -51,6 +60,24 @@ class CHttpRequest : public IJob POST, POST_JSON, }; + + static constexpr const char *GetRequestType(REQUEST Type) + { + switch(Type) + { + case REQUEST::GET: + return "GET"; + case REQUEST::HEAD: + return "HEAD"; + case REQUEST::POST: + case REQUEST::POST_JSON: + return "POST"; + } + + // Unreachable, maybe assert instead? + return "UNKNOWN"; + } + char m_aUrl[256] = {0}; void *m_pHeaders = nullptr; @@ -61,7 +88,8 @@ class CHttpRequest : public IJob int64_t m_MaxResponseSize = -1; REQUEST m_Type = REQUEST::GET; - SHA256_CTX m_ActualSha256; + SHA256_DIGEST m_ActualSha256 = SHA256_ZEROED; + SHA256_CTX m_ActualSha256Ctx; SHA256_DIGEST m_ExpectedSha256 = SHA256_ZEROED; bool m_WriteToFile = false; @@ -83,33 +111,48 @@ class CHttpRequest : public IJob HTTPLOG m_LogProgress = HTTPLOG::ALL; IPRESOLVE m_IpResolve = IPRESOLVE::WHATEVER; - std::atomic m_State{HTTP_QUEUED}; + bool m_FailOnErrorStatus = true; + + char m_aErr[256]; // 256 == CURL_ERROR_SIZE + std::atomic m_State{EHttpState::QUEUED}; std::atomic m_Abort{false}; - void Run() override; + int m_StatusCode = 0; + bool m_HeadersEnded = false; + std::optional m_ResultDate = {}; + std::optional m_ResultLastModified = {}; + // Abort the request with an error if `BeforeInit()` returns false. bool BeforeInit(); - int RunImpl(void *pUser); + bool ConfigureHandle(void *pHandle); // void * == CURL * + // `pHandle` can be nullptr if no handle was ever created for this request. + void OnCompletionInternal(void *pHandle, unsigned int Result); // void * == CURL *, unsigned int == CURLcode + // Abort the request if `OnHeader()` returns something other than + // `DataSize`. `pHeader` is NOT null-terminated. + size_t OnHeader(char *pHeader, size_t HeaderSize); // Abort the request if `OnData()` returns something other than // `DataSize`. size_t OnData(char *pData, size_t DataSize); static int ProgressCallback(void *pUser, double DlTotal, double DlCurr, double UlTotal, double UlCurr); + static size_t HeaderCallback(char *pData, size_t Size, size_t Number, void *pUser); static size_t WriteCallback(char *pData, size_t Size, size_t Number, void *pUser); protected: + // These run on the curl thread now, DO NOT STALL THE THREAD virtual void OnProgress() {} - virtual int OnCompletion(int State); + virtual void OnCompletion(EHttpState State) {} public: CHttpRequest(const char *pUrl); - ~CHttpRequest(); + virtual ~CHttpRequest(); void Timeout(CTimeout Timeout) { m_Timeout = Timeout; } void MaxResponseSize(int64_t MaxResponseSize) { m_MaxResponseSize = MaxResponseSize; } void LogProgress(HTTPLOG LogProgress) { m_LogProgress = LogProgress; } void IpResolve(IPRESOLVE IpResolve) { m_IpResolve = IpResolve; } + void FailOnErrorStatus(bool FailOnErrorStatus) { m_FailOnErrorStatus = FailOnErrorStatus; } void WriteToFile(IStorage *pStorage, const char *pDest, int StorageType); void ExpectSha256(const SHA256_DIGEST &Sha256) { m_ExpectedSha256 = Sha256; } void Head() { m_Type = REQUEST::HEAD; } @@ -156,11 +199,23 @@ class CHttpRequest : public IJob double Current() const { return m_Current.load(std::memory_order_relaxed); } double Size() const { return m_Size.load(std::memory_order_relaxed); } int Progress() const { return m_Progress.load(std::memory_order_relaxed); } - int State() const { return m_State; } + EHttpState State() const { return m_State; } + bool Done() const + { + EHttpState State = m_State; + return State != EHttpState::QUEUED && State != EHttpState::RUNNING; + } void Abort() { m_Abort = true; } + void Wait(); + void Result(unsigned char **ppResult, size_t *pResultLength) const; json_value *ResultJson() const; + const SHA256_DIGEST &ResultSha256() const; + + int StatusCode() const; + std::optional ResultAgeSeconds() const; + std::optional ResultLastModified() const; }; inline std::unique_ptr HttpHead(const char *pUrl) @@ -199,7 +254,44 @@ inline std::unique_ptr HttpPostJson(const char *pUrl, const char * return pResult; } -bool HttpInit(IStorage *pStorage); void EscapeUrl(char *pBuf, int Size, const char *pStr); bool HttpHasIpresolveBug(); + +// In an ideal world this would be a kernel interface +class CHttp : public IHttp +{ + enum EState + { + UNINITIALIZED, + RUNNING, + ERROR, + }; + + void *m_pThread = nullptr; + + std::mutex m_Lock{}; + std::condition_variable m_Cv{}; + std::atomic m_State = UNINITIALIZED; + std::deque> m_PendingRequests{}; + std::unordered_map> m_RunningRequests{}; // void * == CURL * + std::chrono::milliseconds m_ShutdownDelay{}; + std::optional> m_ShutdownTime{}; + std::atomic m_Shutdown = false; + + // Only to be used with curl_multi_wakeup + void *m_pMultiH = nullptr; // void * == CURLM * + + static void ThreadMain(void *pUser); + void RunLoop(); + +public: + // Startup + bool Init(std::chrono::milliseconds ShutdownDelay); + + // User + virtual void Run(std::shared_ptr pRequest) override; + void Shutdown() override; + ~CHttp(); +}; + #endif // ENGINE_SHARED_HTTP_H diff --git a/src/engine/shared/jobs.cpp b/src/engine/shared/jobs.cpp index 852243367c..0959c0a0de 100644 --- a/src/engine/shared/jobs.cpp +++ b/src/engine/shared/jobs.cpp @@ -1,95 +1,223 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include "jobs.h" +#include IJob::IJob() : - m_Status(STATE_PENDING) + m_pNext(nullptr), + m_State(STATE_QUEUED), + m_Abortable(false) { } IJob::~IJob() = default; -int IJob::Status() +IJob::EJobState IJob::State() const { - return m_Status.load(); + return m_State; +} + +bool IJob::Done() const +{ + EJobState State = m_State; + return State != STATE_QUEUED && State != STATE_RUNNING; +} + +bool IJob::Abort() +{ + if(!IsAbortable()) + return false; + + m_State = STATE_ABORTED; + return true; +} + +void IJob::Abortable(bool Abortable) +{ + m_Abortable = Abortable; +} + +bool IJob::IsAbortable() const +{ + return m_Abortable; } CJobPool::CJobPool() { - // empty the pool - m_Shutdown = false; - sphore_init(&m_Semaphore); - m_pFirstJob = 0; - m_pLastJob = 0; + m_Shutdown = true; } CJobPool::~CJobPool() { if(!m_Shutdown) { - Destroy(); + Shutdown(); } } void CJobPool::WorkerThread(void *pUser) { - CJobPool *pPool = (CJobPool *)pUser; + static_cast(pUser)->RunLoop(); +} - while(!pPool->m_Shutdown) +void CJobPool::RunLoop() +{ + while(true) { - std::shared_ptr pJob = 0; + // wait for job to become available + sphore_wait(&m_Semaphore); // fetch job from queue - sphore_wait(&pPool->m_Semaphore); + std::shared_ptr pJob = nullptr; { - CLockScope ls(pPool->m_Lock); - if(pPool->m_pFirstJob) + const CLockScope LockScope(m_Lock); + if(m_pFirstJob) { - pJob = pPool->m_pFirstJob; - pPool->m_pFirstJob = pPool->m_pFirstJob->m_pNext; + pJob = m_pFirstJob; + m_pFirstJob = m_pFirstJob->m_pNext; // allow remaining objects in list to destruct, even when current object stays alive pJob->m_pNext = nullptr; - if(!pPool->m_pFirstJob) - pPool->m_pLastJob = 0; + if(!m_pFirstJob) + m_pLastJob = nullptr; } } - // do the job if we have one if(pJob) { - RunBlocking(pJob.get()); + IJob::EJobState OldStateQueued = IJob::STATE_QUEUED; + if(!pJob->m_State.compare_exchange_strong(OldStateQueued, IJob::STATE_RUNNING)) + { + if(OldStateQueued == IJob::STATE_ABORTED) + { + // job was aborted before it was started + pJob->m_State = IJob::STATE_ABORTED; + continue; + } + dbg_assert(false, "Job state invalid. Job was reused or uninitialized."); + dbg_break(); + } + + // remember running jobs so we can abort them + { + const CLockScope LockScope(m_LockRunning); + m_RunningJobs.push_back(pJob); + } + pJob->Run(); + { + const CLockScope LockScope(m_LockRunning); + m_RunningJobs.erase(std::find(m_RunningJobs.begin(), m_RunningJobs.end(), pJob)); + } + + // do not change state to done if job was not completed successfully + IJob::EJobState OldStateRunning = IJob::STATE_RUNNING; + if(!pJob->m_State.compare_exchange_strong(OldStateRunning, IJob::STATE_DONE)) + { + if(OldStateRunning != IJob::STATE_ABORTED) + { + dbg_assert(false, "Job state invalid, must be either running or aborted"); + } + } + } + else if(m_Shutdown) + { + // shut down worker thread when pool is shutting down and no more jobs are left + break; } } } void CJobPool::Init(int NumThreads) { - // start threads - char aName[32]; + dbg_assert(m_Shutdown, "Job pool already running"); + m_Shutdown = false; + + const CLockScope LockScope(m_Lock); + sphore_init(&m_Semaphore); + m_pFirstJob = nullptr; + m_pLastJob = nullptr; + + // start worker threads + char aName[16]; // unix kernel length limit m_vpThreads.reserve(NumThreads); for(int i = 0; i < NumThreads; i++) { - str_format(aName, sizeof(aName), "CJobPool worker %d", i); + str_format(aName, sizeof(aName), "CJobPool W%d", i); m_vpThreads.push_back(thread_init(WorkerThread, this, aName)); } } -void CJobPool::Destroy() +void CJobPool::Shutdown() { + dbg_assert(!m_Shutdown, "Job pool already shut down"); m_Shutdown = true; + + // abort queued jobs + { + const CLockScope LockScope(m_Lock); + std::shared_ptr pJob = m_pFirstJob; + std::shared_ptr pPrev = nullptr; + while(pJob != nullptr) + { + std::shared_ptr pNext = pJob->m_pNext; + if(pJob->Abort()) + { + // only remove abortable jobs from queue + pJob->m_pNext = nullptr; + if(pPrev) + { + pPrev->m_pNext = pNext; + } + else + { + m_pFirstJob = pNext; + } + } + else + { + pPrev = pJob; + } + pJob = pNext; + } + m_pLastJob = pPrev; + } + + // abort running jobs + { + const CLockScope LockScope(m_LockRunning); + for(const std::shared_ptr &pJob : m_RunningJobs) + { + pJob->Abort(); + } + } + + // wake up all worker threads for(size_t i = 0; i < m_vpThreads.size(); i++) + { sphore_signal(&m_Semaphore); + } + + // wait for all worker threads to finish for(void *pThread : m_vpThreads) + { thread_wait(pThread); + } + m_vpThreads.clear(); sphore_destroy(&m_Semaphore); } void CJobPool::Add(std::shared_ptr pJob) { + if(m_Shutdown) + { + // no jobs are accepted when the job pool is already shutting down + pJob->Abort(); + return; + } + + // add job to queue { - CLockScope ls(m_Lock); - // add job to queue + const CLockScope LockScope(m_Lock); if(m_pLastJob) m_pLastJob->m_pNext = pJob; m_pLastJob = std::move(pJob); @@ -97,12 +225,6 @@ void CJobPool::Add(std::shared_ptr pJob) m_pFirstJob = m_pLastJob; } + // signal a worker thread that a job is available sphore_signal(&m_Semaphore); } - -void CJobPool::RunBlocking(IJob *pJob) -{ - pJob->m_Status = IJob::STATE_RUNNING; - pJob->Run(); - pJob->m_Status = IJob::STATE_DONE; -} diff --git a/src/engine/shared/jobs.h b/src/engine/shared/jobs.h index 8f347b16c3..fcc36d5ac9 100644 --- a/src/engine/shared/jobs.h +++ b/src/engine/shared/jobs.h @@ -7,36 +7,123 @@ #include #include +#include #include #include -class CJobPool; - +/** + * A job which runs in a worker thread of a job pool. + * + * @see CJobPool + */ class IJob { - friend CJobPool; + friend class CJobPool; + +public: + /** + * The state of a job in the job pool. + */ + enum EJobState + { + /** + * Job has been created/queued but not started on a worker thread yet. + */ + STATE_QUEUED = 0, + + /** + * Job is currently running on a worker thread. + */ + STATE_RUNNING, + + /** + * Job was completed successfully. + */ + STATE_DONE, + + /** + * Job was aborted. Note the job may or may not still be running while + * in this state. + * + * @see IsAbortable + */ + STATE_ABORTED, + }; private: std::shared_ptr m_pNext; + std::atomic m_State; + std::atomic m_Abortable; - std::atomic m_Status; +protected: + /** + * Performs tasks in a worker thread. + */ virtual void Run() = 0; + /** + * Sets whether this job can be aborted. + * + * @remark Has no effect if the job has already been aborted. + * + * @see IsAbortable + */ + void Abortable(bool Abortable); + public: IJob(); + virtual ~IJob(); + IJob(const IJob &Other) = delete; IJob &operator=(const IJob &Other) = delete; - virtual ~IJob(); - int Status(); - enum - { - STATE_PENDING = 0, - STATE_RUNNING, - STATE_DONE - }; + /** + * Returns the state of the job. + * + * @remark Accessing jobs in any other way that with the base functions of `IJob` + * is generally not thread-safe unless the job is in @link STATE_DONE @endlink + * or has not been enqueued yet. + * + * @return State of the job. + */ + EJobState State() const; + + /** + * Returns whether the job was completed, i.e. whether it's not still queued + * or running. + * + * @return `true` if the job is done, `false` otherwise. + */ + bool Done() const; + + /** + * Aborts the job, if it can be aborted. + * + * @return `true` if abort was accepted, `false` otherwise. + * + * @remark May be overridden to delegate abort to other jobs. Note that this + * function may be called from any thread and should be thread-safe. + */ + virtual bool Abort(); + + /** + * Returns whether the job can be aborted. Jobs that are abortable may have + * their state set to `STATE_ABORTED` at any point if the job was aborted. + * The job state should be checked periodically in the `Run` function and the + * job should terminate at the earliest, safe opportunity when aborted. + * Scheduled jobs which are not abortable are guaranteed to fully complete + * before the job pool is shut down. + * + * @return `true` if the job can be aborted, `false` otherwise. + */ + bool IsAbortable() const; }; +/** + * A job pool which runs jobs in one or more worker threads. + * + * @see IJob + */ class CJobPool { std::vector m_vpThreads; @@ -47,15 +134,41 @@ class CJobPool std::shared_ptr m_pFirstJob GUARDED_BY(m_Lock); std::shared_ptr m_pLastJob GUARDED_BY(m_Lock); + CLock m_LockRunning; + std::deque> m_RunningJobs GUARDED_BY(m_LockRunning); + static void WorkerThread(void *pUser) NO_THREAD_SAFETY_ANALYSIS; + void RunLoop() NO_THREAD_SAFETY_ANALYSIS; public: CJobPool(); ~CJobPool(); - void Init(int NumThreads); - void Destroy(); + /** + * Initializes the job pool with the given number of worker threads. + * + * @param NumTheads The number of worker threads. + * + * @remark Must be called on the main thread. + */ + void Init(int NumThreads) REQUIRES(!m_Lock); + + /** + * Shuts down the job pool. Aborts all abortable jobs. Then waits for all + * worker threads to complete all remaining queued jobs and terminate. + * + * @remark Must be called on the main thread. + */ + void Shutdown() REQUIRES(!m_Lock) REQUIRES(!m_LockRunning); + + /** + * Adds a job to the queue of the job pool. + * + * @param pJob The job to enqueue. + * + * @remark If the job pool is already shutting down, no additional jobs + * will be enqueue anymore. Abortable jobs will immediately be aborted. + */ void Add(std::shared_ptr pJob) REQUIRES(!m_Lock); - static void RunBlocking(IJob *pJob); }; #endif diff --git a/src/engine/shared/json.cpp b/src/engine/shared/json.cpp index 37338ff1bf..93660edccb 100644 --- a/src/engine/shared/json.cpp +++ b/src/engine/shared/json.cpp @@ -1,46 +1,46 @@ #include #include -const struct _json_value *json_object_get(const json_value *object, const char *index) +const struct _json_value *json_object_get(const json_value *pObject, const char *pIndex) { unsigned int i; - if(object->type != json_object) + if(pObject->type != json_object) return &json_value_none; - for(i = 0; i < object->u.object.length; ++i) - if(!str_comp(object->u.object.values[i].name, index)) - return object->u.object.values[i].value; + for(i = 0; i < pObject->u.object.length; ++i) + if(!str_comp(pObject->u.object.values[i].name, pIndex)) + return pObject->u.object.values[i].value; return &json_value_none; } -const struct _json_value *json_array_get(const json_value *array, int index) +const struct _json_value *json_array_get(const json_value *pArray, int Index) { - if(array->type != json_array || index >= (int)array->u.array.length) + if(pArray->type != json_array || Index >= (int)pArray->u.array.length) return &json_value_none; - return array->u.array.values[index]; + return pArray->u.array.values[Index]; } -int json_array_length(const json_value *array) +int json_array_length(const json_value *pArray) { - return array->u.array.length; + return pArray->u.array.length; } -const char *json_string_get(const json_value *string) +const char *json_string_get(const json_value *pString) { - return string->u.string.ptr; + return pString->u.string.ptr; } -int json_int_get(const json_value *integer) +int json_int_get(const json_value *pInteger) { - return integer->u.integer; + return pInteger->u.integer; } -int json_boolean_get(const json_value *boolean) +int json_boolean_get(const json_value *pBoolean) { - return boolean->u.boolean != 0; + return pBoolean->u.boolean != 0; } static char EscapeJsonChar(char c) diff --git a/src/engine/shared/jsonwriter.cpp b/src/engine/shared/jsonwriter.cpp index ca703bf49c..b47b3e4d00 100644 --- a/src/engine/shared/jsonwriter.cpp +++ b/src/engine/shared/jsonwriter.cpp @@ -81,7 +81,7 @@ void CJsonWriter::WriteIntValue(int Value) dbg_assert(CanWriteDatatype(), "Cannot write value here"); WriteIndent(false); char aBuf[32]; - str_from_int(Value, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", Value); WriteInternal(aBuf); CompleteDataType(); } diff --git a/src/engine/shared/kernel.cpp b/src/engine/shared/kernel.cpp index f578cb6850..bdeb46ee3a 100644 --- a/src/engine/shared/kernel.cpp +++ b/src/engine/shared/kernel.cpp @@ -1,23 +1,29 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ + #include + #include +#include + class CKernel : public IKernel { - enum - { - MAX_INTERFACES = 32, - }; - class CInterfaceInfo { public: - CInterfaceInfo() + CInterfaceInfo() : + m_pInterface(nullptr), + m_AutoDestroy(false) + { + m_aName[0] = '\0'; + } + + CInterfaceInfo(const char *pName, IInterface *pInterface, bool AutoDestroy) : + m_pInterface(pInterface), + m_AutoDestroy(AutoDestroy) { - m_aName[0] = 0; - m_pInterface = 0x0; - m_AutoDestroy = false; + str_copy(m_aName, pName); } char m_aName[64]; @@ -25,98 +31,64 @@ class CKernel : public IKernel bool m_AutoDestroy; }; - CInterfaceInfo m_aInterfaces[MAX_INTERFACES]; - int m_NumInterfaces; + std::vector m_vInterfaces; CInterfaceInfo *FindInterfaceInfo(const char *pName) { - for(int i = 0; i < m_NumInterfaces; i++) + for(CInterfaceInfo &Info : m_vInterfaces) { - if(str_comp(pName, m_aInterfaces[i].m_aName) == 0) - return &m_aInterfaces[i]; + if(str_comp(pName, Info.m_aName) == 0) + return &Info; } - return 0x0; + return nullptr; } public: - CKernel() - { - m_NumInterfaces = 0; - } + CKernel() = default; void Shutdown() override { - for(int i = m_NumInterfaces - 1; i >= 0; i--) + for(int i = (int)m_vInterfaces.size() - 1; i >= 0; --i) { - if(m_aInterfaces[i].m_AutoDestroy) - m_aInterfaces[i].m_pInterface->Shutdown(); + if(m_vInterfaces[i].m_AutoDestroy) + { + m_vInterfaces[i].m_pInterface->Shutdown(); + } } } ~CKernel() override { // delete interfaces in reverse order just the way it would happen to objects on the stack - for(int i = m_NumInterfaces - 1; i >= 0; i--) + for(int i = (int)m_vInterfaces.size() - 1; i >= 0; --i) { - if(m_aInterfaces[i].m_AutoDestroy) + if(m_vInterfaces[i].m_AutoDestroy) { - delete m_aInterfaces[i].m_pInterface; - m_aInterfaces[i].m_pInterface = 0; + delete m_vInterfaces[i].m_pInterface; + m_vInterfaces[i].m_pInterface = nullptr; } } } - bool RegisterInterfaceImpl(const char *pName, IInterface *pInterface, bool Destroy) override + void RegisterInterfaceImpl(const char *pName, IInterface *pInterface, bool Destroy) override { - // TODO: More error checks here - if(!pInterface) - { - dbg_msg("kernel", "ERROR: couldn't register interface %s. null pointer given", pName); - return false; - } - - if(m_NumInterfaces == MAX_INTERFACES) - { - dbg_msg("kernel", "ERROR: couldn't register interface '%s'. maximum of interfaces reached", pName); - return false; - } - - if(FindInterfaceInfo(pName) != 0) - { - dbg_msg("kernel", "ERROR: couldn't register interface '%s'. interface already exists", pName); - return false; - } + dbg_assert(str_length(pName) < (int)sizeof(CInterfaceInfo().m_aName), "Interface name too long"); + dbg_assert(FindInterfaceInfo(pName) == nullptr, "Duplicate interface name"); pInterface->m_pKernel = this; - m_aInterfaces[m_NumInterfaces].m_pInterface = pInterface; - str_copy(m_aInterfaces[m_NumInterfaces].m_aName, pName); - m_aInterfaces[m_NumInterfaces].m_AutoDestroy = Destroy; - m_NumInterfaces++; - - return true; + m_vInterfaces.emplace_back(pName, pInterface, Destroy); } - bool ReregisterInterfaceImpl(const char *pName, IInterface *pInterface) override + void ReregisterInterfaceImpl(const char *pName, IInterface *pInterface) override { - if(FindInterfaceInfo(pName) == 0) - { - dbg_msg("kernel", "ERROR: couldn't reregister interface '%s'. interface doesn't exist", pName); - return false; - } - + dbg_assert(FindInterfaceInfo(pName) != nullptr, "Cannot reregister interface that is not registered"); pInterface->m_pKernel = this; - - return true; } IInterface *RequestInterfaceImpl(const char *pName) override { CInterfaceInfo *pInfo = FindInterfaceInfo(pName); - if(!pInfo) - { - dbg_msg("kernel", "failed to find interface with the name '%s'", pName); - return 0; - } + dbg_assert(pInfo != nullptr, "Interface not found"); return pInfo->m_pInterface; } }; diff --git a/src/engine/shared/linereader.cpp b/src/engine/shared/linereader.cpp index d322ee2792..ae56cc39f9 100644 --- a/src/engine/shared/linereader.cpp +++ b/src/engine/shared/linereader.cpp @@ -4,81 +4,92 @@ #include -void CLineReader::Init(IOHANDLE File) +CLineReader::CLineReader() { - m_BufferMaxSize = sizeof(m_aBuffer) - 1; - m_BufferSize = 0; - m_BufferPos = 0; - m_File = File; + m_pBuffer = nullptr; } -char *CLineReader::Get() +CLineReader::~CLineReader() { - unsigned LineStart = m_BufferPos; - bool CRLFBreak = false; + free(m_pBuffer); +} - while(true) +bool CLineReader::OpenFile(IOHANDLE File) +{ + if(!File) { - if(m_BufferPos >= m_BufferSize) - { - // fetch more + return false; + } + char *pBuffer = io_read_all_str(File); + io_close(File); + if(pBuffer == nullptr) + { + return false; + } + OpenBuffer(pBuffer); + return true; +} - // move the remaining part to the front - unsigned Read; - unsigned Left = m_BufferSize - LineStart; +void CLineReader::OpenBuffer(char *pBuffer) +{ + dbg_assert(pBuffer != nullptr, "Line reader initialized without valid buffer"); - if(LineStart > m_BufferSize) - Left = 0; - if(Left) - mem_move(m_aBuffer, &m_aBuffer[LineStart], Left); - m_BufferPos = Left; + m_pBuffer = pBuffer; + m_BufferPos = 0; + m_ReadLastLine = false; + + // Skip UTF-8 BOM + if(m_pBuffer[0] == '\xEF' && m_pBuffer[1] == '\xBB' && m_pBuffer[2] == '\xBF') + { + m_BufferPos += 3; + } +} - // fill the buffer - Read = io_read(m_File, &m_aBuffer[m_BufferPos], m_BufferMaxSize - m_BufferPos); - m_BufferSize = Left + Read; - LineStart = 0; +const char *CLineReader::Get() +{ + dbg_assert(m_pBuffer != nullptr, "Line reader not initialized"); + if(m_ReadLastLine) + { + return nullptr; + } - if(!Read) + unsigned LineStart = m_BufferPos; + while(true) + { + if(m_pBuffer[m_BufferPos] == '\0' || m_pBuffer[m_BufferPos] == '\n' || (m_pBuffer[m_BufferPos] == '\r' && m_pBuffer[m_BufferPos + 1] == '\n')) + { + if(m_pBuffer[m_BufferPos] == '\0') { - if(Left) + m_ReadLastLine = true; + } + else + { + if(m_pBuffer[m_BufferPos] == '\r') { - m_aBuffer[Left] = 0; // return the last line - m_BufferPos = Left; - m_BufferSize = Left; - return m_aBuffer; + m_pBuffer[m_BufferPos] = '\0'; + ++m_BufferPos; } - else - return 0x0; // we are done! + m_pBuffer[m_BufferPos] = '\0'; + ++m_BufferPos; } - } - else - { - if(m_aBuffer[m_BufferPos] == '\n' || m_aBuffer[m_BufferPos] == '\r') + + if(!str_utf8_check(&m_pBuffer[LineStart])) { - // line found - if(m_aBuffer[m_BufferPos] == '\r') + // Skip lines containing invalid UTF-8 + if(m_ReadLastLine) { - if(m_BufferPos + 1 >= m_BufferSize) - { - // read more to get the connected '\n' - CRLFBreak = true; - ++m_BufferPos; - continue; - } - else if(m_aBuffer[m_BufferPos + 1] == '\n') - m_aBuffer[m_BufferPos++] = 0; + return nullptr; } - m_aBuffer[m_BufferPos++] = 0; - return &m_aBuffer[LineStart]; + LineStart = m_BufferPos; + continue; } - else if(CRLFBreak) + // Skip trailing empty line + if(m_ReadLastLine && m_pBuffer[LineStart] == '\0') { - if(m_aBuffer[m_BufferPos] == '\n') - m_aBuffer[m_BufferPos++] = 0; - return &m_aBuffer[LineStart]; + return nullptr; } - else - m_BufferPos++; + return &m_pBuffer[LineStart]; } + ++m_BufferPos; } } diff --git a/src/engine/shared/linereader.h b/src/engine/shared/linereader.h index 7d5f1c5ab4..4575b76592 100644 --- a/src/engine/shared/linereader.h +++ b/src/engine/shared/linereader.h @@ -4,17 +4,20 @@ #define ENGINE_SHARED_LINEREADER_H #include -// buffered stream for reading lines, should perhaps be something smaller +// buffered stream for reading lines class CLineReader { - char m_aBuffer[4 * 8192 + 1]; // 1 additional byte for null termination + char *m_pBuffer; unsigned m_BufferPos; - unsigned m_BufferSize; - unsigned m_BufferMaxSize; - IOHANDLE m_File; + bool m_ReadLastLine; public: - void Init(IOHANDLE File); - char *Get(); // Returned string is only valid until next Get() call + CLineReader(); + ~CLineReader(); + + bool OpenFile(IOHANDLE File); + void OpenBuffer(char *pBuffer); // Buffer must have been allocated with malloc, will be freed by the line reader + + const char *Get(); // Returned string is valid until the line reader is destroyed }; #endif diff --git a/src/engine/shared/map.cpp b/src/engine/shared/map.cpp index 605f95e3e9..c1f619ea4e 100644 --- a/src/engine/shared/map.cpp +++ b/src/engine/shared/map.cpp @@ -45,9 +45,9 @@ int CMap::GetItemSize(int Index) return m_DataFile.GetItemSize(Index); } -void *CMap::GetItem(int Index, int *pType, int *pID) +void *CMap::GetItem(int Index, int *pType, int *pId) { - return m_DataFile.GetItem(Index, pType, pID); + return m_DataFile.GetItem(Index, pType, pId); } void CMap::GetType(int Type, int *pStart, int *pNum) @@ -55,14 +55,14 @@ void CMap::GetType(int Type, int *pStart, int *pNum) m_DataFile.GetType(Type, pStart, pNum); } -int CMap::FindItemIndex(int Type, int ID) +int CMap::FindItemIndex(int Type, int Id) { - return m_DataFile.FindItemIndex(Type, ID); + return m_DataFile.FindItemIndex(Type, Id); } -void *CMap::FindItem(int Type, int ID) +void *CMap::FindItem(int Type, int Id) { - return m_DataFile.FindItem(Type, ID); + return m_DataFile.FindItem(Type, Id); } int CMap::NumItems() const @@ -106,8 +106,17 @@ bool CMap::Load(const char *pMapName) CMapItemLayerTilemap *pTilemap = reinterpret_cast(pLayer); if(pTilemap->m_Version >= CMapItemLayerTilemap::TILE_SKIP_MIN_VERSION) { - const size_t TilemapSize = (size_t)pTilemap->m_Width * pTilemap->m_Height * sizeof(CTile); + const size_t TilemapCount = (size_t)pTilemap->m_Width * pTilemap->m_Height; + const size_t TilemapSize = TilemapCount * sizeof(CTile); + + if(((int)TilemapCount / pTilemap->m_Width != pTilemap->m_Height) || (TilemapSize / sizeof(CTile) != TilemapCount)) + { + log_error("map/load", "map layer too big (%d * %d * %d causes an integer overflow)", pTilemap->m_Width, pTilemap->m_Height, sizeof(CTile)); + return false; + } CTile *pTiles = static_cast(malloc(TilemapSize)); + if(!pTiles) + return false; ExtractTiles(pTiles, (size_t)pTilemap->m_Width * pTilemap->m_Height, static_cast(NewDataFile.GetData(pTilemap->m_Data)), NewDataFile.GetDataSize(pTilemap->m_Data) / sizeof(CTile)); NewDataFile.ReplaceData(pTilemap->m_Data, reinterpret_cast(pTiles), TilemapSize); } diff --git a/src/engine/shared/map.h b/src/engine/shared/map.h index c104c6c0a6..03beb7edd9 100644 --- a/src/engine/shared/map.h +++ b/src/engine/shared/map.h @@ -25,10 +25,10 @@ class CMap : public IEngineMap int NumData() const override; int GetItemSize(int Index) override; - void *GetItem(int Index, int *pType = nullptr, int *pID = nullptr) override; + void *GetItem(int Index, int *pType = nullptr, int *pId = nullptr) override; void GetType(int Type, int *pStart, int *pNum) override; - int FindItemIndex(int Type, int ID) override; - void *FindItem(int Type, int ID) override; + int FindItemIndex(int Type, int Id) override; + void *FindItem(int Type, int Id) override; int NumItems() const override; bool Load(const char *pMapName) override; diff --git a/src/engine/shared/netban.cpp b/src/engine/shared/netban.cpp index 4f9d0ff441..0c42f4c326 100644 --- a/src/engine/shared/netban.cpp +++ b/src/engine/shared/netban.cpp @@ -208,20 +208,21 @@ typename CNetBan::CBan *CNetBan::CBanPool::Get(int Index) const } template -int CNetBan::Ban(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason) +int CNetBan::Ban(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason, bool VerbatimReason) { // do not ban localhost - if(NetMatch(pData, &m_LocalhostIPV4) || NetMatch(pData, &m_LocalhostIPV6)) + if(NetMatch(pData, &m_LocalhostIpV4) || NetMatch(pData, &m_LocalhostIpV6)) { Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", "ban failed (localhost)"); return -1; } - int Stamp = Seconds > 0 ? time_timestamp() + Seconds : CBanInfo::EXPIRES_NEVER; + int64_t Stamp = Seconds > 0 ? time_timestamp() + Seconds : static_cast(CBanInfo::EXPIRES_NEVER); // set up info CBanInfo Info = {0}; Info.m_Expires = Stamp; + Info.m_VerbatimReason = VerbatimReason; str_copy(Info.m_aReason, pReason); // check if it already exists @@ -231,7 +232,7 @@ int CNetBan::Ban(T *pBanPool, const typename T::CDataType *pData, int Seconds, c { // adjust the ban pBanPool->Update(pBan, &Info); - char aBuf[128]; + char aBuf[256]; MakeBanInfo(pBan, aBuf, sizeof(aBuf), MSGTYPE_LIST); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", aBuf); return 1; @@ -241,7 +242,7 @@ int CNetBan::Ban(T *pBanPool, const typename T::CDataType *pData, int Seconds, c pBan = pBanPool->Add(pData, &Info, &NetHash); if(pBan) { - char aBuf[128]; + char aBuf[256]; MakeBanInfo(pBan, aBuf, sizeof(aBuf), MSGTYPE_BANADD); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", aBuf); return 0; @@ -276,21 +277,21 @@ void CNetBan::Init(IConsole *pConsole, IStorage *pStorage) m_BanAddrPool.Reset(); m_BanRangePool.Reset(); - net_host_lookup("localhost", &m_LocalhostIPV4, NETTYPE_IPV4); - net_host_lookup("localhost", &m_LocalhostIPV6, NETTYPE_IPV6); + net_host_lookup("localhost", &m_LocalhostIpV4, NETTYPE_IPV4); + net_host_lookup("localhost", &m_LocalhostIpV6, NETTYPE_IPV6); Console()->Register("ban", "s[ip|id] ?i[minutes] r[reason]", CFGFLAG_SERVER | CFGFLAG_MASTER | CFGFLAG_STORE, ConBan, this, "Ban ip for x minutes for any reason"); Console()->Register("ban_range", "s[first ip] s[last ip] ?i[minutes] r[reason]", CFGFLAG_SERVER | CFGFLAG_MASTER | CFGFLAG_STORE, ConBanRange, this, "Ban ip range for x minutes for any reason"); Console()->Register("unban", "s[ip|entry]", CFGFLAG_SERVER | CFGFLAG_MASTER | CFGFLAG_STORE, ConUnban, this, "Unban ip/banlist entry"); Console()->Register("unban_range", "s[first ip] s[last ip]", CFGFLAG_SERVER | CFGFLAG_MASTER | CFGFLAG_STORE, ConUnbanRange, this, "Unban ip range"); Console()->Register("unban_all", "", CFGFLAG_SERVER | CFGFLAG_MASTER | CFGFLAG_STORE, ConUnbanAll, this, "Unban all entries"); - Console()->Register("bans", "?i[page]", CFGFLAG_SERVER | CFGFLAG_MASTER, ConBans, this, "Show banlist (page 0 by default, 20 entries per page)"); + Console()->Register("bans", "?i[page]", CFGFLAG_SERVER | CFGFLAG_MASTER, ConBans, this, "Show banlist (page 1 by default, 20 entries per page)"); Console()->Register("bans_save", "s[file]", CFGFLAG_SERVER | CFGFLAG_MASTER | CFGFLAG_STORE, ConBansSave, this, "Save banlist in a file"); } void CNetBan::Update() { - int Now = time_timestamp(); + int64_t Now = time_timestamp(); // remove expired bans char aBuf[256], aNetStr[256]; @@ -308,15 +309,15 @@ void CNetBan::Update() } } -int CNetBan::BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason) +int CNetBan::BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason, bool VerbatimReason) { - return Ban(&m_BanAddrPool, pAddr, Seconds, pReason); + return Ban(&m_BanAddrPool, pAddr, Seconds, pReason, VerbatimReason); } int CNetBan::BanRange(const CNetRange *pRange, int Seconds, const char *pReason) { if(pRange->IsValid()) - return Ban(&m_BanRangePool, pRange, Seconds, pReason); + return Ban(&m_BanRangePool, pRange, Seconds, pReason, false); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", "ban failed (invalid range)"); return -1; @@ -414,7 +415,7 @@ void CNetBan::ConBan(IConsole::IResult *pResult, void *pUser) NETADDR Addr; if(net_addr_from_str(&Addr, pStr) == 0) - pThis->BanAddr(&Addr, Minutes * 60, pReason); + pThis->BanAddr(&Addr, Minutes * 60, pReason, false); else pThis->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", "ban error (invalid network address)"); } @@ -478,13 +479,31 @@ void CNetBan::ConBans(IConsole::IResult *pResult, void *pUser) { CNetBan *pThis = static_cast(pUser); - int Page = pResult->NumArguments() > 0 ? pResult->GetInteger(0) : 0; + int Page = pResult->NumArguments() > 0 ? pResult->GetInteger(0) : 1; static const int s_EntriesPerPage = 20; - const int Start = Page * s_EntriesPerPage; - const int End = (Page + 1) * s_EntriesPerPage; + const int Start = (Page - 1) * s_EntriesPerPage; + const int End = Page * s_EntriesPerPage; + const int NumBans = pThis->m_BanAddrPool.Num() + pThis->m_BanRangePool.Num(); + const int NumPages = NumBans / s_EntriesPerPage + 1; - int Count = 0; char aBuf[256], aMsg[256]; + + if(NumBans == 0) + { + pThis->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", "The ban list is empty."); + + return; + } + + if(Page <= 0 || Page > NumPages) + { + str_format(aMsg, sizeof(aMsg), "Invalid page number. There %s %d %s available.", NumPages == 1 ? "is" : "are", NumPages, NumPages == 1 ? "page" : "pages"); + pThis->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", aMsg); + + return; + } + + int Count = 0; for(CBanAddr *pBan = pThis->m_BanAddrPool.First(); pBan; pBan = pBan->m_pNext, Count++) { if(Count < Start || Count >= End) @@ -505,7 +524,7 @@ void CNetBan::ConBans(IConsole::IResult *pResult, void *pUser) str_format(aMsg, sizeof(aMsg), "#%i %s", Count, aBuf); pThis->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", aMsg); } - str_format(aMsg, sizeof(aMsg), "%d %s, showing entries %d - %d", Count, Count == 1 ? "ban" : "bans", Start, End - 1); + str_format(aMsg, sizeof(aMsg), "%d %s, showing entries %d - %d (page %d/%d)", Count, Count == 1 ? "ban" : "bans", Start, End > Count ? Count - 1 : End - 1, Page, NumPages); pThis->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "net_ban", aMsg); } @@ -522,7 +541,7 @@ void CNetBan::ConBansSave(IConsole::IResult *pResult, void *pUser) return; } - int Now = time_timestamp(); + int64_t Now = time_timestamp(); char aAddrStr1[NETADDR_MAXSTRSIZE], aAddrStr2[NETADDR_MAXSTRSIZE]; for(CBanAddr *pBan = pThis->m_BanAddrPool.First(); pBan; pBan = pBan->m_pNext) { diff --git a/src/engine/shared/netban.h b/src/engine/shared/netban.h index 6de37e51d9..f51afc07c3 100644 --- a/src/engine/shared/netban.h +++ b/src/engine/shared/netban.h @@ -77,10 +77,11 @@ class CNetBan enum { EXPIRES_NEVER = -1, - REASON_LENGTH = 64, + REASON_LENGTH = 128, }; - int m_Expires; + int64_t m_Expires; char m_aReason[REASON_LENGTH]; + bool m_VerbatimReason; }; template @@ -150,7 +151,7 @@ class CNetBan template void MakeBanInfo(const CBan *pBan, char *pBuf, unsigned BuffSize, int Type) const; template - int Ban(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason); + int Ban(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason, bool VerbatimReason); template int Unban(T *pBanPool, const typename T::CDataType *pData); @@ -158,7 +159,7 @@ class CNetBan class IStorage *m_pStorage; CBanAddrPool m_BanAddrPool; CBanRangePool m_BanRangePool; - NETADDR m_LocalhostIPV4, m_LocalhostIPV6; + NETADDR m_LocalhostIpV4, m_LocalhostIpV6; public: enum @@ -176,7 +177,7 @@ class CNetBan void Init(class IConsole *pConsole, class IStorage *pStorage); void Update(); - virtual int BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason); + virtual int BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason, bool VerbatimReason); virtual int BanRange(const CNetRange *pRange, int Seconds, const char *pReason); int UnbanByAddr(const NETADDR *pAddr); int UnbanByRange(const CNetRange *pRange); @@ -227,7 +228,7 @@ void CNetBan::MakeBanInfo(const CBan *pBan, char *pBuf, unsigned BuffSize, in } // add info part - if(pBan->m_Info.m_Expires != CBanInfo::EXPIRES_NEVER) + if(!pBan->m_Info.m_VerbatimReason && pBan->m_Info.m_Expires != CBanInfo::EXPIRES_NEVER) { int Mins = ((pBan->m_Info.m_Expires - time_timestamp()) + 59) / 60; if(Mins <= 1) diff --git a/src/engine/shared/network.cpp b/src/engine/shared/network.cpp index 92e87ce8a0..d427c153d1 100644 --- a/src/engine/shared/network.cpp +++ b/src/engine/shared/network.cpp @@ -8,16 +8,26 @@ const unsigned char SECURITY_TOKEN_MAGIC[4] = {'T', 'K', 'E', 'N'}; +SECURITY_TOKEN ToSecurityToken(const unsigned char *pData) +{ + return bytes_be_to_uint(pData); +} + +void WriteSecurityToken(unsigned char *pData, SECURITY_TOKEN Token) +{ + uint_to_bytes_be(pData, Token); +} + void CNetRecvUnpacker::Clear() { m_Valid = false; } -void CNetRecvUnpacker::Start(const NETADDR *pAddr, CNetConnection *pConnection, int ClientID) +void CNetRecvUnpacker::Start(const NETADDR *pAddr, CNetConnection *pConnection, int ClientId) { m_Addr = *pAddr; m_pConnection = pConnection; - m_ClientID = ClientID; + m_ClientId = ClientId; m_CurrentChunk = 0; m_Valid = true; } @@ -82,7 +92,7 @@ int CNetRecvUnpacker::FetchChunk(CNetChunk *pChunk) } // fill in the info - pChunk->m_ClientID = m_ClientID; + pChunk->m_ClientId = m_ClientId; pChunk->m_Address = m_Addr; pChunk->m_Flags = Header.m_Flags; pChunk->m_DataSize = Header.m_Size; @@ -131,13 +141,13 @@ void CNetBase::SendPacket(NETSOCKET Socket, NETADDR *pAddr, CNetPacketConstruct if(Sixup) { HeaderSize += sizeof(SecurityToken); - mem_copy(&aBuffer[3], &SecurityToken, sizeof(SecurityToken)); + WriteSecurityToken(aBuffer + 3, SecurityToken); } else if(SecurityToken != NET_SECURITY_TOKEN_UNSUPPORTED) { // append security token // if SecurityToken is NET_SECURITY_TOKEN_UNKNOWN we will still append it hoping to negotiate it - mem_copy(&pPacket->m_aChunkData[pPacket->m_DataSize], &SecurityToken, sizeof(SecurityToken)); + WriteSecurityToken(pPacket->m_aChunkData + pPacket->m_DataSize, SecurityToken); pPacket->m_DataSize += sizeof(SecurityToken); } @@ -223,8 +233,8 @@ int CNetBase::UnpackPacket(unsigned char *pBuffer, int Size, CNetPacketConstruct if(Sixup) { - mem_copy(pSecurityToken, &pBuffer[1], sizeof(*pSecurityToken)); - mem_copy(pResponseToken, &pBuffer[5], sizeof(*pResponseToken)); + *pSecurityToken = ToSecurityToken(pBuffer + 1); + *pResponseToken = ToSecurityToken(pBuffer + 5); } pPacket->m_Flags = NET_PACKETFLAG_CONNLESS; @@ -264,7 +274,7 @@ int CNetBase::UnpackPacket(unsigned char *pBuffer, int Size, CNetPacketConstruct Flags |= NET_PACKETFLAG_COMPRESSION; pPacket->m_Flags = Flags; - mem_copy(pSecurityToken, &pBuffer[3], sizeof(*pSecurityToken)); + *pSecurityToken = ToSecurityToken(pBuffer + 3); } if(pPacket->m_Flags & NET_PACKETFLAG_COMPRESSION) @@ -288,6 +298,18 @@ int CNetBase::UnpackPacket(unsigned char *pBuffer, int Size, CNetPacketConstruct return -1; } + // set the response token (a bit hacky because this function shouldn't know about control packets) + if(pPacket->m_Flags & NET_PACKETFLAG_CONTROL) + { + if(pPacket->m_DataSize >= 5) // control byte + token + { + if(pPacket->m_aChunkData[0] == NET_CTRLMSG_CONNECT || pPacket->m_aChunkData[0] == NET_CTRLMSG_TOKEN) + { + *pResponseToken = ToSecurityToken(&pPacket->m_aChunkData[1]); + } + } + } + // log the data if(ms_DataLogRecv) { @@ -317,6 +339,19 @@ void CNetBase::SendControlMsg(NETSOCKET Socket, NETADDR *pAddr, int Ack, int Con CNetBase::SendPacket(Socket, pAddr, &Construct, SecurityToken, Sixup, true); } +void CNetBase::SendControlMsgWithToken7(NETSOCKET Socket, NETADDR *pAddr, TOKEN Token, int Ack, int ControlMsg, TOKEN MyToken, bool Extended) +{ + dbg_assert((Token & ~NET_TOKEN_MASK) == 0, "token out of range"); + dbg_assert((MyToken & ~NET_TOKEN_MASK) == 0, "resp token out of range"); + + unsigned char s_aRequestTokenBuf[NET_TOKENREQUEST_DATASIZE]; + s_aRequestTokenBuf[0] = (MyToken >> 24) & 0xff; + s_aRequestTokenBuf[1] = (MyToken >> 16) & 0xff; + s_aRequestTokenBuf[2] = (MyToken >> 8) & 0xff; + s_aRequestTokenBuf[3] = (MyToken)&0xff; + CNetBase::SendControlMsg(Socket, pAddr, 0, ControlMsg, s_aRequestTokenBuf, Extended ? sizeof(s_aRequestTokenBuf) : 4, Token, true); +} + unsigned char *CNetChunkHeader::Pack(unsigned char *pData, int Split) const { pData[0] = ((m_Flags & 3) << 6) | ((m_Size >> Split) & 0x3f); diff --git a/src/engine/shared/network.h b/src/engine/shared/network.h index 7ab0de5b5f..7cea700c10 100644 --- a/src/engine/shared/network.h +++ b/src/engine/shared/network.h @@ -65,10 +65,11 @@ enum NET_SEQUENCE_MASK = NET_MAX_SEQUENCE - 1, NET_CONNSTATE_OFFLINE = 0, - NET_CONNSTATE_CONNECT = 1, - NET_CONNSTATE_PENDING = 2, - NET_CONNSTATE_ONLINE = 3, - NET_CONNSTATE_ERROR = 4, + NET_CONNSTATE_TOKEN = 1, + NET_CONNSTATE_CONNECT = 2, + NET_CONNSTATE_PENDING = 3, + NET_CONNSTATE_ONLINE = 4, + NET_CONNSTATE_ERROR = 5, NET_PACKETFLAG_UNUSED = 1 << 0, NET_PACKETFLAG_TOKEN = 1 << 1, @@ -87,6 +88,7 @@ enum NET_CTRLMSG_CONNECTACCEPT = 2, NET_CTRLMSG_ACCEPT = 3, NET_CTRLMSG_CLOSE = 4, + NET_CTRLMSG_TOKEN = 5, NET_CONN_BUFFERSIZE = 1024 * 32, @@ -94,10 +96,20 @@ enum NET_ENUM_TERMINATOR }; +enum +{ + NET_TOKEN_MAX = 0xffffffff, + NET_TOKEN_NONE = NET_TOKEN_MAX, + NET_TOKEN_MASK = NET_TOKEN_MAX, + + NET_TOKENREQUEST_DATASIZE = 512, +}; typedef int SECURITY_TOKEN; +typedef unsigned int TOKEN; -SECURITY_TOKEN ToSecurityToken(unsigned char *pData); +SECURITY_TOKEN ToSecurityToken(const unsigned char *pData); +void WriteSecurityToken(unsigned char *pData, SECURITY_TOKEN Token); extern const unsigned char SECURITY_TOKEN_MAGIC[4]; @@ -107,17 +119,17 @@ enum NET_SECURITY_TOKEN_UNSUPPORTED = 0, }; -typedef int (*NETFUNC_DELCLIENT)(int ClientID, const char *pReason, void *pUser); -typedef int (*NETFUNC_NEWCLIENT_CON)(int ClientID, void *pUser); -typedef int (*NETFUNC_NEWCLIENT)(int ClientID, void *pUser, bool Sixup); -typedef int (*NETFUNC_NEWCLIENT_NOAUTH)(int ClientID, void *pUser); -typedef int (*NETFUNC_CLIENTREJOIN)(int ClientID, void *pUser); +typedef int (*NETFUNC_DELCLIENT)(int ClientId, const char *pReason, void *pUser); +typedef int (*NETFUNC_NEWCLIENT_CON)(int ClientId, void *pUser); +typedef int (*NETFUNC_NEWCLIENT)(int ClientId, void *pUser, bool Sixup); +typedef int (*NETFUNC_NEWCLIENT_NOAUTH)(int ClientId, void *pUser); +typedef int (*NETFUNC_CLIENTREJOIN)(int ClientId, void *pUser); struct CNetChunk { // -1 means that it's a stateless packet // 0 on the client means the server - int m_ClientID; + int m_ClientId; NETADDR m_Address; // only used when client_id == -1 int m_Flags; int m_DataSize; @@ -216,7 +228,10 @@ class CNetConnection unsigned short m_PeerAck; unsigned m_State; +public: SECURITY_TOKEN m_SecurityToken; + +private: int m_RemoteClosed; bool m_BlockCloseMsg; bool m_UnknownSeq; @@ -237,6 +252,12 @@ class CNetConnection NETSOCKET m_Socket; NETSTATS m_Stats; + char m_aPeerAddrStr[NETADDR_MAXSTRSIZE]; + // client 0.7 + static TOKEN GenerateToken7(const NETADDR *pPeerAddr); + class CNetBase *m_pNetBase; + bool IsSixup() { return m_Sixup; } + // void ResetStats(); void SetError(const char *pString); @@ -245,6 +266,7 @@ class CNetConnection int QueueChunkEx(int Flags, int DataSize, const void *pData, int Sequence); void SendConnect(); void SendControl(int ControlMsg, const void *pExtra, int ExtraSize); + void SendControlWithToken7(int ControlMsg, SECURITY_TOKEN ResponseToken); void ResendChunk(CNetChunkResend *pResend); void Resend(); @@ -252,21 +274,25 @@ class CNetConnection bool m_TimeoutProtected; bool m_TimeoutSituation; + void SetToken7(TOKEN Token); + void Reset(bool Rejoin = false); void Init(NETSOCKET Socket, bool BlockCloseMsg); int Connect(const NETADDR *pAddr, int NumAddrs); + int Connect7(const NETADDR *pAddr, int NumAddrs); void Disconnect(const char *pReason); int Update(); int Flush(); - int Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_TOKEN SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED); + int Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_TOKEN SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED, SECURITY_TOKEN ResponseToken = NET_SECURITY_TOKEN_UNSUPPORTED); int QueueChunk(int Flags, int DataSize, const void *pData); const char *ErrorString(); void SignalResend(); int State() const { return m_State; } const NETADDR *PeerAddress() const { return &m_PeerAddr; } + const char (*PeerAddressString() const)[NETADDR_MAXSTRSIZE] { return &m_aPeerAddrStr; } void ConnectAddresses(const NETADDR **ppAddrs, int *pNumAddrs) const { *ppAddrs = m_aConnectAddrs; @@ -334,13 +360,13 @@ class CNetRecvUnpacker NETADDR m_Addr; CNetConnection *m_pConnection; int m_CurrentChunk; - int m_ClientID; + int m_ClientId; CNetPacketConstruct m_Data; unsigned char m_aBuffer[NET_MAX_PACKETSIZE]; CNetRecvUnpacker() { Clear(); } void Clear(); - void Start(const NETADDR *pAddr, CNetConnection *pConnection, int ClientID); + void Start(const NETADDR *pAddr, CNetConnection *pConnection, int ClientId); int FetchChunk(CNetChunk *pChunk); }; @@ -365,7 +391,7 @@ class CNetServer CNetBan *m_pNetBan; CSlot m_aSlots[NET_MAX_CLIENTS]; int m_MaxClients; - int m_MaxClientsPerIP; + int m_MaxClientsPerIp; NETFUNC_NEWCLIENT m_pfnNewClient; NETFUNC_NEWCLIENT_NOAUTH m_pfnNewClientNoAuth; @@ -388,7 +414,7 @@ class CNetServer void OnTokenCtrlMsg(NETADDR &Addr, int ControlMsg, const CNetPacketConstruct &Packet); int OnSixupCtrlMsg(NETADDR &Addr, CNetChunk *pChunk, int ControlMsg, const CNetPacketConstruct &Packet, SECURITY_TOKEN &ResponseToken, SECURITY_TOKEN Token); void OnPreConnMsg(NETADDR &Addr, CNetPacketConstruct &Packet); - void OnConnCtrlMsg(NETADDR &Addr, int ClientID, int ControlMsg, const CNetPacketConstruct &Packet); + void OnConnCtrlMsg(NETADDR &Addr, int ClientId, int ControlMsg, const CNetPacketConstruct &Packet); bool ClientExists(const NETADDR &Addr) { return GetClientSlot(Addr) != -1; } int GetClientSlot(const NETADDR &Addr); void SendControl(NETADDR &Addr, int ControlMsg, const void *pExtra, int ExtraSize, SECURITY_TOKEN SecurityToken); @@ -403,7 +429,7 @@ class CNetServer int SetCallbacks(NETFUNC_NEWCLIENT pfnNewClient, NETFUNC_NEWCLIENT_NOAUTH pfnNewClientNoAuth, NETFUNC_CLIENTREJOIN pfnClientRejoin, NETFUNC_DELCLIENT pfnDelClient, void *pUser); // - bool Open(NETADDR BindAddr, CNetBan *pNetBan, int MaxClients, int MaxClientsPerIP); + bool Open(NETADDR BindAddr, CNetBan *pNetBan, int MaxClients, int MaxClientsPerIp); int Close(); // @@ -412,11 +438,12 @@ class CNetServer int Update(); // - int Drop(int ClientID, const char *pReason); + int Drop(int ClientId, const char *pReason); // status requests - const NETADDR *ClientAddr(int ClientID) const { return m_aSlots[ClientID].m_Connection.PeerAddress(); } - bool HasSecurityToken(int ClientID) const { return m_aSlots[ClientID].m_Connection.SecurityToken() != NET_SECURITY_TOKEN_UNSUPPORTED; } + const NETADDR *ClientAddr(int ClientId) const { return m_aSlots[ClientId].m_Connection.PeerAddress(); } + const char (*ClientAddrString(int ClientID) const)[NETADDR_MAXSTRSIZE] { return m_aSlots[ClientID].m_Connection.PeerAddressString(); } + bool HasSecurityToken(int ClientId) const { return m_aSlots[ClientId].m_Connection.SecurityToken() != NET_SECURITY_TOKEN_UNSUPPORTED; } NETADDR Address() const { return m_Address; } NETSOCKET Socket() const { return m_Socket; } CNetBan *NetBan() const { return m_pNetBan; } @@ -427,12 +454,12 @@ class CNetServer int SendConnlessSixup(CNetChunk *pChunk, SECURITY_TOKEN ResponseToken); // - void SetMaxClientsPerIP(int Max); - bool SetTimedOut(int ClientID, int OrigID); - void SetTimeoutProtected(int ClientID); + void SetMaxClientsPerIp(int Max); + bool SetTimedOut(int ClientId, int OrigId); + void SetTimeoutProtected(int ClientId); - int ResetErrorString(int ClientID); - const char *ErrorString(int ClientID); + int ResetErrorString(int ClientId); + const char *ErrorString(int ClientId); // anti spoof SECURITY_TOKEN GetGlobalToken(); @@ -466,16 +493,16 @@ class CNetConsole int Close(); // - int Recv(char *pLine, int MaxLength, int *pClientID = nullptr); - int Send(int ClientID, const char *pLine); + int Recv(char *pLine, int MaxLength, int *pClientId = nullptr); + int Send(int ClientId, const char *pLine); int Update(); // int AcceptClient(NETSOCKET Socket, const NETADDR *pAddr); - int Drop(int ClientID, const char *pReason); + int Drop(int ClientId, const char *pReason); // status requests - const NETADDR *ClientAddr(int ClientID) const { return m_aSlots[ClientID].m_Connection.PeerAddress(); } + const NETADDR *ClientAddr(int ClientId) const { return m_aSlots[ClientId].m_Connection.PeerAddress(); } CNetBan *NetBan() const { return m_pNetBan; } }; @@ -496,9 +523,10 @@ class CNetClient // connection state int Disconnect(const char *pReason); int Connect(const NETADDR *pAddr, int NumAddrs); + int Connect7(const NETADDR *pAddr, int NumAddrs); // communication - int Recv(CNetChunk *pChunk); + int Recv(CNetChunk *pChunk, SECURITY_TOKEN *pResponseToken, bool Sixup); int Send(CNetChunk *pChunk); // pumping @@ -515,8 +543,6 @@ class CNetClient int GotProblems(int64_t MaxLatency) const; const char *ErrorString() const; - bool SecurityTokenUnknown() { return m_Connection.SecurityToken() == NET_SECURITY_TOKEN_UNKNOWN; } - // stun void FeedStunServer(NETADDR StunServer); void RefreshStun(); @@ -538,6 +564,7 @@ class CNetBase static int Decompress(const void *pData, int DataSize, void *pOutput, int OutputSize); static void SendControlMsg(NETSOCKET Socket, NETADDR *pAddr, int Ack, int ControlMsg, const void *pExtra, int ExtraSize, SECURITY_TOKEN SecurityToken, bool Sixup = false); + static void SendControlMsgWithToken7(NETSOCKET Socket, NETADDR *pAddr, TOKEN Token, int Ack, int ControlMsg, TOKEN MyToken, bool Extended); static void SendPacketConnless(NETSOCKET Socket, NETADDR *pAddr, const void *pData, int DataSize, bool Extended, unsigned char aExtra[4]); static void SendPacket(NETSOCKET Socket, NETADDR *pAddr, CNetPacketConstruct *pPacket, SECURITY_TOKEN SecurityToken, bool Sixup = false, bool NoCompress = false); diff --git a/src/engine/shared/network_client.cpp b/src/engine/shared/network_client.cpp index a978154752..7d80272153 100644 --- a/src/engine/shared/network_client.cpp +++ b/src/engine/shared/network_client.cpp @@ -56,13 +56,19 @@ int CNetClient::Connect(const NETADDR *pAddr, int NumAddrs) return 0; } +int CNetClient::Connect7(const NETADDR *pAddr, int NumAddrs) +{ + m_Connection.Connect7(pAddr, NumAddrs); + return 0; +} + int CNetClient::ResetErrorString() { m_Connection.ResetErrorString(); return 0; } -int CNetClient::Recv(CNetChunk *pChunk) +int CNetClient::Recv(CNetChunk *pChunk, SECURITY_TOKEN *pResponseToken, bool Sixup) { while(true) { @@ -83,14 +89,17 @@ int CNetClient::Recv(CNetChunk *pChunk) { continue; } + if(Sixup) + Addr.type |= NETTYPE_TW7; - bool Sixup = false; - if(CNetBase::UnpackPacket(pData, Bytes, &m_RecvUnpacker.m_Data, Sixup) == 0) + SECURITY_TOKEN Token; + *pResponseToken = NET_SECURITY_TOKEN_UNKNOWN; + if(CNetBase::UnpackPacket(pData, Bytes, &m_RecvUnpacker.m_Data, Sixup, &Token, pResponseToken) == 0) { if(m_RecvUnpacker.m_Data.m_Flags & NET_PACKETFLAG_CONNLESS) { pChunk->m_Flags = NETSENDFLAG_CONNLESS; - pChunk->m_ClientID = -1; + pChunk->m_ClientId = -1; pChunk->m_Address = Addr; pChunk->m_DataSize = m_RecvUnpacker.m_Data.m_DataSize; pChunk->m_pData = m_RecvUnpacker.m_Data.m_aChunkData; @@ -103,7 +112,7 @@ int CNetClient::Recv(CNetChunk *pChunk) } else { - if(m_Connection.State() != NET_CONNSTATE_OFFLINE && m_Connection.State() != NET_CONNSTATE_ERROR && m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr)) + if(m_Connection.State() != NET_CONNSTATE_OFFLINE && m_Connection.State() != NET_CONNSTATE_ERROR && m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr, Token, *pResponseToken)) m_RecvUnpacker.Start(&Addr, &m_Connection, 0); } } @@ -128,7 +137,7 @@ int CNetClient::Send(CNetChunk *pChunk) else { int Flags = 0; - dbg_assert(pChunk->m_ClientID == 0, "erroneous client id"); + dbg_assert(pChunk->m_ClientId == 0, "erroneous client id"); if(pChunk->m_Flags & NETSENDFLAG_VITAL) Flags = NET_CHUNKFLAG_VITAL; diff --git a/src/engine/shared/network_conn.cpp b/src/engine/shared/network_conn.cpp index 50961465d8..2826b1a6e3 100644 --- a/src/engine/shared/network_conn.cpp +++ b/src/engine/shared/network_conn.cpp @@ -4,15 +4,11 @@ #include "network.h" #include -SECURITY_TOKEN ToSecurityToken(unsigned char *pData) -{ - return (int)pData[0] | (pData[1] << 8) | (pData[2] << 16) | (pData[3] << 24); -} - void CNetConnection::ResetStats() { mem_zero(&m_Stats, sizeof(m_Stats)); mem_zero(&m_PeerAddr, sizeof(m_PeerAddr)); + m_aPeerAddrStr[0] = '\0'; m_LastUpdateTime = 0; } @@ -201,6 +197,8 @@ int CNetConnection::Connect(const NETADDR *pAddr, int NumAddrs) // init connection Reset(); mem_zero(&m_PeerAddr, sizeof(m_PeerAddr)); + m_aPeerAddrStr[0] = '\0'; + for(int i = 0; i < NumAddrs; i++) { m_aConnectAddrs[i] = pAddr[i]; @@ -212,6 +210,51 @@ int CNetConnection::Connect(const NETADDR *pAddr, int NumAddrs) return 0; } +void CNetConnection::SendControlWithToken7(int ControlMsg, SECURITY_TOKEN ResponseToken) +{ + m_LastSendTime = time_get(); + + CNetBase::SendControlMsgWithToken7(m_Socket, &m_PeerAddr, ResponseToken, 0, ControlMsg, m_Token, true); +} + +int CNetConnection::Connect7(const NETADDR *pAddr, int NumAddrs) +{ + if(State() != NET_CONNSTATE_OFFLINE) + return -1; + + // init connection + Reset(); + mem_zero(&m_PeerAddr, sizeof(m_PeerAddr)); + for(int i = 0; i < NumAddrs; i++) + { + m_aConnectAddrs[i] = pAddr[i]; + } + m_LastRecvTime = time_get(); + m_NumConnectAddrs = NumAddrs; + m_PeerAddr = *pAddr; + SetToken7(GenerateToken7(pAddr)); + mem_zero(m_aErrorString, sizeof(m_aErrorString)); + m_State = NET_CONNSTATE_TOKEN; + SendControlWithToken7(NET_CTRLMSG_TOKEN, NET_TOKEN_NONE); + m_Sixup = true; + return 0; +} + +void CNetConnection::SetToken7(TOKEN Token) +{ + if(State() != NET_CONNSTATE_OFFLINE) + return; + + m_Token = Token; +} + +TOKEN CNetConnection::GenerateToken7(const NETADDR *pPeerAddr) +{ + TOKEN Token; + secure_random_fill(&Token, sizeof(Token)); + return Token; +} + void CNetConnection::Disconnect(const char *pReason) { if(State() == NET_CONNSTATE_OFFLINE) @@ -245,6 +288,7 @@ void CNetConnection::DirectInit(const NETADDR &Addr, SECURITY_TOKEN SecurityToke m_State = NET_CONNSTATE_ONLINE; m_PeerAddr = Addr; + net_addr_str(&Addr, m_aPeerAddrStr, sizeof(m_aPeerAddrStr), true); mem_zero(m_aErrorString, sizeof(m_aErrorString)); int64_t Now = time_get(); @@ -257,7 +301,7 @@ void CNetConnection::DirectInit(const NETADDR &Addr, SECURITY_TOKEN SecurityToke m_Sixup = Sixup; } -int CNetConnection::Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_TOKEN SecurityToken) +int CNetConnection::Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_TOKEN SecurityToken, SECURITY_TOKEN ResponseToken) { // Disregard packets from the wrong address, unless we don't know our peer yet. if(State() != NET_CONNSTATE_OFFLINE && State() != NET_CONNSTATE_CONNECT && *pAddr != m_PeerAddr) @@ -326,10 +370,10 @@ int CNetConnection::Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_ m_State = NET_CONNSTATE_ERROR; m_RemoteClosed = 1; - char aStr[128] = {0}; + char aStr[256] = {0}; if(pPacket->m_DataSize > 1) { - // make sure to sanitize the error string form the other party + // make sure to sanitize the error string from the other party str_copy(aStr, (char *)&pPacket->m_aChunkData[1], minimum(pPacket->m_DataSize, (int)sizeof(aStr))); str_sanitize_cc(aStr); } @@ -347,61 +391,80 @@ int CNetConnection::Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_ } else { - if(State() == NET_CONNSTATE_OFFLINE) + if(CtrlMsg == NET_CTRLMSG_TOKEN) { - if(CtrlMsg == NET_CTRLMSG_CONNECT) + if(State() == NET_CONNSTATE_TOKEN) { - if(net_addr_comp_noport(&m_PeerAddr, pAddr) == 0 && time_get() - m_LastUpdateTime < time_freq() * 3) - return 0; - - // send response and init connection - Reset(); - m_State = NET_CONNSTATE_PENDING; - m_PeerAddr = *pAddr; - mem_zero(m_aErrorString, sizeof(m_aErrorString)); - m_LastSendTime = Now; m_LastRecvTime = Now; - m_LastUpdateTime = Now; - if(m_SecurityToken == NET_SECURITY_TOKEN_UNKNOWN && pPacket->m_DataSize >= (int)(1 + sizeof(SECURITY_TOKEN_MAGIC) + sizeof(m_SecurityToken)) && !mem_comp(&pPacket->m_aChunkData[1], SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC))) - { - m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; - if(g_Config.m_Debug) - dbg_msg("security", "generated token %d", m_SecurityToken); - } - else - { - if(g_Config.m_Debug) - dbg_msg("security", "token not supported by client (packet size %d)", pPacket->m_DataSize); - m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; - } - SendControl(NET_CTRLMSG_CONNECTACCEPT, SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC)); - if(g_Config.m_Debug) - dbg_msg("connection", "got connection, sending connect+accept"); + m_State = NET_CONNSTATE_CONNECT; + m_SecurityToken = ResponseToken; + SendControlWithToken7(NET_CTRLMSG_CONNECT, m_SecurityToken); + dbg_msg("connection", "got token, replying, token=%x mytoken=%x", m_SecurityToken, m_Token); } + else if(g_Config.m_Debug) + dbg_msg("connection", "got token, token=%x", ResponseToken); } - else if(State() == NET_CONNSTATE_CONNECT) + else { - // connection made - if(CtrlMsg == NET_CTRLMSG_CONNECTACCEPT) + if(State() == NET_CONNSTATE_OFFLINE) { - m_PeerAddr = *pAddr; - if(m_SecurityToken == NET_SECURITY_TOKEN_UNKNOWN && pPacket->m_DataSize >= (int)(1 + sizeof(SECURITY_TOKEN_MAGIC) + sizeof(m_SecurityToken)) && !mem_comp(&pPacket->m_aChunkData[1], SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC))) + if(CtrlMsg == NET_CTRLMSG_CONNECT) { - m_SecurityToken = ToSecurityToken(&pPacket->m_aChunkData[1 + sizeof(SECURITY_TOKEN_MAGIC)]); + if(net_addr_comp_noport(&m_PeerAddr, pAddr) == 0 && time_get() - m_LastUpdateTime < time_freq() * 3) + return 0; + + // send response and init connection + Reset(); + m_State = NET_CONNSTATE_PENDING; + m_PeerAddr = *pAddr; + net_addr_str(pAddr, m_aPeerAddrStr, sizeof(m_aPeerAddrStr), true); + mem_zero(m_aErrorString, sizeof(m_aErrorString)); + m_LastSendTime = Now; + m_LastRecvTime = Now; + m_LastUpdateTime = Now; + if(m_SecurityToken == NET_SECURITY_TOKEN_UNKNOWN && pPacket->m_DataSize >= (int)(1 + sizeof(SECURITY_TOKEN_MAGIC) + sizeof(m_SecurityToken)) && !mem_comp(&pPacket->m_aChunkData[1], SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC))) + { + m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; + if(g_Config.m_Debug) + dbg_msg("security", "generated token %d", m_SecurityToken); + } + else + { + if(g_Config.m_Debug) + dbg_msg("security", "token not supported by client (packet size %d)", pPacket->m_DataSize); + m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; + } + SendControl(NET_CTRLMSG_CONNECTACCEPT, SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC)); if(g_Config.m_Debug) - dbg_msg("security", "got token %d", m_SecurityToken); + dbg_msg("connection", "got connection, sending connect+accept"); } - else + } + else if(State() == NET_CONNSTATE_CONNECT) + { + // connection made + if(CtrlMsg == NET_CTRLMSG_CONNECTACCEPT) { - m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; + m_PeerAddr = *pAddr; + net_addr_str(pAddr, m_aPeerAddrStr, sizeof(m_aPeerAddrStr), true); + if(m_SecurityToken == NET_SECURITY_TOKEN_UNKNOWN && pPacket->m_DataSize >= (int)(1 + sizeof(SECURITY_TOKEN_MAGIC) + sizeof(m_SecurityToken)) && !mem_comp(&pPacket->m_aChunkData[1], SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC))) + { + m_SecurityToken = ToSecurityToken(&pPacket->m_aChunkData[1 + sizeof(SECURITY_TOKEN_MAGIC)]); + if(g_Config.m_Debug) + dbg_msg("security", "got token %d", m_SecurityToken); + } + else if(!IsSixup()) + { + m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; + if(g_Config.m_Debug) + dbg_msg("security", "token not supported by server"); + } + if(!IsSixup()) + SendControl(NET_CTRLMSG_ACCEPT, 0, 0); + m_LastRecvTime = Now; + m_State = NET_CONNSTATE_ONLINE; if(g_Config.m_Debug) - dbg_msg("security", "token not supported by server"); + dbg_msg("connection", "got connect+accept, sending accept. connection online"); } - m_LastRecvTime = Now; - SendControl(NET_CTRLMSG_ACCEPT, 0, 0); - m_State = NET_CONNSTATE_ONLINE; - if(g_Config.m_Debug) - dbg_msg("connection", "got connect+accept, sending accept. connection online"); } } } @@ -460,7 +523,7 @@ int CNetConnection::Update() if(Now - pResend->m_FirstSendTime > time_freq() * g_Config.m_ConnTimeout) { m_State = NET_CONNSTATE_ERROR; - char aBuf[512]; + char aBuf[128]; str_format(aBuf, sizeof(aBuf), "Too weak connection (not acked for %d seconds)", g_Config.m_ConnTimeout); SetError(aBuf); m_TimeoutSituation = true; @@ -510,6 +573,7 @@ void CNetConnection::SetTimedOut(const NETADDR *pAddr, int Sequence, int Ack, SE m_State = NET_CONNSTATE_ONLINE; m_PeerAddr = *pAddr; + net_addr_str(pAddr, m_aPeerAddrStr, sizeof(m_aPeerAddrStr), true); mem_zero(m_aErrorString, sizeof(m_aErrorString)); m_LastSendTime = Now; m_LastRecvTime = Now; diff --git a/src/engine/shared/network_console.cpp b/src/engine/shared/network_console.cpp index 27c2fd2d16..b03dd95889 100644 --- a/src/engine/shared/network_console.cpp +++ b/src/engine/shared/network_console.cpp @@ -42,12 +42,12 @@ int CNetConsole::Close() return 0; } -int CNetConsole::Drop(int ClientID, const char *pReason) +int CNetConsole::Drop(int ClientId, const char *pReason) { if(m_pfnDelClient) - m_pfnDelClient(ClientID, pReason, m_pUser); + m_pfnDelClient(ClientId, pReason, m_pUser); - m_aSlots[ClientID].m_Connection.Disconnect(pReason); + m_aSlots[ClientId].m_Connection.Disconnect(pReason); return 0; } @@ -121,24 +121,24 @@ int CNetConsole::Update() return 0; } -int CNetConsole::Recv(char *pLine, int MaxLength, int *pClientID) +int CNetConsole::Recv(char *pLine, int MaxLength, int *pClientId) { for(int i = 0; i < NET_MAX_CONSOLE_CLIENTS; i++) { if(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_ONLINE && m_aSlots[i].m_Connection.Recv(pLine, MaxLength)) { - if(pClientID) - *pClientID = i; + if(pClientId) + *pClientId = i; return 1; } } return 0; } -int CNetConsole::Send(int ClientID, const char *pLine) +int CNetConsole::Send(int ClientId, const char *pLine) { - if(m_aSlots[ClientID].m_Connection.State() == NET_CONNSTATE_ONLINE) - return m_aSlots[ClientID].m_Connection.Send(pLine); + if(m_aSlots[ClientId].m_Connection.State() == NET_CONNSTATE_ONLINE) + return m_aSlots[ClientId].m_Connection.Send(pLine); else return -1; } diff --git a/src/engine/shared/network_console_conn.cpp b/src/engine/shared/network_console_conn.cpp index 7bae792fc3..9ab375d4c3 100644 --- a/src/engine/shared/network_console_conn.cpp +++ b/src/engine/shared/network_console_conn.cpp @@ -143,7 +143,7 @@ int CConsoleNetConnection::Recv(char *pLine, int MaxLength) str_sanitize_cc(pLine); mem_move(m_aBuffer, m_aBuffer + EndOffset, m_BufferOffset - EndOffset); m_BufferOffset -= EndOffset; - return 1; + return str_utf8_check(pLine); } } return 0; diff --git a/src/engine/shared/network_server.cpp b/src/engine/shared/network_server.cpp index 0dd81393c7..e72bc2000a 100644 --- a/src/engine/shared/network_server.cpp +++ b/src/engine/shared/network_server.cpp @@ -36,12 +36,7 @@ const unsigned char g_aDummyMapData[] = { 0x78, 0x9C, 0x63, 0x64, 0x60, 0x60, 0x60, 0x44, 0xC2, 0x00, 0x00, 0x38, 0x00, 0x05}; -static SECURITY_TOKEN ToSecurityToken(const unsigned char *pData) -{ - return (int)pData[0] | (pData[1] << 8) | (pData[2] << 16) | (pData[3] << 24); -} - -bool CNetServer::Open(NETADDR BindAddr, CNetBan *pNetBan, int MaxClients, int MaxClientsPerIP) +bool CNetServer::Open(NETADDR BindAddr, CNetBan *pNetBan, int MaxClients, int MaxClientsPerIp) { // zero out the whole structure this->~CNetServer(); @@ -56,7 +51,7 @@ bool CNetServer::Open(NETADDR BindAddr, CNetBan *pNetBan, int MaxClients, int Ma m_pNetBan = pNetBan; m_MaxClients = clamp(MaxClients, 1, (int)NET_MAX_CLIENTS); - m_MaxClientsPerIP = MaxClientsPerIP; + m_MaxClientsPerIp = MaxClientsPerIp; m_NumConAttempts = 0; m_TimeNumConAttempts = time_get(); @@ -97,14 +92,14 @@ int CNetServer::Close() return net_udp_close(m_Socket); } -int CNetServer::Drop(int ClientID, const char *pReason) +int CNetServer::Drop(int ClientId, const char *pReason) { // TODO: insert lots of checks here if(m_pfnDelClient) - m_pfnDelClient(ClientID, pReason, m_pUser); + m_pfnDelClient(ClientId, pReason, m_pUser); - m_aSlots[ClientID].m_Connection.Disconnect(pReason); + m_aSlots[ClientId].m_Connection.Disconnect(pReason); return 0; } @@ -127,8 +122,8 @@ int CNetServer::Update() SECURITY_TOKEN CNetServer::GetGlobalToken() { - static NETADDR NullAddr = {0}; - return GetToken(NullAddr); + static const NETADDR NULL_ADDR = {0}; + return GetToken(NULL_ADDR); } SECURITY_TOKEN CNetServer::GetToken(const NETADDR &Addr) { @@ -219,10 +214,10 @@ int CNetServer::TryAcceptClient(NETADDR &Addr, SECURITY_TOKEN SecurityToken, boo } // check for sv_max_clients_per_ip - if(NumClientsWithAddr(Addr) + 1 > m_MaxClientsPerIP) + if(NumClientsWithAddr(Addr) + 1 > m_MaxClientsPerIp) { char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "Only %d players with the same IP are allowed", m_MaxClientsPerIP); + str_format(aBuf, sizeof(aBuf), "Only %d players with the same IP are allowed", m_MaxClientsPerIp); CNetBase::SendControlMsg(m_Socket, &Addr, 0, NET_CTRLMSG_CLOSE, aBuf, str_length(aBuf) + 1, SecurityToken, Sixup); return -1; // failed to add client } @@ -465,7 +460,7 @@ void CNetServer::OnPreConnMsg(NETADDR &Addr, CNetPacketConstruct &Packet) } } -void CNetServer::OnConnCtrlMsg(NETADDR &Addr, int ClientID, int ControlMsg, const CNetPacketConstruct &Packet) +void CNetServer::OnConnCtrlMsg(NETADDR &Addr, int ClientId, int ControlMsg, const CNetPacketConstruct &Packet) { if(ControlMsg == NET_CTRLMSG_CONNECT) { @@ -483,7 +478,7 @@ void CNetServer::OnConnCtrlMsg(NETADDR &Addr, int ClientID, int ControlMsg, cons } if(g_Config.m_Debug) - dbg_msg("security", "client %d wants to reconnect", ClientID); + dbg_msg("security", "client %d wants to reconnect", ClientId); } else if(ControlMsg == NET_CTRLMSG_ACCEPT && Packet.m_DataSize == 1 + sizeof(SECURITY_TOKEN)) { @@ -493,11 +488,11 @@ void CNetServer::OnConnCtrlMsg(NETADDR &Addr, int ClientID, int ControlMsg, cons // correct token // try to accept client if(g_Config.m_Debug) - dbg_msg("security", "client %d reconnect", ClientID); + dbg_msg("security", "client %d reconnect", ClientId); // reset netconn and process rejoin - m_aSlots[ClientID].m_Connection.Reset(true); - m_pfnClientRejoin(ClientID, m_pUser); + m_aSlots[ClientId].m_Connection.Reset(true); + m_pfnClientRejoin(ClientId, m_pUser); } } } @@ -545,7 +540,7 @@ int CNetServer::OnSixupCtrlMsg(NETADDR &Addr, CNetChunk *pChunk, int ControlMsg, if(m_RecvUnpacker.m_Data.m_DataSize < 5 || ClientExists(Addr)) return 0; // silently ignore - mem_copy(&ResponseToken, Packet.m_aChunkData + 1, 4); + ResponseToken = ToSecurityToken(Packet.m_aChunkData + 1); if(ControlMsg == 5) { @@ -557,7 +552,7 @@ int CNetServer::OnSixupCtrlMsg(NETADDR &Addr, CNetChunk *pChunk, int ControlMsg, // Is this behaviour safe to rely on? pChunk->m_Flags = 0; - pChunk->m_ClientID = -1; + pChunk->m_ClientId = -1; pChunk->m_Address = Addr; pChunk->m_DataSize = 0; return 1; @@ -645,7 +640,6 @@ int CNetServer::Recv(CNetChunk *pChunk, SECURITY_TOKEN *pResponseToken) SECURITY_TOKEN Token; bool Sixup = false; - *pResponseToken = NET_SECURITY_TOKEN_UNKNOWN; if(CNetBase::UnpackPacket(pData, Bytes, &m_RecvUnpacker.m_Data, Sixup, &Token, pResponseToken) == 0) { if(m_RecvUnpacker.m_Data.m_Flags & NET_PACKETFLAG_CONNLESS) @@ -654,7 +648,7 @@ int CNetServer::Recv(CNetChunk *pChunk, SECURITY_TOKEN *pResponseToken) continue; pChunk->m_Flags = NETSENDFLAG_CONNLESS; - pChunk->m_ClientID = -1; + pChunk->m_ClientId = -1; pChunk->m_Address = Addr; pChunk->m_DataSize = m_RecvUnpacker.m_Data.m_DataSize; pChunk->m_pData = m_RecvUnpacker.m_Data.m_aChunkData; @@ -690,7 +684,7 @@ int CNetServer::Recv(CNetChunk *pChunk, SECURITY_TOKEN *pResponseToken) if(m_RecvUnpacker.m_Data.m_Flags & NET_PACKETFLAG_CONTROL) OnConnCtrlMsg(Addr, Slot, m_RecvUnpacker.m_Data.m_aChunkData[0], m_RecvUnpacker.m_Data); - if(m_aSlots[Slot].m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr, Token)) + if(m_aSlots[Slot].m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr, Token, *pResponseToken)) { if(m_RecvUnpacker.m_Data.m_DataSize) m_RecvUnpacker.Start(&Addr, &m_aSlots[Slot].m_Connection, Slot); @@ -740,20 +734,20 @@ int CNetServer::Send(CNetChunk *pChunk) else { int Flags = 0; - dbg_assert(pChunk->m_ClientID >= 0, "erroneous client id"); - dbg_assert(pChunk->m_ClientID < MaxClients(), "erroneous client id"); + dbg_assert(pChunk->m_ClientId >= 0, "erroneous client id"); + dbg_assert(pChunk->m_ClientId < MaxClients(), "erroneous client id"); if(pChunk->m_Flags & NETSENDFLAG_VITAL) Flags = NET_CHUNKFLAG_VITAL; - if(m_aSlots[pChunk->m_ClientID].m_Connection.QueueChunk(Flags, pChunk->m_DataSize, pChunk->m_pData) == 0) + if(m_aSlots[pChunk->m_ClientId].m_Connection.QueueChunk(Flags, pChunk->m_DataSize, pChunk->m_pData) == 0) { if(pChunk->m_Flags & NETSENDFLAG_FLUSH) - m_aSlots[pChunk->m_ClientID].m_Connection.Flush(); + m_aSlots[pChunk->m_ClientId].m_Connection.Flush(); } else { - //Drop(pChunk->m_ClientID, "Error sending data"); + //Drop(pChunk->m_ClientId, "Error sending data"); } } return 0; @@ -763,7 +757,7 @@ void CNetServer::SendTokenSixup(NETADDR &Addr, SECURITY_TOKEN Token) { SECURITY_TOKEN MyToken = GetToken(Addr); unsigned char aBuf[512] = {}; - mem_copy(aBuf, &MyToken, 4); + WriteSecurityToken(aBuf, MyToken); int Size = (Token == NET_SECURITY_TOKEN_UNKNOWN) ? 512 : 4; CNetBase::SendControlMsg(m_Socket, &Addr, 0, 5, aBuf, Size, Token, true); } @@ -776,15 +770,15 @@ int CNetServer::SendConnlessSixup(CNetChunk *pChunk, SECURITY_TOKEN ResponseToke unsigned char aBuffer[NET_MAX_PACKETSIZE]; aBuffer[0] = NET_PACKETFLAG_CONNLESS << 2 | 1; SECURITY_TOKEN Token = GetToken(pChunk->m_Address); - mem_copy(aBuffer + 1, &ResponseToken, 4); - mem_copy(aBuffer + 5, &Token, 4); + WriteSecurityToken(aBuffer + 1, ResponseToken); + WriteSecurityToken(aBuffer + 5, Token); mem_copy(aBuffer + 9, pChunk->m_pData, pChunk->m_DataSize); net_udp_send(m_Socket, &pChunk->m_Address, aBuffer, pChunk->m_DataSize + 9); return 0; } -void CNetServer::SetMaxClientsPerIP(int Max) +void CNetServer::SetMaxClientsPerIp(int Max) { // clamp if(Max < 1) @@ -792,31 +786,31 @@ void CNetServer::SetMaxClientsPerIP(int Max) else if(Max > NET_MAX_CLIENTS) Max = NET_MAX_CLIENTS; - m_MaxClientsPerIP = Max; + m_MaxClientsPerIp = Max; } -bool CNetServer::SetTimedOut(int ClientID, int OrigID) +bool CNetServer::SetTimedOut(int ClientId, int OrigId) { - if(m_aSlots[ClientID].m_Connection.State() != NET_CONNSTATE_ERROR) + if(m_aSlots[ClientId].m_Connection.State() != NET_CONNSTATE_ERROR) return false; - m_aSlots[ClientID].m_Connection.SetTimedOut(ClientAddr(OrigID), m_aSlots[OrigID].m_Connection.SeqSequence(), m_aSlots[OrigID].m_Connection.AckSequence(), m_aSlots[OrigID].m_Connection.SecurityToken(), m_aSlots[OrigID].m_Connection.ResendBuffer(), m_aSlots[OrigID].m_Connection.m_Sixup); - m_aSlots[OrigID].m_Connection.Reset(); + m_aSlots[ClientId].m_Connection.SetTimedOut(ClientAddr(OrigId), m_aSlots[OrigId].m_Connection.SeqSequence(), m_aSlots[OrigId].m_Connection.AckSequence(), m_aSlots[OrigId].m_Connection.SecurityToken(), m_aSlots[OrigId].m_Connection.ResendBuffer(), m_aSlots[OrigId].m_Connection.m_Sixup); + m_aSlots[OrigId].m_Connection.Reset(); return true; } -void CNetServer::SetTimeoutProtected(int ClientID) +void CNetServer::SetTimeoutProtected(int ClientId) { - m_aSlots[ClientID].m_Connection.m_TimeoutProtected = true; + m_aSlots[ClientId].m_Connection.m_TimeoutProtected = true; } -int CNetServer::ResetErrorString(int ClientID) +int CNetServer::ResetErrorString(int ClientId) { - m_aSlots[ClientID].m_Connection.ResetErrorString(); + m_aSlots[ClientId].m_Connection.ResetErrorString(); return 0; } -const char *CNetServer::ErrorString(int ClientID) +const char *CNetServer::ErrorString(int ClientId) { - return m_aSlots[ClientID].m_Connection.ErrorString(); + return m_aSlots[ClientId].m_Connection.ErrorString(); } diff --git a/src/engine/shared/packer.cpp b/src/engine/shared/packer.cpp index 2b183113b1..3593929ea6 100644 --- a/src/engine/shared/packer.cpp +++ b/src/engine/shared/packer.cpp @@ -172,6 +172,12 @@ const char *CUnpacker::GetString(int SanitizeType) } m_pCurrent++; + if(!str_utf8_check(pPtr)) + { + m_Error = true; + return ""; + } + // sanitize all strings if(SanitizeType & SANITIZE) str_sanitize(pPtr); diff --git a/src/engine/shared/protocol.h b/src/engine/shared/protocol.h index 6c00a487bd..e0f807b10e 100644 --- a/src/engine/shared/protocol.h +++ b/src/engine/shared/protocol.h @@ -4,6 +4,7 @@ #define ENGINE_SHARED_PROTOCOL_H #include +#include /* Connection diagram - How the initialization works. diff --git a/src/engine/shared/protocol7.h b/src/engine/shared/protocol7.h index c8524ef78c..73bbe92fda 100644 --- a/src/engine/shared/protocol7.h +++ b/src/engine/shared/protocol7.h @@ -3,6 +3,8 @@ #ifndef ENGINE_SHARED_PROTOCOL7_H #define ENGINE_SHARED_PROTOCOL7_H +#include + namespace protocol7 { enum @@ -52,6 +54,16 @@ enum NETMSG_MAPLIST_ENTRY_REM, }; -} +enum +{ + MAX_NAME_LENGTH = 16, + MAX_NAME_ARRAY_SIZE = MAX_NAME_LENGTH * UTF8_BYTE_LENGTH + 1, + MAX_CLAN_LENGTH = 12, + MAX_CLAN_ARRAY_SIZE = MAX_CLAN_LENGTH * UTF8_BYTE_LENGTH + 1, + MAX_SKIN_LENGTH = 24, + MAX_SKIN_ARRAY_SIZE = MAX_SKIN_LENGTH * UTF8_BYTE_LENGTH + 1, +}; + +} // namespace protocol7 #endif diff --git a/src/engine/shared/protocol_ex.cpp b/src/engine/shared/protocol_ex.cpp index 51f4b286ad..4041265d7d 100644 --- a/src/engine/shared/protocol_ex.cpp +++ b/src/engine/shared/protocol_ex.cpp @@ -15,52 +15,52 @@ void RegisterUuids(CUuidManager *pManager) #undef UUID } -int UnpackMessageID(int *pID, bool *pSys, CUuid *pUuid, CUnpacker *pUnpacker, CMsgPacker *pPacker) +int UnpackMessageId(int *pId, bool *pSys, CUuid *pUuid, CUnpacker *pUnpacker, CMsgPacker *pPacker) { - *pID = 0; + *pId = 0; *pSys = false; mem_zero(pUuid, sizeof(*pUuid)); - int MsgID = pUnpacker->GetInt(); + int MsgId = pUnpacker->GetInt(); if(pUnpacker->Error()) { return UNPACKMESSAGE_ERROR; } - *pID = MsgID >> 1; - *pSys = MsgID & 1; + *pId = MsgId >> 1; + *pSys = MsgId & 1; - if(*pID < 0 || *pID >= OFFSET_UUID) + if(*pId < 0 || *pId >= OFFSET_UUID) { return UNPACKMESSAGE_ERROR; } - if(*pID != 0) // NETMSG_EX, NETMSGTYPE_EX + if(*pId != 0) // NETMSG_EX, NETMSGTYPE_EX { return UNPACKMESSAGE_OK; } - *pID = g_UuidManager.UnpackUuid(pUnpacker, pUuid); + *pId = g_UuidManager.UnpackUuid(pUnpacker, pUuid); - if(*pID == UUID_INVALID || *pID == UUID_UNKNOWN) + if(*pId == UUID_INVALID || *pId == UUID_UNKNOWN) { return UNPACKMESSAGE_ERROR; } if(*pSys) { - switch(*pID) + switch(*pId) { case NETMSG_WHATIS: { CUuid Uuid2; - int ID2 = g_UuidManager.UnpackUuid(pUnpacker, &Uuid2); - if(ID2 == UUID_INVALID) + int Id2 = g_UuidManager.UnpackUuid(pUnpacker, &Uuid2); + if(Id2 == UUID_INVALID) { break; } - if(ID2 == UUID_UNKNOWN) + if(Id2 == UUID_UNKNOWN) { new(pPacker) CMsgPacker(NETMSG_IDONTKNOW, true); pPacker->AddRaw(&Uuid2, sizeof(Uuid2)); @@ -69,7 +69,7 @@ int UnpackMessageID(int *pID, bool *pSys, CUuid *pUuid, CUnpacker *pUnpacker, CM { new(pPacker) CMsgPacker(NETMSG_ITIS, true); pPacker->AddRaw(&Uuid2, sizeof(Uuid2)); - pPacker->AddString(g_UuidManager.GetName(ID2), 0); + pPacker->AddString(g_UuidManager.GetName(Id2), 0); } return UNPACKMESSAGE_ANSWER; } diff --git a/src/engine/shared/protocol_ex.h b/src/engine/shared/protocol_ex.h index 2ca61ac0ce..6ef72f163f 100644 --- a/src/engine/shared/protocol_ex.h +++ b/src/engine/shared/protocol_ex.h @@ -33,8 +33,8 @@ enum }; void RegisterUuids(CUuidManager *pManager); -bool NetworkExDefaultHandler(int *pID, CUuid *pUuid, CUnpacker *pUnpacker, CMsgPacker *pPacker, int Type); +bool NetworkExDefaultHandler(int *pId, CUuid *pUuid, CUnpacker *pUnpacker, CMsgPacker *pPacker, int Type); -int UnpackMessageID(int *pID, bool *pSys, CUuid *pUuid, CUnpacker *pUnpacker, CMsgPacker *pPacker); +int UnpackMessageId(int *pId, bool *pSys, CUuid *pUuid, CUnpacker *pUnpacker, CMsgPacker *pPacker); #endif // ENGINE_SHARED_PROTOCOL_EX_H diff --git a/src/engine/shared/protocol_ex_msgs.h b/src/engine/shared/protocol_ex_msgs.h index c3f760a845..d137d5c176 100644 --- a/src/engine/shared/protocol_ex_msgs.h +++ b/src/engine/shared/protocol_ex_msgs.h @@ -22,7 +22,6 @@ UUID(NETMSG_WHATIS, "what-is@ddnet.tw") UUID(NETMSG_ITIS, "it-is@ddnet.tw") UUID(NETMSG_IDONTKNOW, "i-dont-know@ddnet.tw") -UUID(NETMSG_IAMSTA, "i-am-StA@stormaxs.github.io/sta.github.io") UUID(NETMSG_RCONTYPE, "rcon-type@ddnet.tw") UUID(NETMSG_MAP_DETAILS, "map-details@ddnet.tw") @@ -34,3 +33,6 @@ UUID(NETMSG_CHECKSUM_REQUEST, "checksum-request@ddnet.tw") UUID(NETMSG_CHECKSUM_RESPONSE, "checksum-response@ddnet.tw") UUID(NETMSG_CHECKSUM_ERROR, "checksum-error@ddnet.tw") UUID(NETMSG_REDIRECT, "redirect@ddnet.org") +UUID(NETMSG_RCON_CMD_GROUP_START, "rcon-cmd-group-start@ddnet.org") +UUID(NETMSG_RCON_CMD_GROUP_END, "rcon-cmd-group-end@ddnet.org") +UUID(NETMSG_MAP_RELOAD, "map-reload@ddnet.org") diff --git a/src/engine/shared/protocolglue.cpp b/src/engine/shared/protocolglue.cpp new file mode 100644 index 0000000000..e690614ca4 --- /dev/null +++ b/src/engine/shared/protocolglue.cpp @@ -0,0 +1,85 @@ +#include +#include +#include + +#include "protocolglue.h" + +int GameFlags_ClampToSix(int Flags) +{ + int Six = 0; + if(Flags & GAMEFLAG_TEAMS) + Six |= GAMEFLAG_TEAMS; + if(Flags & GAMEFLAG_FLAGS) + Six |= GAMEFLAG_FLAGS; + return Six; +} + +int PlayerFlags_SevenToSix(int Flags) +{ + int Six = 0; + if(Flags & protocol7::PLAYERFLAG_CHATTING) + Six |= PLAYERFLAG_CHATTING; + if(Flags & protocol7::PLAYERFLAG_SCOREBOARD) + Six |= PLAYERFLAG_SCOREBOARD; + if(Flags & protocol7::PLAYERFLAG_AIM) + Six |= PLAYERFLAG_AIM; + return Six; +} + +int PlayerFlags_SixToSeven(int Flags) +{ + int Seven = 0; + if(Flags & PLAYERFLAG_CHATTING) + Seven |= protocol7::PLAYERFLAG_CHATTING; + if(Flags & PLAYERFLAG_SCOREBOARD) + Seven |= protocol7::PLAYERFLAG_SCOREBOARD; + if(Flags & PLAYERFLAG_AIM) + Seven |= protocol7::PLAYERFLAG_AIM; + return Seven; +} + +void PickupType_SevenToSix(int Type7, int &Type6, int &SubType6) +{ + SubType6 = 0; + Type6 = POWERUP_WEAPON; + switch(Type7) + { + case protocol7::PICKUP_HEALTH: + case protocol7::PICKUP_ARMOR: + Type6 = Type7; + break; + case protocol7::PICKUP_GRENADE: + SubType6 = WEAPON_GRENADE; + break; + case protocol7::PICKUP_SHOTGUN: + SubType6 = WEAPON_SHOTGUN; + break; + case protocol7::PICKUP_LASER: + SubType6 = WEAPON_LASER; + break; + case protocol7::PICKUP_GUN: + SubType6 = WEAPON_GUN; + break; + case protocol7::PICKUP_HAMMER: + SubType6 = WEAPON_HAMMER; + break; + case protocol7::PICKUP_NINJA: + SubType6 = WEAPON_NINJA; + Type6 = POWERUP_NINJA; + break; + default: + // dbg_msg("sixup", "ERROR: failed to translate weapon=%d to 0.6", Type7); + break; + } +} + +int PickupType_SixToSeven(int Type6, int SubType6) +{ + if(Type6 == POWERUP_WEAPON) + return SubType6 == WEAPON_SHOTGUN ? protocol7::PICKUP_SHOTGUN : SubType6 == WEAPON_GRENADE ? protocol7::PICKUP_GRENADE : protocol7::PICKUP_LASER; + else if(Type6 == POWERUP_NINJA) + return protocol7::PICKUP_NINJA; + else if(Type6 == POWERUP_ARMOR) + return protocol7::PICKUP_ARMOR; + return 0; +} diff --git a/src/engine/shared/protocolglue.h b/src/engine/shared/protocolglue.h new file mode 100644 index 0000000000..614cc62988 --- /dev/null +++ b/src/engine/shared/protocolglue.h @@ -0,0 +1,10 @@ +#ifndef ENGINE_SHARED_PROTOCOLGLUE_H +#define ENGINE_SHARED_PROTOCOLGLUE_H + +int GameFlags_ClampToSix(int Flags); +int PlayerFlags_SevenToSix(int Flags); +int PlayerFlags_SixToSeven(int Flags); +void PickupType_SevenToSix(int Type7, int &Type6, int &SubType6); +int PickupType_SixToSeven(int Type6, int SubType6); + +#endif diff --git a/src/engine/shared/ringbuffer.cpp b/src/engine/shared/ringbuffer.cpp index 23301a669b..c8697942e4 100644 --- a/src/engine/shared/ringbuffer.cpp +++ b/src/engine/shared/ringbuffer.cpp @@ -1,7 +1,5 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ -#include - #include "ringbuffer.h" CRingBufferBase::CItem *CRingBufferBase::NextBlock(CItem *pItem) @@ -125,11 +123,21 @@ void *CRingBufferBase::Allocate(int Size) return (void *)(pBlock + 1); } +void CRingBufferBase::SetPopCallback(std::function PopCallback) +{ + m_PopCallback = std::move(PopCallback); +} + int CRingBufferBase::PopFirst() { if(m_pConsume->m_Free) return 0; + if(m_PopCallback) + { + m_PopCallback(m_pConsume + 1); + } + // set the free flag m_pConsume->m_Free = 1; diff --git a/src/engine/shared/ringbuffer.h b/src/engine/shared/ringbuffer.h index 842e0520c4..11af0fa320 100644 --- a/src/engine/shared/ringbuffer.h +++ b/src/engine/shared/ringbuffer.h @@ -3,6 +3,10 @@ #ifndef ENGINE_SHARED_RINGBUFFER_H #define ENGINE_SHARED_RINGBUFFER_H +#include + +#include + class CRingBufferBase { class CItem @@ -23,6 +27,8 @@ class CRingBufferBase int m_Size; int m_Flags; + std::function m_PopCallback = nullptr; + CItem *NextBlock(CItem *pItem); CItem *PrevBlock(CItem *pItem); CItem *MergeBack(CItem *pItem); @@ -37,6 +43,7 @@ class CRingBufferBase void Init(void *pMemory, int Size, int Flags); int PopFirst(); + void SetPopCallback(const std::function PopCallback); public: enum @@ -44,10 +51,30 @@ class CRingBufferBase // Will start to destroy items to try to fit the next one FLAG_RECYCLE = 1 }; + static constexpr int ITEM_SIZE = sizeof(CItem); +}; + +template +class CTypedRingBuffer : public CRingBufferBase +{ +public: + T *Allocate(int Size) { return (T *)CRingBufferBase::Allocate(Size); } + int PopFirst() { return CRingBufferBase::PopFirst(); } + void SetPopCallback(std::function PopCallback) + { + CRingBufferBase::SetPopCallback([PopCallback](void *pCurrent) { + PopCallback((T *)pCurrent); + }); + } + + T *Prev(T *pCurrent) { return (T *)CRingBufferBase::Prev(pCurrent); } + T *Next(T *pCurrent) { return (T *)CRingBufferBase::Next(pCurrent); } + T *First() { return (T *)CRingBufferBase::First(); } + T *Last() { return (T *)CRingBufferBase::Last(); } }; template -class CStaticRingBuffer : public CRingBufferBase +class CStaticRingBuffer : public CTypedRingBuffer { unsigned char m_aBuffer[TSIZE]; @@ -55,14 +82,27 @@ class CStaticRingBuffer : public CRingBufferBase CStaticRingBuffer() { Init(); } void Init() { CRingBufferBase::Init(m_aBuffer, TSIZE, TFLAGS); } +}; - T *Allocate(int Size) { return (T *)CRingBufferBase::Allocate(Size); } - int PopFirst() { return CRingBufferBase::PopFirst(); } +template +class CDynamicRingBuffer : public CTypedRingBuffer +{ + unsigned char *m_pBuffer = nullptr; - T *Prev(T *pCurrent) { return (T *)CRingBufferBase::Prev(pCurrent); } - T *Next(T *pCurrent) { return (T *)CRingBufferBase::Next(pCurrent); } - T *First() { return (T *)CRingBufferBase::First(); } - T *Last() { return (T *)CRingBufferBase::Last(); } +public: + CDynamicRingBuffer(int Size, int Flags = 0) { Init(Size, Flags); } + + virtual ~CDynamicRingBuffer() + { + free(m_pBuffer); + } + + void Init(int Size, int Flags) + { + free(m_pBuffer); + m_pBuffer = static_cast(malloc(Size)); + CRingBufferBase::Init(m_pBuffer, Size, Flags); + } }; #endif diff --git a/src/engine/shared/serverinfo.cpp b/src/engine/shared/serverinfo.cpp index ecccfa5994..3e6ef8e570 100644 --- a/src/engine/shared/serverinfo.cpp +++ b/src/engine/shared/serverinfo.cpp @@ -71,6 +71,8 @@ bool CServerInfo2::FromJsonRaw(CServerInfo2 *pOut, const json_value *pJson) const json_value &MapName = ServerInfo["map"]["name"]; const json_value &Version = ServerInfo["version"]; const json_value &Clients = ServerInfo["clients"]; + const json_value &RequiresLogin = ServerInfo["requires_login"]; + Error = false; Error = Error || MaxClients.type != json_integer; Error = Error || MaxPlayers.type != json_integer; @@ -96,6 +98,11 @@ bool CServerInfo2::FromJsonRaw(CServerInfo2 *pOut, const json_value *pJson) { pOut->m_ClientScoreKind = CServerInfo::CLIENT_SCORE_KIND_TIME; } + pOut->m_RequiresLogin = false; + if(RequiresLogin.type == json_boolean) + { + pOut->m_RequiresLogin = RequiresLogin; + } pOut->m_Passworded = Passworded; str_copy(pOut->m_aGameType, GameType); str_copy(pOut->m_aName, Name); @@ -196,6 +203,7 @@ bool CServerInfo2::operator==(const CServerInfo2 &Other) const Unequal = Unequal || str_comp(m_aName, Other.m_aName) != 0; Unequal = Unequal || str_comp(m_aMapName, Other.m_aMapName) != 0; Unequal = Unequal || str_comp(m_aVersion, Other.m_aVersion) != 0; + Unequal = Unequal || m_RequiresLogin != Other.m_RequiresLogin; if(Unequal) { return false; @@ -225,6 +233,7 @@ CServerInfo2::operator CServerInfo() const Result.m_MaxPlayers = m_MaxPlayers; Result.m_NumPlayers = m_NumPlayers; Result.m_ClientScoreKind = m_ClientScoreKind; + Result.m_RequiresLogin = m_RequiresLogin; Result.m_Flags = m_Passworded ? SERVER_FLAG_PASSWORD : 0; str_copy(Result.m_aGameType, m_aGameType); str_copy(Result.m_aName, m_aName); diff --git a/src/engine/shared/serverinfo.h b/src/engine/shared/serverinfo.h index edb06a467d..4e4c28902c 100644 --- a/src/engine/shared/serverinfo.h +++ b/src/engine/shared/serverinfo.h @@ -38,6 +38,7 @@ class CServerInfo2 char m_aName[64]; char m_aMapName[MAX_MAP_LENGTH]; char m_aVersion[32]; + bool m_RequiresLogin; bool operator==(const CServerInfo2 &Other) const; bool operator!=(const CServerInfo2 &Other) const { return !(*this == Other); } diff --git a/src/engine/shared/sixup_translate_snapshot.cpp b/src/engine/shared/sixup_translate_snapshot.cpp new file mode 100644 index 0000000000..2b06e53692 --- /dev/null +++ b/src/engine/shared/sixup_translate_snapshot.cpp @@ -0,0 +1,15 @@ +#include + +#include "snapshot.h" + +void CSnapshotBuilder::Init7(const CSnapshot *pSnapshot) +{ + // the method is called Init7 because it is only used for 0.7 support + // but the snap we are building is a 0.6 snap + m_Sixup = false; + + m_DataSize = pSnapshot->m_DataSize; + m_NumItems = pSnapshot->m_NumItems; + mem_copy(m_aOffsets, pSnapshot->Offsets(), sizeof(int) * m_NumItems); + mem_copy(m_aData, pSnapshot->DataStart(), m_DataSize); +} diff --git a/src/engine/shared/snapshot.cpp b/src/engine/shared/snapshot.cpp index 7481eb63e4..99d2b50ccc 100644 --- a/src/engine/shared/snapshot.cpp +++ b/src/engine/shared/snapshot.cpp @@ -10,6 +10,7 @@ #include #include +#include #include // CSnapshot @@ -65,7 +66,12 @@ int CSnapshot::GetItemIndex(int Key) const return -1; } -const void *CSnapshot::FindItem(int Type, int ID) const +void CSnapshot::InvalidateItem(int Index) +{ + ((CSnapshotItem *)(DataStart() + Offsets()[Index]))->Invalidate(); +} + +const void *CSnapshot::FindItem(int Type, int Id) const { int InternalType = Type; if(Type >= OFFSET_UUID) @@ -79,11 +85,11 @@ const void *CSnapshot::FindItem(int Type, int ID) const for(int i = 0; i < m_NumItems; i++) { const CSnapshotItem *pItem = GetItem(i); - if(pItem->Type() == 0 && pItem->ID() >= OFFSET_UUID_TYPE) // NETOBJTYPE_EX + if(pItem->Type() == 0 && pItem->Id() >= OFFSET_UUID_TYPE) // NETOBJTYPE_EX { if(mem_comp(pItem->Data(), aTypeUuidItem, sizeof(CUuid)) == 0) { - InternalType = pItem->ID(); + InternalType = pItem->Id(); Found = true; break; } @@ -94,7 +100,7 @@ const void *CSnapshot::FindItem(int Type, int ID) const return nullptr; } } - int Index = GetItemIndex((InternalType << 16) | ID); + int Index = GetItemIndex((InternalType << 16) | Id); return Index < 0 ? nullptr : GetItem(Index)->Data(); } @@ -120,7 +126,7 @@ void CSnapshot::DebugDump() const { const CSnapshotItem *pItem = GetItem(i); int Size = GetItemSize(i); - dbg_msg("snapshot", "\ttype=%d id=%d", pItem->Type(), pItem->ID()); + dbg_msg("snapshot", "\ttype=%d id=%d", pItem->Type(), pItem->Id()); for(size_t b = 0; b < Size / sizeof(int32_t); b++) dbg_msg("snapshot", "\t\t%3d %12d\t%08x", (int)b, pItem->Data()[b], pItem->Data()[b]); } @@ -129,8 +135,15 @@ void CSnapshot::DebugDump() const bool CSnapshot::IsValid(size_t ActualSize) const { // validate total size - if(ActualSize < sizeof(CSnapshot) || m_NumItems < 0 || m_DataSize < 0 || ActualSize != TotalSize()) + if(ActualSize < sizeof(CSnapshot) || + ActualSize > MAX_SIZE || + m_NumItems < 0 || + m_NumItems > MAX_ITEMS || + m_DataSize < 0 || + ActualSize != TotalSize()) + { return false; + } // validate item offsets const int *pOffsets = Offsets(); @@ -161,7 +174,7 @@ struct CItemList int m_aIndex[HASHLIST_BUCKET_SIZE]; }; -inline size_t CalcHashID(int Key) +inline size_t CalcHashId(int Key) { // djb2 (http://www.cse.yorku.ca/~oz/hash.html) unsigned Hash = 5381; @@ -178,23 +191,23 @@ static void GenerateHash(CItemList *pHashlist, const CSnapshot *pSnapshot) for(int i = 0; i < pSnapshot->NumItems(); i++) { int Key = pSnapshot->GetItem(i)->Key(); - size_t HashID = CalcHashID(Key); - if(pHashlist[HashID].m_Num < HASHLIST_BUCKET_SIZE) + size_t HashId = CalcHashId(Key); + if(pHashlist[HashId].m_Num < HASHLIST_BUCKET_SIZE) { - pHashlist[HashID].m_aIndex[pHashlist[HashID].m_Num] = i; - pHashlist[HashID].m_aKeys[pHashlist[HashID].m_Num] = Key; - pHashlist[HashID].m_Num++; + pHashlist[HashId].m_aIndex[pHashlist[HashId].m_Num] = i; + pHashlist[HashId].m_aKeys[pHashlist[HashId].m_Num] = Key; + pHashlist[HashId].m_Num++; } } } static int GetItemIndexHashed(int Key, const CItemList *pHashlist) { - size_t HashID = CalcHashID(Key); - for(int i = 0; i < pHashlist[HashID].m_Num; i++) + size_t HashId = CalcHashId(Key); + for(int i = 0; i < pHashlist[HashId].m_Num; i++) { - if(pHashlist[HashID].m_aKeys[i] == Key) - return pHashlist[HashID].m_aIndex[i]; + if(pHashlist[HashId].m_aKeys[i] == Key) + return pHashlist[HashId].m_aIndex[i]; } return -1; @@ -243,6 +256,7 @@ void CSnapshotDelta::UndiffItem(const int *pPast, const int *pDiff, int *pOut, i CSnapshotDelta::CSnapshotDelta() { mem_zero(m_aItemSizes, sizeof(m_aItemSizes)); + mem_zero(m_aItemSizes7, sizeof(m_aItemSizes7)); mem_zero(m_aSnapshotDataRate, sizeof(m_aSnapshotDataRate)); mem_zero(m_aSnapshotDataUpdates, sizeof(m_aSnapshotDataUpdates)); mem_zero(&m_Empty, sizeof(m_Empty)); @@ -251,6 +265,7 @@ CSnapshotDelta::CSnapshotDelta() CSnapshotDelta::CSnapshotDelta(const CSnapshotDelta &Old) { mem_copy(m_aItemSizes, Old.m_aItemSizes, sizeof(m_aItemSizes)); + mem_copy(m_aItemSizes7, Old.m_aItemSizes7, sizeof(m_aItemSizes7)); mem_copy(m_aSnapshotDataRate, Old.m_aSnapshotDataRate, sizeof(m_aSnapshotDataRate)); mem_copy(m_aSnapshotDataUpdates, Old.m_aSnapshotDataUpdates, sizeof(m_aSnapshotDataUpdates)); mem_zero(&m_Empty, sizeof(m_Empty)); @@ -263,6 +278,13 @@ void CSnapshotDelta::SetStaticsize(int ItemType, size_t Size) m_aItemSizes[ItemType] = Size; } +void CSnapshotDelta::SetStaticsize7(int ItemType, size_t Size) +{ + dbg_assert(ItemType >= 0 && ItemType < MAX_NETOBJSIZES, "ItemType invalid"); + dbg_assert(Size <= (size_t)std::numeric_limits::max(), "Size invalid"); + m_aItemSizes7[ItemType] = Size; +} + const CSnapshotDelta::CData *CSnapshotDelta::EmptyDelta() const { return &m_Empty; @@ -326,7 +348,7 @@ int CSnapshotDelta::CreateDelta(const CSnapshot *pFrom, const CSnapshot *pTo, vo if(DiffItem(pPastItem->Data(), pCurItem->Data(), pItemDataDst, ItemSize / sizeof(int32_t))) { *pData++ = pCurItem->Type(); - *pData++ = pCurItem->ID(); + *pData++ = pCurItem->Id(); if(IncludeSize) *pData++ = ItemSize / sizeof(int32_t); pData += ItemSize / sizeof(int32_t); @@ -336,7 +358,7 @@ int CSnapshotDelta::CreateDelta(const CSnapshot *pFrom, const CSnapshot *pTo, vo else { *pData++ = pCurItem->Type(); - *pData++ = pCurItem->ID(); + *pData++ = pCurItem->Id(); if(IncludeSize) *pData++ = ItemSize / sizeof(int32_t); @@ -352,7 +374,139 @@ int CSnapshotDelta::CreateDelta(const CSnapshot *pFrom, const CSnapshot *pTo, vo return (int)((char *)pData - (char *)pDstData); } -int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const void *pSrcData, int DataSize) +int CSnapshotDelta::DebugDumpDelta(const void *pSrcData, int DataSize) +{ + CData *pDelta = (CData *)pSrcData; + int *pData = (int *)pDelta->m_aData; + int *pEnd = (int *)(((char *)pSrcData + DataSize)); + + dbg_msg("delta_dump", "+-----------------------------------------------"); + if(DataSize < 3 * (int)sizeof(int32_t)) + { + dbg_msg("delta_dump", "| delta size %d too small. Should at least fit the empty delta header.", DataSize); + return -505; + } + + dbg_msg("delta_dump", "| data_size=%d", DataSize); + + int DumpIndex = 0; + + // dump header + { + int *pDumpHeader = (int *)pSrcData; + dbg_msg("delta_dump", "| %3d %12d %08x m_NumDeletedItems=%d", DumpIndex++, *pDumpHeader, *pDumpHeader, *pDumpHeader); + pDumpHeader++; + dbg_msg("delta_dump", "| %3d %12d %08x m_NumUpdatedItems=%d", DumpIndex++, *pDumpHeader, *pDumpHeader, *pDumpHeader); + pDumpHeader++; + dbg_msg("delta_dump", "| %3d %12d %08x _zero=%d", DumpIndex++, *pDumpHeader, *pDumpHeader, *pDumpHeader); + pDumpHeader++; + + dbg_assert(pDumpHeader == pData, "invalid header size"); + } + + // unpack deleted stuff + int *pDeleted = pData; + if(pDelta->m_NumDeletedItems < 0) + { + dbg_msg("delta_dump", "| Invalid delta. Number of deleted items %d is negative.", pDelta->m_NumDeletedItems); + return -201; + } + pData += pDelta->m_NumDeletedItems; + if(pData > pEnd) + { + dbg_msg("delta_dump", "| Invalid delta. Read past the end."); + return -101; + } + + // list deleted items + // (all other items should be copied from the last full snap) + for(int d = 0; d < pDelta->m_NumDeletedItems; d++) + { + int Type = pDeleted[d] >> 16; + int Id = pDeleted[d] & 0xffff; + dbg_msg("delta_dump", " %3d %12d %08x deleted Type=%d Id=%d", DumpIndex++, pDeleted[d], pDeleted[d], Type, Id); + } + + // unpack updated stuff + for(int i = 0; i < pDelta->m_NumUpdateItems; i++) + { + if(pData + 2 > pEnd) + { + dbg_msg("delta_dump", "| Invalid delta. NumUpdateItems=%d can't be fit into DataSize=%d", pDelta->m_NumUpdateItems, DataSize); + return -102; + } + + dbg_msg("delta_dump", "| --------------------------------"); + dbg_msg("delta_dump", "| %3d %12d %08x updated Type=%d", DumpIndex++, *pData, *pData, *pData); + const int Type = *pData++; + if(Type < 0 || Type > CSnapshot::MAX_TYPE) + { + dbg_msg("delta_dump", "| Invalid delta. Type=%d out of range (0 - %d)", Type, CSnapshot::MAX_TYPE); + return -202; + } + + dbg_msg("delta_dump", "| %3d %12d %08x updated Id=%d", DumpIndex++, *pData, *pData, *pData); + const int Id = *pData++; + if(Id < 0 || Id > CSnapshot::MAX_ID) + { + dbg_msg("delta_dump", "| Invalid delta. Id=%d out of range (0 - %d)", Id, CSnapshot::MAX_ID); + return -203; + } + + // size of the item in bytes + int ItemSize; + if(Type < MAX_NETOBJSIZES && m_aItemSizes[Type]) + { + ItemSize = m_aItemSizes[Type]; + dbg_msg("delta_dump", "| updated size=%d (known)", ItemSize); + } + else + { + if(pData + 1 > pEnd) + { + dbg_msg("delta_dump", "| Invalid delta. Expected item size but got end of data."); + return -103; + } + if(*pData < 0 || (size_t)*pData > std::numeric_limits::max() / sizeof(int32_t)) + { + dbg_msg("delta_dump", "| Invalid delta. Item size %d out of range (0 - %" PRIzu ")", *pData, std::numeric_limits::max() / sizeof(int32_t)); + return -204; + } + dbg_msg("delta_dump", "| %3d %12d %08x updated size=%d", DumpIndex++, *pData, *pData, *pData); + ItemSize = (*pData++) * sizeof(int32_t); + } + + if(ItemSize < 0) + { + dbg_msg("delta_dump", "| Invalid delta. Item size %d is negative.", ItemSize); + return -205; + } + if((const char *)pEnd - (const char *)pData < ItemSize) + { + dbg_msg("delta_dump", "| Invalid delta. Item with type=%d id=%d size=%d does not fit into the delta.", Type, Id, ItemSize); + return -205; + } + + // divide item size in bytes by size of integers + // to get the number of integers we want to increment the pointer + const int *pItemEnd = pData + (ItemSize / sizeof(int32_t)); + + for(size_t b = 0; b < ItemSize / sizeof(int32_t); b++) + { + dbg_msg("delta_dump", "| %3d %12d %08x item data", DumpIndex++, *pData, *pData); + pData++; + } + + dbg_assert(pItemEnd == pData, "Incorrect amount of data dumped for this item."); + } + + dbg_msg("delta_dump", "| Finished with expected_data_size=%d parsed_data_size=%" PRIzu, DataSize, (pData - (int *)pSrcData) * sizeof(int32_t)); + dbg_msg("delta_dump", "+--------------------"); + + return 0; +} + +int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const void *pSrcData, int DataSize, bool Sixup) { CData *pDelta = (CData *)pSrcData; int *pData = (int *)pDelta->m_aData; @@ -386,7 +540,7 @@ int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const vo if(Keep) { - void *pObj = Builder.NewItem(pFromItem->Type(), pFromItem->ID(), ItemSize); + void *pObj = Builder.NewItem(pFromItem->Type(), pFromItem->Id(), ItemSize); if(!pObj) return -301; @@ -405,13 +559,14 @@ int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const vo if(Type < 0 || Type > CSnapshot::MAX_TYPE) return -202; - const int ID = *pData++; - if(ID < 0 || ID > CSnapshot::MAX_ID) + const int Id = *pData++; + if(Id < 0 || Id > CSnapshot::MAX_ID) return -203; int ItemSize; - if(Type < MAX_NETOBJSIZES && m_aItemSizes[Type]) - ItemSize = m_aItemSizes[Type]; + const short *pItemSizes = Sixup ? m_aItemSizes7 : m_aItemSizes; + if(Type < MAX_NETOBJSIZES && pItemSizes[Type]) + ItemSize = pItemSizes[Type]; else { if(pData + 1 > pEnd) @@ -424,12 +579,12 @@ int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const vo if(ItemSize < 0 || (const char *)pEnd - (const char *)pData < ItemSize) return -205; - const int Key = (Type << 16) | ID; + const int Key = (Type << 16) | Id; // create the item if needed int *pNewData = Builder.GetItemData(Key); if(!pNewData) - pNewData = (int *)Builder.NewItem(Type, ID, ItemSize); + pNewData = (int *)Builder.NewItem(Type, Id, ItemSize); if(!pNewData) return -302; @@ -587,8 +742,11 @@ int *CSnapshotBuilder::GetItemData(int Key) { for(int i = 0; i < m_NumItems; i++) { - if(GetItem(i)->Key() == Key) - return GetItem(i)->Data(); + CSnapshotItem *pItem = GetItem(i); + if(pItem->Key() == Key) + { + return pItem->Data(); + } } return nullptr; } @@ -596,12 +754,15 @@ int *CSnapshotBuilder::GetItemData(int Key) int CSnapshotBuilder::Finish(void *pSnapData) { // flatten and make the snapshot + dbg_assert(m_NumItems <= CSnapshot::MAX_ITEMS, "Too many snap items"); CSnapshot *pSnap = (CSnapshot *)pSnapData; pSnap->m_DataSize = m_DataSize; pSnap->m_NumItems = m_NumItems; + const size_t TotalSize = pSnap->TotalSize(); + dbg_assert(TotalSize <= (size_t)CSnapshot::MAX_SIZE, "Snapshot too large"); mem_copy(pSnap->Offsets(), m_aOffsets, pSnap->OffsetSize()); mem_copy(pSnap->DataStart(), m_aData, m_DataSize); - return pSnap->TotalSize(); + return TotalSize; } int CSnapshotBuilder::GetTypeFromIndex(int Index) const @@ -609,55 +770,73 @@ int CSnapshotBuilder::GetTypeFromIndex(int Index) const return CSnapshot::MAX_TYPE - Index; } -void CSnapshotBuilder::AddExtendedItemType(int Index) +bool CSnapshotBuilder::AddExtendedItemType(int Index) { dbg_assert(0 <= Index && Index < m_NumExtendedItemTypes, "index out of range"); - int TypeID = m_aExtendedItemTypes[Index]; - CUuid Uuid = g_UuidManager.GetUuid(TypeID); - int *pUuidItem = (int *)NewItem(0, GetTypeFromIndex(Index), sizeof(Uuid)); // NETOBJTYPE_EX - if(pUuidItem) + int *pUuidItem = static_cast(NewItem(0, GetTypeFromIndex(Index), sizeof(CUuid))); // NETOBJTYPE_EX + if(pUuidItem == nullptr) { - for(size_t i = 0; i < sizeof(CUuid) / sizeof(int32_t); i++) - pUuidItem[i] = bytes_be_to_uint(&Uuid.m_aData[i * sizeof(int32_t)]); + return false; + } + + const int TypeId = m_aExtendedItemTypes[Index]; + const CUuid Uuid = g_UuidManager.GetUuid(TypeId); + for(size_t i = 0; i < sizeof(CUuid) / sizeof(int32_t); i++) + { + pUuidItem[i] = bytes_be_to_uint(&Uuid.m_aData[i * sizeof(int32_t)]); } + return true; } -int CSnapshotBuilder::GetExtendedItemTypeIndex(int TypeID) +int CSnapshotBuilder::GetExtendedItemTypeIndex(int TypeId) { for(int i = 0; i < m_NumExtendedItemTypes; i++) { - if(m_aExtendedItemTypes[i] == TypeID) + if(m_aExtendedItemTypes[i] == TypeId) { return i; } } dbg_assert(m_NumExtendedItemTypes < MAX_EXTENDED_ITEM_TYPES, "too many extended item types"); int Index = m_NumExtendedItemTypes; - m_aExtendedItemTypes[Index] = TypeID; m_NumExtendedItemTypes++; - return Index; + m_aExtendedItemTypes[Index] = TypeId; + if(AddExtendedItemType(Index)) + { + return Index; + } + m_NumExtendedItemTypes--; + return -1; } -void *CSnapshotBuilder::NewItem(int Type, int ID, int Size) +void *CSnapshotBuilder::NewItem(int Type, int Id, int Size) { - if(ID == -1) + if(Id == -1) { return nullptr; } - if(m_DataSize + sizeof(CSnapshotItem) + Size >= CSnapshot::MAX_SIZE || - m_NumItems + 1 >= CSnapshot::MAX_ITEMS) + if(m_NumItems >= CSnapshot::MAX_ITEMS) { - dbg_assert(m_DataSize < CSnapshot::MAX_SIZE, "too much data"); - dbg_assert(m_NumItems < CSnapshot::MAX_ITEMS, "too many items"); return nullptr; } - bool Extended = false; - if(Type >= OFFSET_UUID) + const size_t OffsetSize = (m_NumItems + 1) * sizeof(int); + const size_t ItemSize = sizeof(CSnapshotItem) + Size; + if(sizeof(CSnapshot) + OffsetSize + m_DataSize + ItemSize > CSnapshot::MAX_SIZE) { - Extended = true; - Type = GetTypeFromIndex(GetExtendedItemTypeIndex(Type)); + return nullptr; + } + + const bool Extended = Type >= OFFSET_UUID; + if(Extended) + { + const int ExtendedItemTypeIndex = GetExtendedItemTypeIndex(Type); + if(ExtendedItemTypeIndex == -1) + { + return nullptr; + } + Type = GetTypeFromIndex(ExtendedItemTypeIndex); } CSnapshotItem *pObj = (CSnapshotItem *)(m_aData + m_DataSize); @@ -675,11 +854,11 @@ void *CSnapshotBuilder::NewItem(int Type, int ID, int Size) else if(Type < 0) return nullptr; - mem_zero(pObj, sizeof(CSnapshotItem) + Size); - pObj->m_TypeAndID = (Type << 16) | ID; + pObj->m_TypeAndId = (Type << 16) | Id; m_aOffsets[m_NumItems] = m_DataSize; - m_DataSize += sizeof(CSnapshotItem) + Size; + m_DataSize += ItemSize; m_NumItems++; + mem_zero(pObj->Data(), Size); return pObj->Data(); } diff --git a/src/engine/shared/snapshot.h b/src/engine/shared/snapshot.h index 6947db1bce..7a001221d5 100644 --- a/src/engine/shared/snapshot.h +++ b/src/engine/shared/snapshot.h @@ -6,6 +6,9 @@ #include #include +#include +#include + // CSnapshot class CSnapshotItem @@ -15,12 +18,13 @@ class CSnapshotItem int *Data() { return (int *)(this + 1); } public: - int m_TypeAndID; + int m_TypeAndId; const int *Data() const { return (int *)(this + 1); } - int Type() const { return m_TypeAndID >> 16; } - int ID() const { return m_TypeAndID & 0xffff; } - int Key() const { return m_TypeAndID; } + int Type() const { return m_TypeAndId >> 16; } + int Id() const { return m_TypeAndId & 0xffff; } + int Key() const { return m_TypeAndId; } + void Invalidate() { m_TypeAndId = -1; } }; class CSnapshot @@ -52,9 +56,10 @@ class CSnapshot const CSnapshotItem *GetItem(int Index) const; int GetItemSize(int Index) const; int GetItemIndex(int Key) const; + void InvalidateItem(int Index); int GetItemType(int Index) const; int GetExternalItemType(int InternalType) const; - const void *FindItem(int Type, int ID) const; + const void *FindItem(int Type, int Id) const; unsigned Crc() const; void DebugDump() const; @@ -83,6 +88,7 @@ class CSnapshotDelta MAX_NETOBJSIZES = 64 }; short m_aItemSizes[MAX_NETOBJSIZES]; + short m_aItemSizes7[MAX_NETOBJSIZES]; int m_aSnapshotDataRate[CSnapshot::MAX_TYPE + 1]; int m_aSnapshotDataUpdates[CSnapshot::MAX_TYPE + 1]; CData m_Empty; @@ -96,9 +102,11 @@ class CSnapshotDelta int GetDataRate(int Index) const { return m_aSnapshotDataRate[Index]; } int GetDataUpdates(int Index) const { return m_aSnapshotDataUpdates[Index]; } void SetStaticsize(int ItemType, size_t Size); + void SetStaticsize7(int ItemType, size_t Size); const CData *EmptyDelta() const; int CreateDelta(const CSnapshot *pFrom, const CSnapshot *pTo, void *pDstData); - int UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const void *pSrcData, int DataSize); + int UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const void *pSrcData, int DataSize, bool Sixup); + int DebugDumpDelta(const void *pSrcData, int DataSize); }; // CSnapshotStorage @@ -150,18 +158,19 @@ class CSnapshotBuilder int m_aExtendedItemTypes[MAX_EXTENDED_ITEM_TYPES]; int m_NumExtendedItemTypes; - void AddExtendedItemType(int Index); - int GetExtendedItemTypeIndex(int TypeID); + bool AddExtendedItemType(int Index); + int GetExtendedItemTypeIndex(int TypeId); int GetTypeFromIndex(int Index) const; - bool m_Sixup; + bool m_Sixup = false; public: CSnapshotBuilder(); void Init(bool Sixup = false); + void Init7(const CSnapshot *pSnapshot); - void *NewItem(int Type, int ID, int Size); + void *NewItem(int Type, int Id, int Size); CSnapshotItem *GetItem(int Index); int *GetItemData(int Key); diff --git a/src/engine/shared/storage.cpp b/src/engine/shared/storage.cpp index 3782830a66..7ff3cdc87b 100644 --- a/src/engine/shared/storage.cpp +++ b/src/engine/shared/storage.cpp @@ -39,8 +39,13 @@ class CStorage : public IStorage int Init(int StorageType, int NumArgs, const char **ppArguments) { -#if !defined(CONF_PLATFORM_ANDROID) - // get userdir, just use data directory on android +#if defined(CONF_PLATFORM_ANDROID) + // See InitAndroid in android_main.cpp for details about Android storage handling. + // The current working directory is set to the app specific external storage location + // on Android. The user data is stored within a folder "user" in the external storage. + str_copy(m_aUserdir, "user"); +#else + // get userdir char aFallbackUserdir[IO_MAX_PATH_LENGTH]; if(fs_storage_path("DDNet", m_aUserdir, sizeof(m_aUserdir))) { @@ -91,6 +96,7 @@ class CStorage : public IStorage CreateFolder("mapres", TYPE_SAVE); CreateFolder("downloadedmaps", TYPE_SAVE); CreateFolder("skins", TYPE_SAVE); + CreateFolder("skins7", TYPE_SAVE); CreateFolder("downloadedskins", TYPE_SAVE); CreateFolder("themes", TYPE_SAVE); CreateFolder("communityicons", TYPE_SAVE); @@ -109,6 +115,7 @@ class CStorage : public IStorage CreateFolder("demos", TYPE_SAVE); CreateFolder("demos/auto", TYPE_SAVE); CreateFolder("demos/auto/race", TYPE_SAVE); + CreateFolder("demos/auto/server", TYPE_SAVE); CreateFolder("demos/replays", TYPE_SAVE); CreateFolder("editor", TYPE_SAVE); CreateFolder("ghosts", TYPE_SAVE); @@ -121,7 +128,7 @@ class CStorage : public IStorage void LoadPaths(const char *pArgv0) { // check current directory - IOHANDLE File = io_open("storage.cfg", IOFLAG_READ | IOFLAG_SKIP_BOM); + IOHANDLE File = io_open("storage.cfg", IOFLAG_READ); if(!File) { // check usable path in argv[0] @@ -134,7 +141,7 @@ class CStorage : public IStorage char aBuffer[IO_MAX_PATH_LENGTH]; str_copy(aBuffer, pArgv0, Pos + 1); str_append(aBuffer, "/storage.cfg"); - File = io_open(aBuffer, IOFLAG_READ | IOFLAG_SKIP_BOM); + File = io_open(aBuffer, IOFLAG_READ); } if(Pos >= IO_MAX_PATH_LENGTH || !File) @@ -145,10 +152,12 @@ class CStorage : public IStorage } CLineReader LineReader; - LineReader.Init(File); - - char *pLine; - while((pLine = LineReader.Get())) + if(!LineReader.OpenFile(File)) + { + dbg_msg("storage", "couldn't open storage.cfg"); + return; + } + while(const char *pLine = LineReader.Get()) { const char *pLineWithoutPrefix = str_startswith(pLine, "add_path "); if(pLineWithoutPrefix) @@ -157,8 +166,6 @@ class CStorage : public IStorage } } - io_close(File); - if(!m_NumPaths) dbg_msg("storage", "no paths found in storage.cfg"); } @@ -559,7 +566,7 @@ class CStorage : public IStorage char *ReadFileStr(const char *pFilename, int Type) override { - IOHANDLE File = OpenFile(pFilename, IOFLAG_READ | IOFLAG_SKIP_BOM, Type); + IOHANDLE File = OpenFile(pFilename, IOFLAG_READ, Type); if(!File) return nullptr; char *pResult = io_read_all_str(File); diff --git a/src/engine/shared/teehistorian_ex_chunks.h b/src/engine/shared/teehistorian_ex_chunks.h index b1f07fefd1..8546e521c0 100644 --- a/src/engine/shared/teehistorian_ex_chunks.h +++ b/src/engine/shared/teehistorian_ex_chunks.h @@ -18,3 +18,6 @@ UUID(TEEHISTORIAN_TEAM_PRACTICE, "teehistorian-team-practice@ddnet.tw") UUID(TEEHISTORIAN_PLAYER_READY, "teehistorian-player-ready@ddnet.tw") UUID(TEEHISTORIAN_PLAYER_REJOIN, "teehistorian-rejoinver6@ddnet.org") UUID(TEEHISTORIAN_ANTIBOT, "teehistorian-antibot@ddnet.org") +UUID(TEEHISTORIAN_PLAYER_NAME, "teehistorian-player-name@ddnet.org") +UUID(TEEHISTORIAN_PLAYER_FINISH, "teehistorian-player-finish@ddnet.org") +UUID(TEEHISTORIAN_TEAM_FINISH, "teehistorian-team-finish@ddnet.org") diff --git a/src/engine/shared/translation_context.cpp b/src/engine/shared/translation_context.cpp new file mode 100644 index 0000000000..0214f2c22d --- /dev/null +++ b/src/engine/shared/translation_context.cpp @@ -0,0 +1,38 @@ +#include + +#include "translation_context.h" + +void CTranslationContext::Reset() +{ + m_ServerSettings.Reset(); + + std::fill(std::begin(m_apPlayerInfosRace), std::end(m_apPlayerInfosRace), nullptr); + + for(CClientData &Client : m_aClients) + Client.Reset(); + + std::fill(std::begin(m_aDamageTaken), std::end(m_aDamageTaken), 0); + std::fill(std::begin(m_aDamageTakenTick), std::end(m_aDamageTakenTick), 0); + std::fill(std::begin(m_aLocalClientId), std::end(m_aLocalClientId), -1); + + m_ShouldSendGameInfo = false; + m_GameStateFlags7 = 0; + + m_GameFlags = 0; + m_ScoreLimit = 0; + m_TimeLimit = 0; + m_MatchNum = 0; + m_MatchCurrent = 0; + + m_MapdownloadTotalsize = -1; + m_MapDownloadChunkSize = 0; + m_MapDownloadChunksPerRequest = 0; + + m_FlagCarrierBlue = 0; + m_FlagCarrierRed = 0; + m_TeamscoreRed = 0; + m_TeamscoreBlue = 0; + + m_GameStartTick7 = 0; + m_GameStateEndTick7 = 0; +} diff --git a/src/engine/shared/translation_context.h b/src/engine/shared/translation_context.h new file mode 100644 index 0000000000..7c574955bc --- /dev/null +++ b/src/engine/shared/translation_context.h @@ -0,0 +1,123 @@ +#ifndef ENGINE_SHARED_TRANSLATION_CONTEXT_H +#define ENGINE_SHARED_TRANSLATION_CONTEXT_H + +#include +#include + +#include + +class CTranslationContext +{ +public: + CTranslationContext() + { + Reset(); + } + + void Reset(); + + // this class is not used + // it could be used in the in game menu + // to grey out buttons and similar + // + // but that can not be done without mixing it + // into the 0.6 code so it is out of scope for ddnet + class CServerSettings + { + public: + bool m_KickVote; + int m_KickMin; + bool m_SpecVote; + bool m_TeamLock; + bool m_TeamBalance; + int m_PlayerSlots; + + CServerSettings() + { + Reset(); + } + + void Reset() + { + m_KickVote = false; + m_KickMin = 0; + m_SpecVote = false; + m_TeamLock = false; + m_TeamBalance = false; + m_PlayerSlots = 0; + } + + } m_ServerSettings; + + class CClientData + { + public: + CClientData() + { + Reset(); + } + + void Reset() + { + m_Active = false; + + m_UseCustomColor = 0; + m_ColorBody = 0; + m_ColorFeet = 0; + + m_aName[0] = '\0'; + m_aClan[0] = '\0'; + m_Country = 0; + m_aSkinName[0] = '\0'; + m_SkinColor = 0; + m_Team = 0; + m_PlayerFlags7 = 0; + } + + bool m_Active; + + int m_UseCustomColor; + int m_ColorBody; + int m_ColorFeet; + + char m_aName[MAX_NAME_LENGTH]; + char m_aClan[MAX_CLAN_LENGTH]; + int m_Country; + char m_aSkinName[protocol7::MAX_SKIN_LENGTH]; + int m_SkinColor; + int m_Team; + int m_PlayerFlags7; + }; + + const protocol7::CNetObj_PlayerInfoRace *m_apPlayerInfosRace[MAX_CLIENTS]; + CClientData m_aClients[MAX_CLIENTS]; + int m_aDamageTaken[MAX_CLIENTS]; + float m_aDamageTakenTick[MAX_CLIENTS]; + + int m_aLocalClientId[NUM_DUMMIES]; + + bool m_ShouldSendGameInfo; + int m_GameStateFlags7; + // 0.7 game flags + // use in combination with protocol7::GAMEFLAG_* + // for example protocol7::GAMEFLAG_TEAMS + int m_GameFlags; + int m_ScoreLimit; + int m_TimeLimit; + int m_MatchNum; + int m_MatchCurrent; + + int m_MapdownloadTotalsize; + int m_MapDownloadChunkSize; + int m_MapDownloadChunksPerRequest; + + int m_FlagCarrierBlue; + int m_FlagCarrierRed; + int m_TeamscoreRed; + int m_TeamscoreBlue; + + int m_GameStartTick7; + int m_GameStateEndTick7; +}; + +#endif diff --git a/src/engine/shared/uuid_manager.cpp b/src/engine/shared/uuid_manager.cpp index 3cff589d60..de69044b81 100644 --- a/src/engine/shared/uuid_manager.cpp +++ b/src/engine/shared/uuid_manager.cpp @@ -106,19 +106,19 @@ bool CUuid::operator<(const CUuid &Other) const return mem_comp(this, &Other, sizeof(*this)) < 0; } -static int GetIndex(int ID) +static int GetIndex(int Id) { - return ID - OFFSET_UUID; + return Id - OFFSET_UUID; } -static int GetID(int Index) +static int GetId(int Index) { return Index + OFFSET_UUID; } -void CUuidManager::RegisterName(int ID, const char *pName) +void CUuidManager::RegisterName(int Id, const char *pName) { - dbg_assert(GetIndex(ID) == (int)m_vNames.size(), "names must be registered with increasing ID"); + dbg_assert(GetIndex(Id) == (int)m_vNames.size(), "names must be registered with increasing ID"); CName Name; Name.m_pName = pName; Name.m_Uuid = CalculateUuid(pName); @@ -128,29 +128,29 @@ void CUuidManager::RegisterName(int ID, const char *pName) CNameIndexed NameIndexed; NameIndexed.m_Uuid = Name.m_Uuid; - NameIndexed.m_ID = GetIndex(ID); + NameIndexed.m_Id = GetIndex(Id); m_vNamesSorted.insert(std::lower_bound(m_vNamesSorted.begin(), m_vNamesSorted.end(), NameIndexed), NameIndexed); } -CUuid CUuidManager::GetUuid(int ID) const +CUuid CUuidManager::GetUuid(int Id) const { - return m_vNames[GetIndex(ID)].m_Uuid; + return m_vNames[GetIndex(Id)].m_Uuid; } -const char *CUuidManager::GetName(int ID) const +const char *CUuidManager::GetName(int Id) const { - return m_vNames[GetIndex(ID)].m_pName; + return m_vNames[GetIndex(Id)].m_pName; } int CUuidManager::LookupUuid(CUuid Uuid) const { CNameIndexed Needle; Needle.m_Uuid = Uuid; - Needle.m_ID = 0; + Needle.m_Id = 0; auto Range = std::equal_range(m_vNamesSorted.begin(), m_vNamesSorted.end(), Needle); if(std::distance(Range.first, Range.second) == 1) { - return GetID(Range.first->m_ID); + return GetId(Range.first->m_Id); } return UUID_UNKNOWN; } @@ -177,9 +177,9 @@ int CUuidManager::UnpackUuid(CUnpacker *pUnpacker, CUuid *pOut) const return LookupUuid(*pUuid); } -void CUuidManager::PackUuid(int ID, CPacker *pPacker) const +void CUuidManager::PackUuid(int Id, CPacker *pPacker) const { - CUuid Uuid = GetUuid(ID); + CUuid Uuid = GetUuid(Id); pPacker->AddRaw(&Uuid, sizeof(Uuid)); } diff --git a/src/engine/shared/uuid_manager.h b/src/engine/shared/uuid_manager.h index 00d1a4822f..d8b10086a0 100644 --- a/src/engine/shared/uuid_manager.h +++ b/src/engine/shared/uuid_manager.h @@ -40,7 +40,7 @@ struct CName struct CNameIndexed { CUuid m_Uuid; - int m_ID; + int m_Id; bool operator<(const CNameIndexed &Other) const { return m_Uuid < Other.m_Uuid; } bool operator==(const CNameIndexed &Other) const { return m_Uuid == Other.m_Uuid; } @@ -55,15 +55,15 @@ class CUuidManager std::vector m_vNamesSorted; public: - void RegisterName(int ID, const char *pName); - CUuid GetUuid(int ID) const; - const char *GetName(int ID) const; + void RegisterName(int Id, const char *pName); + CUuid GetUuid(int Id) const; + const char *GetName(int Id) const; int LookupUuid(CUuid Uuid) const; int NumUuids() const; int UnpackUuid(CUnpacker *pUnpacker) const; int UnpackUuid(CUnpacker *pUnpacker, CUuid *pOut) const; - void PackUuid(int ID, CPacker *pPacker) const; + void PackUuid(int Id, CPacker *pPacker) const; void DebugDump() const; }; diff --git a/src/engine/shared/video.h b/src/engine/shared/video.h index c93cdd80a9..1a97f5a52e 100644 --- a/src/engine/shared/video.h +++ b/src/engine/shared/video.h @@ -12,7 +12,7 @@ class IVideo public: virtual ~IVideo(){}; - virtual void Start() = 0; + virtual bool Start() = 0; virtual void Stop() = 0; virtual void Pause(bool Pause) = 0; virtual bool IsRecording() = 0; diff --git a/src/engine/sound.h b/src/engine/sound.h index f5e2c08d54..e4462a95a1 100644 --- a/src/engine/sound.h +++ b/src/engine/sound.h @@ -6,6 +6,8 @@ #include #include +#include + class ISound : public IInterface { MACRO_INTERFACE("sound") @@ -28,7 +30,7 @@ class ISound : public IInterface // unused struct CSampleHandle { - int m_SampleID; + int m_SampleId; }; struct CVoiceShapeCircle @@ -67,32 +69,34 @@ class ISound : public IInterface virtual int LoadWV(const char *pFilename, int StorageType = IStorage::TYPE_ALL) = 0; virtual int LoadOpusFromMem(const void *pData, unsigned DataSize, bool FromEditor = false) = 0; virtual int LoadWVFromMem(const void *pData, unsigned DataSize, bool FromEditor = false) = 0; - virtual void UnloadSample(int SampleID) = 0; + virtual void UnloadSample(int SampleId) = 0; - virtual float GetSampleTotalTime(int SampleID) = 0; // in s - virtual float GetSampleCurrentTime(int SampleID) = 0; // in s - virtual void SetSampleCurrentTime(int SampleID, float Time) = 0; + virtual float GetSampleTotalTime(int SampleId) = 0; // in s + virtual float GetSampleCurrentTime(int SampleId) = 0; // in s + virtual void SetSampleCurrentTime(int SampleId, float Time) = 0; - virtual void SetChannel(int ChannelID, float Volume, float Panning) = 0; - virtual void SetListenerPos(float x, float y) = 0; + virtual void SetChannel(int ChannelId, float Volume, float Panning) = 0; + virtual void SetListenerPosition(vec2 Position) = 0; virtual void SetVoiceVolume(CVoiceHandle Voice, float Volume) = 0; virtual void SetVoiceFalloff(CVoiceHandle Voice, float Falloff) = 0; - virtual void SetVoiceLocation(CVoiceHandle Voice, float x, float y) = 0; + virtual void SetVoicePosition(CVoiceHandle Voice, vec2 Position) = 0; virtual void SetVoiceTimeOffset(CVoiceHandle Voice, float TimeOffset) = 0; // in s virtual void SetVoiceCircle(CVoiceHandle Voice, float Radius) = 0; virtual void SetVoiceRectangle(CVoiceHandle Voice, float Width, float Height) = 0; - virtual CVoiceHandle PlayAt(int ChannelID, int SampleID, int Flags, float x, float y) = 0; - virtual CVoiceHandle Play(int ChannelID, int SampleID, int Flags) = 0; - virtual void Pause(int SampleID) = 0; - virtual void Stop(int SampleID) = 0; + virtual CVoiceHandle PlayAt(int ChannelId, int SampleId, int Flags, float Volume, vec2 Position) = 0; + virtual CVoiceHandle Play(int ChannelId, int SampleId, int Flags, float Volume) = 0; + virtual void Pause(int SampleId) = 0; + virtual void Stop(int SampleId) = 0; virtual void StopAll() = 0; virtual void StopVoice(CVoiceHandle Voice) = 0; - virtual bool IsPlaying(int SampleID) = 0; + virtual bool IsPlaying(int SampleId) = 0; + virtual int MixingRate() const = 0; virtual void Mix(short *pFinalOut, unsigned Frames) = 0; + // useful for thread synchronization virtual void PauseAudioDevice() = 0; virtual void UnpauseAudioDevice() = 0; diff --git a/src/engine/textrender.h b/src/engine/textrender.h index 0d1287c3d4..3b26e0aaa7 100644 --- a/src/engine/textrender.h +++ b/src/engine/textrender.h @@ -66,6 +66,7 @@ enum class EFontPreset namespace FontIcons { // Each font icon is named according to its official name in Font Awesome MAYBE_UNUSED static const char *FONT_ICON_PLUS = "+"; +MAYBE_UNUSED static const char *FONT_ICON_MINUS = "-"; MAYBE_UNUSED static const char *FONT_ICON_LOCK = "\xEF\x80\xA3"; MAYBE_UNUSED static const char *FONT_ICON_MAGNIFYING_GLASS = "\xEF\x80\x82"; MAYBE_UNUSED static const char *FONT_ICON_HEART = "\xEF\x80\x84"; @@ -89,8 +90,10 @@ MAYBE_UNUSED static const char *FONT_ICON_GEAR = "\xEF\x80\x93"; MAYBE_UNUSED static const char *FONT_ICON_PEN_TO_SQUARE = "\xEF\x81\x84"; MAYBE_UNUSED static const char *FONT_ICON_CLAPPERBOARD = "\xEE\x84\xB1"; MAYBE_UNUSED static const char *FONT_ICON_EARTH_AMERICAS = "\xEF\x95\xBD"; +MAYBE_UNUSED static const char *FONT_ICON_NETWORK_WIRED = "\xEF\x9B\xBF"; MAYBE_UNUSED static const char *FONT_ICON_LIST_UL = "\xEF\x83\x8A"; MAYBE_UNUSED static const char *FONT_ICON_INFO = "\xEF\x84\xA9"; +MAYBE_UNUSED static const char *FONT_ICON_TERMINAL = "\xEF\x84\xA0"; MAYBE_UNUSED static const char *FONT_ICON_SLASH = "\xEF\x9C\x95"; MAYBE_UNUSED static const char *FONT_ICON_PLAY = "\xEF\x81\x8B"; @@ -131,6 +134,7 @@ MAYBE_UNUSED static const char *FONT_ICON_CIRCLE_PLAY = "\xEF\x85\x84"; MAYBE_UNUSED static const char *FONT_ICON_BORDER_ALL = "\xEF\xA1\x8C"; MAYBE_UNUSED static const char *FONT_ICON_EYE = "\xEF\x81\xAE"; MAYBE_UNUSED static const char *FONT_ICON_EYE_SLASH = "\xEF\x81\xB0"; +MAYBE_UNUSED static const char *FONT_ICON_EYE_DROPPER = "\xEF\x87\xBB"; MAYBE_UNUSED static const char *FONT_ICON_DICE_ONE = "\xEF\x94\xA5"; MAYBE_UNUSED static const char *FONT_ICON_DICE_TWO = "\xEF\x94\xA8"; @@ -142,6 +146,9 @@ MAYBE_UNUSED static const char *FONT_ICON_DICE_SIX = "\xEF\x94\xA6"; MAYBE_UNUSED static const char *FONT_ICON_LAYER_GROUP = "\xEF\x97\xBD"; MAYBE_UNUSED static const char *FONT_ICON_UNDO = "\xEF\x8B\xAA"; MAYBE_UNUSED static const char *FONT_ICON_REDO = "\xEF\x8B\xB9"; + +MAYBE_UNUSED static const char *FONT_ICON_ARROWS_ROTATE = "\xEF\x80\xA1"; +MAYBE_UNUSED static const char *FONT_ICON_QUESTION = "?"; } // end namespace FontIcons enum ETextCursorSelectionMode @@ -212,6 +219,7 @@ class CTextCursor float m_FontSize; float m_AlignedFontSize; float m_LineSpacing; + float m_AlignedLineSpacing; ETextCursorSelectionMode m_CalculateSelectionMode; float m_SelectionHeightFactor; @@ -237,7 +245,7 @@ class CTextCursor float Height() const { - return m_LineCount * m_AlignedFontSize + std::max(0, m_LineCount - 1) * m_LineSpacing; + return m_LineCount * (m_AlignedFontSize + m_AlignedLineSpacing); } STextBoundingBox BoundingBox() const @@ -337,18 +345,18 @@ class ITextRender : public IInterface virtual STextBoundingBox GetBoundingBoxTextContainer(STextContainerIndex TextContainerIndex) = 0; - virtual void UploadEntityLayerText(void *pTexBuff, size_t PixelSize, size_t TexWidth, size_t TexHeight, int TexSubWidth, int TexSubHeight, const char *pText, int Length, float x, float y, int FontSize) = 0; + virtual void UploadEntityLayerText(const CImageInfo &TextImage, int TexSubWidth, int TexSubHeight, const char *pText, int Length, float x, float y, int FontSize) = 0; virtual int AdjustFontSize(const char *pText, int TextLength, int MaxSize, int MaxWidth) const = 0; virtual float GetGlyphOffsetX(int FontSize, char TextCharacter) const = 0; virtual int CalculateTextWidth(const char *pText, int TextLength, int FontWidth, int FontSize) const = 0; // old foolish interface virtual void TextColor(float r, float g, float b, float a) = 0; - virtual void TextColor(ColorRGBA rgb) = 0; + virtual void TextColor(ColorRGBA Color) = 0; virtual void TextOutlineColor(float r, float g, float b, float a) = 0; - virtual void TextOutlineColor(ColorRGBA rgb) = 0; + virtual void TextOutlineColor(ColorRGBA Color) = 0; virtual void TextSelectionColor(float r, float g, float b, float a) = 0; - virtual void TextSelectionColor(ColorRGBA rgb) = 0; + virtual void TextSelectionColor(ColorRGBA Color) = 0; virtual void Text(float x, float y, float Size, const char *pText, float LineWidth = -1.0f) = 0; virtual float TextWidth(float Size, const char *pText, int StrLength = -1, float LineWidth = -1.0f, int Flags = 0, const STextSizeProperties &TextSizeProps = {}) = 0; virtual STextBoundingBox TextBoundingBox(float Size, const char *pText, int StrLength = -1, float LineWidth = -1.0f, float LineSpacing = 0.0f, int Flags = 0) = 0; diff --git a/src/engine/updater.h b/src/engine/updater.h index 8a3be8651f..3de864091c 100644 --- a/src/engine/updater.h +++ b/src/engine/updater.h @@ -7,9 +7,9 @@ class IUpdater : public IInterface { MACRO_INTERFACE("updater") public: - enum + enum EUpdaterState { - CLEAN = 0, + CLEAN, GETTING_MANIFEST, GOT_MANIFEST, PARSING_UPDATE, @@ -22,7 +22,7 @@ class IUpdater : public IInterface virtual void Update() = 0; virtual void InitiateUpdate() = 0; - virtual int GetCurrentState() = 0; + virtual EUpdaterState GetCurrentState() = 0; virtual void GetCurrentFile(char *pBuf, int BufSize) = 0; virtual int GetCurrentPercent() = 0; }; diff --git a/src/engine/warning.h b/src/engine/warning.h index bd1e45e752..4532a10353 100644 --- a/src/engine/warning.h +++ b/src/engine/warning.h @@ -7,8 +7,8 @@ struct SWarning SWarning(const char *pMsg); SWarning(const char *pTitle, const char *pMsg); - char m_aWarningTitle[128]; - char m_aWarningMsg[256]; + char m_aWarningTitle[128] = ""; + char m_aWarningMsg[256] = ""; bool m_WasShown = false; bool m_AutoHide = true; }; diff --git a/src/game/alloc.h b/src/game/alloc.h index b4a76a9bd9..4a829a1184 100644 --- a/src/game/alloc.h +++ b/src/game/alloc.h @@ -22,9 +22,9 @@ public: \ void *operator new(size_t Size) \ { \ - void *p = malloc(Size); \ - mem_zero(p, Size); \ - return p; \ + void *pObj = malloc(Size); \ + mem_zero(pObj, Size); \ + return pObj; \ } \ void operator delete(void *pPtr) \ { \ @@ -35,9 +35,9 @@ public: \ #define MACRO_ALLOC_POOL_ID() \ public: \ - void *operator new(size_t Size, int id); \ - void operator delete(void *p, int id); \ - void operator delete(void *p); /* NOLINT(misc-new-delete-overloads) */ \ + void *operator new(size_t Size, int Id); \ + void operator delete(void *pObj, int Id); \ + void operator delete(void *pObj); /* NOLINT(misc-new-delete-overloads) */ \ \ private: @@ -51,30 +51,30 @@ public: \ static char gs_PoolData##POOLTYPE[PoolSize][MACRO_ALLOC_GET_SIZE(POOLTYPE)] = {{0}}; \ static int gs_PoolUsed##POOLTYPE[PoolSize] = {0}; \ MAYBE_UNUSED static int gs_PoolDummy##POOLTYPE = (ASAN_POISON_MEMORY_REGION(gs_PoolData##POOLTYPE, sizeof(gs_PoolData##POOLTYPE)), 0); \ - void *POOLTYPE::operator new(size_t Size, int id) \ + void *POOLTYPE::operator new(size_t Size, int Id) \ { \ dbg_assert(sizeof(POOLTYPE) >= Size, "size error"); \ - dbg_assert(!gs_PoolUsed##POOLTYPE[id], "already used"); \ - ASAN_UNPOISON_MEMORY_REGION(gs_PoolData##POOLTYPE[id], sizeof(gs_PoolData##POOLTYPE[id])); \ - gs_PoolUsed##POOLTYPE[id] = 1; \ - mem_zero(gs_PoolData##POOLTYPE[id], sizeof(gs_PoolData##POOLTYPE[id])); \ - return gs_PoolData##POOLTYPE[id]; \ + dbg_assert(!gs_PoolUsed##POOLTYPE[Id], "already used"); \ + ASAN_UNPOISON_MEMORY_REGION(gs_PoolData##POOLTYPE[Id], sizeof(gs_PoolData##POOLTYPE[Id])); \ + gs_PoolUsed##POOLTYPE[Id] = 1; \ + mem_zero(gs_PoolData##POOLTYPE[Id], sizeof(gs_PoolData##POOLTYPE[Id])); \ + return gs_PoolData##POOLTYPE[Id]; \ } \ - void POOLTYPE::operator delete(void *p, int id) \ + void POOLTYPE::operator delete(void *pObj, int Id) \ { \ - dbg_assert(gs_PoolUsed##POOLTYPE[id], "not used"); \ - dbg_assert(id == (POOLTYPE *)p - (POOLTYPE *)gs_PoolData##POOLTYPE, "invalid id"); \ - gs_PoolUsed##POOLTYPE[id] = 0; \ - mem_zero(gs_PoolData##POOLTYPE[id], sizeof(gs_PoolData##POOLTYPE[id])); \ - ASAN_POISON_MEMORY_REGION(gs_PoolData##POOLTYPE[id], sizeof(gs_PoolData##POOLTYPE[id])); \ + dbg_assert(gs_PoolUsed##POOLTYPE[Id], "not used"); \ + dbg_assert(Id == (POOLTYPE *)pObj - (POOLTYPE *)gs_PoolData##POOLTYPE, "invalid id"); \ + gs_PoolUsed##POOLTYPE[Id] = 0; \ + mem_zero(gs_PoolData##POOLTYPE[Id], sizeof(gs_PoolData##POOLTYPE[Id])); \ + ASAN_POISON_MEMORY_REGION(gs_PoolData##POOLTYPE[Id], sizeof(gs_PoolData##POOLTYPE[Id])); \ } \ - void POOLTYPE::operator delete(void *p) /* NOLINT(misc-new-delete-overloads) */ \ + void POOLTYPE::operator delete(void *pObj) /* NOLINT(misc-new-delete-overloads) */ \ { \ - int id = (POOLTYPE *)p - (POOLTYPE *)gs_PoolData##POOLTYPE; \ - dbg_assert(gs_PoolUsed##POOLTYPE[id], "not used"); \ - gs_PoolUsed##POOLTYPE[id] = 0; \ - mem_zero(gs_PoolData##POOLTYPE[id], sizeof(gs_PoolData##POOLTYPE[id])); \ - ASAN_POISON_MEMORY_REGION(gs_PoolData##POOLTYPE[id], sizeof(gs_PoolData##POOLTYPE[id])); \ + int Id = (POOLTYPE *)pObj - (POOLTYPE *)gs_PoolData##POOLTYPE; \ + dbg_assert(gs_PoolUsed##POOLTYPE[Id], "not used"); \ + gs_PoolUsed##POOLTYPE[Id] = 0; \ + mem_zero(gs_PoolData##POOLTYPE[Id], sizeof(gs_PoolData##POOLTYPE[Id])); \ + ASAN_POISON_MEMORY_REGION(gs_PoolData##POOLTYPE[Id], sizeof(gs_PoolData##POOLTYPE[Id])); \ } #endif diff --git a/src/game/client/component.cpp b/src/game/client/component.cpp index 754aea48d5..c682d7457d 100644 --- a/src/game/client/component.cpp +++ b/src/game/client/component.cpp @@ -2,13 +2,15 @@ #include "gameclient.h" +#include + class IKernel *CComponent::Kernel() const { return m_pClient->Kernel(); } class IEngine *CComponent::Engine() const { return m_pClient->Engine(); } class IGraphics *CComponent::Graphics() const { return m_pClient->Graphics(); } class ITextRender *CComponent::TextRender() const { return m_pClient->TextRender(); } class IInput *CComponent::Input() const { return m_pClient->Input(); } class IStorage *CComponent::Storage() const { return m_pClient->Storage(); } -class CUI *CComponent::UI() const { return m_pClient->UI(); } +class CUi *CComponent::Ui() const { return m_pClient->Ui(); } class ISound *CComponent::Sound() const { return m_pClient->Sound(); } class CRenderTools *CComponent::RenderTools() const { return m_pClient->RenderTools(); } class IConfigManager *CComponent::ConfigManager() const { return m_pClient->ConfigManager(); } @@ -27,6 +29,15 @@ class IUpdater *CComponent::Updater() const } #endif +int64_t CComponent::time() const +{ +#if defined(CONF_VIDEORECORDER) + return IVideo::Current() ? IVideo::Time() : time_get(); +#else + return time_get(); +#endif +} + float CComponent::LocalTime() const { #if defined(CONF_VIDEORECORDER) @@ -40,3 +51,5 @@ class IClient *CComponent::Client() const { return m_pClient->Client(); } + +class IHttp *CComponent::Http() const { return m_pClient->Http(); } diff --git a/src/game/client/component.h b/src/game/client/component.h index 8e754e7f32..639c8b140a 100644 --- a/src/game/client/component.h +++ b/src/game/client/component.h @@ -49,7 +49,7 @@ class CComponent /** * Get the ui interface. */ - class CUI *UI() const; + class CUi *Ui() const; /** * Get the sound interface. */ @@ -104,30 +104,22 @@ class CComponent class IUpdater *Updater() const; #endif -#if defined(CONF_VIDEORECORDER) - /** - * Gets the current time. - * @see time_get() - */ - int64_t time() const - { - return IVideo::Current() ? IVideo::Time() : time_get(); - } -#else /** * Gets the current time. * @see time_get() */ - int64_t time() const - { - return time_get(); - } -#endif + int64_t time() const; + /** * Gets the local time. */ float LocalTime() const; + /** + * Get the http interface + */ + class IHttp *Http() const; + public: /** * The component virtual destructor. @@ -177,6 +169,10 @@ class CComponent * Called when the window has been resized. */ virtual void OnWindowResize() {} + /** + * Called when skins have been invalidated and must be updated. + */ + virtual void OnRefreshSkins() {} /** * Called when the component should get rendered. * diff --git a/src/game/client/components/background.cpp b/src/game/client/components/background.cpp index 425ec5bf64..1d7c41c61f 100644 --- a/src/game/client/components/background.cpp +++ b/src/game/client/components/background.cpp @@ -60,7 +60,7 @@ void CBackground::LoadBackground() bool NeedImageLoading = false; char aBuf[IO_MAX_PATH_LENGTH]; - str_format(aBuf, sizeof(aBuf), "maps/%s", g_Config.m_ClBackgroundEntities); + str_format(aBuf, sizeof(aBuf), "maps/%s%s", g_Config.m_ClBackgroundEntities, str_endswith(g_Config.m_ClBackgroundEntities, ".map") ? "" : ".map"); if(str_comp(g_Config.m_ClBackgroundEntities, CURRENT_MAP) == 0) { m_pMap = Kernel()->RequestInterface(); diff --git a/src/game/client/components/binds.cpp b/src/game/client/components/binds.cpp index 1ec52bc76f..3e7ca824de 100644 --- a/src/game/client/components/binds.cpp +++ b/src/game/client/components/binds.cpp @@ -5,32 +5,23 @@ #include #include +#include +#include #include static const ColorRGBA gs_BindPrintColor{1.0f, 1.0f, 0.8f, 1.0f}; bool CBinds::CBindsSpecial::OnInput(const IInput::CEvent &Event) { + if((Event.m_Flags & (IInput::FLAG_PRESS | IInput::FLAG_RELEASE)) == 0) + return false; + // only handle F and composed F binds - if(((Event.m_Key >= KEY_F1 && Event.m_Key <= KEY_F12) || (Event.m_Key >= KEY_F13 && Event.m_Key <= KEY_F24)) && (Event.m_Key != KEY_F5 || !m_pClient->m_Menus.IsActive())) + // do not handle F5 bind while menu is active + if(((Event.m_Key >= KEY_F1 && Event.m_Key <= KEY_F12) || (Event.m_Key >= KEY_F13 && Event.m_Key <= KEY_F24)) && + (Event.m_Key != KEY_F5 || !m_pClient->m_Menus.IsActive())) { - int Mask = CBinds::GetModifierMask(Input()); - - // Look for a composed bind - bool ret = false; - if(m_pBinds->m_aapKeyBindings[Mask][Event.m_Key]) - { - m_pBinds->GetConsole()->ExecuteLineStroked(Event.m_Flags & IInput::FLAG_PRESS, m_pBinds->m_aapKeyBindings[Mask][Event.m_Key]); - ret = true; - } - // Look for a non composed bind - if(!ret && m_pBinds->m_aapKeyBindings[0][Event.m_Key]) - { - m_pBinds->GetConsole()->ExecuteLineStroked(Event.m_Flags & IInput::FLAG_PRESS, m_pBinds->m_aapKeyBindings[0][Event.m_Key]); - ret = true; - } - - return ret; + return m_pBinds->OnInput(Event); } return false; @@ -44,37 +35,33 @@ CBinds::CBinds() CBinds::~CBinds() { - for(int i = 0; i < KEY_LAST; i++) - for(auto &apKeyBinding : m_aapKeyBindings) - free(apKeyBinding[i]); + UnbindAll(); } -void CBinds::Bind(int KeyID, const char *pStr, bool FreeOnly, int ModifierCombination) +void CBinds::Bind(int KeyId, const char *pStr, bool FreeOnly, int ModifierCombination) { - if(KeyID < 0 || KeyID >= KEY_LAST) - return; + dbg_assert(KeyId >= KEY_FIRST && KeyId < KEY_LAST, "KeyId invalid"); + dbg_assert(ModifierCombination >= MODIFIER_NONE && ModifierCombination < MODIFIER_COMBINATION_COUNT, "ModifierCombination invalid"); - if(FreeOnly && Get(KeyID, ModifierCombination)[0]) + if(FreeOnly && Get(KeyId, ModifierCombination)[0]) return; - free(m_aapKeyBindings[ModifierCombination][KeyID]); - m_aapKeyBindings[ModifierCombination][KeyID] = 0; - - // skip modifiers for +xxx binds - if(pStr[0] == '+') - ModifierCombination = 0; + free(m_aapKeyBindings[ModifierCombination][KeyId]); + m_aapKeyBindings[ModifierCombination][KeyId] = nullptr; char aBuf[256]; + char aModifiers[128]; + GetKeyBindModifiersName(ModifierCombination, aModifiers, sizeof(aModifiers)); if(!pStr[0]) { - str_format(aBuf, sizeof(aBuf), "unbound %s%s (%d)", GetKeyBindModifiersName(ModifierCombination), Input()->KeyName(KeyID), KeyID); + str_format(aBuf, sizeof(aBuf), "unbound %s%s (%d)", aModifiers, Input()->KeyName(KeyId), KeyId); } else { int Size = str_length(pStr) + 1; - m_aapKeyBindings[ModifierCombination][KeyID] = (char *)malloc(Size); - str_copy(m_aapKeyBindings[ModifierCombination][KeyID], pStr, Size); - str_format(aBuf, sizeof(aBuf), "bound %s%s (%d) = %s", GetKeyBindModifiersName(ModifierCombination), Input()->KeyName(KeyID), KeyID, m_aapKeyBindings[ModifierCombination][KeyID]); + m_aapKeyBindings[ModifierCombination][KeyId] = (char *)malloc(Size); + str_copy(m_aapKeyBindings[ModifierCombination][KeyId], pStr, Size); + str_format(aBuf, sizeof(aBuf), "bound %s%s (%d) = %s", aModifiers, Input()->KeyName(KeyId), KeyId, m_aapKeyBindings[ModifierCombination][KeyId]); } Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "binds", aBuf, gs_BindPrintColor); } @@ -120,59 +107,113 @@ int CBinds::GetModifierMaskOfKey(int Key) case KEY_RGUI: return 1 << CBinds::MODIFIER_GUI; default: - return 0; + return CBinds::MODIFIER_NONE; } } bool CBinds::OnInput(const IInput::CEvent &Event) { - // don't handle invalid events - if(Event.m_Key <= KEY_FIRST || Event.m_Key >= KEY_LAST) + if((Event.m_Flags & (IInput::FLAG_PRESS | IInput::FLAG_RELEASE)) == 0) return false; - int Mask = GetModifierMask(Input()); - int KeyModifierMask = GetModifierMaskOfKey(Event.m_Key); - Mask &= ~KeyModifierMask; + const int KeyModifierMask = GetModifierMaskOfKey(Event.m_Key); + const int ModifierMask = GetModifierMask(Input()) & ~KeyModifierMask; - bool ret = false; - const char *pKey = nullptr; + bool Handled = false; - if(m_aapKeyBindings[Mask][Event.m_Key]) + if(Event.m_Flags & IInput::FLAG_PRESS) { - if(Event.m_Flags & IInput::FLAG_PRESS) + auto ActiveBind = std::find_if(m_vActiveBinds.begin(), m_vActiveBinds.end(), [&](const CBindSlot &Bind) { + return Event.m_Key == Bind.m_Key; + }); + if(ActiveBind == m_vActiveBinds.end()) { - Console()->ExecuteLineStroked(1, m_aapKeyBindings[Mask][Event.m_Key]); - pKey = m_aapKeyBindings[Mask][Event.m_Key]; + const auto &&OnKeyPress = [&](int Mask) { + const char *pBind = m_aapKeyBindings[Mask][Event.m_Key]; + if(g_Config.m_ClSubTickAiming) + { + if(str_comp("+fire", pBind) == 0 || str_comp("+hook", pBind) == 0) + { + m_MouseOnAction = true; + } + } + Console()->ExecuteLineStroked(1, pBind); + m_vActiveBinds.emplace_back(Event.m_Key, Mask); + }; + + if(m_aapKeyBindings[ModifierMask][Event.m_Key]) + { + OnKeyPress(ModifierMask); + Handled = true; + } + else if(m_aapKeyBindings[MODIFIER_NONE][Event.m_Key] && + ModifierMask != ((1 << MODIFIER_CTRL) | (1 << MODIFIER_SHIFT)) && + ModifierMask != ((1 << MODIFIER_GUI) | (1 << MODIFIER_SHIFT))) + { + OnKeyPress(MODIFIER_NONE); + Handled = true; + } + } + else + { + // Repeat active bind while key is held down + // Have to check for nullptr again because the previous execute can unbind itself + if(m_aapKeyBindings[ActiveBind->m_ModifierMask][ActiveBind->m_Key]) + { + Console()->ExecuteLineStroked(1, m_aapKeyBindings[ActiveBind->m_ModifierMask][ActiveBind->m_Key]); + } + Handled = true; } - // Have to check for nullptr again because the previous execute can unbind itself - if(Event.m_Flags & IInput::FLAG_RELEASE && m_aapKeyBindings[Mask][Event.m_Key]) - Console()->ExecuteLineStroked(0, m_aapKeyBindings[Mask][Event.m_Key]); - ret = true; } - if(m_aapKeyBindings[0][Event.m_Key] && !ret) + if(Event.m_Flags & IInput::FLAG_RELEASE) { - // When ctrl+shift are pressed (ctrl+shift binds and also the hard-coded ctrl+shift+d, ctrl+shift+g, ctrl+shift+e), ignore other +xxx binds - if(Event.m_Flags & IInput::FLAG_PRESS && Mask != ((1 << MODIFIER_CTRL) | (1 << MODIFIER_SHIFT)) && Mask != ((1 << MODIFIER_GUI) | (1 << MODIFIER_SHIFT))) + const auto &&OnKeyRelease = [&](const CBindSlot &Bind) { + // Prevent binds from being deactivated while chat, console and menus are open, as these components will + // still allow key release events to be forwarded to this component, so the active binds can be cleared. + if(GameClient()->m_Chat.IsActive() || + !GameClient()->m_GameConsole.IsClosed() || + GameClient()->m_Menus.IsActive()) + { + return; + } + // Have to check for nullptr again because the previous execute can unbind itself + if(!m_aapKeyBindings[Bind.m_ModifierMask][Bind.m_Key]) + { + return; + } + Console()->ExecuteLineStroked(0, m_aapKeyBindings[Bind.m_ModifierMask][Bind.m_Key]); + }; + + // Release active bind that uses this primary key + auto ActiveBind = std::find_if(m_vActiveBinds.begin(), m_vActiveBinds.end(), [&](const CBindSlot &Bind) { + return Event.m_Key == Bind.m_Key; + }); + if(ActiveBind != m_vActiveBinds.end()) { - Console()->ExecuteLineStroked(1, m_aapKeyBindings[0][Event.m_Key]); - pKey = m_aapKeyBindings[Mask][Event.m_Key]; + OnKeyRelease(*ActiveBind); + m_vActiveBinds.erase(ActiveBind); + Handled = true; } - // Have to check for nullptr again because the previous execute can unbind itself - if(Event.m_Flags & IInput::FLAG_RELEASE && m_aapKeyBindings[0][Event.m_Key]) - Console()->ExecuteLineStroked(0, m_aapKeyBindings[0][Event.m_Key]); - ret = true; - } - if(g_Config.m_ClSubTickAiming && pKey) - { - if(str_comp("+fire", pKey) == 0 || str_comp("+hook", pKey) == 0) + // Release all active binds that use this modifier key + if(KeyModifierMask != MODIFIER_NONE) { - m_MouseOnAction = true; + while(true) + { + auto ActiveModifierBind = std::find_if(m_vActiveBinds.begin(), m_vActiveBinds.end(), [&](const CBindSlot &Bind) { + return (Bind.m_ModifierMask & KeyModifierMask) != 0; + }); + if(ActiveModifierBind == m_vActiveBinds.end()) + break; + OnKeyRelease(*ActiveModifierBind); + m_vActiveBinds.erase(ActiveModifierBind); + Handled = true; + } } } - return ret; + return Handled; } void CBinds::UnbindAll() @@ -182,51 +223,44 @@ void CBinds::UnbindAll() for(auto &pKeyBinding : apKeyBinding) { free(pKeyBinding); - pKeyBinding = 0; + pKeyBinding = nullptr; } } } -const char *CBinds::Get(int KeyID, int ModifierCombination) +const char *CBinds::Get(int KeyId, int ModifierCombination) { - if(KeyID > 0 && KeyID < KEY_LAST && m_aapKeyBindings[ModifierCombination][KeyID]) - return m_aapKeyBindings[ModifierCombination][KeyID]; - return ""; + dbg_assert(KeyId >= KEY_FIRST && KeyId < KEY_LAST, "KeyId invalid"); + dbg_assert(ModifierCombination >= MODIFIER_NONE && ModifierCombination < MODIFIER_COMBINATION_COUNT, "ModifierCombination invalid"); + return m_aapKeyBindings[ModifierCombination][KeyId] ? m_aapKeyBindings[ModifierCombination][KeyId] : ""; } void CBinds::GetKey(const char *pBindStr, char *pBuf, size_t BufSize) { - pBuf[0] = 0; - for(int Mod = 0; Mod < MODIFIER_COMBINATION_COUNT; Mod++) + pBuf[0] = '\0'; + for(int Modifier = MODIFIER_NONE; Modifier < MODIFIER_COMBINATION_COUNT; Modifier++) { - for(int KeyId = 0; KeyId < KEY_LAST; KeyId++) + char aModifiers[128]; + GetKeyBindModifiersName(Modifier, aModifiers, sizeof(aModifiers)); + for(int KeyId = KEY_FIRST; KeyId < KEY_LAST; KeyId++) { - const char *pBind = Get(KeyId, Mod); + const char *pBind = Get(KeyId, Modifier); if(!pBind[0]) continue; if(str_comp(pBind, pBindStr) == 0) { - if(Mod) - str_format(pBuf, BufSize, "%s+%s", GetModifierName(Mod), Input()->KeyName(KeyId)); - else - str_copy(pBuf, Input()->KeyName(KeyId), BufSize); + str_format(pBuf, BufSize, "%s%s", aModifiers, Input()->KeyName(KeyId)); return; } } } } -void CBinds::StaBinds() -{ - Bind(KEY_X, "exec TripleFlyON.cfg "); - Bind(KEY_V, "exec DeepFlyOn.cfg "); -} - void CBinds::SetDefaults() { - // set default key bindings UnbindAll(); + Bind(KEY_F1, "toggle_local_console"); Bind(KEY_F2, "toggle_remote_console"); Bind(KEY_TAB, "+scoreboard"); @@ -266,7 +300,6 @@ void CBinds::SetDefaults() Bind(KEY_Q, "say /pause"); Bind(KEY_P, "say /pause"); - // DDRace g_Config.m_ClDDRaceBindsSet = 0; SetDDRaceBinds(false); } @@ -280,7 +313,6 @@ void CBinds::OnConsoleInit() Console()->Register("unbind", "s[key]", CFGFLAG_CLIENT, ConUnbind, this, "Unbind key"); Console()->Register("unbindall", "", CFGFLAG_CLIENT, ConUnbindAll, this, "Unbind all keys"); - // default bindings SetDefaults(); } @@ -288,10 +320,9 @@ void CBinds::ConBind(IConsole::IResult *pResult, void *pUserData) { CBinds *pBinds = (CBinds *)pUserData; const char *pBindStr = pResult->GetString(0); - int Modifier; - int KeyID = pBinds->GetBindSlot(pBindStr, &Modifier); + const CBindSlot BindSlot = pBinds->GetBindSlot(pBindStr); - if(!KeyID) + if(!BindSlot.m_Key) { char aBuf[256]; str_format(aBuf, sizeof(aBuf), "key %s not found", pBindStr); @@ -304,16 +335,16 @@ void CBinds::ConBind(IConsole::IResult *pResult, void *pUserData) char aBuf[256]; const char *pKeyName = pResult->GetString(0); - if(!pBinds->m_aapKeyBindings[Modifier][KeyID]) - str_format(aBuf, sizeof(aBuf), "%s (%d) is not bound", pKeyName, KeyID); + if(!pBinds->m_aapKeyBindings[BindSlot.m_ModifierMask][BindSlot.m_Key]) + str_format(aBuf, sizeof(aBuf), "%s (%d) is not bound", pKeyName, BindSlot.m_Key); else - str_format(aBuf, sizeof(aBuf), "%s (%d) = %s", pKeyName, KeyID, pBinds->m_aapKeyBindings[Modifier][KeyID]); + str_format(aBuf, sizeof(aBuf), "%s (%d) = %s", pKeyName, BindSlot.m_Key, pBinds->m_aapKeyBindings[BindSlot.m_ModifierMask][BindSlot.m_Key]); pBinds->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "binds", aBuf, gs_BindPrintColor); return; } - pBinds->Bind(KeyID, pResult->GetString(1), false, Modifier); + pBinds->Bind(BindSlot.m_Key, pResult->GetString(1), false, BindSlot.m_ModifierMask); } void CBinds::ConBinds(IConsole::IResult *pResult, void *pUserData) @@ -323,35 +354,35 @@ void CBinds::ConBinds(IConsole::IResult *pResult, void *pUserData) { char aBuf[256]; const char *pKeyName = pResult->GetString(0); - - int Modifier; - int KeyID = pBinds->GetBindSlot(pKeyName, &Modifier); - if(!KeyID) + const CBindSlot BindSlot = pBinds->GetBindSlot(pKeyName); + if(!BindSlot.m_Key) { str_format(aBuf, sizeof(aBuf), "key '%s' not found", pKeyName); pBinds->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "binds", aBuf, gs_BindPrintColor); } else { - if(!pBinds->m_aapKeyBindings[Modifier][KeyID]) - str_format(aBuf, sizeof(aBuf), "%s (%d) is not bound", pKeyName, KeyID); + if(!pBinds->m_aapKeyBindings[BindSlot.m_ModifierMask][BindSlot.m_Key]) + str_format(aBuf, sizeof(aBuf), "%s (%d) is not bound", pKeyName, BindSlot.m_Key); else - str_format(aBuf, sizeof(aBuf), "%s (%d) = %s", pKeyName, KeyID, pBinds->m_aapKeyBindings[Modifier][KeyID]); + str_format(aBuf, sizeof(aBuf), "%s (%d) = %s", pKeyName, BindSlot.m_Key, pBinds->m_aapKeyBindings[BindSlot.m_ModifierMask][BindSlot.m_Key]); pBinds->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "binds", aBuf, gs_BindPrintColor); } } - else if(pResult->NumArguments() == 0) + else { char aBuf[1024]; - for(int i = 0; i < MODIFIER_COMBINATION_COUNT; i++) + for(int Modifier = MODIFIER_NONE; Modifier < MODIFIER_COMBINATION_COUNT; Modifier++) { - for(int j = 0; j < KEY_LAST; j++) + char aModifiers[128]; + GetKeyBindModifiersName(Modifier, aModifiers, sizeof(aModifiers)); + for(int Key = KEY_FIRST; Key < KEY_LAST; Key++) { - if(!pBinds->m_aapKeyBindings[i][j]) + if(!pBinds->m_aapKeyBindings[Modifier][Key]) continue; - str_format(aBuf, sizeof(aBuf), "%s%s (%d) = %s", GetKeyBindModifiersName(i), pBinds->Input()->KeyName(j), j, pBinds->m_aapKeyBindings[i][j]); + str_format(aBuf, sizeof(aBuf), "%s%s (%d) = %s", aModifiers, pBinds->Input()->KeyName(Key), Key, pBinds->m_aapKeyBindings[Modifier][Key]); pBinds->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "binds", aBuf, gs_BindPrintColor); } } @@ -362,10 +393,9 @@ void CBinds::ConUnbind(IConsole::IResult *pResult, void *pUserData) { CBinds *pBinds = (CBinds *)pUserData; const char *pKeyName = pResult->GetString(0); - int Modifier; - int KeyID = pBinds->GetBindSlot(pKeyName, &Modifier); + const CBindSlot BindSlot = pBinds->GetBindSlot(pKeyName); - if(!KeyID) + if(!BindSlot.m_Key) { char aBuf[256]; str_format(aBuf, sizeof(aBuf), "key %s not found", pKeyName); @@ -373,7 +403,7 @@ void CBinds::ConUnbind(IConsole::IResult *pResult, void *pUserData) return; } - pBinds->Bind(KeyID, "", false, Modifier); + pBinds->Bind(BindSlot.m_Key, "", false, BindSlot.m_ModifierMask); } void CBinds::ConUnbindAll(IConsole::IResult *pResult, void *pUserData) @@ -382,51 +412,31 @@ void CBinds::ConUnbindAll(IConsole::IResult *pResult, void *pUserData) pBinds->UnbindAll(); } -int CBinds::GetKeyID(const char *pKeyName) -{ - // check for numeric - if(pKeyName[0] == '&') - { - int i = str_toint(pKeyName + 1); - if(i > 0 && i < KEY_LAST) - return i; // numeric - } - - // search for key - for(int i = 0; i < KEY_LAST; i++) - { - if(str_comp_nocase(pKeyName, Input()->KeyName(i)) == 0) - return i; - } - - return 0; -} - -int CBinds::GetBindSlot(const char *pBindString, int *pModifierCombination) +CBinds::CBindSlot CBinds::GetBindSlot(const char *pBindString) const { - *pModifierCombination = MODIFIER_NONE; + int ModifierMask = MODIFIER_NONE; char aMod[32]; aMod[0] = '\0'; const char *pKey = str_next_token(pBindString, "+", aMod, sizeof(aMod)); while(aMod[0] && *(pKey)) { if(!str_comp_nocase(aMod, "shift")) - *pModifierCombination |= (1 << MODIFIER_SHIFT); + ModifierMask |= (1 << MODIFIER_SHIFT); else if(!str_comp_nocase(aMod, "ctrl")) - *pModifierCombination |= (1 << MODIFIER_CTRL); + ModifierMask |= (1 << MODIFIER_CTRL); else if(!str_comp_nocase(aMod, "alt")) - *pModifierCombination |= (1 << MODIFIER_ALT); + ModifierMask |= (1 << MODIFIER_ALT); else if(!str_comp_nocase(aMod, "gui")) - *pModifierCombination |= (1 << MODIFIER_GUI); + ModifierMask |= (1 << MODIFIER_GUI); else - return 0; + return {KEY_UNKNOWN, MODIFIER_NONE}; if(str_find(pKey + 1, "+")) pKey = str_next_token(pKey + 1, "+", aMod, sizeof(aMod)); else break; } - return GetKeyID(*pModifierCombination == MODIFIER_NONE ? aMod : pKey + 1); + return {Input()->FindKeyByName(ModifierMask == MODIFIER_NONE ? aMod : pKey + 1), ModifierMask}; } const char *CBinds::GetModifierName(int Modifier) @@ -447,19 +457,17 @@ const char *CBinds::GetModifierName(int Modifier) } } -const char *CBinds::GetKeyBindModifiersName(int ModifierCombination) +void CBinds::GetKeyBindModifiersName(int ModifierCombination, char *pBuf, size_t BufSize) { - static char aModifier[256]; - aModifier[0] = '\0'; + pBuf[0] = '\0'; for(int k = 1; k < MODIFIER_COUNT; k++) { if(ModifierCombination & (1 << k)) { - str_append(aModifier, GetModifierName(k)); - str_append(aModifier, "+"); + str_append(pBuf, GetModifierName(k), BufSize); + str_append(pBuf, "+", BufSize); } } - return aModifier; } void CBinds::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData) @@ -467,22 +475,24 @@ void CBinds::ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData) CBinds *pSelf = (CBinds *)pUserData; pConfigManager->WriteLine("unbindall"); - for(int i = 0; i < MODIFIER_COMBINATION_COUNT; i++) + for(int Modifier = MODIFIER_NONE; Modifier < MODIFIER_COMBINATION_COUNT; Modifier++) { - for(int j = 0; j < KEY_LAST; j++) + char aModifiers[128]; + GetKeyBindModifiersName(Modifier, aModifiers, sizeof(aModifiers)); + for(int Key = KEY_FIRST; Key < KEY_LAST; Key++) { - if(!pSelf->m_aapKeyBindings[i][j]) + if(!pSelf->m_aapKeyBindings[Modifier][Key]) continue; // worst case the str_escape can double the string length - int Size = str_length(pSelf->m_aapKeyBindings[i][j]) * 2 + 30; + int Size = str_length(pSelf->m_aapKeyBindings[Modifier][Key]) * 2 + 30; char *pBuffer = (char *)malloc(Size); char *pEnd = pBuffer + Size; - str_format(pBuffer, Size, "bind %s%s \"", GetKeyBindModifiersName(i), pSelf->Input()->KeyName(j)); + str_format(pBuffer, Size, "bind %s%s \"", aModifiers, pSelf->Input()->KeyName(Key)); // process the string. we need to escape some characters char *pDst = pBuffer + str_length(pBuffer); - str_escape(&pDst, pSelf->m_aapKeyBindings[i][j], pEnd); + str_escape(&pDst, pSelf->m_aapKeyBindings[Modifier][Key], pEnd); str_append(pBuffer, "\"", Size); pConfigManager->WriteLine(pBuffer); diff --git a/src/game/client/components/binds.h b/src/game/client/components/binds.h index 5b9ceadb8d..21daf9c988 100644 --- a/src/game/client/components/binds.h +++ b/src/game/client/components/binds.h @@ -8,12 +8,12 @@ #include +#include + class IConfigManager; class CBinds : public CComponent { - int GetKeyID(const char *pKeyName); - static void ConBind(IConsole::IResult *pResult, void *pUserData); static void ConBinds(IConsole::IResult *pResult, void *pUserData); static void ConUnbind(IConsole::IResult *pResult, void *pUserData); @@ -22,6 +22,20 @@ class CBinds : public CComponent static void ConfigSaveCallback(IConfigManager *pConfigManager, void *pUserData); + class CBindSlot + { + public: + int m_Key; + int m_ModifierMask; + + CBindSlot(int Key, int ModifierMask) : + m_Key(Key), + m_ModifierMask(ModifierMask) + { + } + }; + CBindSlot GetBindSlot(const char *pBindString) const; + public: CBinds(); ~CBinds(); @@ -50,17 +64,15 @@ class CBinds : public CComponent CBindsSpecial m_SpecialBinds; - void Bind(int KeyID, const char *pStr, bool FreeOnly = false, int ModifierCombination = MODIFIER_NONE); + void Bind(int KeyId, const char *pStr, bool FreeOnly = false, int ModifierCombination = MODIFIER_NONE); void SetDefaults(); - void StaBinds(); void UnbindAll(); - const char *Get(int KeyID, int ModifierCombination); + const char *Get(int KeyId, int ModifierCombination); void GetKey(const char *pBindStr, char *pBuf, size_t BufSize); - int GetBindSlot(const char *pBindString, int *pModifierCombination); static int GetModifierMask(IInput *pInput); static int GetModifierMaskOfKey(int Key); static const char *GetModifierName(int Modifier); - static const char *GetKeyBindModifiersName(int ModifierCombination); + static void GetKeyBindModifiersName(int ModifierCombination, char *pBuf, size_t BufSize); virtual void OnConsoleInit() override; virtual bool OnInput(const IInput::CEvent &Event) override; @@ -71,5 +83,6 @@ class CBinds : public CComponent private: char *m_aapKeyBindings[MODIFIER_COMBINATION_COUNT][KEY_LAST]; + std::vector m_vActiveBinds; }; #endif diff --git a/src/game/client/components/broadcast.cpp b/src/game/client/components/broadcast.cpp index 5683705dae..87a1c0f604 100644 --- a/src/game/client/components/broadcast.cpp +++ b/src/game/client/components/broadcast.cpp @@ -74,20 +74,20 @@ void CBroadcast::OnMessage(int MsgType, void *pRawMsg) { if(MsgType == NETMSGTYPE_SV_BROADCAST) { - OnBroadcastMessage((CNetMsg_Sv_Broadcast *)pRawMsg); + const CNetMsg_Sv_Broadcast *pMsg = (CNetMsg_Sv_Broadcast *)pRawMsg; + DoBroadcast(pMsg->m_pMessage); } } -void CBroadcast::OnBroadcastMessage(const CNetMsg_Sv_Broadcast *pMsg) +void CBroadcast::DoBroadcast(const char *pText) { - str_copy(m_aBroadcastText, pMsg->m_pMessage); + str_copy(m_aBroadcastText, pText); m_BroadcastTick = Client()->GameTick(g_Config.m_ClDummy) + Client()->GameTickSpeed() * 10; m_BroadcastRenderOffset = -1.0f; TextRender()->DeleteTextContainer(m_TextContainerIndex); if(g_Config.m_ClPrintBroadcasts) { - const char *pText = m_aBroadcastText; char aLine[sizeof(m_aBroadcastText)]; while((pText = str_next_token(pText, "\n", aLine, sizeof(aLine)))) { diff --git a/src/game/client/components/broadcast.h b/src/game/client/components/broadcast.h index 056adce91b..11942764dd 100644 --- a/src/game/client/components/broadcast.h +++ b/src/game/client/components/broadcast.h @@ -4,6 +4,7 @@ #define GAME_CLIENT_COMPONENTS_BROADCAST_H #include +#include #include @@ -24,6 +25,8 @@ class CBroadcast : public CComponent virtual void OnWindowResize() override; virtual void OnRender() override; virtual void OnMessage(int MsgType, void *pRawMsg) override; + + void DoBroadcast(const char *pText); }; #endif diff --git a/src/game/client/components/camera.cpp b/src/game/client/components/camera.cpp index 4837be0ba3..307f3c3261 100644 --- a/src/game/client/components/camera.cpp +++ b/src/game/client/components/camera.cpp @@ -3,7 +3,9 @@ #include +#include #include +#include #include #include #include @@ -19,7 +21,7 @@ CCamera::CCamera() m_ZoomSet = false; m_Zoom = 1.0f; m_Zooming = false; - m_ForceFreeviewPos = vec2(-1, -1); + m_ForceFreeview = false; m_GotoSwitchOffset = 0; m_GotoTeleOffset = 0; m_GotoSwitchLastPos = ivec2(-1, -1); @@ -28,6 +30,16 @@ CCamera::CCamera() mem_zero(m_aLastPos, sizeof(m_aLastPos)); m_PrevCenter = vec2(0, 0); m_Center = vec2(0, 0); + + m_PrevSpecId = -1; + m_WasSpectating = false; + + m_CameraSmoothing = false; +} + +float CCamera::CameraSmoothingProgress(float CurrentTime) const +{ + return (CurrentTime - m_CameraSmoothingStart) / (m_CameraSmoothingEnd - m_CameraSmoothingStart); } float CCamera::ZoomProgress(float CurrentTime) const @@ -99,7 +111,34 @@ void CCamera::OnRender() m_Zoom = clamp(m_Zoom, MinZoomLevel(), MaxZoomLevel()); } - if(!(m_pClient->m_Snap.m_SpecInfo.m_Active || GameClient()->m_GameInfo.m_AllowZoom || Client()->State() == IClient::STATE_DEMOPLAYBACK)) + if(m_CameraSmoothing) + { + if(!m_pClient->m_Snap.m_SpecInfo.m_Active) + { + m_Center = m_CameraSmoothingTarget; + m_CameraSmoothing = false; + } + else + { + float Time = Client()->LocalTime(); + if(Time >= m_CameraSmoothingEnd) + { + m_Center = m_CameraSmoothingTarget; + m_CameraSmoothing = false; + } + else + { + m_CameraSmoothingCenter = vec2(m_CameraSmoothingBezierX.Evaluate(CameraSmoothingProgress(Time)), m_CameraSmoothingBezierY.Evaluate(CameraSmoothingProgress(Time))); + if(distance(m_CameraSmoothingCenter, m_CameraSmoothingTarget) <= 0.1f) + { + m_Center = m_CameraSmoothingTarget; + m_CameraSmoothing = false; + } + } + } + } + + if(!ZoomAllowed()) { m_ZoomSet = false; m_Zoom = 1.0f; @@ -177,12 +216,61 @@ void CCamera::OnRender() m_Center = m_pClient->m_LocalCharacterPos + s_aCurrentCameraOffset[g_Config.m_ClDummy]; } - if(m_ForceFreeviewPos != vec2(-1, -1) && m_CamType == CAMTYPE_SPEC) + if(m_ForceFreeview && m_CamType == CAMTYPE_SPEC) { m_Center = m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy] = m_ForceFreeviewPos; - m_ForceFreeviewPos = vec2(-1, -1); + m_ForceFreeview = false; + } + else + m_ForceFreeviewPos = m_Center; + + const int SpecId = m_pClient->m_Snap.m_SpecInfo.m_SpectatorId; + + // start smoothing from the current position when the target changes + if(m_CameraSmoothing && SpecId != m_PrevSpecId) + m_CameraSmoothing = false; + + if(m_pClient->m_Snap.m_SpecInfo.m_Active && + (SpecId != m_PrevSpecId || + (m_CameraSmoothing && m_CameraSmoothingTarget != m_Center)) && // the target is moving during camera smoothing + !(!m_WasSpectating && m_Center != m_PrevCenter) && // dont smooth when starting to spectate + m_CamType != CAMTYPE_SPEC && + !GameClient()->m_MultiViewActivated) + { + float Now = Client()->LocalTime(); + if(!m_CameraSmoothing) + m_CenterBeforeSmoothing = m_PrevCenter; + + vec2 Derivative = {0.f, 0.f}; + if(m_CameraSmoothing) + { + float Progress = CameraSmoothingProgress(Now); + Derivative.x = m_CameraSmoothingBezierX.Derivative(Progress); + Derivative.y = m_CameraSmoothingBezierY.Derivative(Progress); + } + + m_CameraSmoothingTarget = m_Center; + m_CameraSmoothingBezierX = CCubicBezier::With(m_CenterBeforeSmoothing.x, Derivative.x, 0, m_CameraSmoothingTarget.x); + m_CameraSmoothingBezierY = CCubicBezier::With(m_CenterBeforeSmoothing.y, Derivative.y, 0, m_CameraSmoothingTarget.y); + + if(!m_CameraSmoothing) + { + m_CameraSmoothingStart = Now; + m_CameraSmoothingEnd = Now + (float)g_Config.m_ClSmoothSpectatingTime / 1000.0f; + } + + if(!m_CameraSmoothing) + m_CameraSmoothingCenter = m_PrevCenter; + + m_CameraSmoothing = true; } + + if(m_CameraSmoothing) + m_Center = m_CameraSmoothingCenter; + m_PrevCenter = m_Center; + m_PrevSpecId = SpecId; + m_WasSpectating = m_pClient->m_Snap.m_SpecInfo.m_Active; } void CCamera::OnConsoleInit() @@ -191,12 +279,15 @@ void CCamera::OnConsoleInit() Console()->Register("zoom-", "", CFGFLAG_CLIENT, ConZoomMinus, this, "Zoom decrease"); Console()->Register("zoom", "?i", CFGFLAG_CLIENT, ConZoom, this, "Change zoom"); Console()->Register("set_view", "i[x]i[y]", CFGFLAG_CLIENT, ConSetView, this, "Set camera position to x and y in the map"); + Console()->Register("set_view_relative", "i[x]i[y]", CFGFLAG_CLIENT, ConSetViewRelative, this, "Set camera position relative to current view in the map"); Console()->Register("goto_switch", "i[number]?i[offset]", CFGFLAG_CLIENT, ConGotoSwitch, this, "View switch found (at offset) with given number"); Console()->Register("goto_tele", "i[number]?i[offset]", CFGFLAG_CLIENT, ConGotoTele, this, "View tele found (at offset) with given number"); } void CCamera::OnReset() { + m_CameraSmoothing = false; + m_Zoom = std::pow(CCamera::ZOOM_STEP, g_Config.m_ClDefaultZoom - 10); m_Zooming = false; } @@ -204,28 +295,31 @@ void CCamera::OnReset() void CCamera::ConZoomPlus(IConsole::IResult *pResult, void *pUserData) { CCamera *pSelf = (CCamera *)pUserData; - if(pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active || pSelf->GameClient()->m_GameInfo.m_AllowZoom || pSelf->Client()->State() == IClient::STATE_DEMOPLAYBACK) - { - pSelf->ScaleZoom(CCamera::ZOOM_STEP); + if(!pSelf->ZoomAllowed()) + return; - if(pSelf->GameClient()->m_MultiViewActivated) - pSelf->GameClient()->m_MultiViewPersonalZoom++; - } + pSelf->ScaleZoom(CCamera::ZOOM_STEP); + + if(pSelf->GameClient()->m_MultiViewActivated) + pSelf->GameClient()->m_MultiViewPersonalZoom++; } void CCamera::ConZoomMinus(IConsole::IResult *pResult, void *pUserData) { CCamera *pSelf = (CCamera *)pUserData; - if(pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active || pSelf->GameClient()->m_GameInfo.m_AllowZoom || pSelf->Client()->State() == IClient::STATE_DEMOPLAYBACK) - { - pSelf->ScaleZoom(1 / CCamera::ZOOM_STEP); + if(!pSelf->ZoomAllowed()) + return; - if(pSelf->GameClient()->m_MultiViewActivated) - pSelf->GameClient()->m_MultiViewPersonalZoom--; - } + pSelf->ScaleZoom(1 / CCamera::ZOOM_STEP); + + if(pSelf->GameClient()->m_MultiViewActivated) + pSelf->GameClient()->m_MultiViewPersonalZoom--; } void CCamera::ConZoom(IConsole::IResult *pResult, void *pUserData) { CCamera *pSelf = (CCamera *)pUserData; + if(!pSelf->ZoomAllowed()) + return; + float TargetLevel = pResult->NumArguments() ? pResult->GetFloat(0) : g_Config.m_ClDefaultZoom; pSelf->ChangeZoom(std::pow(CCamera::ZOOM_STEP, TargetLevel - 10), pSelf->m_pClient->m_Snap.m_SpecInfo.m_Active && pSelf->GameClient()->m_MultiViewActivated ? g_Config.m_ClMultiViewZoomSmoothness : g_Config.m_ClSmoothZoomTime); @@ -238,6 +332,12 @@ void CCamera::ConSetView(IConsole::IResult *pResult, void *pUserData) // wait until free view camera type to update the position pSelf->SetView(ivec2(pResult->GetInteger(0), pResult->GetInteger(1))); } +void CCamera::ConSetViewRelative(IConsole::IResult *pResult, void *pUserData) +{ + CCamera *pSelf = (CCamera *)pUserData; + // wait until free view camera type to update the position + pSelf->SetView(ivec2(pResult->GetInteger(0), pResult->GetInteger(1)), true); +} void CCamera::ConGotoSwitch(IConsole::IResult *pResult, void *pUserData) { CCamera *pSelf = (CCamera *)pUserData; @@ -249,11 +349,16 @@ void CCamera::ConGotoTele(IConsole::IResult *pResult, void *pUserData) pSelf->GotoTele(pResult->GetInteger(0), pResult->NumArguments() > 1 ? pResult->GetInteger(1) : -1); } -void CCamera::SetView(ivec2 Pos) +void CCamera::SetView(ivec2 Pos, bool Relative) { + vec2 RealPos = vec2(Pos.x * 32.0, Pos.y * 32.0); + vec2 UntestedViewPos = Relative ? m_ForceFreeviewPos + RealPos : RealPos; + + m_ForceFreeview = true; + m_ForceFreeviewPos = vec2( - clamp(Pos.x * 32.0f, 200.0f, Collision()->GetWidth() * 32 - 200.0f), - clamp(Pos.y * 32.0f, 200.0f, Collision()->GetWidth() * 32 - 200.0f)); + clamp(UntestedViewPos.x, 200.0f, Collision()->GetWidth() * 32 - 200.0f), + clamp(UntestedViewPos.y, 200.0f, Collision()->GetWidth() * 32 - 200.0f)); } void CCamera::GotoSwitch(int Number, int Offset) @@ -304,57 +409,66 @@ void CCamera::GotoTele(int Number, int Offset) { if(Collision()->TeleLayer() == nullptr) return; + Number--; + + if(m_GotoTeleLastNumber != Number) + m_GotoTeleLastPos = ivec2(-1, -1); - int Match = -1; ivec2 MatchPos = ivec2(-1, -1); + const size_t NumTeles = Collision()->TeleAllSize(Number); + if(!NumTeles) + { + log_error("camera", "No teleporter with number %d found.", Number + 1); + return; + } - auto FindTile = [this, &Match, &MatchPos, &Number, &Offset]() { - for(int x = 0; x < Collision()->GetWidth(); x++) + if(Offset != -1 || m_GotoTeleLastPos == ivec2(-1, -1)) + { + if((size_t)Offset >= NumTeles || Offset < 0) + Offset = 0; + vec2 Tele = Collision()->TeleAllGet(Number, Offset); + MatchPos = ivec2(Tele.x / 32, Tele.y / 32); + m_GotoTeleOffset = Offset; + } + else + { + bool FullRound = false; + do { - for(int y = 0; y < Collision()->GetHeight(); y++) + vec2 Tele = Collision()->TeleAllGet(Number, m_GotoTeleOffset); + MatchPos = ivec2(Tele.x / 32, Tele.y / 32); + m_GotoTeleOffset++; + if((size_t)m_GotoTeleOffset >= NumTeles) { - int i = y * Collision()->GetWidth() + x; - int Tele = Collision()->TeleLayer()[i].m_Number; - if(Number == Tele) + m_GotoTeleOffset = 0; + if(FullRound) { - Match++; - if(Offset != -1) - { - if(Match == Offset) - { - MatchPos = ivec2(x, y); - m_GotoTeleOffset = Match; - return; - } - continue; - } - MatchPos = ivec2(x, y); - if(m_GotoTeleLastPos != ivec2(-1, -1)) - { - if(distance(m_GotoTeleLastPos, MatchPos) < 10.0f) - { - m_GotoTeleOffset++; - continue; - } - } - m_GotoTeleLastPos = MatchPos; - if(Match == m_GotoTeleOffset) - return; + MatchPos = m_GotoTeleLastPos; + break; + } + else + { + FullRound = true; } } - } - }; - FindTile(); + } while(distance(m_GotoTeleLastPos, MatchPos) < 10.0f); + } if(MatchPos == ivec2(-1, -1)) return; - if(Match < m_GotoTeleOffset) - m_GotoTeleOffset = -1; + m_GotoTeleLastPos = MatchPos; + m_GotoTeleLastNumber = Number; SetView(MatchPos); - m_GotoTeleOffset++; } void CCamera::SetZoom(float Target, int Smoothness) { ChangeZoom(Target, Smoothness); } + +bool CCamera::ZoomAllowed() const +{ + return GameClient()->m_Snap.m_SpecInfo.m_Active || + GameClient()->m_GameInfo.m_AllowZoom || + Client()->State() == IClient::STATE_DEMOPLAYBACK; +} diff --git a/src/game/client/components/camera.h b/src/game/client/components/camera.h index ae8798d863..72b42230f9 100644 --- a/src/game/client/components/camera.h +++ b/src/game/client/components/camera.h @@ -25,6 +25,20 @@ class CCamera : public CComponent vec2 m_aLastPos[NUM_DUMMIES]; vec2 m_PrevCenter; + int m_PrevSpecId; + bool m_WasSpectating; + + bool m_CameraSmoothing; + vec2 m_CameraSmoothingCenter; + vec2 m_CameraSmoothingTarget; + CCubicBezier m_CameraSmoothingBezierX; + CCubicBezier m_CameraSmoothingBezierY; + float m_CameraSmoothingStart; + float m_CameraSmoothingEnd; + vec2 m_CenterBeforeSmoothing; + + float CameraSmoothingProgress(float CurrentTime) const; + CCubicBezier m_ZoomSmoothing; float m_ZoomSmoothingStart; float m_ZoomSmoothingEnd; @@ -54,24 +68,29 @@ class CCamera : public CComponent virtual void OnConsoleInit() override; virtual void OnReset() override; - void SetZoom(float Target, int Smoothness); - void SetView(ivec2 Pos); + void SetView(ivec2 Pos, bool Relative = false); void GotoSwitch(int Number, int Offset = -1); void GotoTele(int Number, int Offset = -1); + void SetZoom(float Target, int Smoothness); + bool ZoomAllowed() const; + private: static void ConZoomPlus(IConsole::IResult *pResult, void *pUserData); static void ConZoomMinus(IConsole::IResult *pResult, void *pUserData); static void ConZoom(IConsole::IResult *pResult, void *pUserData); static void ConSetView(IConsole::IResult *pResult, void *pUserData); + static void ConSetViewRelative(IConsole::IResult *pResult, void *pUserData); static void ConGotoSwitch(IConsole::IResult *pResult, void *pUserData); static void ConGotoTele(IConsole::IResult *pResult, void *pUserData); + bool m_ForceFreeview; vec2 m_ForceFreeviewPos; int m_GotoSwitchOffset; int m_GotoTeleOffset; ivec2 m_GotoSwitchLastPos; ivec2 m_GotoTeleLastPos; + int m_GotoTeleLastNumber = -1; }; #endif diff --git a/src/game/client/components/chat.cpp b/src/game/client/components/chat.cpp index 3d9ea8a105..8779787264 100644 --- a/src/game/client/components/chat.cpp +++ b/src/game/client/components/chat.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -30,14 +31,9 @@ CChat::CChat() Line.m_QuadContainerIndex = -1; } -#define CHAT_COMMAND(name, params, flags, callback, userdata, help) m_vDefaultCommands.emplace_back(name, params, help); -#include -#undef CHAT_COMMAND - - std::sort(m_vDefaultCommands.begin(), m_vDefaultCommands.end()); m_Mode = MODE_NONE; - m_Input.SetClipboardLineCallback([this](const char *pStr) { SayChat(pStr); }); + m_Input.SetClipboardLineCallback([this](const char *pStr) { SendChatQueued(pStr); }); m_Input.SetCalculateOffsetCallback([this]() { return m_IsInputCensored; }); m_Input.SetDisplayTextCallback([this](char *pStr, size_t NumChars) { m_IsInputCensored = false; @@ -136,6 +132,7 @@ void CChat::Reset() m_CommandsNeedSorting = false; mem_zero(m_aCurrentInputText, sizeof(m_aCurrentInputText)); DisableMode(); + m_vCommands.clear(); for(int64_t &LastSoundPlayed : m_aLastSoundPlayed) LastSoundPlayed = 0; @@ -154,12 +151,12 @@ void CChat::OnStateChange(int NewState, int OldState) void CChat::ConSay(IConsole::IResult *pResult, void *pUserData) { - ((CChat *)pUserData)->Say(0, pResult->GetString(0)); + ((CChat *)pUserData)->SendChat(0, pResult->GetString(0)); } void CChat::ConSayTeam(IConsole::IResult *pResult, void *pUserData) { - ((CChat *)pUserData)->Say(1, pResult->GetString(0)); + ((CChat *)pUserData)->SendChat(1, pResult->GetString(0)); } void CChat::ConChat(IConsole::IResult *pResult, void *pUserData) @@ -230,11 +227,6 @@ void CChat::OnInit() Console()->Chain("cl_chat_width", ConchainChatWidth, this); } -void CChat::OnMapLoad() -{ - m_vCommands = m_vDefaultCommands; -} - bool CChat::OnInput(const IInput::CEvent &Event) { if(m_Mode == MODE_NONE) @@ -258,28 +250,7 @@ bool CChat::OnInput(const IInput::CEvent &Event) m_CommandsNeedSorting = false; } - if(m_Input.GetString()[0]) - { - bool AddEntry = false; - - if(m_LastChatSend + time_freq() < time()) - { - Say(m_Mode == MODE_ALL ? 0 : 1, m_Input.GetString()); - AddEntry = true; - } - else if(m_PendingChatCounter < 3) - { - ++m_PendingChatCounter; - AddEntry = true; - } - - if(AddEntry) - { - CHistoryEntry *pEntry = m_History.Allocate(sizeof(CHistoryEntry) + m_Input.GetLength()); - pEntry->m_Team = m_Mode == MODE_ALL ? 0 : 1; - mem_copy(pEntry->m_aText, m_Input.GetString(), m_Input.GetLength() + 1); - } - } + SendChatQueued(m_Input.GetString()); m_pHistoryEntry = nullptr; DisableMode(); m_pClient->OnRelease(); @@ -312,11 +283,11 @@ bool CChat::OnInput(const IInput::CEvent &Event) { if(PlayerInfo) { - PlayerName = m_pClient->m_aClients[PlayerInfo->m_ClientID].m_aName; + PlayerName = m_pClient->m_aClients[PlayerInfo->m_ClientId].m_aName; FoundInput = str_utf8_find_nocase(PlayerName, m_aCompletionBuffer); if(FoundInput != 0) { - m_aPlayerCompletionList[m_PlayerCompletionListLength].ClientID = PlayerInfo->m_ClientID; + m_aPlayerCompletionList[m_PlayerCompletionListLength].ClientId = PlayerInfo->m_ClientId; // The score for suggesting a player name is determined by the distance of the search input to the beginning of the player name m_aPlayerCompletionList[m_PlayerCompletionListLength].Score = (int)(FoundInput - PlayerName); m_PlayerCompletionListLength++; @@ -362,7 +333,7 @@ bool CChat::OnInput(const IInput::CEvent &Event) auto &Command = m_vCommands[Index]; - if(str_startswith(Command.m_aName, pCommandStart)) + if(str_startswith_nocase(Command.m_aName, pCommandStart)) { pCompletionCommand = &Command; m_CompletionChosen = Index + SearchType * NumCommands; @@ -384,8 +355,6 @@ bool CChat::OnInput(const IInput::CEvent &Event) // add separator const char *pSeparator = pCompletionCommand->m_aParams[0] == '\0' ? "" : " "; str_append(aBuf, pSeparator); - if(*pSeparator) - str_append(aBuf, pSeparator); // add part after the name str_append(aBuf, m_Input.GetString() + m_PlaceholderOffset + m_PlaceholderLength); @@ -420,7 +389,7 @@ bool CChat::OnInput(const IInput::CEvent &Event) m_CompletionChosen %= m_PlayerCompletionListLength; m_CompletionUsed = true; - pCompletionClientData = &m_pClient->m_aClients[m_aPlayerCompletionList[m_CompletionChosen].ClientID]; + pCompletionClientData = &m_pClient->m_aClients[m_aPlayerCompletionList[m_CompletionChosen].ClientId]; if(!pCompletionClientData->m_Active) { continue; @@ -560,7 +529,7 @@ void CChat::OnMessage(int MsgType, void *pRawMsg) if(MsgType == NETMSGTYPE_SV_CHAT) { CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; - AddLine(pMsg->m_ClientID, pMsg->m_Team, pMsg->m_pMessage); + AddLine(pMsg->m_ClientId, pMsg->m_Team, pMsg->m_pMessage); } else if(MsgType == NETMSGTYPE_SV_COMMANDINFO) { @@ -581,14 +550,16 @@ void CChat::OnMessage(int MsgType, void *pRawMsg) bool CChat::LineShouldHighlight(const char *pLine, const char *pName) { - const char *pHL = str_utf8_find_nocase(pLine, pName); + const char *pHit = str_utf8_find_nocase(pLine, pName); - if(pHL) + while(pHit) { int Length = str_length(pName); - if(Length > 0 && (pLine == pHL || pHL[-1] == ' ') && (pHL[Length] == 0 || pHL[Length] == ' ' || pHL[Length] == '.' || pHL[Length] == '!' || pHL[Length] == ',' || pHL[Length] == '?' || pHL[Length] == ':')) + if(Length > 0 && (pLine == pHit || pHit[-1] == ' ') && (pHit[Length] == 0 || pHit[Length] == ' ' || pHit[Length] == '.' || pHit[Length] == '!' || pHit[Length] == ',' || pHit[Length] == '?' || pHit[Length] == ':')) return true; + + pHit = str_utf8_find_nocase(pHit + 1, pName); } return false; @@ -634,6 +605,7 @@ void CChat::StoreSave(const char *pText) } */ + const bool SavesFileExists = Storage()->FileExists(SAVES_FILE, IStorage::TYPE_SAVE); IOHANDLE File = Storage()->OpenFile(SAVES_FILE, IOFLAG_APPEND, IStorage::TYPE_SAVE); if(!File) return; @@ -645,7 +617,7 @@ void CChat::StoreSave(const char *pText) aSaveCode, }; - if(io_tell(File) == 0) + if(!SavesFileExists) { CsvWrite(File, 4, SAVES_HEADER); } @@ -653,14 +625,15 @@ void CChat::StoreSave(const char *pText) io_close(File); } -void CChat::AddLine(int ClientID, int Team, const char *pLine) +void CChat::AddLine(int ClientId, int Team, const char *pLine) { if(*pLine == 0 || - (ClientID == SERVER_MSG && !g_Config.m_ClShowChatSystem) || - (ClientID >= 0 && (m_pClient->m_aClients[ClientID].m_aName[0] == '\0' || // unknown client - m_pClient->m_aClients[ClientID].m_ChatIgnore || - (m_pClient->m_Snap.m_LocalClientID != ClientID && g_Config.m_ClShowChatFriends && !m_pClient->m_aClients[ClientID].m_Friend) || - (m_pClient->m_Snap.m_LocalClientID != ClientID && m_pClient->m_aClients[ClientID].m_Foe)))) + (ClientId == SERVER_MSG && !g_Config.m_ClShowChatSystem) || + (ClientId >= 0 && (m_pClient->m_aClients[ClientId].m_aName[0] == '\0' || // unknown client + m_pClient->m_aClients[ClientId].m_ChatIgnore || + (m_pClient->m_Snap.m_LocalClientId != ClientId && g_Config.m_ClShowChatFriends && !m_pClient->m_aClients[ClientId].m_Friend) || + (m_pClient->m_Snap.m_LocalClientId != ClientId && g_Config.m_ClShowChatTeamMembersOnly && m_pClient->IsOtherTeam(ClientId) && m_pClient->m_Teams.Team(m_pClient->m_Snap.m_LocalClientId) != TEAM_FLOCK) || + (m_pClient->m_Snap.m_LocalClientId != ClientId && m_pClient->m_aClients[ClientId].m_Foe)))) return; // trim right and set maximum length to 256 utf8-characters @@ -689,22 +662,20 @@ void CChat::AddLine(int ClientID, int Team, const char *pLine) if(pEnd != 0) *(const_cast(pEnd)) = 0; - bool Highlighted = false; - char *p = const_cast(pLine); - - // Only empty string left - if(*p == 0) + if(*pLine == 0) return; + bool Highlighted = false; + auto &&FChatMsgCheckAndPrint = [this](CLine *pLine_) { - if(pLine_->m_ClientID < 0) // server or client message + if(pLine_->m_ClientId < 0) // server or client message { if(Client()->State() != IClient::STATE_DEMOPLAYBACK) StoreSave(pLine_->m_aText); } char aBuf[1024]; - str_format(aBuf, sizeof(aBuf), "%s%s%s", pLine_->m_aName, pLine_->m_ClientID >= 0 ? ": " : "", pLine_->m_aText); + str_format(aBuf, sizeof(aBuf), "%s%s%s", pLine_->m_aName, pLine_->m_ClientId >= 0 ? ": " : "", pLine_->m_aText); ColorRGBA ChatLogColor{1, 1, 1, 1}; if(pLine_->m_Highlighted) @@ -717,9 +688,9 @@ void CChat::AddLine(int ClientID, int Team, const char *pLine) ChatLogColor = color_cast(ColorHSLA(g_Config.m_ClMessageFriendColor)); else if(pLine_->m_Team) ChatLogColor = color_cast(ColorHSLA(g_Config.m_ClMessageTeamColor)); - else if(pLine_->m_ClientID == SERVER_MSG) + else if(pLine_->m_ClientId == SERVER_MSG) ChatLogColor = color_cast(ColorHSLA(g_Config.m_ClMessageSystemColor)); - else if(pLine_->m_ClientID == CLIENT_MSG) + else if(pLine_->m_ClientId == CLIENT_MSG) ChatLogColor = color_cast(ColorHSLA(g_Config.m_ClMessageClientColor)); else // regular message ChatLogColor = color_cast(ColorHSLA(g_Config.m_ClMessageColor)); @@ -728,159 +699,135 @@ void CChat::AddLine(int ClientID, int Team, const char *pLine) Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, pLine_->m_Whisper ? "whisper" : (pLine_->m_Team ? "teamchat" : "chat"), aBuf, ChatLogColor); }; - while(*p) - { - Highlighted = false; - pLine = p; - // find line separator and strip multiline - while(*p) - { - if(*p++ == '\n') - { - *(p - 1) = 0; - break; - } - } + CLine *pCurrentLine = &m_aLines[m_CurrentLine]; - CLine *pCurrentLine = &m_aLines[m_CurrentLine]; + // Team Number: + // 0 = global; 1 = team; 2 = sending whisper; 3 = receiving whisper - // Team Number: - // 0 = global; 1 = team; 2 = sending whisper; 3 = receiving whisper + // If it's a client message, m_aText will have ": " prepended so we have to work around it. + if(pCurrentLine->m_TeamNumber == Team && pCurrentLine->m_ClientId == ClientId && str_comp(pCurrentLine->m_aText, pLine) == 0) + { + pCurrentLine->m_TimesRepeated++; + TextRender()->DeleteTextContainer(pCurrentLine->m_TextContainerIndex); + Graphics()->DeleteQuadContainer(pCurrentLine->m_QuadContainerIndex); + pCurrentLine->m_Time = time(); + pCurrentLine->m_aYOffset[0] = -1.f; + pCurrentLine->m_aYOffset[1] = -1.f; - // If it's a client message, m_aText will have ": " prepended so we have to work around it. - if(pCurrentLine->m_TeamNumber == Team && pCurrentLine->m_ClientID == ClientID && str_comp(pCurrentLine->m_aText, pLine) == 0) + FChatMsgCheckAndPrint(pCurrentLine); + return; + } + + m_CurrentLine = (m_CurrentLine + 1) % MAX_LINES; + + pCurrentLine = &m_aLines[m_CurrentLine]; + pCurrentLine->m_TimesRepeated = 0; + pCurrentLine->m_Time = time(); + pCurrentLine->m_aYOffset[0] = -1.0f; + pCurrentLine->m_aYOffset[1] = -1.0f; + pCurrentLine->m_ClientId = ClientId; + pCurrentLine->m_TeamNumber = Team; + pCurrentLine->m_Team = Team == 1; + pCurrentLine->m_Whisper = Team >= 2; + pCurrentLine->m_NameColor = -2; + pCurrentLine->m_Friend = false; + pCurrentLine->m_HasRenderTee = false; + + TextRender()->DeleteTextContainer(pCurrentLine->m_TextContainerIndex); + Graphics()->DeleteQuadContainer(pCurrentLine->m_QuadContainerIndex); + + // check for highlighted name + if(Client()->State() != IClient::STATE_DEMOPLAYBACK) + { + if(ClientId >= 0 && ClientId != m_pClient->m_aLocalIds[0] && (!m_pClient->Client()->DummyConnected() || ClientId != m_pClient->m_aLocalIds[1])) { - pCurrentLine->m_TimesRepeated++; - TextRender()->DeleteTextContainer(pCurrentLine->m_TextContainerIndex); - Graphics()->DeleteQuadContainer(pCurrentLine->m_QuadContainerIndex); - pCurrentLine->m_Time = time(); - pCurrentLine->m_aYOffset[0] = -1.f; - pCurrentLine->m_aYOffset[1] = -1.f; - - FChatMsgCheckAndPrint(pCurrentLine); - return; + // main character + Highlighted |= LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_aLocalIds[0]].m_aName); + // dummy + Highlighted |= m_pClient->Client()->DummyConnected() && LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_aLocalIds[1]].m_aName); } + } + else + { + // on demo playback use local id from snap directly, + // since m_aLocalIds isn't valid there + Highlighted |= m_pClient->m_Snap.m_LocalClientId >= 0 && LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId].m_aName); + } - m_CurrentLine = (m_CurrentLine + 1) % MAX_LINES; + pCurrentLine->m_Highlighted = Highlighted; - pCurrentLine = &m_aLines[m_CurrentLine]; - pCurrentLine->m_TimesRepeated = 0; - pCurrentLine->m_Time = time(); - pCurrentLine->m_aYOffset[0] = -1.0f; - pCurrentLine->m_aYOffset[1] = -1.0f; - pCurrentLine->m_ClientID = ClientID; - pCurrentLine->m_TeamNumber = Team; - pCurrentLine->m_Team = Team == 1; - pCurrentLine->m_Whisper = Team >= 2; - pCurrentLine->m_NameColor = -2; + if(pCurrentLine->m_ClientId == SERVER_MSG) + { + str_copy(pCurrentLine->m_aName, "*** "); + str_copy(pCurrentLine->m_aText, pLine); + } + else if(pCurrentLine->m_ClientId == CLIENT_MSG) + { + str_copy(pCurrentLine->m_aName, "— "); + str_copy(pCurrentLine->m_aText, pLine); + } + else + { + auto &LineAuthor = m_pClient->m_aClients[pCurrentLine->m_ClientId]; - TextRender()->DeleteTextContainer(pCurrentLine->m_TextContainerIndex); - Graphics()->DeleteQuadContainer(pCurrentLine->m_QuadContainerIndex); + if(LineAuthor.m_Team == TEAM_SPECTATORS) + pCurrentLine->m_NameColor = TEAM_SPECTATORS; - // check for highlighted name - if(Client()->State() != IClient::STATE_DEMOPLAYBACK) + if(m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS) { - if(ClientID >= 0 && ClientID != m_pClient->m_aLocalIDs[0] && (!m_pClient->Client()->DummyConnected() || ClientID != m_pClient->m_aLocalIDs[1])) - { - // main character - Highlighted |= LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_aLocalIDs[0]].m_aName); - // dummy - Highlighted |= m_pClient->Client()->DummyConnected() && LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_aLocalIDs[1]].m_aName); - } - } - else - { - // on demo playback use local id from snap directly, - // since m_aLocalIDs isn't valid there - Highlighted |= m_pClient->m_Snap.m_LocalClientID >= 0 && LineShouldHighlight(pLine, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID].m_aName); + if(LineAuthor.m_Team == TEAM_RED) + pCurrentLine->m_NameColor = TEAM_RED; + else if(LineAuthor.m_Team == TEAM_BLUE) + pCurrentLine->m_NameColor = TEAM_BLUE; } - pCurrentLine->m_Highlighted = Highlighted; - - if(pCurrentLine->m_ClientID == SERVER_MSG) + if(Team == TEAM_WHISPER_SEND) { - str_copy(pCurrentLine->m_aName, "*** "); - str_copy(pCurrentLine->m_aText, pLine); + str_format(pCurrentLine->m_aName, sizeof(pCurrentLine->m_aName), "→ %s", LineAuthor.m_aName); + pCurrentLine->m_NameColor = TEAM_BLUE; + pCurrentLine->m_Highlighted = false; + Highlighted = false; } - else if(pCurrentLine->m_ClientID == CLIENT_MSG) + else if(Team == TEAM_WHISPER_RECV) { - str_copy(pCurrentLine->m_aName, "— "); - str_copy(pCurrentLine->m_aText, pLine); + str_format(pCurrentLine->m_aName, sizeof(pCurrentLine->m_aName), "← %s", LineAuthor.m_aName); + pCurrentLine->m_NameColor = TEAM_RED; + pCurrentLine->m_Highlighted = true; + Highlighted = true; } else - { - if(m_pClient->m_aClients[ClientID].m_Team == TEAM_SPECTATORS) - pCurrentLine->m_NameColor = TEAM_SPECTATORS; + str_copy(pCurrentLine->m_aName, LineAuthor.m_aName); - if(m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS) - { - if(m_pClient->m_aClients[ClientID].m_Team == TEAM_RED) - pCurrentLine->m_NameColor = TEAM_RED; - else if(m_pClient->m_aClients[ClientID].m_Team == TEAM_BLUE) - pCurrentLine->m_NameColor = TEAM_BLUE; - } + str_copy(pCurrentLine->m_aText, pLine); + pCurrentLine->m_Friend = LineAuthor.m_Friend; - if(Team == 2) // whisper send - { - str_format(pCurrentLine->m_aName, sizeof(pCurrentLine->m_aName), "→ %s", m_pClient->m_aClients[ClientID].m_aName); - pCurrentLine->m_NameColor = TEAM_BLUE; - pCurrentLine->m_Highlighted = false; - Highlighted = false; - } - else if(Team == 3) // whisper recv - { - str_format(pCurrentLine->m_aName, sizeof(pCurrentLine->m_aName), "← %s", m_pClient->m_aClients[ClientID].m_aName); - pCurrentLine->m_NameColor = TEAM_RED; - pCurrentLine->m_Highlighted = true; - Highlighted = true; - } - else - str_copy(pCurrentLine->m_aName, m_pClient->m_aClients[ClientID].m_aName); - - str_copy(pCurrentLine->m_aText, pLine); - pCurrentLine->m_Friend = m_pClient->m_aClients[ClientID].m_Friend; - } - - pCurrentLine->m_HasRenderTee = false; - - pCurrentLine->m_Friend = ClientID >= 0 ? m_pClient->m_aClients[ClientID].m_Friend : false; - - if(pCurrentLine->m_ClientID >= 0 && pCurrentLine->m_aName[0] != '\0') + if(pCurrentLine->m_aName[0] != '\0') { if(!g_Config.m_ClChatOld) { - pCurrentLine->m_CustomColoredSkin = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_CustomColoredSkin; - if(pCurrentLine->m_CustomColoredSkin) - pCurrentLine->m_RenderSkin = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_ColorableRenderSkin; - else - pCurrentLine->m_RenderSkin = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_OriginalRenderSkin; - - str_copy(pCurrentLine->m_aSkinName, m_pClient->m_aClients[pCurrentLine->m_ClientID].m_aSkinName); - pCurrentLine->m_ColorBody = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_ColorBody; - pCurrentLine->m_ColorFeet = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_ColorFeet; - - pCurrentLine->m_RenderSkinMetrics = m_pClient->m_aClients[pCurrentLine->m_ClientID].m_RenderInfo.m_SkinMetrics; + str_copy(pCurrentLine->m_aSkinName, LineAuthor.m_aSkinName); + pCurrentLine->m_TeeRenderInfo = LineAuthor.m_RenderInfo; pCurrentLine->m_HasRenderTee = true; } } - - FChatMsgCheckAndPrint(pCurrentLine); } + FChatMsgCheckAndPrint(pCurrentLine); + // play sound int64_t Now = time(); - if(ClientID == SERVER_MSG) + if(ClientId == SERVER_MSG) { if(Now - m_aLastSoundPlayed[CHAT_SERVER] >= time_freq() * 3 / 10) { if(g_Config.m_SndServerMessage) { - m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_SERVER, 0); + m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_SERVER, 1.0f); m_aLastSoundPlayed[CHAT_SERVER] = Now; } } } - else if(ClientID == CLIENT_MSG) + else if(ClientId == CLIENT_MSG) { // No sound yet } @@ -893,7 +840,7 @@ void CChat::AddLine(int ClientID, int Team, const char *pLine) Client()->Notify("DDNet Chat", aBuf); if(g_Config.m_SndHighlight) { - m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_HIGHLIGHT, 0); + m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_HIGHLIGHT, 1.0f); m_aLastSoundPlayed[CHAT_HIGHLIGHT] = Now; } @@ -903,7 +850,7 @@ void CChat::AddLine(int ClientID, int Team, const char *pLine) } } } - else if(Team != 2) + else if(Team != TEAM_WHISPER_SEND) { if(Now - m_aLastSoundPlayed[CHAT_CLIENT] >= time_freq() * 3 / 10) { @@ -916,30 +863,24 @@ void CChat::AddLine(int ClientID, int Team, const char *pLine) #endif if(PlaySound) { - m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_CLIENT, 0); + m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_CLIENT, 1.0f); m_aLastSoundPlayed[CHAT_CLIENT] = Now; } } } } -void CChat::RefindSkins() +void CChat::OnRefreshSkins() { for(auto &Line : m_aLines) { if(Line.m_HasRenderTee) { - const CSkin *pSkin = m_pClient->m_Skins.Find(Line.m_aSkinName); - if(Line.m_CustomColoredSkin) - Line.m_RenderSkin = pSkin->m_ColorableSkin; - else - Line.m_RenderSkin = pSkin->m_OriginalSkin; - - Line.m_RenderSkinMetrics = pSkin->m_Metrics; + Line.m_TeeRenderInfo.Apply(m_pClient->m_Skins.Find(Line.m_aSkinName)); } else { - Line.m_RenderSkin.Reset(); + Line.m_TeeRenderInfo.Reset(); } } } @@ -949,9 +890,7 @@ void CChat::OnPrepareLines(float y) float x = 5.0f; float FontSize = this->FontSize(); - float ScreenRatio = Graphics()->ScreenAspect(); - - const bool IsScoreBoardOpen = m_pClient->m_Scoreboard.Active() && (ScreenRatio > 1.7f); // only assume scoreboard when screen ratio is widescreen(something around 16:9) + const bool IsScoreBoardOpen = m_pClient->m_Scoreboard.Active() && (Graphics()->ScreenAspect() > 1.7f); // only assume scoreboard when screen ratio is widescreen(something around 16:9) const bool ShowLargeArea = m_Show || (m_Mode != MODE_NONE && g_Config.m_ClShowChat == 1) || g_Config.m_ClShowChat == 2; const bool ForceRecreate = IsScoreBoardOpen != m_PrevScoreBoardShowed || ShowLargeArea != m_PrevShowChat; m_PrevScoreBoardShowed = IsScoreBoardOpen; @@ -980,63 +919,75 @@ void CChat::OnPrepareLines(float y) for(int i = 0; i < MAX_LINES; i++) { - int r = ((m_CurrentLine - i) + MAX_LINES) % MAX_LINES; + CLine &Line = m_aLines[((m_CurrentLine - i) + MAX_LINES) % MAX_LINES]; - if(Now > m_aLines[r].m_Time + 16 * time_freq() && !m_PrevShowChat) + if(Now > Line.m_Time + 16 * time_freq() && !m_PrevShowChat) break; - if(m_aLines[r].m_TextContainerIndex.Valid() && !ForceRecreate) + if(Line.m_TextContainerIndex.Valid() && !ForceRecreate) continue; - TextRender()->DeleteTextContainer(m_aLines[r].m_TextContainerIndex); - Graphics()->DeleteQuadContainer(m_aLines[r].m_QuadContainerIndex); - - char aName[64 + 12] = ""; + TextRender()->DeleteTextContainer(Line.m_TextContainerIndex); + Graphics()->DeleteQuadContainer(Line.m_QuadContainerIndex); - if(g_Config.m_ClShowIDs && m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0') + char aClientId[16] = ""; + if(g_Config.m_ClShowIds && Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') { - if(m_aLines[r].m_ClientID < 10) - str_format(aName, sizeof(aName), " %d: ", m_aLines[r].m_ClientID); - else - str_format(aName, sizeof(aName), "%d: ", m_aLines[r].m_ClientID); + GameClient()->FormatClientId(Line.m_ClientId, aClientId, EClientIdFormat::INDENT_AUTO); } - str_append(aName, m_aLines[r].m_aName); - char aCount[12]; - if(m_aLines[r].m_ClientID < 0) - str_format(aCount, sizeof(aCount), "[%d] ", m_aLines[r].m_TimesRepeated + 1); + if(Line.m_ClientId < 0) + str_format(aCount, sizeof(aCount), "[%d] ", Line.m_TimesRepeated + 1); else - str_format(aCount, sizeof(aCount), " [%d]", m_aLines[r].m_TimesRepeated + 1); + str_format(aCount, sizeof(aCount), " [%d]", Line.m_TimesRepeated + 1); + + const char *pText = Line.m_aText; + if(Config()->m_ClStreamerMode && Line.m_ClientId == SERVER_MSG) + { + if(str_startswith(Line.m_aText, "Team save in progress. You'll be able to load with '/load ") && str_endswith(Line.m_aText, "'")) + { + pText = "Team save in progress. You'll be able to load with '/load ***'"; + } + else if(str_startswith(Line.m_aText, "Team save in progress. You'll be able to load with '/load") && str_endswith(Line.m_aText, "if it fails")) + { + pText = "Team save in progress. You'll be able to load with '/load ***' if save is successful or with '/load *** *** ***' if it fails"; + } + else if(str_startswith(Line.m_aText, "Team successfully saved by ") && str_endswith(Line.m_aText, " to continue")) + { + pText = "Team successfully saved by ***. Use '/load ***' to continue"; + } + } if(g_Config.m_ClChatOld) { - m_aLines[r].m_HasRenderTee = false; + Line.m_HasRenderTee = false; } // get the y offset (calculate it if we haven't done that yet) - if(m_aLines[r].m_aYOffset[OffsetType] < 0.0f) + if(Line.m_aYOffset[OffsetType] < 0.0f) { TextRender()->SetCursor(&Cursor, TextBegin, 0.0f, FontSize, 0); Cursor.m_LineWidth = LineWidth; - if(m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0') + if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') { Cursor.m_X += RealMsgPaddingTee; - if(m_aLines[r].m_Friend && g_Config.m_ClMessageFriend) + if(Line.m_Friend && g_Config.m_ClMessageFriend) { - TextRender()->TextEx(&Cursor, "♥ ", -1); + TextRender()->TextEx(&Cursor, "♥ "); } } - TextRender()->TextEx(&Cursor, aName, -1); - if(m_aLines[r].m_TimesRepeated > 0) - TextRender()->TextEx(&Cursor, aCount, -1); + TextRender()->TextEx(&Cursor, aClientId); + TextRender()->TextEx(&Cursor, Line.m_aName); + if(Line.m_TimesRepeated > 0) + TextRender()->TextEx(&Cursor, aCount); - if(m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0') + if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') { - TextRender()->TextEx(&Cursor, ": ", -1); + TextRender()->TextEx(&Cursor, ": "); } CTextCursor AppendCursor = Cursor; @@ -1047,84 +998,83 @@ void CChat::OnPrepareLines(float y) AppendCursor.m_LineWidth -= Cursor.m_LongestLineWidth; } - TextRender()->TextEx(&AppendCursor, m_aLines[r].m_aText, -1); + TextRender()->TextEx(&AppendCursor, pText); - m_aLines[r].m_aYOffset[OffsetType] = AppendCursor.m_Y + AppendCursor.m_FontSize + RealMsgPaddingY; + Line.m_aYOffset[OffsetType] = AppendCursor.Height() + RealMsgPaddingY; } - y -= m_aLines[r].m_aYOffset[OffsetType]; + y -= Line.m_aYOffset[OffsetType]; // cut off if msgs waste too much space if(y < HeightLimit) break; // the position the text was created - m_aLines[r].m_TextYOffset = y + RealMsgPaddingY / 2.f; + Line.m_TextYOffset = y + RealMsgPaddingY / 2.f; int CurRenderFlags = TextRender()->GetRenderFlags(); TextRender()->SetRenderFlags(CurRenderFlags | ETextRenderFlags::TEXT_RENDER_FLAG_NO_AUTOMATIC_QUAD_UPLOAD); // reset the cursor - TextRender()->SetCursor(&Cursor, TextBegin, m_aLines[r].m_TextYOffset, FontSize, TEXTFLAG_RENDER); + TextRender()->SetCursor(&Cursor, TextBegin, Line.m_TextYOffset, FontSize, TEXTFLAG_RENDER); Cursor.m_LineWidth = LineWidth; // Message is from valid player - if(m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0') + if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') { Cursor.m_X += RealMsgPaddingTee; - if(m_aLines[r].m_Friend && g_Config.m_ClMessageFriend) + if(Line.m_Friend && g_Config.m_ClMessageFriend) { - const char *pHeartStr = "♥ "; - ColorRGBA rgb = color_cast(ColorHSLA(g_Config.m_ClMessageFriendColor)); - TextRender()->TextColor(rgb.WithAlpha(1.f)); - TextRender()->CreateOrAppendTextContainer(m_aLines[r].m_TextContainerIndex, &Cursor, pHeartStr); + TextRender()->TextColor(color_cast(ColorHSLA(g_Config.m_ClMessageFriendColor)).WithAlpha(1.f)); + TextRender()->CreateOrAppendTextContainer(Line.m_TextContainerIndex, &Cursor, "♥ "); } } // render name ColorRGBA NameColor; - if(m_aLines[r].m_ClientID == SERVER_MSG) + if(Line.m_ClientId == SERVER_MSG) NameColor = color_cast(ColorHSLA(g_Config.m_ClMessageSystemColor)); - else if(m_aLines[r].m_ClientID == CLIENT_MSG) + else if(Line.m_ClientId == CLIENT_MSG) NameColor = color_cast(ColorHSLA(g_Config.m_ClMessageClientColor)); - else if(m_aLines[r].m_Team) + else if(Line.m_Team) NameColor = CalculateNameColor(ColorHSLA(g_Config.m_ClMessageTeamColor)); - else if(m_aLines[r].m_NameColor == TEAM_RED) + else if(Line.m_NameColor == TEAM_RED) NameColor = ColorRGBA(1.0f, 0.5f, 0.5f, 1.f); - else if(m_aLines[r].m_NameColor == TEAM_BLUE) + else if(Line.m_NameColor == TEAM_BLUE) NameColor = ColorRGBA(0.7f, 0.7f, 1.0f, 1.f); - else if(m_aLines[r].m_NameColor == TEAM_SPECTATORS) + else if(Line.m_NameColor == TEAM_SPECTATORS) NameColor = ColorRGBA(0.75f, 0.5f, 0.75f, 1.f); - else if(m_aLines[r].m_ClientID >= 0 && g_Config.m_ClChatTeamColors && m_pClient->m_Teams.Team(m_aLines[r].m_ClientID)) - NameColor = m_pClient->GetDDTeamColor(m_pClient->m_Teams.Team(m_aLines[r].m_ClientID), 0.75f); + else if(Line.m_ClientId >= 0 && g_Config.m_ClChatTeamColors && m_pClient->m_Teams.Team(Line.m_ClientId)) + NameColor = m_pClient->GetDDTeamColor(m_pClient->m_Teams.Team(Line.m_ClientId), 0.75f); else NameColor = ColorRGBA(0.8f, 0.8f, 0.8f, 1.f); TextRender()->TextColor(NameColor); - TextRender()->CreateOrAppendTextContainer(m_aLines[r].m_TextContainerIndex, &Cursor, aName); + TextRender()->CreateOrAppendTextContainer(Line.m_TextContainerIndex, &Cursor, aClientId); + TextRender()->CreateOrAppendTextContainer(Line.m_TextContainerIndex, &Cursor, Line.m_aName); - if(m_aLines[r].m_TimesRepeated > 0) + if(Line.m_TimesRepeated > 0) { TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.3f); - TextRender()->CreateOrAppendTextContainer(m_aLines[r].m_TextContainerIndex, &Cursor, aCount); + TextRender()->CreateOrAppendTextContainer(Line.m_TextContainerIndex, &Cursor, aCount); } - if(m_aLines[r].m_ClientID >= 0 && m_aLines[r].m_aName[0] != '\0') + if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') { TextRender()->TextColor(NameColor); - TextRender()->CreateOrAppendTextContainer(m_aLines[r].m_TextContainerIndex, &Cursor, ": "); + TextRender()->CreateOrAppendTextContainer(Line.m_TextContainerIndex, &Cursor, ": "); } // render line ColorRGBA Color; - if(m_aLines[r].m_ClientID == SERVER_MSG) + if(Line.m_ClientId == SERVER_MSG) Color = color_cast(ColorHSLA(g_Config.m_ClMessageSystemColor)); - else if(m_aLines[r].m_ClientID == CLIENT_MSG) + else if(Line.m_ClientId == CLIENT_MSG) Color = color_cast(ColorHSLA(g_Config.m_ClMessageClientColor)); - else if(m_aLines[r].m_Highlighted) + else if(Line.m_Highlighted) Color = color_cast(ColorHSLA(g_Config.m_ClMessageHighlightColor)); - else if(m_aLines[r].m_Team) + else if(Line.m_Team) Color = color_cast(ColorHSLA(g_Config.m_ClMessageTeamColor)); else // regular message Color = color_cast(ColorHSLA(g_Config.m_ClMessageColor)); @@ -1133,34 +1083,32 @@ void CChat::OnPrepareLines(float y) CTextCursor AppendCursor = Cursor; AppendCursor.m_LongestLineWidth = 0.0f; - float OriginalWidth = 0.0f; if(!IsScoreBoardOpen && !g_Config.m_ClChatOld) { AppendCursor.m_StartX = Cursor.m_X; AppendCursor.m_LineWidth -= Cursor.m_LongestLineWidth; - OriginalWidth = Cursor.m_LongestLineWidth; } - const char *pText = m_aLines[r].m_aText; - if(Config()->m_ClStreamerMode && m_aLines[r].m_ClientID == SERVER_MSG) - { - if(str_startswith(m_aLines[r].m_aText, "Team save in progress. You'll be able to load with '/load") && str_endswith(m_aLines[r].m_aText, "if it fails")) - pText = "Team save in progress. You'll be able to load with '/load ***' if save is successful or with '/load *** *** ***' if it fails"; - else if(str_startswith(m_aLines[r].m_aText, "Team successfully saved by ") && str_endswith(m_aLines[r].m_aText, " to continue")) - pText = "Team successfully saved by ***. Use '/load ***' to continue"; - } - TextRender()->CreateOrAppendTextContainer(m_aLines[r].m_TextContainerIndex, &AppendCursor, pText); + TextRender()->CreateOrAppendTextContainer(Line.m_TextContainerIndex, &AppendCursor, pText); - if(!g_Config.m_ClChatOld && (m_aLines[r].m_aText[0] != '\0' || m_aLines[r].m_aName[0] != '\0')) + if(!g_Config.m_ClChatOld && (Line.m_aText[0] != '\0' || Line.m_aName[0] != '\0')) { - float Height = m_aLines[r].m_aYOffset[OffsetType]; + float FullWidth = RealMsgPaddingX * 1.5f; + if(!IsScoreBoardOpen && !g_Config.m_ClChatOld) + { + FullWidth += Cursor.m_LongestLineWidth + AppendCursor.m_LongestLineWidth; + } + else + { + FullWidth += maximum(Cursor.m_LongestLineWidth, AppendCursor.m_LongestLineWidth); + } Graphics()->SetColor(1, 1, 1, 1); - m_aLines[r].m_QuadContainerIndex = Graphics()->CreateRectQuadContainer(Begin, y, OriginalWidth + AppendCursor.m_LongestLineWidth + RealMsgPaddingX * 1.5f, Height, MessageRounding(), IGraphics::CORNER_ALL); + Line.m_QuadContainerIndex = Graphics()->CreateRectQuadContainer(Begin, y, FullWidth, Line.m_aYOffset[OffsetType], MessageRounding(), IGraphics::CORNER_ALL); } TextRender()->SetRenderFlags(CurRenderFlags); - if(m_aLines[r].m_TextContainerIndex.Valid()) - TextRender()->UploadTextContainer(m_aLines[r].m_TextContainerIndex); + if(Line.m_TextContainerIndex.Valid()) + TextRender()->UploadTextContainer(Line.m_TextContainerIndex); } TextRender()->TextColor(TextRender()->DefaultTextColor()); @@ -1168,6 +1116,9 @@ void CChat::OnPrepareLines(float y) void CChat::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + // send pending chat messages if(m_PendingChatCounter > 0 && m_LastChatSend + time_freq() < time()) { @@ -1176,7 +1127,7 @@ void CChat::OnRender() { if(i == 0) { - Say(pEntry->m_Team, pEntry->m_aText); + SendChat(pEntry->m_Team, pEntry->m_aText); break; } } @@ -1198,13 +1149,13 @@ void CChat::OnRender() Cursor.m_LineWidth = Width - 190.0f; if(m_Mode == MODE_ALL) - TextRender()->TextEx(&Cursor, Localize("All"), -1); + TextRender()->TextEx(&Cursor, Localize("All")); else if(m_Mode == MODE_TEAM) - TextRender()->TextEx(&Cursor, Localize("Team"), -1); + TextRender()->TextEx(&Cursor, Localize("Team")); else - TextRender()->TextEx(&Cursor, Localize("Chat"), -1); + TextRender()->TextEx(&Cursor, Localize("Chat")); - TextRender()->TextEx(&Cursor, ": ", -1); + TextRender()->TextEx(&Cursor, ": "); const float MessageMaxWidth = Cursor.m_LineWidth - (Cursor.m_X - Cursor.m_StartX); const CUIRect ClippingRect = {Cursor.m_X, Cursor.m_Y, MessageMaxWidth, 2.25f * Cursor.m_FontSize}; @@ -1231,10 +1182,27 @@ void CChat::OnRender() else if(CaretPositionY + Cursor.m_FontSize > ClippingRect.y + ClippingRect.h) ScrollOffsetChange += CaretPositionY + Cursor.m_FontSize - (ClippingRect.y + ClippingRect.h); - UI()->DoSmoothScrollLogic(&ScrollOffset, &ScrollOffsetChange, ClippingRect.h, BoundingBox.m_H); + Ui()->DoSmoothScrollLogic(&ScrollOffset, &ScrollOffsetChange, ClippingRect.h, BoundingBox.m_H); m_Input.SetScrollOffset(ScrollOffset); m_Input.SetScrollOffsetChange(ScrollOffsetChange); + + // Autocompletion hint + if(m_Input.GetString()[0] == '/' && m_Input.GetString()[1] != '\0' && !m_vCommands.empty()) + { + for(const auto &Command : m_vCommands) + { + if(str_startswith_nocase(Command.m_aName, m_Input.GetString() + 1)) + { + Cursor.m_X = m_Input.GetCaretPosition().x; + Cursor.m_Y = m_Input.GetCaretPosition().y; + TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.5f); + TextRender()->TextEx(&Cursor, Command.m_aName + str_length(m_Input.GetString() + 1)); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + break; + } + } + } } #if defined(CONF_VIDEORECORDER) @@ -1248,8 +1216,7 @@ void CChat::OnRender() OnPrepareLines(y); - float ScreenRatio = Graphics()->ScreenAspect(); - bool IsScoreBoardOpen = m_pClient->m_Scoreboard.Active() && (ScreenRatio > 1.7f); // only assume scoreboard when screen ratio is widescreen(something around 16:9) + bool IsScoreBoardOpen = m_pClient->m_Scoreboard.Active() && (Graphics()->ScreenAspect() > 1.7f); // only assume scoreboard when screen ratio is widescreen(something around 16:9) int64_t Now = time(); float HeightLimit = IsScoreBoardOpen ? 180.0f : (m_PrevShowChat ? 50.0f : 200.0f); @@ -1266,45 +1233,35 @@ void CChat::OnRender() for(int i = 0; i < MAX_LINES; i++) { - int r = ((m_CurrentLine - i) + MAX_LINES) % MAX_LINES; - if(Now > m_aLines[r].m_Time + 16 * time_freq() && !m_PrevShowChat) + CLine &Line = m_aLines[((m_CurrentLine - i) + MAX_LINES) % MAX_LINES]; + if(Now > Line.m_Time + 16 * time_freq() && !m_PrevShowChat) break; - y -= m_aLines[r].m_aYOffset[OffsetType]; + y -= Line.m_aYOffset[OffsetType]; // cut off if msgs waste too much space if(y < HeightLimit) break; - float Blend = Now > m_aLines[r].m_Time + 14 * time_freq() && !m_PrevShowChat ? 1.0f - (Now - m_aLines[r].m_Time - 14 * time_freq()) / (2.0f * time_freq()) : 1.0f; + float Blend = Now > Line.m_Time + 14 * time_freq() && !m_PrevShowChat ? 1.0f - (Now - Line.m_Time - 14 * time_freq()) / (2.0f * time_freq()) : 1.0f; // Draw backgrounds for messages in one batch if(!g_Config.m_ClChatOld) { Graphics()->TextureClear(); - if(m_aLines[r].m_QuadContainerIndex != -1) + if(Line.m_QuadContainerIndex != -1) { Graphics()->SetColor(0, 0, 0, 0.12f * Blend); - Graphics()->RenderQuadContainerEx(m_aLines[r].m_QuadContainerIndex, 0, -1, 0, ((y + RealMsgPaddingY / 2.0f) - m_aLines[r].m_TextYOffset)); + Graphics()->RenderQuadContainerEx(Line.m_QuadContainerIndex, 0, -1, 0, ((y + RealMsgPaddingY / 2.0f) - Line.m_TextYOffset)); } } - if(m_aLines[r].m_TextContainerIndex.Valid()) + if(Line.m_TextContainerIndex.Valid()) { - if(!g_Config.m_ClChatOld && m_aLines[r].m_HasRenderTee) + if(!g_Config.m_ClChatOld && Line.m_HasRenderTee) { const int TeeSize = MessageTeeSize(); - CTeeRenderInfo RenderInfo; - RenderInfo.m_CustomColoredSkin = m_aLines[r].m_CustomColoredSkin; - if(m_aLines[r].m_CustomColoredSkin) - RenderInfo.m_ColorableRenderSkin = m_aLines[r].m_RenderSkin; - else - RenderInfo.m_OriginalRenderSkin = m_aLines[r].m_RenderSkin; - RenderInfo.m_SkinMetrics = m_aLines[r].m_RenderSkinMetrics; - - RenderInfo.m_ColorBody = m_aLines[r].m_ColorBody; - RenderInfo.m_ColorFeet = m_aLines[r].m_ColorFeet; - RenderInfo.m_Size = TeeSize; + Line.m_TeeRenderInfo.m_Size = TeeSize; float RowHeight = FontSize() + RealMsgPaddingY; float OffsetTeeY = TeeSize / 2.0f; @@ -1312,19 +1269,41 @@ void CChat::OnRender() const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &RenderInfo, OffsetToMid); + CRenderTools::GetRenderTeeOffsetToRenderedTee(pIdleState, &Line.m_TeeRenderInfo, OffsetToMid); vec2 TeeRenderPos(x + (RealMsgPaddingX + TeeSize) / 2.0f, y + OffsetTeeY + FullHeightMinusTee / 2.0f + OffsetToMid.y); - RenderTools()->RenderTee(pIdleState, &RenderInfo, EMOTE_NORMAL, vec2(1, 0.1f), TeeRenderPos, Blend); + RenderTools()->RenderTee(pIdleState, &Line.m_TeeRenderInfo, EMOTE_NORMAL, vec2(1, 0.1f), TeeRenderPos, Blend); } const ColorRGBA TextColor = TextRender()->DefaultTextColor().WithMultipliedAlpha(Blend); const ColorRGBA TextOutlineColor = TextRender()->DefaultTextOutlineColor().WithMultipliedAlpha(Blend); - TextRender()->RenderTextContainer(m_aLines[r].m_TextContainerIndex, TextColor, TextOutlineColor, 0, (y + RealMsgPaddingY / 2.0f) - m_aLines[r].m_TextYOffset); + TextRender()->RenderTextContainer(Line.m_TextContainerIndex, TextColor, TextOutlineColor, 0, (y + RealMsgPaddingY / 2.0f) - Line.m_TextYOffset); } } } -void CChat::Say(int Team, const char *pLine) +void CChat::EnsureCoherentFontSize() const +{ + // Adjust font size based on width + if(g_Config.m_ClChatWidth / (float)g_Config.m_ClChatFontSize >= CHAT_FONTSIZE_WIDTH_RATIO) + return; + + // We want to keep a ration between font size and font width so that we don't have a weird rendering + g_Config.m_ClChatFontSize = g_Config.m_ClChatWidth / CHAT_FONTSIZE_WIDTH_RATIO; +} + +void CChat::EnsureCoherentWidth() const +{ + // Adjust width based on font size + if(g_Config.m_ClChatWidth / (float)g_Config.m_ClChatFontSize >= CHAT_FONTSIZE_WIDTH_RATIO) + return; + + // We want to keep a ration between font size and font width so that we don't have a weird rendering + g_Config.m_ClChatWidth = CHAT_FONTSIZE_WIDTH_RATIO * g_Config.m_ClChatFontSize; +} + +// ----- send functions ----- + +void CChat::SendChat(int Team, const char *pLine) { // don't send empty messages if(*str_utf8_skip_whitespaces(pLine) == '\0') @@ -1332,6 +1311,16 @@ void CChat::Say(int Team, const char *pLine) m_LastChatSend = time(); + if(m_pClient->Client()->IsSixup()) + { + protocol7::CNetMsg_Cl_Say Msg7; + Msg7.m_Mode = Team == 1 ? protocol7::CHAT_TEAM : protocol7::CHAT_ALL; + Msg7.m_Target = -1; + Msg7.m_pMessage = pLine; + Client()->SendPackMsgActive(&Msg7, MSGFLAG_VITAL, true); + return; + } + // send chat message CNetMsg_Cl_Say Msg; Msg.m_Team = Team; @@ -1339,7 +1328,7 @@ void CChat::Say(int Team, const char *pLine) Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); } -void CChat::SayChat(const char *pLine) +void CChat::SendChatQueued(const char *pLine) { if(!pLine || str_length(pLine) < 1) return; @@ -1348,7 +1337,7 @@ void CChat::SayChat(const char *pLine) if(m_LastChatSend + time_freq() < time()) { - Say(m_Mode == MODE_ALL ? 0 : 1, pLine); + SendChat(m_Mode == MODE_ALL ? 0 : 1, pLine); AddEntry = true; } else if(m_PendingChatCounter < 3) @@ -1359,28 +1348,9 @@ void CChat::SayChat(const char *pLine) if(AddEntry) { - CHistoryEntry *pEntry = m_History.Allocate(sizeof(CHistoryEntry) + str_length(pLine) - 1); + const int Length = str_length(pLine); + CHistoryEntry *pEntry = m_History.Allocate(sizeof(CHistoryEntry) + Length); pEntry->m_Team = m_Mode == MODE_ALL ? 0 : 1; - mem_copy(pEntry->m_aText, pLine, str_length(pLine)); + str_copy(pEntry->m_aText, pLine, Length + 1); } } - -void CChat::EnsureCoherentFontSize() const -{ - // Adjust font size based on width - if(g_Config.m_ClChatWidth / (float)g_Config.m_ClChatFontSize >= CHAT_FONTSIZE_WIDTH_RATIO) - return; - - // We want to keep a ration between font size and font width so that we don't have a weird rendering - g_Config.m_ClChatFontSize = g_Config.m_ClChatWidth / CHAT_FONTSIZE_WIDTH_RATIO; -} - -void CChat::EnsureCoherentWidth() const -{ - // Adjust width based on font size - if(g_Config.m_ClChatWidth / (float)g_Config.m_ClChatFontSize >= CHAT_FONTSIZE_WIDTH_RATIO) - return; - - // We want to keep a ration between font size and font width so that we don't have a weird rendering - g_Config.m_ClChatWidth = CHAT_FONTSIZE_WIDTH_RATIO * g_Config.m_ClChatFontSize; -} diff --git a/src/game/client/components/chat.h b/src/game/client/components/chat.h index cf23efc935..b585dfdd2f 100644 --- a/src/game/client/components/chat.h +++ b/src/game/client/components/chat.h @@ -11,7 +11,9 @@ #include #include +#include #include +#include class CChat : public CComponent { @@ -30,7 +32,7 @@ class CChat : public CComponent { int64_t m_Time; float m_aYOffset[2]; - int m_ClientID; + int m_ClientId; int m_TeamNumber; bool m_Team; bool m_Whisper; @@ -44,13 +46,9 @@ class CChat : public CComponent int m_QuadContainerIndex; char m_aSkinName[std::size(g_Config.m_ClPlayerSkin)]; - CSkin::SSkinTextures m_RenderSkin; - CSkin::SSkinMetrics m_RenderSkinMetrics; - bool m_CustomColoredSkin; - ColorRGBA m_ColorBody; - ColorRGBA m_ColorFeet; - bool m_HasRenderTee; + CTeeRenderInfo m_TeeRenderInfo; + float m_TextYOffset; int m_TimesRepeated; @@ -88,7 +86,7 @@ class CChat : public CComponent static char ms_aDisplayText[MAX_LINE_LENGTH]; struct CRateablePlayer { - int ClientID; + int ClientId; int Score; }; CRateablePlayer m_aPlayerCompletionList[MAX_CLIENTS]; @@ -114,7 +112,6 @@ class CChat : public CComponent }; std::vector m_vCommands; - std::vector m_vDefaultCommands; bool m_CommandsNeedSorting; struct CHistoryEntry @@ -153,11 +150,9 @@ class CChat : public CComponent static constexpr float MESSAGE_TEE_PADDING_RIGHT = 0.5f; bool IsActive() const { return m_Mode != MODE_NONE; } - void AddLine(int ClientID, int Team, const char *pLine); + void AddLine(int ClientId, int Team, const char *pLine); void EnableMode(int Team); void DisableMode(); - void Say(int Team, const char *pLine); - void SayChat(const char *pLine); void RegisterCommand(const char *pName, const char *pParams, const char *pHelpText); void UnregisterCommand(const char *pName); void Echo(const char *pString); @@ -165,15 +160,14 @@ class CChat : public CComponent void OnWindowResize() override; void OnConsoleInit() override; void OnStateChange(int NewState, int OldState) override; + void OnRefreshSkins() override; void OnRender() override; - void RefindSkins(); void OnPrepareLines(float y); void Reset(); void OnRelease() override; void OnMessage(int MsgType, void *pRawMsg) override; bool OnInput(const IInput::CEvent &Event) override; void OnInit() override; - void OnMapLoad() override; void RebuildChat(); @@ -185,5 +179,22 @@ class CChat : public CComponent float MessagePaddingY() const { return FontSize() * (1 / 6.f); } float MessageTeeSize() const { return FontSize() * (7 / 6.f); } float MessageRounding() const { return FontSize() * (1 / 2.f); } + + // ----- send functions ----- + + // Sends a chat message to the server. + // + // @param Team MODE_ALL=0 MODE_TEAM=1 + // @param pLine the chat message + void SendChat(int Team, const char *pLine); + + // Sends a chat message to the server. + // + // It uses a queue with a maximum of 3 entries + // that ensures there is a minimum delay of one second + // between sent messages. + // + // It uses team or public chat depending on m_Mode. + void SendChatQueued(const char *pLine); }; #endif diff --git a/src/game/client/components/console.cpp b/src/game/client/components/console.cpp index 4c11fdd78b..1c08e61553 100644 --- a/src/game/client/components/console.cpp +++ b/src/game/client/components/console.cpp @@ -73,11 +73,76 @@ void CConsoleLogger::OnConsoleDeletion() m_pConsole = nullptr; } -// TODO: support "tune_zone", which has tuning as second argument -static const char *gs_apTuningCommands[] = {"tune ", "tune_reset ", "toggle_tune "}; -static bool IsTuningCommandPrefix(const char *pStr) +enum class EArgumentCompletionType { - return std::any_of(std::begin(gs_apTuningCommands), std::end(gs_apTuningCommands), [pStr](auto *pCmd) { return str_startswith_nocase(pStr, pCmd); }); + NONE, + TUNE, + SETTING, + KEY, +}; + +class CArgumentCompletionEntry +{ +public: + EArgumentCompletionType m_Type; + const char *m_pCommandName; + int m_ArgumentIndex; +}; + +static const CArgumentCompletionEntry gs_aArgumentCompletionEntries[] = { + {EArgumentCompletionType::TUNE, "tune", 0}, + {EArgumentCompletionType::TUNE, "tune_reset", 0}, + {EArgumentCompletionType::TUNE, "toggle_tune", 0}, + {EArgumentCompletionType::TUNE, "tune_zone", 1}, + {EArgumentCompletionType::SETTING, "reset", 0}, + {EArgumentCompletionType::SETTING, "toggle", 0}, + {EArgumentCompletionType::SETTING, "access_level", 0}, + {EArgumentCompletionType::SETTING, "+toggle", 0}, + {EArgumentCompletionType::KEY, "bind", 0}, + {EArgumentCompletionType::KEY, "binds", 0}, + {EArgumentCompletionType::KEY, "unbind", 0}, +}; + +static std::pair ArgumentCompletion(const char *pStr) +{ + const char *pCommandStart = pStr; + const char *pIt = pStr; + pIt = str_skip_to_whitespace_const(pIt); + int CommandLength = pIt - pCommandStart; + const char *pCommandEnd = pIt; + + if(!CommandLength) + return {EArgumentCompletionType::NONE, -1}; + + pIt = str_skip_whitespaces_const(pIt); + if(pIt == pCommandEnd) + return {EArgumentCompletionType::NONE, -1}; + + for(const auto &Entry : gs_aArgumentCompletionEntries) + { + int Length = maximum(str_length(Entry.m_pCommandName), CommandLength); + if(str_comp_nocase_num(Entry.m_pCommandName, pCommandStart, Length) == 0) + { + int CurrentArg = 0; + const char *pArgStart = nullptr, *pArgEnd = nullptr; + while(CurrentArg < Entry.m_ArgumentIndex) + { + pArgStart = pIt; + pIt = str_skip_to_whitespace_const(pIt); // Skip argument value + pArgEnd = pIt; + + if(!pIt[0] || pArgStart == pIt) // Check that argument is not empty + return {EArgumentCompletionType::NONE, -1}; + + pIt = str_skip_whitespaces_const(pIt); // Go to next argument position + CurrentArg++; + } + if(pIt == pArgEnd) + return {EArgumentCompletionType::NONE, -1}; // Check that there is at least one space after + return {Entry.m_Type, pIt - pStr}; + } + } + return {EArgumentCompletionType::NONE, -1}; } static int PossibleTunings(const char *pStr, IConsole::FPossibleCallback pfnCallback = IConsole::EmptyPossibleCommandCallback, void *pUser = nullptr) @@ -94,10 +159,20 @@ static int PossibleTunings(const char *pStr, IConsole::FPossibleCallback pfnCall return Index; } -static const char *gs_apSettingCommands[] = {"reset ", "toggle ", "access_level ", "+toggle "}; -static bool IsSettingCommandPrefix(const char *pStr) +static int PossibleKeys(const char *pStr, IInput *pInput, IConsole::FPossibleCallback pfnCallback = IConsole::EmptyPossibleCommandCallback, void *pUser = nullptr) { - return std::any_of(std::begin(gs_apSettingCommands), std::end(gs_apSettingCommands), [pStr](auto *pCmd) { return str_startswith_nocase(pStr, pCmd); }); + int Index = 0; + for(int Key = KEY_A; Key < KEY_JOY_AXIS_11_RIGHT; Key++) + { + // Ignore unnamed keys starting with '&' + const char *pKeyName = pInput->KeyName(Key); + if(pKeyName[0] != '&' && str_find_nocase(pKeyName, pStr)) + { + pfnCallback(Index, pKeyName, pUser); + Index++; + } + } + return Index; } const ColorRGBA CGameConsole::ms_SearchHighlightColor = ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f); @@ -124,6 +199,7 @@ CGameConsole::CInstance::CInstance(int Type) m_CompletionChosen = -1; m_aCompletionBufferArgument[0] = 0; m_CompletionChosenArgument = -1; + m_CompletionArgumentPosition = 0; Reset(); m_aUser[0] = '\0'; @@ -132,6 +208,13 @@ CGameConsole::CInstance::CInstance(int Type) m_IsCommand = false; + m_Backlog.SetPopCallback([this](CBacklogEntry *pEntry) { + if(pEntry->m_LineCount != -1) + { + m_NewLineCounter -= pEntry->m_LineCount; + } + }); + m_Input.SetClipboardLineCallback([this](const char *pStr) { ExecuteLine(pStr); }); m_CurrentMatchIndex = -1; @@ -160,7 +243,7 @@ void CGameConsole::CInstance::ClearBacklog() void CGameConsole::CInstance::UpdateBacklogTextAttributes() { // Pending backlog entries are not handled because they don't have text attributes yet. - for(CInstance::CBacklogEntry *pEntry = m_Backlog.First(); pEntry; pEntry = m_Backlog.Next(pEntry)) + for(CBacklogEntry *pEntry = m_Backlog.First(); pEntry; pEntry = m_Backlog.Next(pEntry)) { UpdateEntryTextAttributes(pEntry); } @@ -168,27 +251,29 @@ void CGameConsole::CInstance::UpdateBacklogTextAttributes() void CGameConsole::CInstance::PumpBacklogPending() { - std::vector vpEntries; { // We must ensure that no log messages are printed while owning // m_BacklogPendingLock or this will result in a dead lock. const CLockScope LockScopePending(m_BacklogPendingLock); - for(CInstance::CBacklogEntry *pPendingEntry = m_BacklogPending.First(); pPendingEntry; pPendingEntry = m_BacklogPending.Next(pPendingEntry)) + for(CBacklogEntry *pPendingEntry = m_BacklogPending.First(); pPendingEntry; pPendingEntry = m_BacklogPending.Next(pPendingEntry)) { const size_t EntrySize = sizeof(CBacklogEntry) + pPendingEntry->m_Length; CBacklogEntry *pEntry = m_Backlog.Allocate(EntrySize); mem_copy(pEntry, pPendingEntry, EntrySize); - vpEntries.push_back(pEntry); } m_BacklogPending.Init(); } - m_pGameConsole->UI()->MapScreen(); - for(CInstance::CBacklogEntry *pEntry : vpEntries) + // Update text attributes and count number of added lines + m_pGameConsole->Ui()->MapScreen(); + for(CBacklogEntry *pEntry = m_Backlog.First(); pEntry; pEntry = m_Backlog.Next(pEntry)) { - UpdateEntryTextAttributes(pEntry); - m_NewLineCounter += pEntry->m_LineCount; + if(pEntry->m_LineCount == -1) + { + UpdateEntryTextAttributes(pEntry); + m_NewLineCounter += pEntry->m_LineCount; + } } } @@ -205,6 +290,7 @@ void CGameConsole::CInstance::Reset() m_pCommandName = ""; m_pCommandHelp = ""; m_pCommandParams = ""; + m_CompletionArgumentPosition = 0; } void CGameConsole::CInstance::ExecuteLine(const char *pLine) @@ -235,7 +321,34 @@ void CGameConsole::CInstance::PossibleCommandsCompleteCallback(int Index, const { CGameConsole::CInstance *pInstance = (CGameConsole::CInstance *)pUser; if(pInstance->m_CompletionChosen == Index) - pInstance->m_Input.Set(pStr); + { + char aBefore[IConsole::CMDLINE_LENGTH]; + str_truncate(aBefore, sizeof(aBefore), pInstance->m_aCompletionBuffer, pInstance->m_CompletionCommandStart); + char aBuf[IConsole::CMDLINE_LENGTH]; + str_format(aBuf, sizeof(aBuf), "%s%s%s", aBefore, pStr, pInstance->m_aCompletionBuffer + pInstance->m_CompletionCommandEnd); + pInstance->m_Input.Set(aBuf); + pInstance->m_Input.SetCursorOffset(str_length(pStr) + pInstance->m_CompletionCommandStart); + } +} + +void CGameConsole::CInstance::GetCommand(const char *pInput, char (&aCmd)[IConsole::CMDLINE_LENGTH]) +{ + char aInput[IConsole::CMDLINE_LENGTH]; + str_copy(aInput, pInput); + m_CompletionCommandStart = 0; + m_CompletionCommandEnd = 0; + + char aaSeparators[][2] = {";", "\""}; + for(auto *pSeparator : aaSeparators) + { + int Start, End; + str_delimiters_around_offset(aInput + m_CompletionCommandStart, pSeparator, m_Input.GetCursorOffset() - m_CompletionCommandStart, &Start, &End); + m_CompletionCommandStart += Start; + m_CompletionCommandEnd = m_CompletionCommandStart + (End - Start); + aInput[m_CompletionCommandEnd] = '\0'; + } + + str_copy(aCmd, aInput + m_CompletionCommandStart, sizeof(aCmd)); } static void StrCopyUntilSpace(char *pDest, size_t DestSize, const char *pSrc) @@ -251,7 +364,7 @@ void CGameConsole::CInstance::PossibleArgumentsCompleteCallback(int Index, const { // get command char aBuf[IConsole::CMDLINE_LENGTH]; - StrCopyUntilSpace(aBuf, sizeof(aBuf), pInstance->GetString()); + str_copy(aBuf, pInstance->GetString(), pInstance->m_CompletionArgumentPosition); str_append(aBuf, " "); // append argument @@ -264,6 +377,10 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) { bool Handled = false; + // Don't allow input while the console is opening/closing + if(m_pGameConsole->m_ConsoleState == CONSOLE_OPENING || m_pGameConsole->m_ConsoleState == CONSOLE_CLOSING) + return Handled; + auto &&SelectNextSearchMatch = [&](int Direction) { if(!m_vSearchMatches.empty()) { @@ -278,6 +395,7 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) } }; + const int BacklogPrevLine = m_BacklogCurLine; if(Event.m_Flags & IInput::FLAG_PRESS) { if(Event.m_Key == KEY_RETURN || Event.m_Key == KEY_KP_ENTER) @@ -358,9 +476,12 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) if(!m_Searching) { + char aSearch[IConsole::CMDLINE_LENGTH]; + GetCommand(m_aCompletionBuffer, aSearch); + // command completion const bool UseTempCommands = m_Type == CGameConsole::CONSOLETYPE_REMOTE && m_pGameConsole->Client()->RconAuthed() && m_pGameConsole->Client()->UseTempRconCommands(); - int CompletionEnumerationCount = m_pGameConsole->m_pConsole->PossibleCommands(m_aCompletionBuffer, m_CompletionFlagmask, UseTempCommands); + int CompletionEnumerationCount = m_pGameConsole->m_pConsole->PossibleCommands(aSearch, m_CompletionFlagmask, UseTempCommands); if(m_Type == CGameConsole::CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed()) { if(CompletionEnumerationCount) @@ -368,7 +489,8 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) if(m_CompletionChosen == -1 && Direction < 0) m_CompletionChosen = 0; m_CompletionChosen = (m_CompletionChosen + Direction + CompletionEnumerationCount) % CompletionEnumerationCount; - m_pGameConsole->m_pConsole->PossibleCommands(m_aCompletionBuffer, m_CompletionFlagmask, UseTempCommands, PossibleCommandsCompleteCallback, this); + m_CompletionArgumentPosition = 0; + m_pGameConsole->m_pConsole->PossibleCommands(aSearch, m_CompletionFlagmask, UseTempCommands, PossibleCommandsCompleteCallback, this); } else if(m_CompletionChosen != -1) { @@ -378,22 +500,26 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) } // Argument completion - const bool TuningCompletion = IsTuningCommandPrefix(GetString()); - const bool SettingCompletion = IsSettingCommandPrefix(GetString()); - if(TuningCompletion) + const auto [CompletionType, CompletionPos] = ArgumentCompletion(GetString()); + if(CompletionType == EArgumentCompletionType::TUNE) CompletionEnumerationCount = PossibleTunings(m_aCompletionBufferArgument); - else if(SettingCompletion) + else if(CompletionType == EArgumentCompletionType::SETTING) CompletionEnumerationCount = m_pGameConsole->m_pConsole->PossibleCommands(m_aCompletionBufferArgument, m_CompletionFlagmask, UseTempCommands); + else if(CompletionType == EArgumentCompletionType::KEY) + CompletionEnumerationCount = PossibleKeys(m_aCompletionBufferArgument, m_pGameConsole->Input()); if(CompletionEnumerationCount) { if(m_CompletionChosenArgument == -1 && Direction < 0) m_CompletionChosenArgument = 0; m_CompletionChosenArgument = (m_CompletionChosenArgument + Direction + CompletionEnumerationCount) % CompletionEnumerationCount; - if(TuningCompletion && m_pGameConsole->Client()->RconAuthed() && m_Type == CGameConsole::CONSOLETYPE_REMOTE) + m_CompletionArgumentPosition = CompletionPos; + if(CompletionType == EArgumentCompletionType::TUNE) PossibleTunings(m_aCompletionBufferArgument, PossibleArgumentsCompleteCallback, this); - else if(SettingCompletion) + else if(CompletionType == EArgumentCompletionType::SETTING) m_pGameConsole->m_pConsole->PossibleCommands(m_aCompletionBufferArgument, m_CompletionFlagmask, UseTempCommands, PossibleArgumentsCompleteCallback, this); + else if(CompletionType == EArgumentCompletionType::KEY) + PossibleKeys(m_aCompletionBufferArgument, m_pGameConsole->Input(), PossibleArgumentsCompleteCallback, this); } else if(m_CompletionChosenArgument != -1) { @@ -406,55 +532,62 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) // Use Tab / Shift-Tab to cycle through search matches SelectNextSearchMatch(Direction); } + Handled = true; } else if(Event.m_Key == KEY_PAGEUP) { m_BacklogCurLine += GetLinesToScroll(-1, m_LinesRendered); - m_HasSelection = false; + Handled = true; } else if(Event.m_Key == KEY_PAGEDOWN) { - m_HasSelection = false; m_BacklogCurLine -= GetLinesToScroll(1, m_LinesRendered); - if(m_BacklogCurLine < 0) + { m_BacklogCurLine = 0; + } + Handled = true; } else if(Event.m_Key == KEY_MOUSE_WHEEL_UP) { m_BacklogCurLine += GetLinesToScroll(-1, 1); - m_HasSelection = false; + Handled = true; } else if(Event.m_Key == KEY_MOUSE_WHEEL_DOWN) { - m_HasSelection = false; --m_BacklogCurLine; if(m_BacklogCurLine < 0) + { m_BacklogCurLine = 0; + } + Handled = true; } // in order not to conflict with CLineInput's handling of Home/End only // react to it when the input is empty else if(Event.m_Key == KEY_HOME && m_Input.IsEmpty()) { - int Lines = GetLinesToScroll(-1, -1); - m_BacklogCurLine += Lines; + m_BacklogCurLine += GetLinesToScroll(-1, -1); m_BacklogLastActiveLine = m_BacklogCurLine; - m_HasSelection = false; + Handled = true; } else if(Event.m_Key == KEY_END && m_Input.IsEmpty()) { m_BacklogCurLine = 0; - m_HasSelection = false; + Handled = true; } - else if(Event.m_Key == KEY_F && m_pGameConsole->Input()->ModifierIsPressed() && Event.m_Flags & IInput::FLAG_PRESS) + else if(Event.m_Key == KEY_F && m_pGameConsole->Input()->ModifierIsPressed()) { m_Searching = !m_Searching; ClearSearch(); - Handled = true; } } + if(m_BacklogCurLine != BacklogPrevLine) + { + m_HasSelection = false; + } + if(!Handled) { Handled = m_Input.ProcessInput(Event); @@ -466,24 +599,24 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) { if(Event.m_Key != KEY_TAB && Event.m_Key != KEY_LSHIFT && Event.m_Key != KEY_RSHIFT) { - m_CompletionChosen = -1; - str_copy(m_aCompletionBuffer, m_Input.GetString()); + const char *pInputStr = m_Input.GetString(); - for(const auto *pCmd : gs_apTuningCommands) - { - if(str_startswith_nocase(m_Input.GetString(), pCmd)) - { - m_CompletionChosenArgument = -1; - str_copy(m_aCompletionBufferArgument, &m_Input.GetString()[str_length(pCmd)]); - } - } + m_CompletionChosen = -1; + str_copy(m_aCompletionBuffer, pInputStr); - for(const auto *pCmd : gs_apSettingCommands) + const auto [CompletionType, CompletionPos] = ArgumentCompletion(GetString()); + if(CompletionType != EArgumentCompletionType::NONE) { - if(str_startswith_nocase(m_Input.GetString(), pCmd)) + for(const auto &Entry : gs_aArgumentCompletionEntries) { - m_CompletionChosenArgument = -1; - str_copy(m_aCompletionBufferArgument, &m_Input.GetString()[str_length(pCmd)]); + if(Entry.m_Type != CompletionType) + continue; + const int Len = str_length(Entry.m_pCommandName); + if(str_comp_nocase_num(pInputStr, Entry.m_pCommandName, Len) == 0 && str_isspace(pInputStr[Len])) + { + m_CompletionChosenArgument = -1; + str_copy(m_aCompletionBufferArgument, &pInputStr[CompletionPos]); + } } } @@ -492,8 +625,11 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) // find the current command { + char aCmd[IConsole::CMDLINE_LENGTH]; + GetCommand(GetString(), aCmd); char aBuf[IConsole::CMDLINE_LENGTH]; - StrCopyUntilSpace(aBuf, sizeof(aBuf), GetString()); + StrCopyUntilSpace(aBuf, sizeof(aBuf), aCmd); + const IConsole::CCommandInfo *pCommand = m_pGameConsole->m_pConsole->GetCommandInfo(aBuf, m_CompletionFlagmask, m_Type != CGameConsole::CONSOLETYPE_LOCAL && m_pGameConsole->Client()->RconAuthed() && m_pGameConsole->Client()->UseTempRconCommands()); if(pCommand) @@ -573,11 +709,11 @@ void CGameConsole::CInstance::UpdateEntryTextAttributes(CBacklogEntry *pEntry) c { CTextCursor Cursor; m_pGameConsole->TextRender()->SetCursor(&Cursor, 0.0f, 0.0f, FONT_SIZE, 0); - Cursor.m_LineWidth = m_pGameConsole->UI()->Screen()->w - 10; + Cursor.m_LineWidth = m_pGameConsole->Ui()->Screen()->w - 10; Cursor.m_MaxLines = 10; Cursor.m_LineSpacing = LINE_SPACING; m_pGameConsole->TextRender()->TextEx(&Cursor, pEntry->m_aText, -1); - pEntry->m_YOffset = Cursor.Height() + LINE_SPACING; + pEntry->m_YOffset = Cursor.Height(); pEntry->m_LineCount = Cursor.m_LineCount; } @@ -613,8 +749,8 @@ void CGameConsole::CInstance::UpdateSearch() m_HasSelection = false; } - ITextRender *pTextRender = m_pGameConsole->UI()->TextRender(); - const int LineWidth = m_pGameConsole->UI()->Screen()->w - 10.0f; + ITextRender *pTextRender = m_pGameConsole->Ui()->TextRender(); + const int LineWidth = m_pGameConsole->Ui()->Screen()->w - 10.0f; CBacklogEntry *pEntry = m_Backlog.Last(); int EntryLine = 0, LineToScrollStart = 0, LineToScrollEnd = 0; @@ -694,6 +830,30 @@ void CGameConsole::CInstance::UpdateSearch() } } +void CGameConsole::CInstance::Dump() +{ + char aTimestamp[20]; + str_timestamp(aTimestamp, sizeof(aTimestamp)); + char aFilename[IO_MAX_PATH_LENGTH]; + str_format(aFilename, sizeof(aFilename), "dumps/%s_dump_%s.txt", m_pName, aTimestamp); + IOHANDLE File = m_pGameConsole->Storage()->OpenFile(aFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE); + if(File) + { + PumpBacklogPending(); + for(CInstance::CBacklogEntry *pEntry = m_Backlog.First(); pEntry; pEntry = m_Backlog.Next(pEntry)) + { + io_write(File, pEntry->m_aText, pEntry->m_Length); + io_write_newline(File); + } + io_close(File); + log_info("console", "%s contents were written to '%s'", m_pName, aFilename); + } + else + { + log_error("console", "Failed to open '%s'", aFilename); + } +} + CGameConsole::CGameConsole() : m_LocalConsole(CONSOLETYPE_LOCAL), m_RemoteConsole(CONSOLETYPE_REMOTE) { @@ -711,13 +871,18 @@ CGameConsole::~CGameConsole() m_pConsoleLogger->OnConsoleDeletion(); } -CGameConsole::CInstance *CGameConsole::CurrentConsole() +CGameConsole::CInstance *CGameConsole::ConsoleForType(int ConsoleType) { - if(m_ConsoleType == CONSOLETYPE_REMOTE) + if(ConsoleType == CONSOLETYPE_REMOTE) return &m_RemoteConsole; return &m_LocalConsole; } +CGameConsole::CInstance *CGameConsole::CurrentConsole() +{ + return ConsoleForType(m_ConsoleType); +} + void CGameConsole::OnReset() { m_RemoteConsole.Reset(); @@ -815,20 +980,17 @@ void CGameConsole::Prompt(char (&aPrompt)[32]) void CGameConsole::OnRender() { - CUIRect Screen = *UI()->Screen(); + CUIRect Screen = *Ui()->Screen(); CInstance *pConsole = CurrentConsole(); - float MaxConsoleHeight = Screen.h * 3 / 5.0f; - float ConsoleHeight; - - float Progress = (Client()->GlobalTime() - (m_StateChangeEnd - m_StateChangeDuration)) / m_StateChangeDuration / 2; + const float MaxConsoleHeight = Screen.h * 3 / 5.0f; + float Progress = (Client()->GlobalTime() - (m_StateChangeEnd - m_StateChangeDuration)) / m_StateChangeDuration; if(Progress >= 1.0f) { if(m_ConsoleState == CONSOLE_CLOSING) { m_ConsoleState = CONSOLE_CLOSED; - pConsole->m_Input.Deactivate(); pConsole->m_BacklogLastActiveLine = -1; } else if(m_ConsoleState == CONSOLE_OPENING) @@ -850,17 +1012,16 @@ void CGameConsole::OnRender() Input()->MouseModeAbsolute(); float ConsoleHeightScale; - if(m_ConsoleState == CONSOLE_OPENING) ConsoleHeightScale = ConsoleScaleFunc(Progress); else if(m_ConsoleState == CONSOLE_CLOSING) ConsoleHeightScale = ConsoleScaleFunc(1.0f - Progress); - else // if (console_state == CONSOLE_OPEN) + else // CONSOLE_OPEN ConsoleHeightScale = ConsoleScaleFunc(1.0f); - ConsoleHeight = ConsoleHeightScale * MaxConsoleHeight; + const float ConsoleHeight = ConsoleHeightScale * MaxConsoleHeight; - UI()->MapScreen(); + Ui()->MapScreen(); // do console shadow Graphics()->TextureClear(); @@ -876,37 +1037,16 @@ void CGameConsole::OnRender() Graphics()->QuadsEnd(); // do background - ColorRGBA ScLocalConsoleColor = color_cast(ColorHSVA(g_Config.m_ScLocalConsoleColor)); - ScLocalConsoleColor.a = g_Config.m_ClLocalConsolaAlpha / 100.0f; - ColorRGBA ScRemoteConsoleColor = color_cast(ColorHSVA(g_Config.m_ScRemoteConsoleColor)); - ScRemoteConsoleColor.a = g_Config.m_ClRemoteConsolaAlpha / 100.0f; - - ColorRGBA ScConsoleBarColor = color_cast(ColorHSVA(g_Config.m_ScConsoleBarColor)); + Graphics()->TextureSet(g_pData->m_aImages[IMAGE_CONSOLE_BG].m_Id); + Graphics()->QuadsBegin(); + Graphics()->SetColor(0.2f, 0.2f, 0.2f, 0.9f); + if(m_ConsoleType == CONSOLETYPE_REMOTE) + Graphics()->SetColor(0.4f, 0.2f, 0.2f, 0.9f); + Graphics()->QuadsSetSubset(0, -ConsoleHeight * 0.075f, Screen.w * 0.075f * 0.5f, 0); + QuadItem = IGraphics::CQuadItem(0, 0, Screen.w, ConsoleHeight); + Graphics()->QuadsDrawTL(&QuadItem, 1); + Graphics()->QuadsEnd(); - if(g_Config.m_ClConsoleSimple == 1) - { - Graphics()->TextureSet(g_pData->m_aImages[IMAGE_CONSOLE_SIMPLE].m_Id); - Graphics()->QuadsBegin(); - Graphics()->SetColor(ScLocalConsoleColor); - if(m_ConsoleType == CONSOLETYPE_REMOTE) - Graphics()->SetColor(ScRemoteConsoleColor); - Graphics()->QuadsSetSubset(0, -ConsoleHeight * 0.075f, Screen.w * 0.075f * 0.5f, 0); - QuadItem = IGraphics::CQuadItem(0, 0, Screen.w, ConsoleHeight); - Graphics()->QuadsDrawTL(&QuadItem, 1); - Graphics()->QuadsEnd(); - } - else - { - Graphics()->TextureSet(g_pData->m_aImages[IMAGE_CONSOLE_BG].m_Id); - Graphics()->QuadsBegin(); - Graphics()->SetColor(ScLocalConsoleColor); - if(m_ConsoleType == CONSOLETYPE_REMOTE) - Graphics()->SetColor(ScRemoteConsoleColor); - Graphics()->QuadsSetSubset(0, -ConsoleHeight * 0.075f, Screen.w * 0.075f * 0.5f, 0); - QuadItem = IGraphics::CQuadItem(0, 0, Screen.w, ConsoleHeight); - Graphics()->QuadsDrawTL(&QuadItem, 1); - Graphics()->QuadsEnd(); - } // do small bar shadow Graphics()->TextureClear(); Graphics()->QuadsBegin(); @@ -914,41 +1054,29 @@ void CGameConsole::OnRender() Array[1] = IGraphics::CColorVertex(1, 0, 0, 0, 0.0f); Array[2] = IGraphics::CColorVertex(2, 0, 0, 0, 0.25f); Array[3] = IGraphics::CColorVertex(3, 0, 0, 0, 0.25f); + Graphics()->SetColorVertex(Array, 4); QuadItem = IGraphics::CQuadItem(0, ConsoleHeight - 20, Screen.w, 10); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); // do the lower bar - if(g_Config.m_ClConsoleBarSimple == 1) - { - Graphics()->TextureSet(g_pData->m_aImages[IMAGE_CONSOLE_BAR_SIMPLE].m_Id); - Graphics()->QuadsBegin(); - Graphics()->SetColor(ScConsoleBarColor); - Graphics()->QuadsSetSubset(0, 0.1f, Screen.w * 0.015f, 1 - 0.1f); - QuadItem = IGraphics::CQuadItem(0, ConsoleHeight - 10.0f, Screen.w, 10.0f); - Graphics()->QuadsDrawTL(&QuadItem, 1); - Graphics()->QuadsEnd(); - } - else - { - Graphics()->TextureSet(g_pData->m_aImages[IMAGE_CONSOLE_BAR].m_Id); - Graphics()->QuadsBegin(); - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.9f); - Graphics()->QuadsSetSubset(0, 0.1f, Screen.w * 0.015f, 1 - 0.1f); - QuadItem = IGraphics::CQuadItem(0, ConsoleHeight - 10.0f, Screen.w, 10.0f); - Graphics()->QuadsDrawTL(&QuadItem, 1); - Graphics()->QuadsEnd(); - } - ConsoleHeight -= 22.0f; + Graphics()->TextureSet(g_pData->m_aImages[IMAGE_CONSOLE_BAR].m_Id); + Graphics()->QuadsBegin(); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.9f); + Graphics()->QuadsSetSubset(0, 0.1f, Screen.w * 0.015f, 1 - 0.1f); + QuadItem = IGraphics::CQuadItem(0, ConsoleHeight - 10.0f, Screen.w, 10.0f); + Graphics()->QuadsDrawTL(&QuadItem, 1); + Graphics()->QuadsEnd(); { // Get height of 1 line - float LineHeight = TextRender()->TextBoundingBox(FONT_SIZE, " ", -1, -1, LINE_SPACING).m_H + LINE_SPACING; + const float LineHeight = TextRender()->TextBoundingBox(FONT_SIZE, " ", -1, -1.0f, LINE_SPACING).m_H; + + const float RowHeight = FONT_SIZE * 1.5f; - float RowHeight = FONT_SIZE * 1.25f; float x = 3; - float y = ConsoleHeight - RowHeight - 5.0f; + float y = ConsoleHeight - RowHeight - 27.0f; const float InitialX = x; const float InitialY = y; @@ -962,24 +1090,48 @@ void CGameConsole::OnRender() TextRender()->TextEx(&Cursor, aPrompt); // check if mouse is pressed - if(!pConsole->m_MouseIsPress && Input()->NativeMousePressed(1)) + const vec2 WindowSize = vec2(Graphics()->WindowWidth(), Graphics()->WindowHeight()); + const vec2 ScreenSize = vec2(Screen.w, Screen.h); + Ui()->UpdateTouchState(m_TouchState); + const auto &&GetMousePosition = [&]() -> vec2 { + if(m_TouchState.m_PrimaryPressed) + { + return m_TouchState.m_PrimaryPosition * ScreenSize; + } + else + { + return Input()->NativeMousePos() / WindowSize * ScreenSize; + } + }; + if(!pConsole->m_MouseIsPress && (m_TouchState.m_PrimaryPressed || Input()->NativeMousePressed(1))) { pConsole->m_MouseIsPress = true; - ivec2 MousePress; - Input()->NativeMousePos(&MousePress.x, &MousePress.y); - pConsole->m_MousePress.x = (MousePress.x / (float)Graphics()->WindowWidth()) * Screen.w; - pConsole->m_MousePress.y = (MousePress.y / (float)Graphics()->WindowHeight()) * Screen.h; + pConsole->m_MousePress = GetMousePosition(); + } + if(pConsole->m_MouseIsPress && !m_TouchState.m_PrimaryPressed && !Input()->NativeMousePressed(1)) + { + pConsole->m_MouseIsPress = false; } if(pConsole->m_MouseIsPress) { - ivec2 MouseRelease; - Input()->NativeMousePos(&MouseRelease.x, &MouseRelease.y); - pConsole->m_MouseRelease.x = (MouseRelease.x / (float)Graphics()->WindowWidth()) * Screen.w; - pConsole->m_MouseRelease.y = (MouseRelease.y / (float)Graphics()->WindowHeight()) * Screen.h; + pConsole->m_MouseRelease = GetMousePosition(); } - if(pConsole->m_MouseIsPress && !Input()->NativeMousePressed(1)) + const float ScaledRowHeight = RowHeight / ScreenSize.y; + if(absolute(m_TouchState.m_ScrollAmount.y) >= ScaledRowHeight) { - pConsole->m_MouseIsPress = false; + if(m_TouchState.m_ScrollAmount.y > 0.0f) + { + pConsole->m_BacklogCurLine += pConsole->GetLinesToScroll(-1, 1); + m_TouchState.m_ScrollAmount.y -= ScaledRowHeight; + } + else + { + --pConsole->m_BacklogCurLine; + if(pConsole->m_BacklogCurLine < 0) + pConsole->m_BacklogCurLine = 0; + m_TouchState.m_ScrollAmount.y += ScaledRowHeight; + } + pConsole->m_HasSelection = false; } x = Cursor.m_X; @@ -1001,7 +1153,10 @@ void CGameConsole::OnRender() // render console input (wrap line) pConsole->m_Input.SetHidden(m_ConsoleType == CONSOLETYPE_REMOTE && Client()->State() == IClient::STATE_ONLINE && !Client()->RconAuthed() && (pConsole->m_UserGot || !pConsole->m_UsernameReq)); - pConsole->m_Input.Activate(EInputPriority::CONSOLE); // Ensure that the input is active + if(m_ConsoleState == CONSOLE_OPEN) + { + pConsole->m_Input.Activate(EInputPriority::CONSOLE); // Ensure that the input is active + } const CUIRect InputCursorRect = {x, y + FONT_SIZE, 0.0f, 0.0f}; const bool WasChanged = pConsole->m_Input.WasChanged(); const bool WasCursorChanged = pConsole->m_Input.WasCursorChanged(); @@ -1034,7 +1189,10 @@ void CGameConsole::OnRender() Info.m_pOffsetChange = &pConsole->m_CompletionRenderOffsetChange; Info.m_Width = Screen.w; Info.m_TotalWidth = 0.0f; - Info.m_pCurrentCmd = pConsole->m_aCompletionBuffer; + char aCmd[IConsole::CMDLINE_LENGTH]; + pConsole->GetCommand(pConsole->m_aCompletionBuffer, aCmd); + Info.m_pCurrentCmd = aCmd; + TextRender()->SetCursor(&Info.m_Cursor, InitialX - Info.m_Offset, InitialY + RowHeight + 2.0f, FONT_SIZE, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END); Info.m_Cursor.m_LineWidth = std::numeric_limits::max(); const int NumCommands = m_pConsole->PossibleCommands(Info.m_pCurrentCmd, pConsole->m_CompletionFlagmask, m_ConsoleType != CGameConsole::CONSOLETYPE_LOCAL && Client()->RconAuthed() && Client()->UseTempRconCommands(), PossibleCommandsRenderCallback, &Info); @@ -1042,18 +1200,19 @@ void CGameConsole::OnRender() if(NumCommands <= 0 && pConsole->m_IsCommand) { - const bool TuningCompletion = IsTuningCommandPrefix(Info.m_pCurrentCmd); - const bool SettingCompletion = IsSettingCommandPrefix(Info.m_pCurrentCmd); + const auto [CompletionType, _] = ArgumentCompletion(Info.m_pCurrentCmd); int NumArguments = 0; - if(TuningCompletion || SettingCompletion) + if(CompletionType != EArgumentCompletionType::NONE) { Info.m_WantedCompletion = pConsole->m_CompletionChosenArgument; Info.m_TotalWidth = 0.0f; Info.m_pCurrentCmd = pConsole->m_aCompletionBufferArgument; - if(TuningCompletion) + if(CompletionType == EArgumentCompletionType::TUNE) NumArguments = PossibleTunings(Info.m_pCurrentCmd, PossibleCommandsRenderCallback, &Info); - else if(SettingCompletion) + else if(CompletionType == EArgumentCompletionType::SETTING) NumArguments = m_pConsole->PossibleCommands(Info.m_pCurrentCmd, pConsole->m_CompletionFlagmask, m_ConsoleType != CGameConsole::CONSOLETYPE_LOCAL && Client()->RconAuthed() && Client()->UseTempRconCommands(), PossibleCommandsRenderCallback, &Info); + else if(CompletionType == EArgumentCompletionType::KEY) + NumArguments = PossibleKeys(Info.m_pCurrentCmd, Input(), PossibleCommandsRenderCallback, &Info); pConsole->m_CompletionRenderOffset = Info.m_Offset; } @@ -1068,7 +1227,7 @@ void CGameConsole::OnRender() } } - UI()->DoSmoothScrollLogic(&pConsole->m_CompletionRenderOffset, &pConsole->m_CompletionRenderOffsetChange, Info.m_Width, Info.m_TotalWidth); + Ui()->DoSmoothScrollLogic(&pConsole->m_CompletionRenderOffset, &pConsole->m_CompletionRenderOffsetChange, Info.m_Width, Info.m_TotalWidth); } else if(pConsole->m_Searching && !pConsole->m_Input.IsEmpty()) { // Render current match and match count @@ -1088,9 +1247,20 @@ void CGameConsole::OnRender() } pConsole->PumpBacklogPending(); - if(pConsole->m_NewLineCounter > 0) + if(pConsole->m_NewLineCounter != 0) + { pConsole->UpdateSearch(); + // keep scroll position when new entries are printed. + if(pConsole->m_BacklogCurLine != 0 || pConsole->m_HasSelection) + { + pConsole->m_BacklogCurLine += pConsole->m_NewLineCounter; + pConsole->m_BacklogLastActiveLine += pConsole->m_NewLineCounter; + } + if(pConsole->m_NewLineCounter < 0) + pConsole->m_NewLineCounter = 0; + } + // render console log (current entry, status, wrap lines) CInstance::CBacklogEntry *pEntry = pConsole->m_Backlog.Last(); float OffsetY = 0.0f; @@ -1108,11 +1278,9 @@ void CGameConsole::OnRender() const float XScale = Graphics()->ScreenWidth() / Screen.w; const float YScale = Graphics()->ScreenHeight() / Screen.h; - float CalcOffsetY = 0; - while(y - (CalcOffsetY + LineHeight) > RowHeight) - CalcOffsetY += LineHeight; - float ClipStartY = y - CalcOffsetY; - Graphics()->ClipEnable(0, ClipStartY * YScale, Screen.w * XScale, y * YScale - ClipStartY * YScale); + const float CalcOffsetY = LineHeight * std::floor((y - RowHeight) / LineHeight); + const float ClipStartY = (y - CalcOffsetY) * YScale; + Graphics()->ClipEnable(0, ClipStartY, Screen.w * XScale, y * YScale - ClipStartY); while(pEntry) { @@ -1120,17 +1288,6 @@ void CGameConsole::OnRender() pConsole->UpdateEntryTextAttributes(pEntry); LineNum += pEntry->m_LineCount; - while(pConsole->m_NewLineCounter > 0) - { - --pConsole->m_NewLineCounter; - - // keep scroll position when new entries are printed. - if(pConsole->m_BacklogCurLine != 0) - { - pConsole->m_BacklogCurLine++; - pConsole->m_BacklogLastActiveLine++; - } - } if(LineNum < pConsole->m_BacklogLastActiveLine) { SkippedLines += pEntry->m_LineCount; @@ -1141,28 +1298,27 @@ void CGameConsole::OnRender() if(First) { - int Diff = pConsole->m_BacklogLastActiveLine - SkippedLines; - OffsetY -= Diff * LineHeight - LINE_SPACING; + OffsetY -= (pConsole->m_BacklogLastActiveLine - SkippedLines) * LineHeight; } - float LocalOffsetY = OffsetY + pEntry->m_YOffset / (float)pEntry->m_LineCount; + const float LocalOffsetY = OffsetY + pEntry->m_YOffset / (float)pEntry->m_LineCount; OffsetY += pEntry->m_YOffset; - if((pConsole->m_HasSelection || pConsole->m_MouseIsPress) && pConsole->m_NewLineCounter > 0) + // Only apply offset if we do not keep scroll position (m_BacklogCurLine == 0) + if((pConsole->m_HasSelection || pConsole->m_MouseIsPress) && pConsole->m_NewLineCounter > 0 && pConsole->m_BacklogCurLine == 0) { - float MouseExtraOff = pEntry->m_YOffset; - pConsole->m_MousePress.y -= MouseExtraOff; + pConsole->m_MousePress.y -= pEntry->m_YOffset; if(!pConsole->m_MouseIsPress) - pConsole->m_MouseRelease.y -= MouseExtraOff; + pConsole->m_MouseRelease.y -= pEntry->m_YOffset; } // stop rendering when lines reach the top - bool Outside = y - OffsetY <= RowHeight; - int CanRenderOneLine = y - LocalOffsetY > RowHeight; + const bool Outside = y - OffsetY <= RowHeight; + const bool CanRenderOneLine = y - LocalOffsetY > RowHeight; if(Outside && !CanRenderOneLine) break; - int LinesNotRendered = pEntry->m_LineCount - minimum((int)std::floor((y - LocalOffsetY) / RowHeight), pEntry->m_LineCount); + const int LinesNotRendered = pEntry->m_LineCount - minimum((int)std::floor((y - LocalOffsetY) / RowHeight), pEntry->m_LineCount); pConsole->m_LinesRendered -= LinesNotRendered; TextRender()->SetCursor(&Cursor, 0.0f, y - OffsetY, FONT_SIZE, TEXTFLAG_RENDER); @@ -1213,6 +1369,9 @@ void CGameConsole::OnRender() pConsole->m_HasSelection = true; } + if(pConsole->m_NewLineCounter > 0) // Decrease by the entry line count since we can have multiline entries + pConsole->m_NewLineCounter -= pEntry->m_LineCount; + pEntry = pConsole->m_Backlog.Prev(pEntry); // reset color @@ -1223,6 +1382,14 @@ void CGameConsole::OnRender() break; } + // Make sure to reset m_NewLineCounter when we are done drawing + // This is because otherwise, if many entries are printed at once while console is + // hidden, m_NewLineCounter will always be > 0 since the console won't be able to render + // them all, thus wont be able to decrease m_NewLineCounter to 0. + // This leads to an infinite increase of m_BacklogCurLine and m_BacklogLastActiveLine + // when we want to keep scroll position. + pConsole->m_NewLineCounter = 0; + Graphics()->ClipDisable(); pConsole->m_BacklogLastActiveLine = pConsole->m_BacklogCurLine; @@ -1243,10 +1410,27 @@ void CGameConsole::OnRender() str_format(aBuf, sizeof(aBuf), Localize("Lines %d - %d (%s)"), pConsole->m_BacklogCurLine + 1, pConsole->m_BacklogCurLine + pConsole->m_LinesRendered, pConsole->m_BacklogCurLine != 0 ? Localize("Locked") : Localize("Following")); TextRender()->Text(10.0f, FONT_SIZE / 2.f, FONT_SIZE, aBuf); + if(m_ConsoleType == CONSOLETYPE_REMOTE && Client()->ReceivingRconCommands()) + { + float Percentage = Client()->GotRconCommandsPercentage(); + SProgressSpinnerProperties ProgressProps; + ProgressProps.m_Progress = Percentage; + Ui()->RenderProgressSpinner(vec2(Screen.w / 4.0f + FONT_SIZE / 2.f, FONT_SIZE), FONT_SIZE / 2.f, ProgressProps); + + char aLoading[128]; + str_copy(aLoading, Localize("Loading commands…")); + if(Percentage > 0) + { + char aPercentage[8]; + str_format(aPercentage, sizeof(aPercentage), " %d%%", (int)(Percentage * 100)); + str_append(aLoading, aPercentage); + } + TextRender()->Text(Screen.w / 4.0f + FONT_SIZE + 2.0f, FONT_SIZE / 2.f, FONT_SIZE, aLoading); + } + // render version str_copy(aBuf, "v" GAME_VERSION " on " CONF_PLATFORM_STRING " " CONF_ARCH_STRING); - float Width = TextRender()->TextWidth(FONT_SIZE, aBuf, -1, -1.0f); - TextRender()->Text(Screen.w - Width - 10.0f, FONT_SIZE / 2.f, FONT_SIZE, aBuf); + TextRender()->Text(Screen.w - TextRender()->TextWidth(FONT_SIZE, aBuf) - 10.0f, FONT_SIZE / 2.f, FONT_SIZE, aBuf); } } @@ -1295,13 +1479,14 @@ void CGameConsole::Toggle(int Type) if(m_ConsoleState == CONSOLE_CLOSED || m_ConsoleState == CONSOLE_CLOSING) { - UI()->SetEnabled(false); + Ui()->SetEnabled(false); m_ConsoleState = CONSOLE_OPENING; } else { + ConsoleForType(Type)->m_Input.Deactivate(); Input()->MouseModeRelative(); - UI()->SetEnabled(true); + Ui()->SetEnabled(true); m_pClient->OnRelease(); m_ConsoleState = CONSOLE_CLOSING; } @@ -1309,32 +1494,6 @@ void CGameConsole::Toggle(int Type) m_ConsoleType = Type; } -void CGameConsole::Dump(int Type) -{ - CInstance *pConsole = Type == CONSOLETYPE_REMOTE ? &m_RemoteConsole : &m_LocalConsole; - char aBuf[IO_MAX_PATH_LENGTH + 64]; - char aFilename[IO_MAX_PATH_LENGTH]; - str_timestamp(aBuf, sizeof(aBuf)); - str_format(aFilename, sizeof(aFilename), "dumps/%s_dump_%s.txt", pConsole->m_pName, aBuf); - IOHANDLE File = Storage()->OpenFile(aFilename, IOFLAG_WRITE, IStorage::TYPE_SAVE); - if(File) - { - pConsole->PumpBacklogPending(); - for(CInstance::CBacklogEntry *pEntry = pConsole->m_Backlog.First(); pEntry; pEntry = pConsole->m_Backlog.Next(pEntry)) - { - io_write(File, pEntry->m_aText, pEntry->m_Length); - io_write_newline(File); - } - io_close(File); - str_format(aBuf, sizeof(aBuf), "%s contents were written to '%s'", pConsole->m_pName, aFilename); - } - else - { - str_format(aBuf, sizeof(aBuf), "Failed to open '%s'", aFilename); - } - Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "console", aBuf); -} - void CGameConsole::ConToggleLocalConsole(IConsole::IResult *pResult, void *pUserData) { ((CGameConsole *)pUserData)->Toggle(CONSOLETYPE_LOCAL); @@ -1357,12 +1516,12 @@ void CGameConsole::ConClearRemoteConsole(IConsole::IResult *pResult, void *pUser void CGameConsole::ConDumpLocalConsole(IConsole::IResult *pResult, void *pUserData) { - ((CGameConsole *)pUserData)->Dump(CONSOLETYPE_LOCAL); + ((CGameConsole *)pUserData)->m_LocalConsole.Dump(); } void CGameConsole::ConDumpRemoteConsole(IConsole::IResult *pResult, void *pUserData) { - ((CGameConsole *)pUserData)->Dump(CONSOLETYPE_REMOTE); + ((CGameConsole *)pUserData)->m_RemoteConsole.Dump(); } void CGameConsole::ConConsolePageUp(IConsole::IResult *pResult, void *pUserData) @@ -1442,7 +1601,7 @@ void CGameConsole::OnInit() void CGameConsole::OnStateChange(int NewState, int OldState) { - if(OldState == IClient::STATE_ONLINE && NewState < IClient::STATE_LOADING) + if(OldState <= IClient::STATE_ONLINE && NewState == IClient::STATE_OFFLINE) { m_RemoteConsole.m_UserGot = false; m_RemoteConsole.m_aUser[0] = '\0'; diff --git a/src/game/client/components/console.h b/src/game/client/components/console.h index 66e3c90e0b..e8ab47900f 100644 --- a/src/game/client/components/console.h +++ b/src/game/client/components/console.h @@ -10,6 +10,7 @@ #include #include +#include enum { @@ -68,6 +69,9 @@ class CGameConsole : public CComponent int m_CompletionFlagmask; float m_CompletionRenderOffset; float m_CompletionRenderOffsetChange; + int m_CompletionArgumentPosition; + int m_CompletionCommandStart = 0; + int m_CompletionCommandEnd = 0; char m_aUser[32]; bool m_UserGot; @@ -109,8 +113,22 @@ class CGameConsole : public CComponent int GetLinesToScroll(int Direction, int LinesToScroll); void ScrollToCenter(int StartLine, int EndLine); void ClearSearch(); + void Dump() REQUIRES(!m_BacklogPendingLock); const char *GetString() const { return m_Input.GetString(); } + /** + * Gets the command at the current cursor including surrounding spaces. + * Commands are split by semicolons. + * + * So if the current console input is for example "hello; world ;foo" + * ^ + * and the cursor is here -------------/ + * The result would be " world " + * + * @param pInput the console input line + * @param aCmd the command the cursor is at + */ + void GetCommand(const char *pInput, char (&aCmd)[IConsole::CMDLINE_LENGTH]); static void PossibleCommandsCompleteCallback(int Index, const char *pStr, void *pUser); static void PossibleArgumentsCompleteCallback(int Index, const char *pStr, void *pUser); @@ -128,6 +146,7 @@ class CGameConsole : public CComponent CInstance m_LocalConsole; CInstance m_RemoteConsole; + CInstance *ConsoleForType(int ConsoleType); CInstance *CurrentConsole(); int m_ConsoleType; @@ -136,13 +155,11 @@ class CGameConsole : public CComponent float m_StateChangeDuration; bool m_WantsSelectionCopy = false; + CUi::CTouchState m_TouchState; static const ColorRGBA ms_SearchHighlightColor; static const ColorRGBA ms_SearchSelectedColor; - void Toggle(int Type); - void Dump(int Type); - static void PossibleCommandsRenderCallback(int Index, const char *pStr, void *pUser); static void ConToggleLocalConsole(IConsole::IResult *pResult, void *pUserData); static void ConToggleRemoteConsole(IConsole::IResult *pResult, void *pUserData); @@ -177,6 +194,7 @@ class CGameConsole : public CComponent virtual bool OnInput(const IInput::CEvent &Event) override; void Prompt(char (&aPrompt)[32]); + void Toggle(int Type); bool IsClosed() { return m_ConsoleState == CONSOLE_CLOSED; } }; #endif diff --git a/src/game/client/components/controls.cpp b/src/game/client/components/controls.cpp index f72cce98f7..fffb564d89 100644 --- a/src/game/client/components/controls.cpp +++ b/src/game/client/components/controls.cpp @@ -2,6 +2,7 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include +#include #include #include @@ -18,9 +19,6 @@ CControls::CControls() { mem_zero(&m_aLastData, sizeof(m_aLastData)); - m_LastDummy = 0; - m_OtherFire = 0; - mem_zero(m_aMousePos, sizeof(m_aMousePos)); mem_zero(m_aMousePosOnAction, sizeof(m_aMousePosOnAction)); mem_zero(m_aTargetPos, sizeof(m_aTargetPos)); @@ -33,13 +31,13 @@ void CControls::OnReset() for(int &AmmoCount : m_aAmmoCount) AmmoCount = 0; - m_OldMouseX = m_OldMouseY = 0.0f; + + m_LastSendTime = 0; } void CControls::ResetInput(int Dummy) { m_aLastData[Dummy].m_Direction = 0; - //m_aLastData[Dummy].m_Hook = 0; // simulate releasing the fire button if((m_aLastData[Dummy].m_Fire & 1) != 0) m_aLastData[Dummy].m_Fire++; @@ -51,11 +49,6 @@ void CControls::ResetInput(int Dummy) m_aInputDirectionRight[Dummy] = 0; } -void CControls::OnRelease() -{ - //OnReset(); -} - void CControls::OnPlayerDeath() { for(int &AmmoCount : m_aAmmoCount) @@ -65,8 +58,7 @@ void CControls::OnPlayerDeath() struct CInputState { CControls *m_pControls; - int *m_pVariable1; - int *m_pVariable2; + int *m_apVariables[NUM_DUMMIES]; }; static void ConKeyInputState(IConsole::IResult *pResult, void *pUserData) @@ -76,10 +68,7 @@ static void ConKeyInputState(IConsole::IResult *pResult, void *pUserData) if(pState->m_pControls->GameClient()->m_GameInfo.m_BugDDRaceInput && pState->m_pControls->GameClient()->m_Snap.m_SpecInfo.m_Active) return; - if(g_Config.m_ClDummy) - *pState->m_pVariable2 = pResult->GetInteger(0); - else - *pState->m_pVariable1 = pResult->GetInteger(0); + *pState->m_apVariables[g_Config.m_ClDummy] = pResult->GetInteger(0); } static void ConKeyInputCounter(IConsole::IResult *pResult, void *pUserData) @@ -89,22 +78,16 @@ static void ConKeyInputCounter(IConsole::IResult *pResult, void *pUserData) if(pState->m_pControls->GameClient()->m_GameInfo.m_BugDDRaceInput && pState->m_pControls->GameClient()->m_Snap.m_SpecInfo.m_Active) return; - int *v; - if(g_Config.m_ClDummy) - v = pState->m_pVariable2; - else - v = pState->m_pVariable1; - - if(((*v) & 1) != pResult->GetInteger(0)) - (*v)++; - *v &= INPUT_STATE_MASK; + int *pVariable = pState->m_apVariables[g_Config.m_ClDummy]; + if(((*pVariable) & 1) != pResult->GetInteger(0)) + (*pVariable)++; + *pVariable &= INPUT_STATE_MASK; } struct CInputSet { CControls *m_pControls; - int *m_pVariable1; - int *m_pVariable2; + int *m_apVariables[NUM_DUMMIES]; int m_Value; }; @@ -113,10 +96,7 @@ static void ConKeyInputSet(IConsole::IResult *pResult, void *pUserData) CInputSet *pSet = (CInputSet *)pUserData; if(pResult->GetInteger(0)) { - if(g_Config.m_ClDummy) - *pSet->m_pVariable2 = pSet->m_Value; - else - *pSet->m_pVariable1 = pSet->m_Value; + *pSet->m_apVariables[g_Config.m_ClDummy] = pSet->m_Value; } } @@ -131,58 +111,58 @@ void CControls::OnConsoleInit() { // game commands { - static CInputState s_State = {this, &m_aInputDirectionLeft[0], &m_aInputDirectionLeft[1]}; - Console()->Register("+left", "", CFGFLAG_CLIENT, ConKeyInputState, (void *)&s_State, "Move left"); + static CInputState s_State = {this, {&m_aInputDirectionLeft[0], &m_aInputDirectionLeft[1]}}; + Console()->Register("+left", "", CFGFLAG_CLIENT, ConKeyInputState, &s_State, "Move left"); } { - static CInputState s_State = {this, &m_aInputDirectionRight[0], &m_aInputDirectionRight[1]}; - Console()->Register("+right", "", CFGFLAG_CLIENT, ConKeyInputState, (void *)&s_State, "Move right"); + static CInputState s_State = {this, {&m_aInputDirectionRight[0], &m_aInputDirectionRight[1]}}; + Console()->Register("+right", "", CFGFLAG_CLIENT, ConKeyInputState, &s_State, "Move right"); } { - static CInputState s_State = {this, &m_aInputData[0].m_Jump, &m_aInputData[1].m_Jump}; - Console()->Register("+jump", "", CFGFLAG_CLIENT, ConKeyInputState, (void *)&s_State, "Jump"); + static CInputState s_State = {this, {&m_aInputData[0].m_Jump, &m_aInputData[1].m_Jump}}; + Console()->Register("+jump", "", CFGFLAG_CLIENT, ConKeyInputState, &s_State, "Jump"); } { - static CInputState s_State = {this, &m_aInputData[0].m_Hook, &m_aInputData[1].m_Hook}; - Console()->Register("+hook", "", CFGFLAG_CLIENT, ConKeyInputState, (void *)&s_State, "Hook"); + static CInputState s_State = {this, {&m_aInputData[0].m_Hook, &m_aInputData[1].m_Hook}}; + Console()->Register("+hook", "", CFGFLAG_CLIENT, ConKeyInputState, &s_State, "Hook"); } { - static CInputState s_State = {this, &m_aInputData[0].m_Fire, &m_aInputData[1].m_Fire}; - Console()->Register("+fire", "", CFGFLAG_CLIENT, ConKeyInputCounter, (void *)&s_State, "Fire"); + static CInputState s_State = {this, {&m_aInputData[0].m_Fire, &m_aInputData[1].m_Fire}}; + Console()->Register("+fire", "", CFGFLAG_CLIENT, ConKeyInputCounter, &s_State, "Fire"); } { - static CInputState s_State = {this, &m_aShowHookColl[0], &m_aShowHookColl[1]}; - Console()->Register("+showhookcoll", "", CFGFLAG_CLIENT, ConKeyInputState, (void *)&s_State, "Show Hook Collision"); + static CInputState s_State = {this, {&m_aShowHookColl[0], &m_aShowHookColl[1]}}; + Console()->Register("+showhookcoll", "", CFGFLAG_CLIENT, ConKeyInputState, &s_State, "Show Hook Collision"); } { - static CInputSet s_Set = {this, &m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon, 1}; - Console()->Register("+weapon1", "", CFGFLAG_CLIENT, ConKeyInputSet, (void *)&s_Set, "Switch to hammer"); + static CInputSet s_Set = {this, {&m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon}, 1}; + Console()->Register("+weapon1", "", CFGFLAG_CLIENT, ConKeyInputSet, &s_Set, "Switch to hammer"); } { - static CInputSet s_Set = {this, &m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon, 2}; - Console()->Register("+weapon2", "", CFGFLAG_CLIENT, ConKeyInputSet, (void *)&s_Set, "Switch to gun"); + static CInputSet s_Set = {this, {&m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon}, 2}; + Console()->Register("+weapon2", "", CFGFLAG_CLIENT, ConKeyInputSet, &s_Set, "Switch to gun"); } { - static CInputSet s_Set = {this, &m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon, 3}; - Console()->Register("+weapon3", "", CFGFLAG_CLIENT, ConKeyInputSet, (void *)&s_Set, "Switch to shotgun"); + static CInputSet s_Set = {this, {&m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon}, 3}; + Console()->Register("+weapon3", "", CFGFLAG_CLIENT, ConKeyInputSet, &s_Set, "Switch to shotgun"); } { - static CInputSet s_Set = {this, &m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon, 4}; - Console()->Register("+weapon4", "", CFGFLAG_CLIENT, ConKeyInputSet, (void *)&s_Set, "Switch to grenade"); + static CInputSet s_Set = {this, {&m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon}, 4}; + Console()->Register("+weapon4", "", CFGFLAG_CLIENT, ConKeyInputSet, &s_Set, "Switch to grenade"); } { - static CInputSet s_Set = {this, &m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon, 5}; - Console()->Register("+weapon5", "", CFGFLAG_CLIENT, ConKeyInputSet, (void *)&s_Set, "Switch to laser"); + static CInputSet s_Set = {this, {&m_aInputData[0].m_WantedWeapon, &m_aInputData[1].m_WantedWeapon}, 5}; + Console()->Register("+weapon5", "", CFGFLAG_CLIENT, ConKeyInputSet, &s_Set, "Switch to laser"); } { - static CInputSet s_Set = {this, &m_aInputData[0].m_NextWeapon, &m_aInputData[1].m_NextWeapon, 0}; - Console()->Register("+nextweapon", "", CFGFLAG_CLIENT, ConKeyInputNextPrevWeapon, (void *)&s_Set, "Switch to next weapon"); + static CInputSet s_Set = {this, {&m_aInputData[0].m_NextWeapon, &m_aInputData[1].m_NextWeapon}, 0}; + Console()->Register("+nextweapon", "", CFGFLAG_CLIENT, ConKeyInputNextPrevWeapon, &s_Set, "Switch to next weapon"); } { - static CInputSet s_Set = {this, &m_aInputData[0].m_PrevWeapon, &m_aInputData[1].m_PrevWeapon, 0}; - Console()->Register("+prevweapon", "", CFGFLAG_CLIENT, ConKeyInputNextPrevWeapon, (void *)&s_Set, "Switch to previous weapon"); + static CInputSet s_Set = {this, {&m_aInputData[0].m_PrevWeapon, &m_aInputData[1].m_PrevWeapon}, 0}; + Console()->Register("+prevweapon", "", CFGFLAG_CLIENT, ConKeyInputNextPrevWeapon, &s_Set, "Switch to previous weapon"); } } @@ -200,9 +180,6 @@ void CControls::OnMessage(int Msg, void *pRawMsg) int CControls::SnapInput(int *pData) { - static int64_t LastSendTime = 0; - bool Send = false; - // update player state if(m_pClient->m_Chat.IsActive()) m_aInputData[g_Config.m_ClDummy].m_PlayerFlags = PLAYERFLAG_CHATTING; @@ -214,11 +191,10 @@ int CControls::SnapInput(int *pData) if(m_pClient->m_Scoreboard.Active()) m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_SCOREBOARD; - if(m_pClient->m_Controls.m_aShowHookColl[g_Config.m_ClDummy]) + if(m_pClient->m_Controls.m_aShowHookColl[g_Config.m_ClDummy] && Client()->ServerCapAnyPlayerFlag()) m_aInputData[g_Config.m_ClDummy].m_PlayerFlags |= PLAYERFLAG_AIM; - if(m_aLastData[g_Config.m_ClDummy].m_PlayerFlags != m_aInputData[g_Config.m_ClDummy].m_PlayerFlags) - Send = true; + bool Send = m_aLastData[g_Config.m_ClDummy].m_PlayerFlags != m_aInputData[g_Config.m_ClDummy].m_PlayerFlags; m_aLastData[g_Config.m_ClDummy].m_PlayerFlags = m_aInputData[g_Config.m_ClDummy].m_PlayerFlags; @@ -235,22 +211,26 @@ int CControls::SnapInput(int *pData) m_aInputData[g_Config.m_ClDummy].m_TargetX = (int)m_aMousePos[g_Config.m_ClDummy].x; m_aInputData[g_Config.m_ClDummy].m_TargetY = (int)m_aMousePos[g_Config.m_ClDummy].y; + // scale TargetX, TargetY by zoom. + if(!m_pClient->m_Snap.m_SpecInfo.m_Active) + { + m_aInputData[g_Config.m_ClDummy].m_TargetX *= m_pClient->m_Camera.m_Zoom; + m_aInputData[g_Config.m_ClDummy].m_TargetY *= m_pClient->m_Camera.m_Zoom; + } + // send once a second just to be sure - if(time_get() > LastSendTime + time_freq()) - Send = true; + Send = Send || time_get() > m_LastSendTime + time_freq(); } else { m_aInputData[g_Config.m_ClDummy].m_TargetX = (int)m_aMousePos[g_Config.m_ClDummy].x; m_aInputData[g_Config.m_ClDummy].m_TargetY = (int)m_aMousePos[g_Config.m_ClDummy].y; - if(g_Config.m_ClSubTickAiming && m_aMousePosOnAction[g_Config.m_ClDummy].x != 0 && m_aMousePosOnAction[g_Config.m_ClDummy].y != 0) + if(g_Config.m_ClSubTickAiming && m_aMousePosOnAction[g_Config.m_ClDummy] != vec2(0.0f, 0.0f)) { m_aInputData[g_Config.m_ClDummy].m_TargetX = (int)m_aMousePosOnAction[g_Config.m_ClDummy].x; m_aInputData[g_Config.m_ClDummy].m_TargetY = (int)m_aMousePosOnAction[g_Config.m_ClDummy].y; - - m_aMousePosOnAction[g_Config.m_ClDummy].x = 0; - m_aMousePosOnAction[g_Config.m_ClDummy].y = 0; + m_aMousePosOnAction[g_Config.m_ClDummy] = vec2(0.0f, 0.0f); } if(!m_aInputData[g_Config.m_ClDummy].m_TargetX && !m_aInputData[g_Config.m_ClDummy].m_TargetY) @@ -285,7 +265,9 @@ int CControls::SnapInput(int *pData) pDummyInput->m_TargetY = m_aInputData[g_Config.m_ClDummy].m_TargetY; pDummyInput->m_WantedWeapon = m_aInputData[g_Config.m_ClDummy].m_WantedWeapon; - pDummyInput->m_Fire += m_aInputData[g_Config.m_ClDummy].m_Fire - m_aLastData[g_Config.m_ClDummy].m_Fire; + if(!g_Config.m_ClDummyControl) + pDummyInput->m_Fire += m_aInputData[g_Config.m_ClDummy].m_Fire - m_aLastData[g_Config.m_ClDummy].m_Fire; + pDummyInput->m_NextWeapon += m_aInputData[g_Config.m_ClDummy].m_NextWeapon - m_aLastData[g_Config.m_ClDummy].m_NextWeapon; pDummyInput->m_PrevWeapon += m_aInputData[g_Config.m_ClDummy].m_PrevWeapon - m_aLastData[g_Config.m_ClDummy].m_PrevWeapon; @@ -296,7 +278,12 @@ int CControls::SnapInput(int *pData) { CNetObj_PlayerInput *pDummyInput = &m_pClient->m_DummyInput; pDummyInput->m_Jump = g_Config.m_ClDummyJump; - pDummyInput->m_Fire = g_Config.m_ClDummyFire; + + if(g_Config.m_ClDummyFire) + pDummyInput->m_Fire = g_Config.m_ClDummyFire; + else if((pDummyInput->m_Fire & 1) != 0) + pDummyInput->m_Fire++; + pDummyInput->m_Hook = g_Config.m_ClDummyHook; } @@ -317,27 +304,15 @@ int CControls::SnapInput(int *pData) } #endif // check if we need to send input - if(m_aInputData[g_Config.m_ClDummy].m_Direction != m_aLastData[g_Config.m_ClDummy].m_Direction) - Send = true; - else if(m_aInputData[g_Config.m_ClDummy].m_Jump != m_aLastData[g_Config.m_ClDummy].m_Jump) - Send = true; - else if(m_aInputData[g_Config.m_ClDummy].m_Fire != m_aLastData[g_Config.m_ClDummy].m_Fire) - Send = true; - else if(m_aInputData[g_Config.m_ClDummy].m_Hook != m_aLastData[g_Config.m_ClDummy].m_Hook) - Send = true; - else if(m_aInputData[g_Config.m_ClDummy].m_WantedWeapon != m_aLastData[g_Config.m_ClDummy].m_WantedWeapon) - Send = true; - else if(m_aInputData[g_Config.m_ClDummy].m_NextWeapon != m_aLastData[g_Config.m_ClDummy].m_NextWeapon) - Send = true; - else if(m_aInputData[g_Config.m_ClDummy].m_PrevWeapon != m_aLastData[g_Config.m_ClDummy].m_PrevWeapon) - Send = true; - - // send at at least 10hz - if(time_get() > LastSendTime + time_freq() / 25) - Send = true; - - if(m_pClient->m_Snap.m_pLocalCharacter && m_pClient->m_Snap.m_pLocalCharacter->m_Weapon == WEAPON_NINJA && (m_aInputData[g_Config.m_ClDummy].m_Direction || m_aInputData[g_Config.m_ClDummy].m_Jump || m_aInputData[g_Config.m_ClDummy].m_Hook)) - Send = true; + Send = Send || m_aInputData[g_Config.m_ClDummy].m_Direction != m_aLastData[g_Config.m_ClDummy].m_Direction; + Send = Send || m_aInputData[g_Config.m_ClDummy].m_Jump != m_aLastData[g_Config.m_ClDummy].m_Jump; + Send = Send || m_aInputData[g_Config.m_ClDummy].m_Fire != m_aLastData[g_Config.m_ClDummy].m_Fire; + Send = Send || m_aInputData[g_Config.m_ClDummy].m_Hook != m_aLastData[g_Config.m_ClDummy].m_Hook; + Send = Send || m_aInputData[g_Config.m_ClDummy].m_WantedWeapon != m_aLastData[g_Config.m_ClDummy].m_WantedWeapon; + Send = Send || m_aInputData[g_Config.m_ClDummy].m_NextWeapon != m_aLastData[g_Config.m_ClDummy].m_NextWeapon; + Send = Send || m_aInputData[g_Config.m_ClDummy].m_PrevWeapon != m_aLastData[g_Config.m_ClDummy].m_PrevWeapon; + Send = Send || time_get() > m_LastSendTime + time_freq() / 25; // send at least 10hz + Send = Send || (m_pClient->m_Snap.m_pLocalCharacter && m_pClient->m_Snap.m_pLocalCharacter->m_Weapon == WEAPON_NINJA && (m_aInputData[g_Config.m_ClDummy].m_Direction || m_aInputData[g_Config.m_ClDummy].m_Jump || m_aInputData[g_Config.m_ClDummy].m_Hook)); } // copy and return size @@ -346,13 +321,16 @@ int CControls::SnapInput(int *pData) if(!Send) return 0; - LastSendTime = time_get(); + m_LastSendTime = time_get(); mem_copy(pData, &m_aInputData[g_Config.m_ClDummy], sizeof(m_aInputData[0])); return sizeof(m_aInputData[0]); } void CControls::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if(g_Config.m_ClAutoswitchWeaponsOutOfAmmo && !GameClient()->m_GameInfo.m_UnlimitedAmmo && m_pClient->m_Snap.m_pLocalCharacter) { // Keep track of ammo count, we know weapon ammo only when we switch to that weapon, this is tracked on server and protocol does not track that @@ -363,16 +341,16 @@ void CControls::OnRender() m_pClient->m_Snap.m_pLocalCharacter->m_Weapon != WEAPON_HAMMER && m_pClient->m_Snap.m_pLocalCharacter->m_Weapon != WEAPON_NINJA) { - int w; - for(w = WEAPON_LASER; w > WEAPON_GUN; w--) + int Weapon; + for(Weapon = WEAPON_LASER; Weapon > WEAPON_GUN; Weapon--) { - if(w == m_pClient->m_Snap.m_pLocalCharacter->m_Weapon) + if(Weapon == m_pClient->m_Snap.m_pLocalCharacter->m_Weapon) continue; - if(m_aAmmoCount[w] > 0) + if(m_aAmmoCount[Weapon] > 0) break; } - if(w != m_pClient->m_Snap.m_pLocalCharacter->m_Weapon) - m_aInputData[g_Config.m_ClDummy].m_WantedWeapon = w + 1; + if(Weapon != m_pClient->m_Snap.m_pLocalCharacter->m_Weapon) + m_aInputData[g_Config.m_ClDummy].m_WantedWeapon = Weapon + 1; } } @@ -392,9 +370,9 @@ bool CControls::OnCursorMove(float x, float y, IInput::ECursorType CursorType) if(CursorType == IInput::CURSOR_JOYSTICK && g_Config.m_InpControllerAbsolute && m_pClient->m_Snap.m_pGameInfoObj && !m_pClient->m_Snap.m_SpecInfo.m_Active) { - float AbsX = 0.0f, AbsY = 0.0f; - if(Input()->GetActiveJoystick()->Absolute(&AbsX, &AbsY)) - m_aMousePos[g_Config.m_ClDummy] = vec2(AbsX, AbsY) * GetMaxMouseDistance(); + vec2 AbsoluteDirection; + if(Input()->GetActiveJoystick()->Absolute(&AbsoluteDirection.x, &AbsoluteDirection.y)) + m_aMousePos[g_Config.m_ClDummy] = AbsoluteDirection * GetMaxMouseDistance(); return true; } @@ -420,7 +398,7 @@ bool CControls::OnCursorMove(float x, float y, IInput::ECursorType CursorType) } } - if(m_pClient->m_Snap.m_SpecInfo.m_Active && m_pClient->m_Snap.m_SpecInfo.m_SpectatorID < 0) + if(m_pClient->m_Snap.m_SpecInfo.m_Active && m_pClient->m_Snap.m_SpecInfo.m_SpectatorId < 0) Factor *= m_pClient->m_Camera.m_Zoom; m_aMousePos[g_Config.m_ClDummy] += vec2(x, y) * Factor; @@ -430,32 +408,36 @@ bool CControls::OnCursorMove(float x, float y, IInput::ECursorType CursorType) void CControls::ClampMousePos() { - if(m_pClient->m_Snap.m_SpecInfo.m_Active && m_pClient->m_Snap.m_SpecInfo.m_SpectatorID < 0) + if(m_pClient->m_Snap.m_SpecInfo.m_Active && m_pClient->m_Snap.m_SpecInfo.m_SpectatorId < 0) { m_aMousePos[g_Config.m_ClDummy].x = clamp(m_aMousePos[g_Config.m_ClDummy].x, -201.0f * 32, (Collision()->GetWidth() + 201.0f) * 32.0f); m_aMousePos[g_Config.m_ClDummy].y = clamp(m_aMousePos[g_Config.m_ClDummy].y, -201.0f * 32, (Collision()->GetHeight() + 201.0f) * 32.0f); } else { - float MouseMax = GetMaxMouseDistance(); - float MinDistance = g_Config.m_ClDyncam ? g_Config.m_ClDyncamMinDistance : g_Config.m_ClMouseMinDistance; - float MouseMin = MinDistance; + const float MouseMin = GetMinMouseDistance(); + const float MouseMax = GetMaxMouseDistance(); - float MDistance = length(m_aMousePos[g_Config.m_ClDummy]); - if(MDistance < 0.001f) + float MouseDistance = length(m_aMousePos[g_Config.m_ClDummy]); + if(MouseDistance < 0.001f) { m_aMousePos[g_Config.m_ClDummy].x = 0.001f; m_aMousePos[g_Config.m_ClDummy].y = 0; - MDistance = 0.001f; + MouseDistance = 0.001f; } - if(MDistance < MouseMin) - m_aMousePos[g_Config.m_ClDummy] = normalize_pre_length(m_aMousePos[g_Config.m_ClDummy], MDistance) * MouseMin; - MDistance = length(m_aMousePos[g_Config.m_ClDummy]); - if(MDistance > MouseMax) - m_aMousePos[g_Config.m_ClDummy] = normalize_pre_length(m_aMousePos[g_Config.m_ClDummy], MDistance) * MouseMax; + if(MouseDistance < MouseMin) + m_aMousePos[g_Config.m_ClDummy] = normalize_pre_length(m_aMousePos[g_Config.m_ClDummy], MouseDistance) * MouseMin; + MouseDistance = length(m_aMousePos[g_Config.m_ClDummy]); + if(MouseDistance > MouseMax) + m_aMousePos[g_Config.m_ClDummy] = normalize_pre_length(m_aMousePos[g_Config.m_ClDummy], MouseDistance) * MouseMax; } } +float CControls::GetMinMouseDistance() const +{ + return g_Config.m_ClDyncam ? g_Config.m_ClDyncamMinDistance : g_Config.m_ClMouseMinDistance; +} + float CControls::GetMaxMouseDistance() const { float CameraMaxDistance = 200.0f; diff --git a/src/game/client/components/controls.h b/src/game/client/components/controls.h index 67323a12a3..ce6bc664dd 100644 --- a/src/game/client/components/controls.h +++ b/src/game/client/components/controls.h @@ -12,30 +12,27 @@ class CControls : public CComponent { + float GetMinMouseDistance() const; float GetMaxMouseDistance() const; public: vec2 m_aMousePos[NUM_DUMMIES]; vec2 m_aMousePosOnAction[NUM_DUMMIES]; vec2 m_aTargetPos[NUM_DUMMIES]; - float m_OldMouseX; - float m_OldMouseY; int m_aAmmoCount[NUM_WEAPONS]; + int64_t m_LastSendTime; CNetObj_PlayerInput m_aInputData[NUM_DUMMIES]; CNetObj_PlayerInput m_aLastData[NUM_DUMMIES]; int m_aInputDirectionLeft[NUM_DUMMIES]; int m_aInputDirectionRight[NUM_DUMMIES]; int m_aShowHookColl[NUM_DUMMIES]; - int m_LastDummy; - int m_OtherFire; CControls(); virtual int Sizeof() const override { return sizeof(*this); } virtual void OnReset() override; - virtual void OnRelease() override; virtual void OnRender() override; virtual void OnMessage(int MsgType, void *pRawMsg) override; virtual bool OnCursorMove(float x, float y, IInput::ECursorType CursorType) override; diff --git a/src/game/client/components/countryflags.cpp b/src/game/client/components/countryflags.cpp index 1f28a4bc5f..b05778596e 100644 --- a/src/game/client/components/countryflags.cpp +++ b/src/game/client/components/countryflags.cpp @@ -16,8 +16,8 @@ void CCountryFlags::LoadCountryflagsIndexfile() { const char *pFilename = "countryflags/index.txt"; - IOHANDLE File = Storage()->OpenFile(pFilename, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL); - if(!File) + CLineReader LineReader; + if(!LineReader.OpenFile(Storage()->OpenFile(pFilename, IOFLAG_READ, IStorage::TYPE_ALL))) { char aBuf[128]; str_format(aBuf, sizeof(aBuf), "couldn't open index file '%s'", pFilename); @@ -26,16 +26,13 @@ void CCountryFlags::LoadCountryflagsIndexfile() } char aOrigin[128]; - CLineReader LineReader; - LineReader.Init(File); - char *pLine; - while((pLine = LineReader.Get())) + while(const char *pLine = LineReader.Get()) { if(!str_length(pLine) || pLine[0] == '#') // skip empty lines and comments continue; str_copy(aOrigin, pLine); - char *pReplacement = LineReader.Get(); + const char *pReplacement = LineReader.Get(); if(!pReplacement) { Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "countryflags", "unexpected end of index file"); @@ -63,7 +60,7 @@ void CCountryFlags::LoadCountryflagsIndexfile() char aBuf[128]; CImageInfo Info; str_format(aBuf, sizeof(aBuf), "countryflags/%s.png", aOrigin); - if(!Graphics()->LoadPNG(&Info, aBuf, IStorage::TYPE_ALL)) + if(!Graphics()->LoadPng(Info, aBuf, IStorage::TYPE_ALL)) { char aMsg[128]; str_format(aMsg, sizeof(aMsg), "failed to load '%s'", aBuf); @@ -75,8 +72,7 @@ void CCountryFlags::LoadCountryflagsIndexfile() CCountryFlag CountryFlag; CountryFlag.m_CountryCode = CountryCode; str_copy(CountryFlag.m_aCountryCodeString, aOrigin); - CountryFlag.m_Texture = Graphics()->LoadTextureRaw(Info.m_Width, Info.m_Height, Info.m_Format, Info.m_pData, 0, aOrigin); - Graphics()->FreePNG(&Info); + CountryFlag.m_Texture = Graphics()->LoadTextureRawMove(Info, 0, aBuf); if(g_Config.m_Debug) { @@ -85,7 +81,7 @@ void CCountryFlags::LoadCountryflagsIndexfile() } m_vCountryFlags.push_back(CountryFlag); } - io_close(File); + std::sort(m_vCountryFlags.begin(), m_vCountryFlags.end()); // find index of default item diff --git a/src/game/client/components/countryflags.h b/src/game/client/components/countryflags.h index d928e8d3b0..9af9302d6e 100644 --- a/src/game/client/components/countryflags.h +++ b/src/game/client/components/countryflags.h @@ -3,6 +3,7 @@ #ifndef GAME_CLIENT_COMPONENTS_COUNTRYFLAGS_H #define GAME_CLIENT_COMPONENTS_COUNTRYFLAGS_H +#include #include #include diff --git a/src/game/client/components/damageind.cpp b/src/game/client/components/damageind.cpp index aeed8b0d09..0ac462da08 100644 --- a/src/game/client/components/damageind.cpp +++ b/src/game/client/components/damageind.cpp @@ -12,90 +12,67 @@ CDamageInd::CDamageInd() { - m_Lastupdate = 0; m_NumItems = 0; } -CDamageInd::CItem *CDamageInd::CreateI() +void CDamageInd::Create(vec2 Pos, vec2 Dir, float Alpha) { - if(m_NumItems < MAX_ITEMS) - { - CItem *p = &m_aItems[m_NumItems]; - m_NumItems++; - return p; - } - return 0; -} + if(m_NumItems >= MAX_ITEMS) + return; -void CDamageInd::DestroyI(CDamageInd::CItem *pItem) -{ - m_NumItems--; - *pItem = m_aItems[m_NumItems]; + CItem *pItem = &m_aItems[m_NumItems]; + pItem->m_Pos = Pos; + pItem->m_Dir = -Dir; + pItem->m_RemainingLife = 0.75f; + pItem->m_StartAngle = -random_angle(); + pItem->m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, Alpha); + ++m_NumItems; } -void CDamageInd::CreateDamageInd(vec2 Pos, float Angle, float Alpha, int Amount) +void CDamageInd::OnRender() { - float a = 3 * pi / 2 + Angle; - float s = a - pi / 3; - float e = a + pi / 3; - for(int i = 0; i < Amount; i++) + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + + static float s_LastLocalTime = LocalTime(); + float LifeAdjustment; + if(Client()->State() == IClient::STATE_DEMOPLAYBACK) { - float f = mix(s, e, (i + 1) / (float)(Amount + 2)); - Create(Pos, direction(f), Alpha); + const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo(); + if(pInfo->m_Paused) + LifeAdjustment = 0.0f; + else + LifeAdjustment = (LocalTime() - s_LastLocalTime) * pInfo->m_Speed; } -} - -void CDamageInd::Create(vec2 Pos, vec2 Dir, float Alpha) -{ - CItem *pItem = CreateI(); - if(pItem) + else { - pItem->m_Pos = Pos; - pItem->m_StartTime = LocalTime(); - pItem->m_Dir = -Dir; - pItem->m_StartAngle = -random_angle(); - pItem->m_Color = ColorRGBA(1.0f, 1.0f, 1.0f, Alpha); - pItem->m_StartAlpha = Alpha; + const auto &pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; + if(pGameInfoObj && pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED) + LifeAdjustment = 0.0f; + else + LifeAdjustment = LocalTime() - s_LastLocalTime; } -} + s_LastLocalTime = LocalTime(); -void CDamageInd::OnRender() -{ Graphics()->TextureSet(GameClient()->m_GameSkin.m_aSpriteStars[0]); - static float s_LastLocalTime = LocalTime(); for(int i = 0; i < m_NumItems;) { - if(Client()->State() == IClient::STATE_DEMOPLAYBACK) + m_aItems[i].m_RemainingLife -= LifeAdjustment; + if(m_aItems[i].m_RemainingLife < 0.0f) { - const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo(); - if(pInfo->m_Paused) - m_aItems[i].m_StartTime += LocalTime() - s_LastLocalTime; - else - m_aItems[i].m_StartTime += (LocalTime() - s_LastLocalTime) * (1.0f - pInfo->m_Speed); + --m_NumItems; + m_aItems[i] = m_aItems[m_NumItems]; } else { - if(m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED) - m_aItems[i].m_StartTime += LocalTime() - s_LastLocalTime; - } - - float Life = 0.75f - (LocalTime() - m_aItems[i].m_StartTime); - if(Life < 0.0f) - DestroyI(&m_aItems[i]); - else - { - vec2 Pos = mix(m_aItems[i].m_Pos + m_aItems[i].m_Dir * 75.0f, m_aItems[i].m_Pos, clamp((Life - 0.60f) / 0.15f, 0.0f, 1.0f)); - ColorRGBA Color = m_aItems[i].m_Color; - - Color.a = m_aItems[i].m_StartAlpha * fmin(1, Life / 0.1f); - - Graphics()->SetColor(Color); - Graphics()->QuadsSetRotation(m_aItems[i].m_StartAngle + Life * 2.0f); + vec2 Pos = mix(m_aItems[i].m_Pos + m_aItems[i].m_Dir * 75.0f, m_aItems[i].m_Pos, clamp((m_aItems[i].m_RemainingLife - 0.60f) / 0.15f, 0.0f, 1.0f)); + const float LifeAlpha = m_aItems[i].m_RemainingLife / 0.1f; + Graphics()->SetColor(m_aItems[i].m_Color.WithMultipliedAlpha(LifeAlpha)); + Graphics()->QuadsSetRotation(m_aItems[i].m_StartAngle + m_aItems[i].m_RemainingLife * 2.0f); Graphics()->RenderQuadContainerAsSprite(m_DmgIndQuadContainerIndex, 0, Pos.x, Pos.y); i++; } } - s_LastLocalTime = LocalTime(); Graphics()->QuadsSetRotation(0); Graphics()->SetColor(1.f, 1.f, 1.f, 1.f); @@ -114,10 +91,7 @@ void CDamageInd::OnInit() Graphics()->QuadContainerUpload(m_DmgIndQuadContainerIndex); } -void CDamageInd::Reset() +void CDamageInd::OnReset() { - for(int i = 0; i < m_NumItems;) - { - DestroyI(&m_aItems[i]); - } + m_NumItems = 0; } diff --git a/src/game/client/components/damageind.h b/src/game/client/components/damageind.h index 9324a68da2..8b45acc8bc 100644 --- a/src/game/client/components/damageind.h +++ b/src/game/client/components/damageind.h @@ -8,15 +8,13 @@ class CDamageInd : public CComponent { - int64_t m_Lastupdate; struct CItem { vec2 m_Pos; vec2 m_Dir; - float m_StartTime; + float m_RemainingLife; float m_StartAngle; ColorRGBA m_Color; - float m_StartAlpha; }; enum @@ -27,18 +25,14 @@ class CDamageInd : public CComponent CItem m_aItems[MAX_ITEMS]; int m_NumItems; - CItem *CreateI(); - void DestroyI(CItem *pItem); - int m_DmgIndQuadContainerIndex; public: CDamageInd(); virtual int Sizeof() const override { return sizeof(*this); } - void CreateDamageInd(vec2 Pos, float Angle, float Alpha, int Amount); void Create(vec2 Pos, vec2 Dir, float Alpha); - void Reset(); + virtual void OnReset() override; virtual void OnRender() override; virtual void OnInit() override; }; diff --git a/src/game/client/components/debughud.cpp b/src/game/client/components/debughud.cpp index 212744b24c..3b0afb0795 100644 --- a/src/game/client/components/debughud.cpp +++ b/src/game/client/components/debughud.cpp @@ -12,6 +12,14 @@ #include "debughud.h" +static constexpr int64_t GRAPH_MAX_VALUES = 128; + +CDebugHud::CDebugHud() : + m_RampGraph(GRAPH_MAX_VALUES), + m_ZoomedInGraph(GRAPH_MAX_VALUES) +{ +} + void CDebugHud::RenderNetCorrections() { if(!g_Config.m_Debug || g_Config.m_DbgGraphs || !m_pClient->m_Snap.m_pLocalCharacter || !m_pClient->m_Snap.m_pLocalPrevCharacter) @@ -25,7 +33,7 @@ void CDebugHud::RenderNetCorrections() const float VelspeedX = m_pClient->m_Snap.m_pLocalCharacter->m_VelX / 256.0f * Client()->GameTickSpeed(); const float VelspeedY = m_pClient->m_Snap.m_pLocalCharacter->m_VelY / 256.0f * Client()->GameTickSpeed(); const float Ramp = VelocityRamp(Velspeed, m_pClient->m_aTuning[g_Config.m_ClDummy].m_VelrampStart, m_pClient->m_aTuning[g_Config.m_ClDummy].m_VelrampRange, m_pClient->m_aTuning[g_Config.m_ClDummy].m_VelrampCurvature); - const CCharacter *pCharacter = m_pClient->m_GameWorld.GetCharacterByID(m_pClient->m_Snap.m_LocalClientID); + const CCharacter *pCharacter = m_pClient->m_GameWorld.GetCharacterById(m_pClient->m_Snap.m_LocalClientId); const float FontSize = 5.0f; const float LineHeight = FontSize + 1.0f; @@ -52,10 +60,10 @@ void CDebugHud::RenderNetCorrections() str_format(aBuf, sizeof(aBuf), "%.2f", Ramp); RenderRow("Ramp:", aBuf); - str_from_int(pCharacter == nullptr ? -1 : pCharacter->m_TeleCheckpoint, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", pCharacter == nullptr ? -1 : pCharacter->m_TeleCheckpoint); RenderRow("Checkpoint:", aBuf); - str_from_int(pCharacter == nullptr ? -1 : pCharacter->m_TuneZone, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", pCharacter == nullptr ? -1 : pCharacter->m_TuneZone); RenderRow("Tune zone:", aBuf); str_format(aBuf, sizeof(aBuf), "%.2f", m_pClient->m_Snap.m_pLocalCharacter->m_X / 32.0f); @@ -64,10 +72,10 @@ void CDebugHud::RenderNetCorrections() str_format(aBuf, sizeof(aBuf), "%.2f", m_pClient->m_Snap.m_pLocalCharacter->m_Y / 32.0f); RenderRow("Pos.y:", aBuf); - str_from_int(m_pClient->m_Snap.m_pLocalCharacter->m_Angle, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", m_pClient->m_Snap.m_pLocalCharacter->m_Angle); RenderRow("Angle:", aBuf); - str_from_int(m_pClient->NetobjNumCorrections(), aBuf); + str_format(aBuf, sizeof(aBuf), "%d", m_pClient->NetobjNumCorrections()); RenderRow("Netobj corrections", aBuf); RenderRow(" on:", m_pClient->NetobjCorrectedOn()); } @@ -84,7 +92,7 @@ void CDebugHud::RenderTuning() if(g_Config.m_DbgTuning == DBG_TUNING_OFF) return; - const CCharacter *pCharacter = m_pClient->m_GameWorld.GetCharacterByID(m_pClient->m_Snap.m_LocalClientID); + const CCharacter *pCharacter = m_pClient->m_GameWorld.GetCharacterById(m_pClient->m_Snap.m_LocalClientId); const CTuningParams StandardTuning; const CTuningParams *pGlobalTuning = m_pClient->GetTuning(0); @@ -179,7 +187,7 @@ void CDebugHud::RenderTuning() m_RampGraph.Init(0.0f, 0.0f); m_SpeedTurningPoint = 0; float PreviousRampedSpeed = 1.0f; - for(size_t i = 0; i < CGraph::MAX_VALUES; i++) + for(int64_t i = 0; i < GRAPH_MAX_VALUES; i++) { // This is a calculation of the speed values per second on the X axis, from 270 to 34560 in steps of 270 const float Speed = (i + 1) * StepSizeRampGraph; @@ -196,12 +204,12 @@ void CDebugHud::RenderTuning() } PreviousRampedSpeed = RampedSpeed; } - m_RampGraph.Scale(); + m_RampGraph.Scale(GRAPH_MAX_VALUES - 1); m_ZoomedInGraph.Init(0.0f, 0.0f); PreviousRampedSpeed = 1.0f; MiddleOfZoomedInGraph = m_SpeedTurningPoint; - for(size_t i = 0; i < CGraph::MAX_VALUES; i++) + for(int64_t i = 0; i < GRAPH_MAX_VALUES; i++) { // This is a calculation of the speed values per second on the X axis, from (MiddleOfZoomedInGraph - 64 * StepSize) to (MiddleOfZoomedInGraph + 64 * StepSize) const float Speed = MiddleOfZoomedInGraph - 64 * StepSizeZoomedInGraph + i * StepSizeZoomedInGraph; @@ -222,7 +230,7 @@ void CDebugHud::RenderTuning() } PreviousRampedSpeed = RampedSpeed; } - m_ZoomedInGraph.Scale(); + m_ZoomedInGraph.Scale(GRAPH_MAX_VALUES - 1); } const float GraphFontSize = 12.0f; @@ -253,6 +261,9 @@ void CDebugHud::RenderHint() void CDebugHud::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + RenderTuning(); RenderNetCorrections(); RenderHint(); diff --git a/src/game/client/components/debughud.h b/src/game/client/components/debughud.h index 67b9019d35..3dc326d73e 100644 --- a/src/game/client/components/debughud.h +++ b/src/game/client/components/debughud.h @@ -21,6 +21,7 @@ class CDebugHud : public CComponent float m_OldVelrampCurvature; public: + CDebugHud(); virtual int Sizeof() const override { return sizeof(*this); } virtual void OnRender() override; }; diff --git a/src/game/client/components/effects.cpp b/src/game/client/components/effects.cpp index d22999367b..d5c7fd6083 100644 --- a/src/game/client/components/effects.cpp +++ b/src/game/client/components/effects.cpp @@ -53,11 +53,6 @@ void CEffects::DamageIndicator(vec2 Pos, vec2 Dir, float Alpha) m_pClient->m_DamageInd.Create(Pos, Dir, Alpha); } -void CEffects::ResetDamageIndicator() -{ - m_pClient->m_DamageInd.Reset(); -} - void CEffects::PowerupShine(vec2 Pos, vec2 Size, float Alpha) { if(!m_Add50hz) @@ -190,11 +185,11 @@ void CEffects::PlayerSpawn(vec2 Pos, float Alpha) m_pClient->m_Sounds.PlayAt(CSounds::CHN_WORLD, SOUND_PLAYER_SPAWN, 1.0f, Pos); } -void CEffects::PlayerDeath(vec2 Pos, int ClientID, float Alpha) +void CEffects::PlayerDeath(vec2 Pos, int ClientId, float Alpha) { ColorRGBA BloodColor(1.0f, 1.0f, 1.0f); - if(ClientID >= 0) + if(ClientId >= 0) { // Use m_RenderInfo.m_CustomColoredSkin instead of m_UseCustomColor // m_UseCustomColor says if the player's skin has a custom color (value sent from the client side) @@ -202,11 +197,11 @@ void CEffects::PlayerDeath(vec2 Pos, int ClientID, float Alpha) // m_RenderInfo.m_CustomColoredSkin Defines if in the context of the game the color is being customized, // Using this value if the game is teams (red and blue), this value will be true even if the skin is with the normal color. // And will use the team body color to create player death effect instead of tee color - if(m_pClient->m_aClients[ClientID].m_RenderInfo.m_CustomColoredSkin) - BloodColor = m_pClient->m_aClients[ClientID].m_RenderInfo.m_ColorBody; + if(m_pClient->m_aClients[ClientId].m_RenderInfo.m_CustomColoredSkin) + BloodColor = m_pClient->m_aClients[ClientId].m_RenderInfo.m_ColorBody; else { - BloodColor = m_pClient->m_aClients[ClientID].m_RenderInfo.m_BloodColor; + BloodColor = m_pClient->m_aClients[ClientId].m_RenderInfo.m_BloodColor; } } @@ -231,6 +226,60 @@ void CEffects::PlayerDeath(vec2 Pos, int ClientID, float Alpha) } } +void CEffects::Confetti(vec2 Pos, float Alpha) +{ + ColorRGBA Red(1.0f, 0.4f, 0.4f); + ColorRGBA Green(0.4f, 1.0f, 0.4f); + ColorRGBA Blue(0.4f, 0.4f, 1.0f); + ColorRGBA Yellow(1.0f, 1.0f, 0.4f); + ColorRGBA Cyan(0.4f, 1.0f, 1.0f); + ColorRGBA Magenta(1.0f, 0.4f, 1.0f); + + ColorRGBA aConfettiColors[] = {Red, Green, Blue, Yellow, Cyan, Magenta}; + + // powerful confettis + for(int i = 0; i < 32; i++) + { + CParticle p; + p.SetDefault(); + p.m_Spr = SPRITE_PART_SPLAT01 + (rand() % 3); + p.m_Pos = Pos; + p.m_Vel = direction(-0.5f * pi + random_float(-0.2f, 0.2f)) * random_float(0.01f, 1.0f) * 2000.0f; + p.m_LifeSpan = random_float(1.0f, 1.2f); + p.m_StartSize = random_float(12.0f, 24.0f); + p.m_EndSize = 0; + p.m_Rot = random_angle(); + p.m_Rotspeed = random_float(-0.5f, 0.5f) * pi; + p.m_Gravity = -700.0f; + p.m_Friction = 0.6f; + ColorRGBA c = aConfettiColors[(rand() % std::size(aConfettiColors))]; + p.m_Color = c.WithMultipliedAlpha(0.75f * Alpha); + p.m_StartAlpha = Alpha; + m_pClient->m_Particles.Add(CParticles::GROUP_GENERAL, &p); + } + + // broader confettis + for(int i = 0; i < 32; i++) + { + CParticle p; + p.SetDefault(); + p.m_Spr = SPRITE_PART_SPLAT01 + (rand() % 3); + p.m_Pos = Pos; + p.m_Vel = direction(-0.5f * pi + random_float(-0.8f, 0.8f)) * random_float(0.01f, 1.0f) * 1500.0f; + p.m_LifeSpan = random_float(0.8f, 1.0f); + p.m_StartSize = random_float(12.0f, 24.0f); + p.m_EndSize = 0; + p.m_Rot = random_angle(); + p.m_Rotspeed = random_float(-0.5f, 0.5f) * pi; + p.m_Gravity = -700.0f; + p.m_Friction = 0.6f; + ColorRGBA c = aConfettiColors[(rand() % std::size(aConfettiColors))]; + p.m_Color = c.WithMultipliedAlpha(0.75f * Alpha); + p.m_StartAlpha = Alpha; + m_pClient->m_Particles.Add(CParticles::GROUP_GENERAL, &p); + } +} + void CEffects::Explosion(vec2 Pos, float Alpha) { // add to flow @@ -257,6 +306,28 @@ void CEffects::Explosion(vec2 Pos, float Alpha) p.m_StartAlpha = Alpha; m_pClient->m_Particles.Add(CParticles::GROUP_EXPLOSIONS, &p); + // Nudge position slightly to edge of closest tile so the + // smoke doesn't get stuck inside the tile. + if(Collision()->CheckPoint(Pos)) + { + const vec2 DistanceToTopLeft = Pos - vec2(round_truncate(Pos.x / 32), round_truncate(Pos.y / 32)) * 32; + + vec2 CheckOffset; + CheckOffset.x = (DistanceToTopLeft.x > 16 ? 32 : -1); + CheckOffset.y = (DistanceToTopLeft.y > 16 ? 32 : -1); + CheckOffset -= DistanceToTopLeft; + + for(vec2 Mask : {vec2(1.0f, 0.0f), vec2(0.0f, 1.0f), vec2(1.0f, 1.0f)}) + { + const vec2 NewPos = Pos + CheckOffset * Mask; + if(!Collision()->CheckPoint(NewPos)) + { + Pos = NewPos; + break; + } + } + } + // add the smoke for(int i = 0; i < 24; i++) { diff --git a/src/game/client/components/effects.h b/src/game/client/components/effects.h index 02acc90f66..de75e63c9e 100644 --- a/src/game/client/components/effects.h +++ b/src/game/client/components/effects.h @@ -2,6 +2,9 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #ifndef GAME_CLIENT_COMPONENTS_EFFECTS_H #define GAME_CLIENT_COMPONENTS_EFFECTS_H + +#include + #include class CEffects : public CComponent @@ -23,11 +26,11 @@ class CEffects : public CComponent void HammerHit(vec2 Pos, float Alpha = 1.0f); void AirJump(vec2 Pos, float Alpha = 1.0f); void DamageIndicator(vec2 Pos, vec2 Dir, float Alpha = 1.0f); - void ResetDamageIndicator(); void PlayerSpawn(vec2 Pos, float Alpha = 1.0f); - void PlayerDeath(vec2 Pos, int ClientID, float Alpha = 1.0f); + void PlayerDeath(vec2 Pos, int ClientId, float Alpha = 1.0f); void PowerupShine(vec2 Pos, vec2 Size, float Alpha = 1.0f); void FreezingFlakes(vec2 Pos, vec2 Size, float Alpha = 1.0f); + void Confetti(vec2 Pos, float Alpha = 1.0f); void Update(); }; diff --git a/src/game/client/components/emoticon.cpp b/src/game/client/components/emoticon.cpp index 16c6c32b47..e7c0963d0c 100644 --- a/src/game/client/components/emoticon.cpp +++ b/src/game/client/components/emoticon.cpp @@ -41,6 +41,7 @@ void CEmoticon::OnReset() m_Active = false; m_SelectedEmote = -1; m_SelectedEyeEmote = -1; + m_TouchPressedOutside = false; } void CEmoticon::OnRelease() @@ -53,15 +54,35 @@ bool CEmoticon::OnCursorMove(float x, float y, IInput::ECursorType CursorType) if(!m_Active) return false; - UI()->ConvertMouseMove(&x, &y, CursorType); + Ui()->ConvertMouseMove(&x, &y, CursorType); m_SelectorMouse += vec2(x, y); return true; } +bool CEmoticon::OnInput(const IInput::CEvent &Event) +{ + if(IsActive() && Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_ESCAPE) + { + OnRelease(); + return true; + } + return false; +} + void CEmoticon::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if(!m_Active) { + if(m_TouchPressedOutside) + { + m_SelectedEmote = -1; + m_SelectedEyeEmote = -1; + m_TouchPressedOutside = false; + } + if(m_WasActive && m_SelectedEmote != -1) Emote(m_SelectedEmote); if(m_WasActive && m_SelectedEyeEmote != -1) @@ -79,6 +100,29 @@ void CEmoticon::OnRender() m_WasActive = true; + const CUIRect Screen = *Ui()->Screen(); + + const bool WasTouchPressed = m_TouchState.m_AnyPressed; + Ui()->UpdateTouchState(m_TouchState); + if(m_TouchState.m_AnyPressed) + { + const vec2 TouchPos = (m_TouchState.m_PrimaryPosition - vec2(0.5f, 0.5f)) * Screen.Size(); + const float TouchCenterDistance = length(TouchPos); + if(TouchCenterDistance <= 170.0f) + { + m_SelectorMouse = TouchPos; + } + else if(TouchCenterDistance > 190.0f) + { + m_TouchPressedOutside = true; + } + } + else if(WasTouchPressed) + { + m_Active = false; + return; + } + if(length(m_SelectorMouse) > 170.0f) m_SelectorMouse = normalize(m_SelectorMouse) * 170.0f; @@ -93,35 +137,31 @@ void CEmoticon::OnRender() else if(length(m_SelectorMouse) > 40.0f) m_SelectedEyeEmote = (int)(SelectedAngle / (2 * pi) * NUM_EMOTES); - CUIRect Screen = *UI()->Screen(); + const vec2 ScreenCenter = Screen.Center(); - UI()->MapScreen(); + Ui()->MapScreen(); Graphics()->BlendNormal(); Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(0, 0, 0, 0.3f); - Graphics()->DrawCircle(Screen.w / 2, Screen.h / 2, 190.0f, 64); + Graphics()->DrawCircle(ScreenCenter.x, ScreenCenter.y, 190.0f, 64); Graphics()->QuadsEnd(); Graphics()->WrapClamp(); - for(int i = 0; i < NUM_EMOTICONS; i++) + for(int Emote = 0; Emote < NUM_EMOTICONS; Emote++) { - float Angle = 2 * pi * i / NUM_EMOTICONS; + float Angle = 2 * pi * Emote / NUM_EMOTICONS; if(Angle > pi) Angle -= 2 * pi; - bool Selected = m_SelectedEmote == i; - - float Size = Selected ? 80.0f : 50.0f; - - Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[i]); + Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[Emote]); Graphics()->QuadsSetSubset(0, 0, 1, 1); - Graphics()->QuadsBegin(); const vec2 Nudge = direction(Angle) * 150.0f; - IGraphics::CQuadItem QuadItem(Screen.w / 2 + Nudge.x, Screen.h / 2 + Nudge.y, Size, Size); + const float Size = m_SelectedEmote == Emote ? 80.0f : 50.0f; + IGraphics::CQuadItem QuadItem(ScreenCenter.x + Nudge.x, ScreenCenter.y + Nudge.y, Size, Size); Graphics()->QuadsDraw(&QuadItem, 1); Graphics()->QuadsEnd(); } @@ -132,34 +172,32 @@ void CEmoticon::OnRender() Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0, 1.0, 1.0, 0.3f); - Graphics()->DrawCircle(Screen.w / 2, Screen.h / 2, 100.0f, 64); + Graphics()->DrawCircle(ScreenCenter.x, ScreenCenter.y, 100.0f, 64); Graphics()->QuadsEnd(); - CTeeRenderInfo TeeInfo = m_pClient->m_aClients[m_pClient->m_aLocalIDs[g_Config.m_ClDummy]].m_RenderInfo; + CTeeRenderInfo TeeInfo = m_pClient->m_aClients[m_pClient->m_aLocalIds[g_Config.m_ClDummy]].m_RenderInfo; - for(int i = 0; i < NUM_EMOTES; i++) + for(int Emote = 0; Emote < NUM_EMOTES; Emote++) { - float Angle = 2 * pi * i / NUM_EMOTES; + float Angle = 2 * pi * Emote / NUM_EMOTES; if(Angle > pi) Angle -= 2 * pi; - const bool Selected = m_SelectedEyeEmote == i; - const vec2 Nudge = direction(Angle) * 70.0f; - TeeInfo.m_Size = Selected ? 64.0f : 48.0f; - RenderTools()->RenderTee(CAnimState::GetIdle(), &TeeInfo, i, vec2(-1, 0), vec2(Screen.w / 2 + Nudge.x, Screen.h / 2 + Nudge.y)); + TeeInfo.m_Size = m_SelectedEyeEmote == Emote ? 64.0f : 48.0f; + RenderTools()->RenderTee(CAnimState::GetIdle(), &TeeInfo, Emote, vec2(-1, 0), ScreenCenter + Nudge); } Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(0, 0, 0, 0.3f); - Graphics()->DrawCircle(Screen.w / 2, Screen.h / 2, 30.0f, 64); + Graphics()->DrawCircle(ScreenCenter.x, ScreenCenter.y, 30.0f, 64); Graphics()->QuadsEnd(); } else m_SelectedEyeEmote = -1; - RenderTools()->RenderCursor(m_SelectorMouse + vec2(Screen.w, Screen.h) / 2, 24.0f); + RenderTools()->RenderCursor(ScreenCenter + m_SelectorMouse, 24.0f); } void CEmoticon::Emote(int Emoticon) @@ -200,5 +238,5 @@ void CEmoticon::EyeEmote(int Emote) str_format(aBuf, sizeof(aBuf), "/emote blink %d", g_Config.m_ClEyeDuration); break; } - GameClient()->m_Chat.Say(0, aBuf); + GameClient()->m_Chat.SendChat(0, aBuf); } diff --git a/src/game/client/components/emoticon.h b/src/game/client/components/emoticon.h index aec896cfa9..890f1c80ef 100644 --- a/src/game/client/components/emoticon.h +++ b/src/game/client/components/emoticon.h @@ -3,7 +3,10 @@ #ifndef GAME_CLIENT_COMPONENTS_EMOTICON_H #define GAME_CLIENT_COMPONENTS_EMOTICON_H #include +#include + #include +#include class CEmoticon : public CComponent { @@ -14,6 +17,9 @@ class CEmoticon : public CComponent int m_SelectedEmote; int m_SelectedEyeEmote; + CUi::CTouchState m_TouchState; + bool m_TouchPressedOutside; + static void ConKeyEmoticon(IConsole::IResult *pResult, void *pUserData); static void ConEmote(IConsole::IResult *pResult, void *pUserData); @@ -26,9 +32,12 @@ class CEmoticon : public CComponent virtual void OnRender() override; virtual void OnRelease() override; virtual bool OnCursorMove(float x, float y, IInput::ECursorType CursorType) override; + virtual bool OnInput(const IInput::CEvent &Event) override; void Emote(int Emoticon); void EyeEmote(int EyeEmote); + + bool IsActive() const { return m_Active; } }; #endif diff --git a/src/game/client/components/freezebars.cpp b/src/game/client/components/freezebars.cpp index 817be51129..1f5f0204ee 100644 --- a/src/game/client/components/freezebars.cpp +++ b/src/game/client/components/freezebars.cpp @@ -2,16 +2,16 @@ #include "freezebars.h" -void CFreezeBars::RenderFreezeBar(const int ClientID) +void CFreezeBars::RenderFreezeBar(const int ClientId) { const float FreezeBarWidth = 64.0f; const float FreezeBarHalfWidth = 32.0f; const float FreezeBarHight = 16.0f; // pCharacter contains the predicted character for local players or the last snap for players who are spectated - CCharacterCore *pCharacter = &m_pClient->m_aClients[ClientID].m_Predicted; + CCharacterCore *pCharacter = &m_pClient->m_aClients[ClientId].m_Predicted; - if(pCharacter->m_FreezeEnd <= 0 || pCharacter->m_FreezeStart == 0 || pCharacter->m_FreezeEnd <= pCharacter->m_FreezeStart || !m_pClient->m_Snap.m_aCharacters[ClientID].m_HasExtendedDisplayInfo || (pCharacter->m_IsInFreeze && g_Config.m_ClFreezeBarsAlphaInsideFreeze == 0)) + if(pCharacter->m_FreezeEnd <= 0 || pCharacter->m_FreezeStart == 0 || pCharacter->m_FreezeEnd <= pCharacter->m_FreezeStart || !m_pClient->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo || (pCharacter->m_IsInFreeze && g_Config.m_ClFreezeBarsAlphaInsideFreeze == 0)) { return; } @@ -23,11 +23,11 @@ void CFreezeBars::RenderFreezeBar(const int ClientID) return; } - vec2 Position = m_pClient->m_aClients[ClientID].m_RenderPos; + vec2 Position = m_pClient->m_aClients[ClientId].m_RenderPos; Position.x -= FreezeBarHalfWidth; Position.y += 32; - float Alpha = m_pClient->IsOtherTeam(ClientID) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f; + float Alpha = m_pClient->IsOtherTeam(ClientId) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f; if(pCharacter->m_IsInFreeze) { Alpha *= g_Config.m_ClFreezeBarsAlphaInsideFreeze / 100.0f; @@ -36,7 +36,7 @@ void CFreezeBars::RenderFreezeBar(const int ClientID) RenderFreezeBarPos(Position.x, Position.y, FreezeBarWidth, FreezeBarHight, FreezeProgress, Alpha); } -void CFreezeBars::RenderFreezeBarPos(float x, const float y, const float width, const float height, float Progress, const float Alpha) +void CFreezeBars::RenderFreezeBarPos(float x, const float y, const float Width, const float Height, float Progress, const float Alpha) { Progress = clamp(Progress, 0.0f, 1.0f); @@ -45,9 +45,9 @@ void CFreezeBars::RenderFreezeBarPos(float x, const float y, const float width, const float RestPct = 0.5f; const float ProgPct = 0.5f; - const float EndWidth = height; // to keep the correct scale - the height of the sprite is as long as the width - const float BarHeight = height; - const float WholeBarWidth = width; + const float EndWidth = Height; // to keep the correct scale - the height of the sprite is as long as the width + const float BarHeight = Height; + const float WholeBarWidth = Width; const float MiddleBarWidth = WholeBarWidth - (EndWidth * 2.0f); const float EndProgressWidth = EndWidth * ProgPct; const float EndRestWidth = EndWidth * RestPct; @@ -187,15 +187,18 @@ void CFreezeBars::RenderFreezeBarPos(float x, const float y, const float width, Graphics()->WrapNormal(); } -inline bool CFreezeBars::IsPlayerInfoAvailable(int ClientID) const +inline bool CFreezeBars::IsPlayerInfoAvailable(int ClientId) const { - const void *pPrevInfo = Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_PLAYERINFO, ClientID); - const void *pInfo = Client()->SnapFindItem(IClient::SNAP_CURRENT, NETOBJTYPE_PLAYERINFO, ClientID); + const void *pPrevInfo = Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_PLAYERINFO, ClientId); + const void *pInfo = Client()->SnapFindItem(IClient::SNAP_CURRENT, NETOBJTYPE_PLAYERINFO, ClientId); return pPrevInfo && pInfo; } void CFreezeBars::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if(!g_Config.m_ClShowFreezeBars) { return; @@ -213,27 +216,27 @@ void CFreezeBars::OnRender() ScreenY0 -= BorderBuffer; ScreenY1 += BorderBuffer; - int LocalClientID = m_pClient->m_Snap.m_LocalClientID; + int LocalClientId = m_pClient->m_Snap.m_LocalClientId; // render everyone else's freeze bar, then our own - for(int ClientID = 0; ClientID < MAX_CLIENTS; ClientID++) + for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) { - if(ClientID == LocalClientID || !m_pClient->m_Snap.m_aCharacters[ClientID].m_Active || !IsPlayerInfoAvailable(ClientID)) + if(ClientId == LocalClientId || !m_pClient->m_Snap.m_aCharacters[ClientId].m_Active || !IsPlayerInfoAvailable(ClientId)) { continue; } //don't render if the tee is offscreen - vec2 *pRenderPos = &m_pClient->m_aClients[ClientID].m_RenderPos; + vec2 *pRenderPos = &m_pClient->m_aClients[ClientId].m_RenderPos; if(pRenderPos->x < ScreenX0 || pRenderPos->x > ScreenX1 || pRenderPos->y < ScreenY0 || pRenderPos->y > ScreenY1) { continue; } - RenderFreezeBar(ClientID); + RenderFreezeBar(ClientId); } - if(LocalClientID != -1 && m_pClient->m_Snap.m_aCharacters[LocalClientID].m_Active && IsPlayerInfoAvailable(LocalClientID)) + if(LocalClientId != -1 && m_pClient->m_Snap.m_aCharacters[LocalClientId].m_Active && IsPlayerInfoAvailable(LocalClientId)) { - RenderFreezeBar(LocalClientID); + RenderFreezeBar(LocalClientId); } -} \ No newline at end of file +} diff --git a/src/game/client/components/freezebars.h b/src/game/client/components/freezebars.h index f0f76110d1..1f8307453e 100644 --- a/src/game/client/components/freezebars.h +++ b/src/game/client/components/freezebars.h @@ -4,9 +4,9 @@ class CFreezeBars : public CComponent { - void RenderFreezeBar(const int ClientID); - void RenderFreezeBarPos(float x, const float y, const float width, const float height, float Progress, float Alpha = 1.0f); - bool IsPlayerInfoAvailable(int ClientID) const; + void RenderFreezeBar(const int ClientId); + void RenderFreezeBarPos(float x, const float y, const float Width, const float Height, float Progress, float Alpha = 1.0f); + bool IsPlayerInfoAvailable(int ClientId) const; public: virtual int Sizeof() const override { return sizeof(*this); } diff --git a/src/game/client/components/ghost.cpp b/src/game/client/components/ghost.cpp index 2a55688f15..26272d3690 100644 --- a/src/game/client/components/ghost.cpp +++ b/src/game/client/components/ghost.cpp @@ -216,7 +216,7 @@ void CGhost::CheckStartLocal(bool Predicted) vec2 PrevPos = m_pClient->m_PredictedPrevChar.m_Pos; vec2 Pos = m_pClient->m_PredictedChar.m_Pos; - if(((!m_Rendering && RenderTick == -1) || m_AllowRestart) && CRaceHelper::IsStart(m_pClient, PrevPos, Pos)) + if(((!m_Rendering && RenderTick == -1) || m_AllowRestart) && GameClient()->RaceHelper()->IsStart(PrevPos, Pos)) { if(m_Rendering && !m_RenderingStartedByServer) // race restarted: stop rendering StopRender(); @@ -240,7 +240,7 @@ void CGhost::CheckStartLocal(bool Predicted) int TickDiff = CurTick - PrevTick; for(int i = 0; i < TickDiff; i++) { - if(CRaceHelper::IsStart(m_pClient, mix(PrevPos, Pos, (float)i / TickDiff), mix(PrevPos, Pos, (float)(i + 1) / TickDiff))) + if(GameClient()->RaceHelper()->IsStart(mix(PrevPos, Pos, (float)i / TickDiff), mix(PrevPos, Pos, (float)(i + 1) / TickDiff))) { RecordTick = PrevTick + i + 1; if(!m_AllowRestart) @@ -287,7 +287,7 @@ void CGhost::OnNewSnapshot() CheckStart(); if(m_Recording) - AddInfos(m_pClient->m_Snap.m_pLocalCharacter, (m_pClient->m_Snap.m_LocalClientID != -1 && m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientID].m_HasExtendedData) ? &m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientID].m_ExtendedData : nullptr); + AddInfos(m_pClient->m_Snap.m_pLocalCharacter, (m_pClient->m_Snap.m_LocalClientId != -1 && m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_HasExtendedData) ? &m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_ExtendedData : nullptr); } // Record m_LastRaceTick for g_Config.m_ClConfirmDisconnect/QuitTime anyway @@ -311,6 +311,9 @@ void CGhost::OnNewPredictedSnapshot() void CGhost::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + // Play the ghost if(!m_Rendering || !g_Config.m_ClRaceShowGhost) return; @@ -363,10 +366,7 @@ void CGhost::OnRender() IsTeamplay = (m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS) != 0; GhostNinjaRenderInfo = Ghost.m_RenderInfo; - GhostNinjaRenderInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin; - GhostNinjaRenderInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin; - GhostNinjaRenderInfo.m_BloodColor = pSkin->m_BloodColor; - GhostNinjaRenderInfo.m_SkinMetrics = pSkin->m_Metrics; + GhostNinjaRenderInfo.Apply(pSkin); GhostNinjaRenderInfo.m_CustomColoredSkin = IsTeamplay; if(!IsTeamplay) { @@ -385,20 +385,16 @@ void CGhost::OnRender() void CGhost::InitRenderInfos(CGhostItem *pGhost) { - char aSkinName[64]; - IntsToStr(&pGhost->m_Skin.m_Skin0, 6, aSkinName); + char aSkinName[24]; + IntsToStr(&pGhost->m_Skin.m_Skin0, 6, aSkinName, std::size(aSkinName)); CTeeRenderInfo *pRenderInfo = &pGhost->m_RenderInfo; - const CSkin *pSkin = m_pClient->m_Skins.Find(aSkinName); - pRenderInfo->m_OriginalRenderSkin = pSkin->m_OriginalSkin; - pRenderInfo->m_ColorableRenderSkin = pSkin->m_ColorableSkin; - pRenderInfo->m_BloodColor = pSkin->m_BloodColor; - pRenderInfo->m_SkinMetrics = pSkin->m_Metrics; + pRenderInfo->Apply(m_pClient->m_Skins.Find(aSkinName)); pRenderInfo->m_CustomColoredSkin = pGhost->m_Skin.m_UseCustomColor; if(pGhost->m_Skin.m_UseCustomColor) { - pRenderInfo->m_ColorBody = color_cast(ColorHSLA(pGhost->m_Skin.m_ColorBody).UnclampLighting()); - pRenderInfo->m_ColorFeet = color_cast(ColorHSLA(pGhost->m_Skin.m_ColorFeet).UnclampLighting()); + pRenderInfo->m_ColorBody = color_cast(ColorHSLA(pGhost->m_Skin.m_ColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT)); + pRenderInfo->m_ColorFeet = color_cast(ColorHSLA(pGhost->m_Skin.m_ColorFeet).UnclampLighting(ColorHSLA::DARKEST_LGT)); } else { @@ -415,7 +411,7 @@ void CGhost::StartRecord(int Tick) m_CurGhost.Reset(); m_CurGhost.m_StartTick = Tick; - const CGameClient::CClientData *pData = &m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID]; + const CGameClient::CClientData *pData = &m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId]; str_copy(m_CurGhost.m_aPlayer, Client()->PlayerName()); GetGhostSkin(&m_CurGhost.m_Skin, pData->m_aSkinName, pData->m_UseCustomColor, pData->m_ColorBody, pData->m_ColorFeet); InitRenderInfos(&m_CurGhost); @@ -620,7 +616,7 @@ void CGhost::OnMessage(int MsgType, void *pRawMsg) if(MsgType == NETMSGTYPE_SV_KILLMSG) { CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg; - if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalClientID) + if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalClientId) { if(m_Recording) StopRecord(); @@ -633,7 +629,7 @@ void CGhost::OnMessage(int MsgType, void *pRawMsg) CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg; for(int i = 0; i < MAX_CLIENTS; i++) { - if(m_pClient->m_Teams.Team(i) == pMsg->m_Team && i == m_pClient->m_Snap.m_LocalClientID) + if(m_pClient->m_Teams.Team(i) == pMsg->m_Team && i == m_pClient->m_Snap.m_LocalClientId) { if(m_Recording) StopRecord(); @@ -645,11 +641,11 @@ void CGhost::OnMessage(int MsgType, void *pRawMsg) else if(MsgType == NETMSGTYPE_SV_CHAT) { CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; - if(pMsg->m_ClientID == -1 && m_Recording) + if(pMsg->m_ClientId == -1 && m_Recording) { char aName[MAX_NAME_LENGTH]; int Time = CRaceHelper::TimeFromFinishMessage(pMsg->m_pMessage, aName, sizeof(aName)); - if(Time > 0 && m_pClient->m_Snap.m_LocalClientID >= 0 && str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID].m_aName) == 0) + if(Time > 0 && m_pClient->m_Snap.m_LocalClientId >= 0 && str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId].m_aName) == 0) { StopRecord(Time); StopRender(); @@ -684,21 +680,17 @@ int CGhost::GetLastRaceTick() const return m_LastRaceTick; } -void CGhost::RefindSkins() +void CGhost::OnRefreshSkins() { const auto &&RefindSkin = [&](auto &Ghost) { if(Ghost.Empty()) return; - char aSkinName[64]; - IntsToStr(&Ghost.m_Skin.m_Skin0, 6, aSkinName); + char aSkinName[24]; + IntsToStr(&Ghost.m_Skin.m_Skin0, 6, aSkinName, std::size(aSkinName)); CTeeRenderInfo *pRenderInfo = &Ghost.m_RenderInfo; if(aSkinName[0] != '\0') { - const CSkin *pSkin = m_pClient->m_Skins.Find(aSkinName); - pRenderInfo->m_OriginalRenderSkin = pSkin->m_OriginalSkin; - pRenderInfo->m_ColorableRenderSkin = pSkin->m_ColorableSkin; - pRenderInfo->m_BloodColor = pSkin->m_BloodColor; - pRenderInfo->m_SkinMetrics = pSkin->m_Metrics; + pRenderInfo->Apply(m_pClient->m_Skins.Find(aSkinName)); } else { diff --git a/src/game/client/components/ghost.h b/src/game/client/components/ghost.h index c7582523e8..e1c263677c 100644 --- a/src/game/client/components/ghost.h +++ b/src/game/client/components/ghost.h @@ -155,6 +155,7 @@ class CGhost : public CComponent virtual void OnRender() override; virtual void OnConsoleInit() override; virtual void OnReset() override; + virtual void OnRefreshSkins() override; virtual void OnMessage(int MsgType, void *pRawMsg) override; virtual void OnMapLoad() override; virtual void OnShutdown() override; @@ -175,8 +176,6 @@ class CGhost : public CComponent class IGhostRecorder *GhostRecorder() const { return m_pGhostRecorder; } int GetLastRaceTick() const; - - void RefindSkins(); }; #endif diff --git a/src/game/client/components/hud.cpp b/src/game/client/components/hud.cpp index 4b4648f71b..91aa908424 100644 --- a/src/game/client/components/hud.cpp +++ b/src/game/client/components/hud.cpp @@ -1,5 +1,6 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include #include #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +30,13 @@ CHud::CHud() m_FrameTimeAvg = 0.0f; m_FPSTextContainerIndex.Reset(); m_DDRaceEffectsTextContainerIndex.Reset(); + m_PlayerAngleTextContainerIndex.Reset(); + + for(int i = 0; i < 2; i++) + { + m_aPlayerSpeedTextContainers[i].Reset(); + m_aPlayerPositionContainers[i].Reset(); + } } void CHud::ResetHudContainers() @@ -44,6 +53,12 @@ void CHud::ResetHudContainers() TextRender()->DeleteTextContainer(m_FPSTextContainerIndex); TextRender()->DeleteTextContainer(m_DDRaceEffectsTextContainerIndex); + TextRender()->DeleteTextContainer(m_PlayerAngleTextContainerIndex); + for(int i = 0; i < 2; i++) + { + TextRender()->DeleteTextContainer(m_aPlayerSpeedTextContainers[i]); + TextRender()->DeleteTextContainer(m_aPlayerPositionContainers[i]); + } } void CHud::OnWindowResize() @@ -61,6 +76,8 @@ void CHud::OnReset() m_ServerRecord = -1.0f; m_aPlayerRecord[0] = -1.0f; m_aPlayerRecord[1] = -1.0f; + m_aLastPlayerSpeedChange[0] = ESpeedChange::NONE; + m_aLastPlayerSpeedChange[1] = ESpeedChange::NONE; ResetHudContainers(); } @@ -171,8 +188,8 @@ void CHud::RenderScoreHud() if(GameFlags & GAMEFLAG_TEAMS && m_pClient->m_Snap.m_pGameDataObj) { char aScoreTeam[2][16]; - str_from_int(m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreRed, aScoreTeam[TEAM_RED]); - str_from_int(m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreBlue, aScoreTeam[TEAM_BLUE]); + str_format(aScoreTeam[TEAM_RED], sizeof(aScoreTeam[TEAM_RED]), "%d", m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreRed); + str_format(aScoreTeam[TEAM_BLUE], sizeof(aScoreTeam[TEAM_BLUE]), "%d", m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreBlue); bool aRecreateTeamScore[2] = {str_comp(aScoreTeam[0], m_aScoreInfo[0].m_aScoreText) != 0, str_comp(aScoreTeam[1], m_aScoreInfo[1].m_aScoreText) != 0}; @@ -186,7 +203,7 @@ void CHud::RenderScoreHud() if(aRecreateTeamScore[t]) { m_aScoreInfo[t].m_ScoreTextWidth = TextRender()->TextWidth(14.0f, aScoreTeam[t == 0 ? TEAM_RED : TEAM_BLUE], -1, -1.0f); - mem_copy(m_aScoreInfo[t].m_aScoreText, aScoreTeam[t == 0 ? TEAM_RED : TEAM_BLUE], sizeof(m_aScoreInfo[t].m_aScoreText)); + str_copy(m_aScoreInfo[t].m_aScoreText, aScoreTeam[t == 0 ? TEAM_RED : TEAM_BLUE]); RecreateRect = true; } } @@ -203,9 +220,9 @@ void CHud::RenderScoreHud() Graphics()->DeleteQuadContainer(m_aScoreInfo[t].m_RoundRectQuadContainerIndex); if(t == 0) - Graphics()->SetColor(1.0f, 0.0f, 0.0f, 0.25f); + Graphics()->SetColor(0.975f, 0.17f, 0.17f, 0.3f); else - Graphics()->SetColor(0.0f, 0.0f, 1.0f, 0.25f); + Graphics()->SetColor(0.17f, 0.46f, 0.975f, 0.3f); m_aScoreInfo[t].m_RoundRectQuadContainerIndex = Graphics()->CreateRectQuadContainer(m_Width - ScoreWidthMax - ImageSize - 2 * Split, StartY + t * 20, ScoreWidthMax + ImageSize + 2 * Split, ScoreSingleBoxHeight, 5.0f, IGraphics::CORNER_L); } Graphics()->TextureClear(); @@ -244,11 +261,11 @@ void CHud::RenderScoreHud() else if(aFlagCarrier[t] >= 0) { // draw name of the flag holder - int ID = aFlagCarrier[t] % MAX_CLIENTS; - const char *pName = m_pClient->m_aClients[ID].m_aName; + int Id = aFlagCarrier[t] % MAX_CLIENTS; + const char *pName = m_pClient->m_aClients[Id].m_aName; if(str_comp(pName, m_aScoreInfo[t].m_aPlayerNameText) != 0 || RecreateRect) { - mem_copy(m_aScoreInfo[t].m_aPlayerNameText, pName, sizeof(m_aScoreInfo[t].m_aPlayerNameText)); + str_copy(m_aScoreInfo[t].m_aPlayerNameText, pName); float w = TextRender()->TextWidth(8.0f, pName, -1, -1.0f); @@ -266,12 +283,12 @@ void CHud::RenderScoreHud() } // draw tee of the flag holder - CTeeRenderInfo TeeInfo = m_pClient->m_aClients[ID].m_RenderInfo; + CTeeRenderInfo TeeInfo = m_pClient->m_aClients[Id].m_RenderInfo; TeeInfo.m_Size = ScoreSingleBoxHeight; const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); + CRenderTools::GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); vec2 TeeRenderPos(m_Width - ScoreWidthMax - TeeInfo.m_Size / 2 - Split, StartY + (t * 20) + ScoreSingleBoxHeight / 2.0f + OffsetToMid.y); RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos); @@ -291,7 +308,7 @@ void CHud::RenderScoreHud() if(m_pClient->m_Snap.m_apInfoByScore[i]->m_Team != TEAM_SPECTATORS) { apPlayerInfo[t] = m_pClient->m_Snap.m_apInfoByScore[i]; - if(apPlayerInfo[t]->m_ClientID == m_pClient->m_Snap.m_LocalClientID) + if(apPlayerInfo[t]->m_ClientId == m_pClient->m_Snap.m_LocalClientId) Local = t; ++t; } @@ -303,7 +320,7 @@ void CHud::RenderScoreHud() { if(m_pClient->m_Snap.m_apInfoByScore[i]->m_Team != TEAM_SPECTATORS) ++aPos[1]; - if(m_pClient->m_Snap.m_apInfoByScore[i]->m_ClientID == m_pClient->m_Snap.m_LocalClientID) + if(m_pClient->m_Snap.m_apInfoByScore[i]->m_ClientId == m_pClient->m_Snap.m_LocalClientId) { apPlayerInfo[1] = m_pClient->m_Snap.m_apInfoByScore[i]; Local = 1; @@ -316,7 +333,9 @@ void CHud::RenderScoreHud() { if(apPlayerInfo[t]) { - if(m_pClient->m_GameInfo.m_TimeScore && g_Config.m_ClDDRaceScoreBoard) + if(Client()->IsSixup() && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & protocol7::GAMEFLAG_RACE) + str_time((int64_t)absolute(apPlayerInfo[t]->m_Score) / 10, TIME_MINS_CENTISECS, aScore[t], sizeof(aScore[t])); + else if(m_pClient->m_GameInfo.m_TimeScore) { if(apPlayerInfo[t]->m_Score != -9999) str_time((int64_t)absolute(apPlayerInfo[t]->m_Score) * 100, TIME_HOURS, aScore[t], sizeof(aScore[t])); @@ -324,15 +343,14 @@ void CHud::RenderScoreHud() aScore[t][0] = 0; } else - str_from_int(apPlayerInfo[t]->m_Score, aScore[t]); + str_format(aScore[t], sizeof(aScore[t]), "%d", apPlayerInfo[t]->m_Score); } else aScore[t][0] = 0; } - static int LocalClientID = -1; - bool RecreateScores = str_comp(aScore[0], m_aScoreInfo[0].m_aScoreText) != 0 || str_comp(aScore[1], m_aScoreInfo[1].m_aScoreText) != 0 || LocalClientID != m_pClient->m_Snap.m_LocalClientID; - LocalClientID = m_pClient->m_Snap.m_LocalClientID; + bool RecreateScores = str_comp(aScore[0], m_aScoreInfo[0].m_aScoreText) != 0 || str_comp(aScore[1], m_aScoreInfo[1].m_aScoreText) != 0 || m_LastLocalClientId != m_pClient->m_Snap.m_LocalClientId; + m_LastLocalClientId = m_pClient->m_Snap.m_LocalClientId; bool RecreateRect = ForceScoreInfoInit; for(int t = 0; t < 2; t++) @@ -340,16 +358,16 @@ void CHud::RenderScoreHud() if(RecreateScores) { m_aScoreInfo[t].m_ScoreTextWidth = TextRender()->TextWidth(14.0f, aScore[t], -1, -1.0f); - mem_copy(m_aScoreInfo[t].m_aScoreText, aScore[t], sizeof(m_aScoreInfo[t].m_aScoreText)); + str_copy(m_aScoreInfo[t].m_aScoreText, aScore[t]); RecreateRect = true; } if(apPlayerInfo[t]) { - int ID = apPlayerInfo[t]->m_ClientID; - if(ID >= 0 && ID < MAX_CLIENTS) + int Id = apPlayerInfo[t]->m_ClientId; + if(Id >= 0 && Id < MAX_CLIENTS) { - const char *pName = m_pClient->m_aClients[ID].m_aName; + const char *pName = m_pClient->m_aClients[Id].m_aName; if(str_comp(pName, m_aScoreInfo[t].m_aPlayerNameText) != 0) RecreateRect = true; } @@ -406,13 +424,13 @@ void CHud::RenderScoreHud() if(apPlayerInfo[t]) { // draw name - int ID = apPlayerInfo[t]->m_ClientID; - if(ID >= 0 && ID < MAX_CLIENTS) + int Id = apPlayerInfo[t]->m_ClientId; + if(Id >= 0 && Id < MAX_CLIENTS) { - const char *pName = m_pClient->m_aClients[ID].m_aName; + const char *pName = m_pClient->m_aClients[Id].m_aName; if(RecreateRect) { - mem_copy(m_aScoreInfo[t].m_aPlayerNameText, pName, sizeof(m_aScoreInfo[t].m_aPlayerNameText)); + str_copy(m_aScoreInfo[t].m_aPlayerNameText, pName); CTextCursor Cursor; float w = TextRender()->TextWidth(8.0f, pName, -1, -1.0f); @@ -429,12 +447,12 @@ void CHud::RenderScoreHud() } // draw tee - CTeeRenderInfo TeeInfo = m_pClient->m_aClients[ID].m_RenderInfo; + CTeeRenderInfo TeeInfo = m_pClient->m_aClients[Id].m_RenderInfo; TeeInfo.m_Size = ScoreSingleBoxHeight; const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); + CRenderTools::GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); vec2 TeeRenderPos(m_Width - ScoreWidthMax - TeeInfo.m_Size / 2 - Split, StartY + (t * 20) + ScoreSingleBoxHeight / 2.0f + OffsetToMid.y); RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos); @@ -450,7 +468,7 @@ void CHud::RenderScoreHud() str_format(aBuf, sizeof(aBuf), "%d.", aPos[t]); if(RecreateRect) { - mem_copy(m_aScoreInfo[t].m_aRankText, aBuf, sizeof(m_aScoreInfo[t].m_aRankText)); + str_copy(m_aScoreInfo[t].m_aRankText, aBuf); CTextCursor Cursor; TextRender()->SetCursor(&Cursor, m_Width - ScoreWidthMax - ImageSize - Split - PosSize, StartY + t * 20 + (18.f - 10.f) / 2.f, 10.0f, TEXTFLAG_RENDER); @@ -484,7 +502,7 @@ void CHud::RenderWarmupTimer() if(Seconds < 5) str_format(aBuf, sizeof(aBuf), "%d.%d", Seconds, (m_pClient->m_Snap.m_pGameInfoObj->m_WarmupTimer * 10 / Client()->GameTickSpeed()) % 10); else - str_from_int(Seconds, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", Seconds); w = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); TextRender()->Text(150 * Graphics()->ScreenAspect() + -w / 2, 75, FontSize, aBuf, -1.0f); } @@ -492,13 +510,18 @@ void CHud::RenderWarmupTimer() void CHud::RenderTextInfo() { - if(g_Config.m_ClShowfps) + int Showfps = g_Config.m_ClShowfps; +#if defined(CONF_VIDEORECORDER) + if(IVideo::Current()) + Showfps = 0; +#endif + if(Showfps) { // calculate avg. fps m_FrameTimeAvg = m_FrameTimeAvg * 0.9f + Client()->RenderFrameTime() * 0.1f; char aBuf[64]; int FrameTime = (int)(1.0f / m_FrameTimeAvg + 0.5f); - str_from_int(FrameTime, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", FrameTime); static float s_TextWidth0 = TextRender()->TextWidth(12.f, "0", -1, -1.0f); static float s_TextWidth00 = TextRender()->TextWidth(12.f, "00", -1, -1.0f); @@ -527,132 +550,8 @@ void CHud::RenderTextInfo() if(g_Config.m_ClShowpred && Client()->State() != IClient::STATE_DEMOPLAYBACK) { char aBuf[64]; - str_from_int(Client()->GetPredictionTime(), aBuf); - TextRender()->Text(m_Width - 10 - TextRender()->TextWidth(12, aBuf, -1, -1.0f), g_Config.m_ClShowfps ? 20 : 5, 12, aBuf, -1.0f); - } - - //initial idea from tater - tater uses "notifyWhenLast" aswell, i dont like that - because it seems scripty to use (alerts you when everyone except you is in freeze) - - if((g_Config.m_ClShowFrozenText > 0 || g_Config.m_ClShowFrozenHud > 0) && GameClient()->m_GameInfo.m_EntitiesDDRace) - { - int NumInTeam = 0; - int NumFrozen = 0; - int LocalTeamID = m_pClient->m_Snap.m_SpecInfo.m_Active == 1 && m_pClient->m_Snap.m_SpecInfo.m_SpectatorID != -1 ? - m_pClient->m_Teams.Team(m_pClient->m_Snap.m_SpecInfo.m_SpectatorID) : - m_pClient->m_Teams.Team(m_pClient->m_Snap.m_LocalClientID); - - for(int i = 0; i < MAX_CLIENTS; i++) - { - if(!m_pClient->m_Snap.m_apPlayerInfos[i]) - continue; - - if(m_pClient->m_Teams.Team(i) == LocalTeamID) - { - NumInTeam++; - if(m_pClient->m_aClients[i].m_FreezeEnd > 0 || m_pClient->m_aClients[i].m_DeepFrozen) - NumFrozen++; - } - } - - char aBuf[64]; - if(g_Config.m_ClShowFrozenText == 1) - str_format(aBuf, sizeof(aBuf), "%d / %d", NumInTeam - NumFrozen, NumInTeam); - else if(g_Config.m_ClShowFrozenText == 2) - str_format(aBuf, sizeof(aBuf), "%d / %d", NumFrozen, NumInTeam); - if(g_Config.m_ClShowFrozenText > 0) - TextRender()->Text(m_Width / 2 - TextRender()->TextWidth(10, aBuf, -1, -1.0f) / 2, 12, 10, aBuf, -1.0f); - - if(g_Config.m_ClShowFrozenHud > 0 && !m_pClient->m_Scoreboard.Active() && !(LocalTeamID == 0 && g_Config.m_ClFrozenHudTeamOnly)) - { - CTeeRenderInfo FreezeInfo; - const CSkin *pSkin = m_pClient->m_Skins.Find("x_ninja"); - FreezeInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin; - FreezeInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin; - FreezeInfo.m_BloodColor = pSkin->m_BloodColor; - FreezeInfo.m_SkinMetrics = pSkin->m_Metrics; - FreezeInfo.m_ColorBody = ColorRGBA(1, 1, 1); - FreezeInfo.m_ColorFeet = ColorRGBA(1, 1, 1); - FreezeInfo.m_CustomColoredSkin = false; - - float progressiveOffset = 0.0f; - float TeeSize = g_Config.m_ClFrozenHudTeeSize; - int MaxTees = (int)(8.3 * (m_Width / m_Height) * 13.0f / TeeSize); - if(!g_Config.m_ClShowfps && !g_Config.m_ClShowpred) - MaxTees = (int)(9.5 * (m_Width / m_Height) * 13.0f / TeeSize); - int MaxRows = g_Config.m_ClFrozenMaxRows; - float StartPos = m_Width / 2 + 38.0f * (m_Width / m_Height) / 1.78; - - int TotalRows = std::min(MaxRows, (NumInTeam + MaxTees - 1) / MaxTees); - //create quad layer a little better than tater >:3 - Graphics()->TextureClear(); - Graphics()->QuadsBegin(); - Graphics()->SetColor(0.0f, 0.0f, 0.0f, 0.35f); - Graphics()->DrawRectExt(StartPos - TeeSize / 2, 0.0f, TeeSize * std::min(NumInTeam, MaxTees), TeeSize + 2.0f + (TotalRows - 1) * TeeSize, 4.0f, IGraphics::CORNER_B); - Graphics()->QuadsEnd(); - - bool Overflow = NumInTeam > MaxTees * MaxRows; - - int NumDisplayed = 0; - int NumInRow = 0; - int CurrentRow = 0; - - for(int OverflowIndex = 0; OverflowIndex < 1 + Overflow; OverflowIndex++) - { - for(int i = 0; i < MAX_CLIENTS && NumDisplayed < MaxTees * MaxRows; i++) - { - if(!m_pClient->m_Snap.m_apPlayerInfos[i]) - continue; - if(m_pClient->m_Teams.Team(i) == LocalTeamID) - { - bool Frozen = false; - CTeeRenderInfo TeeInfo = m_pClient->m_aClients[i].m_RenderInfo; - if(m_pClient->m_aClients[i].m_FreezeEnd > 0 || m_pClient->m_aClients[i].m_DeepFrozen) - { - if(!g_Config.m_ClShowFrozenHudSkins) - TeeInfo = FreezeInfo; - Frozen = true; - } - - if(Overflow && Frozen && OverflowIndex == 0) - continue; - if(Overflow && !Frozen && OverflowIndex == 1) - continue; - - NumDisplayed++; - NumInRow++; - if(NumInRow > MaxTees) - { - NumInRow = 1; - progressiveOffset = 0.0f; - CurrentRow++; - } - - TeeInfo.m_Size = TeeSize; - const CAnimState *pIdleState = CAnimState::GetIdle(); - vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); - vec2 TeeRenderPos(StartPos + progressiveOffset, TeeSize * (0.7f) + CurrentRow * TeeSize); - float Alpha = 1.0f; - CNetObj_Character CurChar = m_pClient->m_aClients[i].m_RenderCur; - if(g_Config.m_ClShowFrozenHudSkins && Frozen) - { - Alpha = 0.6f; - TeeInfo.m_ColorBody.r *= 0.4; - TeeInfo.m_ColorBody.g *= 0.4; - TeeInfo.m_ColorBody.b *= 0.4; - TeeInfo.m_ColorFeet.r *= 0.4; - TeeInfo.m_ColorFeet.g *= 0.4; - TeeInfo.m_ColorFeet.b *= 0.4; - } - if(Frozen) - RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_HAPPY, vec2(1.0f, 0.0f), TeeRenderPos, Alpha); - else - RenderTools()->RenderTee(pIdleState, &TeeInfo, CurChar.m_Emote, vec2(1.0f, 0.0f), TeeRenderPos); - progressiveOffset += TeeSize; - } - } - } - } + str_format(aBuf, sizeof(aBuf), "%d", Client()->GetPredictionTime()); + TextRender()->Text(m_Width - 10 - TextRender()->TextWidth(12, aBuf, -1, -1.0f), Showfps ? 20 : 5, 12, aBuf, -1.0f); } } @@ -660,7 +559,7 @@ void CHud::RenderConnectionWarning() { if(Client()->ConnectionProblems()) { - const char *pText = Localize("Connection Problems..."); + const char *pText = Localize("Connection Problems…"); float w = TextRender()->TextWidth(24, pText, -1, -1.0f); TextRender()->Text(150 * Graphics()->ScreenAspect() - w / 2, 50, 24, pText, -1.0f); } @@ -686,57 +585,6 @@ void CHud::RenderTeambalanceWarning() } } -void CHud::RenderVoting() -{ - if((!g_Config.m_ClShowVotesAfterVoting && !m_pClient->m_Scoreboard.Active() && m_pClient->m_Voting.TakenChoice()) || !m_pClient->m_Voting.IsVoting() || Client()->State() == IClient::STATE_DEMOPLAYBACK) - return; - - Graphics()->DrawRect(-10, 60 - 2, 100 + 10 + 4 + 5, 46, ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_ALL, 5.0f); - - TextRender()->TextColor(TextRender()->DefaultTextColor()); - - CTextCursor Cursor; - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), Localize("%ds left"), m_pClient->m_Voting.SecondsLeft()); - float tw = TextRender()->TextWidth(6, aBuf, -1, -1.0f); - TextRender()->SetCursor(&Cursor, 5.0f + 100.0f - tw, 60.0f, 6.0f, TEXTFLAG_RENDER); - TextRender()->TextEx(&Cursor, aBuf, -1); - - TextRender()->SetCursor(&Cursor, 5.0f, 60.0f, 6.0f, TEXTFLAG_RENDER); - Cursor.m_LineWidth = 100.0f - tw; - Cursor.m_MaxLines = 3; - TextRender()->TextEx(&Cursor, m_pClient->m_Voting.VoteDescription(), -1); - - // reason - str_format(aBuf, sizeof(aBuf), "%s %s", Localize("Reason:"), m_pClient->m_Voting.VoteReason()); - TextRender()->SetCursor(&Cursor, 5.0f, 79.0f, 6.0f, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END); - Cursor.m_LineWidth = 100.0f; - TextRender()->TextEx(&Cursor, aBuf, -1); - - CUIRect Base = {5, 88, 100, 4}; - m_pClient->m_Voting.RenderBars(Base, false); - - char aKey[64]; - m_pClient->m_Binds.GetKey("vote yes", aKey, sizeof(aKey)); - - str_format(aBuf, sizeof(aBuf), "%s - %s", aKey, Localize("Vote yes")); - Base.y += Base.h; - Base.h = 12.0f; - if(m_pClient->m_Voting.TakenChoice() == 1) - TextRender()->TextColor(ColorRGBA(0.2f, 0.9f, 0.2f, 0.85f)); - UI()->DoLabel(&Base, aBuf, 6.0f, TEXTALIGN_ML); - - TextRender()->TextColor(TextRender()->DefaultTextColor()); - - m_pClient->m_Binds.GetKey("vote no", aKey, sizeof(aKey)); - str_format(aBuf, sizeof(aBuf), "%s - %s", Localize("Vote no"), aKey); - if(m_pClient->m_Voting.TakenChoice() == -1) - TextRender()->TextColor(ColorRGBA(0.9f, 0.2f, 0.2f, 0.85f)); - UI()->DoLabel(&Base, aBuf, 6.0f, TEXTALIGN_MR); - - TextRender()->TextColor(TextRender()->DefaultTextColor()); -} - void CHud::RenderCursor() { if(!m_pClient->m_Snap.m_pLocalCharacter || Client()->State() == IClient::STATE_DEMOPLAYBACK) @@ -919,22 +767,24 @@ void CHud::PreparePlayerStateQuads() m_DummyHammerOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f); m_DummyCopyOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f); - // Quad for displaying practice mode + // Quads for displaying team modes m_PracticeModeOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f); + m_LockModeOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f); + m_Team0ModeOffset = RenderTools()->QuadContainerAddSprite(m_HudQuadContainerIndex, 0.f, 0.f, 12.f, 12.f); } -void CHud::RenderPlayerState(const int ClientID) +void CHud::RenderPlayerState(const int ClientId) { Graphics()->SetColor(1.f, 1.f, 1.f, 1.f); // pCharacter contains the predicted character for local players or the last snap for players who are spectated - CCharacterCore *pCharacter = &m_pClient->m_aClients[ClientID].m_Predicted; - CNetObj_Character *pPlayer = &m_pClient->m_aClients[ClientID].m_RenderCur; + CCharacterCore *pCharacter = &m_pClient->m_aClients[ClientId].m_Predicted; + CNetObj_Character *pPlayer = &m_pClient->m_aClients[ClientId].m_RenderCur; int TotalJumpsToDisplay = 0; if(g_Config.m_ClShowhudJumpsIndicator) { int AvailableJumpsToDisplay; - if(m_pClient->m_Snap.m_aCharacters[ClientID].m_HasExtendedDisplayInfo) + if(m_pClient->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo) { bool Grounded = false; if(Collision()->CheckPoint(pPlayer->m_X + CCharacterCore::PhysicalSize() / 2, @@ -980,7 +830,7 @@ void CHud::RenderPlayerState(const int ClientID) } else { - TotalJumpsToDisplay = AvailableJumpsToDisplay = absolute(m_pClient->m_Snap.m_aCharacters[ClientID].m_ExtendedData.m_Jumps); + TotalJumpsToDisplay = AvailableJumpsToDisplay = absolute(m_pClient->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Jumps); } // render available and used jumps @@ -1033,7 +883,7 @@ void CHud::RenderPlayerState(const int ClientID) { const int Max = g_pData->m_Weapons.m_Ninja.m_Duration * Client()->GameTickSpeed() / 1000; float NinjaProgress = clamp(pCharacter->m_Ninja.m_ActivationTick + g_pData->m_Weapons.m_Ninja.m_Duration * Client()->GameTickSpeed() / 1000 - Client()->GameTick(g_Config.m_ClDummy), 0, Max) / (float)Max; - if(NinjaProgress > 0.0f && m_pClient->m_Snap.m_aCharacters[ClientID].m_HasExtendedDisplayInfo) + if(NinjaProgress > 0.0f && m_pClient->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo) { RenderNinjaBarPos(x, y - 12, 6.f, 24.f, NinjaProgress); } @@ -1159,12 +1009,24 @@ void CHud::RenderPlayerState(const int ClientID) { y += 12; } - if(m_pClient->m_Snap.m_aCharacters[ClientID].m_HasExtendedDisplayInfo && m_pClient->m_Snap.m_aCharacters[ClientID].m_ExtendedData.m_Flags & CHARACTERFLAG_PRACTICE_MODE) + if(m_pClient->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo && m_pClient->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Flags & CHARACTERFLAG_LOCK_MODE) + { + Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudLockMode); + Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_LockModeOffset, x, y); + x += 12; + } + if(m_pClient->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo && m_pClient->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Flags & CHARACTERFLAG_PRACTICE_MODE) { Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudPracticeMode); Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_PracticeModeOffset, x, y); x += 12; } + if(m_pClient->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo && m_pClient->m_Snap.m_aCharacters[ClientId].m_ExtendedData.m_Flags & CHARACTERFLAG_TEAM0_MODE) + { + Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudTeam0Mode); + Graphics()->RenderQuadContainerAsSprite(m_HudQuadContainerIndex, m_Team0ModeOffset, x, y); + x += 12; + } if(pCharacter->m_DeepFrozen) { Graphics()->TextureSet(m_pClient->m_HudSkin.m_SpriteHudDeepFrozen); @@ -1178,7 +1040,7 @@ void CHud::RenderPlayerState(const int ClientID) } } -void CHud::RenderNinjaBarPos(const float x, float y, const float width, const float height, float Progress, const float Alpha) +void CHud::RenderNinjaBarPos(const float x, float y, const float Width, const float Height, float Progress, const float Alpha) { Progress = clamp(Progress, 0.0f, 1.0f); @@ -1187,9 +1049,9 @@ void CHud::RenderNinjaBarPos(const float x, float y, const float width, const fl const float RestPct = 0.5f; const float ProgPct = 0.5f; - const float EndHeight = width; // to keep the correct scale - the width of the sprite is as long as the height - const float BarWidth = width; - const float WholeBarHeight = height; + const float EndHeight = Width; // to keep the correct scale - the width of the sprite is as long as the height + const float BarWidth = Width; + const float WholeBarHeight = Height; const float MiddleBarHeight = WholeBarHeight - (EndHeight * 2.0f); const float EndProgressHeight = EndHeight * ProgPct; const float EndRestHeight = EndHeight * RestPct; @@ -1401,7 +1263,29 @@ inline float CHud::GetMovementInformationBoxHeight() return BoxHeight; } -void CHud::RenderMovementInformation(const int ClientID) +void CHud::UpdateMovementInformationTextContainer(STextContainerIndex &TextContainer, float FontSize, float Value, char *pPrevValue, size_t Size) +{ + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "%.2f", Value); + + if(!TextContainer.Valid() || str_comp(pPrevValue, aBuf) != 0) + { + CTextCursor Cursor; + TextRender()->SetCursor(&Cursor, 0, 0, FontSize, TEXTFLAG_RENDER); + TextRender()->RecreateTextContainer(TextContainer, &Cursor, aBuf); + str_copy(pPrevValue, aBuf, Size); + } +} + +void CHud::RenderMovementInformationTextContainer(STextContainerIndex &TextContainer, const ColorRGBA &Color, float X, float Y) +{ + if(TextContainer.Valid()) + { + TextRender()->RenderTextContainer(TextContainer, Color, TextRender()->DefaultTextOutlineColor(), X - TextRender()->GetBoundingBoxTextContainer(TextContainer).m_W, Y); + } +} + +void CHud::RenderMovementInformation(const int ClientId) { // Draw the infomations depending on settings: Position, speed and target angle // This display is only to present the available information from the last snapshot, not to interpolate or predict @@ -1424,8 +1308,8 @@ void CHud::RenderMovementInformation(const int ClientID) Graphics()->DrawRect(StartX, StartY, BoxWidth, BoxHeight, ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_L, 5.0f); - const CNetObj_Character *pPrevChar = &m_pClient->m_Snap.m_aCharacters[ClientID].m_Prev; - const CNetObj_Character *pCurChar = &m_pClient->m_Snap.m_aCharacters[ClientID].m_Cur; + const CNetObj_Character *pPrevChar = &m_pClient->m_Snap.m_aCharacters[ClientId].m_Prev; + const CNetObj_Character *pCurChar = &m_pClient->m_Snap.m_aCharacters[ClientId].m_Cur; const float IntraTick = Client()->IntraGameTick(g_Config.m_ClDummy); // To make the player position relative to blocks we need to divide by the block size @@ -1452,35 +1336,16 @@ void CHud::RenderMovementInformation(const int ClientID) DisplaySpeedX *= Ramp; float DisplaySpeedY = VelspeedY / 32; - float Angle = m_pClient->m_Players.GetPlayerTargetAngle(pPrevChar, pCurChar, ClientID, IntraTick); + float Angle = m_pClient->m_Players.GetPlayerTargetAngle(pPrevChar, pCurChar, ClientId, IntraTick); if(Angle < 0) { Angle += 2.0f * pi; } float DisplayAngle = Angle * 180.0f / pi; - char aBuf[128]; - float w; - float y = StartY + LineSpacer * 2; float xl = StartX + 2; float xr = m_Width - 2; - int DigitsIndex; - - static float s_TextWidth0 = TextRender()->TextWidth(Fontsize, "0.00", -1, -1.0f); - static float s_TextWidth00 = TextRender()->TextWidth(Fontsize, "00.00", -1, -1.0f); - static float s_TextWidth000 = TextRender()->TextWidth(Fontsize, "000.00", -1, -1.0f); - static float s_TextWidth0000 = TextRender()->TextWidth(Fontsize, "0000.00", -1, -1.0f); - static float s_TextWidth00000 = TextRender()->TextWidth(Fontsize, "00000.00", -1, -1.0f); - static float s_TextWidth000000 = TextRender()->TextWidth(Fontsize, "000000.00", -1, -1.0f); - static float s_aTextWidth[6] = {s_TextWidth0, s_TextWidth00, s_TextWidth000, s_TextWidth0000, s_TextWidth00000, s_TextWidth000000}; - static float s_TextWidthMinus0 = TextRender()->TextWidth(Fontsize, "-0.00", -1, -1.0f); - static float s_TextWidthMinus00 = TextRender()->TextWidth(Fontsize, "-00.00", -1, -1.0f); - static float s_TextWidthMinus000 = TextRender()->TextWidth(Fontsize, "-000.00", -1, -1.0f); - static float s_TextWidthMinus0000 = TextRender()->TextWidth(Fontsize, "-0000.00", -1, -1.0f); - static float s_TextWidthMinus00000 = TextRender()->TextWidth(Fontsize, "-00000.00", -1, -1.0f); - static float s_TextWidthMinus000000 = TextRender()->TextWidth(Fontsize, "-000000.00", -1, -1.0f); - static float s_aTextWidthMinus[6] = {s_TextWidthMinus0, s_TextWidthMinus00, s_TextWidthMinus000, s_TextWidthMinus0000, s_TextWidthMinus00000, s_TextWidthMinus000000}; if(g_Config.m_ClShowhudPlayerPosition) { @@ -1488,17 +1353,13 @@ void CHud::RenderMovementInformation(const int ClientID) y += MOVEMENT_INFORMATION_LINE_HEIGHT; TextRender()->Text(xl, y, Fontsize, "X:", -1.0f); - str_format(aBuf, sizeof(aBuf), "%.2f", Pos.x); - DigitsIndex = GetDigitsIndex(Pos.x, 5); - w = (Pos.x < 0) ? s_aTextWidthMinus[DigitsIndex] : s_aTextWidth[DigitsIndex]; - TextRender()->Text(xr - w, y, Fontsize, aBuf, -1.0f); + UpdateMovementInformationTextContainer(m_aPlayerPositionContainers[0], Fontsize, Pos.x, m_aaPlayerPositionText[0], sizeof(m_aaPlayerPositionText[0])); + RenderMovementInformationTextContainer(m_aPlayerPositionContainers[0], TextRender()->DefaultTextColor(), xr, y); y += MOVEMENT_INFORMATION_LINE_HEIGHT; TextRender()->Text(xl, y, Fontsize, "Y:", -1.0f); - str_format(aBuf, sizeof(aBuf), "%.2f", Pos.y); - DigitsIndex = GetDigitsIndex(Pos.y, 5); - w = (Pos.y < 0) ? s_aTextWidthMinus[DigitsIndex] : s_aTextWidth[DigitsIndex]; - TextRender()->Text(xr - w, y, Fontsize, aBuf, -1.0f); + UpdateMovementInformationTextContainer(m_aPlayerPositionContainers[1], Fontsize, Pos.y, m_aaPlayerPositionText[1], sizeof(m_aaPlayerPositionText[1])); + RenderMovementInformationTextContainer(m_aPlayerPositionContainers[1], TextRender()->DefaultTextColor(), xr, y); y += MOVEMENT_INFORMATION_LINE_HEIGHT; } @@ -1507,29 +1368,30 @@ void CHud::RenderMovementInformation(const int ClientID) TextRender()->Text(xl, y, Fontsize, Localize("Speed:"), -1.0f); y += MOVEMENT_INFORMATION_LINE_HEIGHT; - TextRender()->Text(xl, y, Fontsize, "X:", -1.0f); - str_format(aBuf, sizeof(aBuf), "%.2f", DisplaySpeedX); - DigitsIndex = GetDigitsIndex(DisplaySpeedX, 5); - w = (DisplaySpeedX < 0) ? s_aTextWidthMinus[DigitsIndex] : s_aTextWidth[DigitsIndex]; - TextRender()->Text(xr - w, y, Fontsize, aBuf, -1.0f); - y += MOVEMENT_INFORMATION_LINE_HEIGHT; + const char aaCoordinates[][4] = {"X:", "Y:"}; + for(int i = 0; i < 2; i++) + { + ColorRGBA Color(1, 1, 1, 1); + if(m_aLastPlayerSpeedChange[i] == ESpeedChange::INCREASE) + Color = ColorRGBA(0, 1, 0, 1); + if(m_aLastPlayerSpeedChange[i] == ESpeedChange::DECREASE) + Color = ColorRGBA(1, 0.5f, 0.5f, 1); + TextRender()->Text(xl, y, Fontsize, aaCoordinates[i], -1.0f); + UpdateMovementInformationTextContainer(m_aPlayerSpeedTextContainers[i], Fontsize, i == 0 ? DisplaySpeedX : DisplaySpeedY, m_aaPlayerSpeedText[i], sizeof(m_aaPlayerSpeedText[i])); + RenderMovementInformationTextContainer(m_aPlayerSpeedTextContainers[i], Color, xr, y); + y += MOVEMENT_INFORMATION_LINE_HEIGHT; + } - TextRender()->Text(xl, y, Fontsize, "Y:", -1.0f); - str_format(aBuf, sizeof(aBuf), "%.2f", DisplaySpeedY); - DigitsIndex = GetDigitsIndex(DisplaySpeedY, 5); - w = (DisplaySpeedY < 0) ? s_aTextWidthMinus[DigitsIndex] : s_aTextWidth[DigitsIndex]; - TextRender()->Text(xr - w, y, Fontsize, aBuf, -1.0f); - y += MOVEMENT_INFORMATION_LINE_HEIGHT; + TextRender()->TextColor(1, 1, 1, 1); } if(g_Config.m_ClShowhudPlayerAngle) { TextRender()->Text(xl, y, Fontsize, Localize("Angle:"), -1.0f); y += MOVEMENT_INFORMATION_LINE_HEIGHT; - str_format(aBuf, sizeof(aBuf), "%.2f", DisplayAngle); - DigitsIndex = GetDigitsIndex(DisplayAngle, 5); - w = (DisplayAngle < 0) ? s_aTextWidthMinus[DigitsIndex] : s_aTextWidth[DigitsIndex]; - TextRender()->Text(xr - w, y, Fontsize, aBuf, -1.0f); + + UpdateMovementInformationTextContainer(m_PlayerAngleTextContainerIndex, Fontsize, DisplayAngle, m_aPlayerAngleText, sizeof(m_aPlayerAngleText)); + RenderMovementInformationTextContainer(m_PlayerAngleTextContainerIndex, TextRender()->DefaultTextColor(), xr, y); } } @@ -1540,7 +1402,18 @@ void CHud::RenderSpectatorHud() // draw the text char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Spectate"), GameClient()->m_MultiViewActivated ? Localize("Multi-View") : m_pClient->m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW ? m_pClient->m_aClients[m_pClient->m_Snap.m_SpecInfo.m_SpectatorID].m_aName : Localize("Free-View")); + if(GameClient()->m_MultiViewActivated) + { + str_copy(aBuf, Localize("Multi-View")); + } + else if(m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) + { + str_format(aBuf, sizeof(aBuf), Localize("Following %s", "Spectating"), m_pClient->m_aClients[m_pClient->m_Snap.m_SpecInfo.m_SpectatorId].m_aName); + } + else + { + str_copy(aBuf, Localize("Free-View")); + } TextRender()->Text(m_Width - 174.0f, m_Height - 15.0f + (15.f - 8.f) / 2.f, 8.0f, aBuf, -1.0f); } @@ -1558,8 +1431,57 @@ void CHud::RenderLocalTime(float x) TextRender()->Text(x - 25.0f, (12.5f - 5.f) / 2.f, 5.0f, aTimeStr, -1.0f); } +void CHud::OnNewSnapshot() +{ + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if(!m_pClient->m_Snap.m_pGameInfoObj) + return; + + int ClientId = -1; + if(m_pClient->m_Snap.m_pLocalCharacter && !m_pClient->m_Snap.m_SpecInfo.m_Active && !(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER)) + ClientId = m_pClient->m_Snap.m_LocalClientId; + else if(m_pClient->m_Snap.m_SpecInfo.m_Active) + ClientId = m_pClient->m_Snap.m_SpecInfo.m_SpectatorId; + + if(ClientId == -1) + return; + + const CNetObj_Character *pPrevChar = &m_pClient->m_Snap.m_aCharacters[ClientId].m_Prev; + const CNetObj_Character *pCurChar = &m_pClient->m_Snap.m_aCharacters[ClientId].m_Cur; + const float IntraTick = Client()->IntraGameTick(g_Config.m_ClDummy); + ivec2 Vel = mix(ivec2(pPrevChar->m_VelX, pPrevChar->m_VelY), ivec2(pCurChar->m_VelX, pCurChar->m_VelY), IntraTick); + + CCharacter *pChar = m_pClient->m_PredictedWorld.GetCharacterById(ClientId); + if(pChar && pChar->IsGrounded()) + Vel.y = 0; + + int aVels[2] = {Vel.x, Vel.y}; + + for(int i = 0; i < 2; i++) + { + int AbsVel = abs(aVels[i]); + if(AbsVel > m_aPlayerSpeed[i]) + { + m_aLastPlayerSpeedChange[i] = ESpeedChange::INCREASE; + } + if(AbsVel < m_aPlayerSpeed[i]) + { + m_aLastPlayerSpeedChange[i] = ESpeedChange::DECREASE; + } + if(AbsVel < 2) + { + m_aLastPlayerSpeedChange[i] = ESpeedChange::NONE; + } + m_aPlayerSpeed[i] = AbsVel; + } +} + void CHud::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if(!m_pClient->m_Snap.m_pGameInfoObj) return; @@ -1579,31 +1501,31 @@ void CHud::OnRender() { RenderAmmoHealthAndArmor(m_pClient->m_Snap.m_pLocalCharacter); } - if(m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientID].m_HasExtendedData && g_Config.m_ClShowhudDDRace && GameClient()->m_GameInfo.m_HudDDRace) + if(m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientId].m_HasExtendedData && g_Config.m_ClShowhudDDRace && GameClient()->m_GameInfo.m_HudDDRace) { - RenderPlayerState(m_pClient->m_Snap.m_LocalClientID); + RenderPlayerState(m_pClient->m_Snap.m_LocalClientId); } - RenderMovementInformation(m_pClient->m_Snap.m_LocalClientID); + RenderMovementInformation(m_pClient->m_Snap.m_LocalClientId); RenderDDRaceEffects(); } else if(m_pClient->m_Snap.m_SpecInfo.m_Active) { - int SpectatorID = m_pClient->m_Snap.m_SpecInfo.m_SpectatorID; - if(SpectatorID != SPEC_FREEVIEW && g_Config.m_ClShowhudHealthAmmo) + int SpectatorId = m_pClient->m_Snap.m_SpecInfo.m_SpectatorId; + if(SpectatorId != SPEC_FREEVIEW && g_Config.m_ClShowhudHealthAmmo) { - RenderAmmoHealthAndArmor(&m_pClient->m_Snap.m_aCharacters[SpectatorID].m_Cur); + RenderAmmoHealthAndArmor(&m_pClient->m_Snap.m_aCharacters[SpectatorId].m_Cur); } - if(SpectatorID != SPEC_FREEVIEW && - m_pClient->m_Snap.m_aCharacters[SpectatorID].m_HasExtendedData && + if(SpectatorId != SPEC_FREEVIEW && + m_pClient->m_Snap.m_aCharacters[SpectatorId].m_HasExtendedData && g_Config.m_ClShowhudDDRace && (!GameClient()->m_MultiViewActivated || GameClient()->m_MultiViewShowHud) && GameClient()->m_GameInfo.m_HudDDRace) { - RenderPlayerState(SpectatorID); + RenderPlayerState(SpectatorId); } - if(SpectatorID != SPEC_FREEVIEW) + if(SpectatorId != SPEC_FREEVIEW) { - RenderMovementInformation(SpectatorID); + RenderMovementInformation(SpectatorId); } RenderSpectatorHud(); } @@ -1621,7 +1543,7 @@ void CHud::OnRender() if(Client()->State() != IClient::STATE_DEMOPLAYBACK) RenderConnectionWarning(); RenderTeambalanceWarning(); - RenderVoting(); + m_pClient->m_Voting.Render(); if(g_Config.m_ClShowRecord) RenderRecord(); } @@ -1724,7 +1646,7 @@ void CHud::RenderDDRaceEffects() } TextRender()->TextColor(TextRender()->DefaultTextColor()); } - else if(!m_ShowFinishTime && m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(g_Config.m_ClDummy)) + else if(g_Config.m_ClShowhudTimeCpDiff && !m_ShowFinishTime && m_TimeCpLastReceivedTick + Client()->GameTickSpeed() * 6 > Client()->GameTick(g_Config.m_ClDummy)) { if(m_TimeCpDiff < 0) { @@ -1773,8 +1695,7 @@ void CHud::RenderRecord() if(m_ServerRecord > 0.0f) { char aBuf[64]; - str_format(aBuf, sizeof(aBuf), Localize("Server best:")); - TextRender()->Text(5, 75, 6, aBuf, -1.0f); + TextRender()->Text(5, 75, 6, Localize("Server best:"), -1.0f); char aTime[32]; str_time_float(m_ServerRecord, TIME_HOURS_CENTISECS, aTime, sizeof(aTime)); str_format(aBuf, sizeof(aBuf), "%s%s", m_ServerRecord > 3600 ? "" : "   ", aTime); @@ -1785,8 +1706,7 @@ void CHud::RenderRecord() if(PlayerRecord > 0.0f) { char aBuf[64]; - str_format(aBuf, sizeof(aBuf), Localize("Personal best:")); - TextRender()->Text(5, 82, 6, aBuf, -1.0f); + TextRender()->Text(5, 82, 6, Localize("Personal best:"), -1.0f); char aTime[32]; str_time_float(PlayerRecord, TIME_HOURS_CENTISECS, aTime, sizeof(aTime)); str_format(aBuf, sizeof(aBuf), "%s%s", PlayerRecord > 3600 ? "" : "   ", aTime); diff --git a/src/game/client/components/hud.h b/src/game/client/components/hud.h index f292dd0b1c..23463bc724 100644 --- a/src/game/client/components/hud.h +++ b/src/game/client/components/hud.h @@ -2,7 +2,11 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #ifndef GAME_CLIENT_COMPONENTS_HUD_H #define GAME_CLIENT_COMPONENTS_HUD_H +#include +#include +#include #include +#include struct SScoreInfo { @@ -45,6 +49,20 @@ class CHud : public CComponent SScoreInfo m_aScoreInfo[2]; STextContainerIndex m_FPSTextContainerIndex; STextContainerIndex m_DDRaceEffectsTextContainerIndex; + STextContainerIndex m_PlayerAngleTextContainerIndex; + char m_aPlayerAngleText[128]; + STextContainerIndex m_aPlayerSpeedTextContainers[2]; + char m_aaPlayerSpeedText[2][128]; + int m_aPlayerSpeed[2]; + enum class ESpeedChange + { + NONE, + INCREASE, + DECREASE + }; + ESpeedChange m_aLastPlayerSpeedChange[2]; + STextContainerIndex m_aPlayerPositionContainers[2]; + char m_aaPlayerPositionText[2][128]; void RenderCursor(); @@ -57,14 +75,20 @@ class CHud : public CComponent void RenderAmmoHealthAndArmor(const CNetObj_Character *pCharacter); void PreparePlayerStateQuads(); - void RenderPlayerState(const int ClientID); + void RenderPlayerState(const int ClientId); void RenderDummyActions(); - void RenderMovementInformation(const int ClientID); + void RenderMovementInformation(const int ClientId); + + void UpdateMovementInformationTextContainer(STextContainerIndex &TextContainer, float FontSize, float Value, char *pPrevValue, size_t Size); + void RenderMovementInformationTextContainer(STextContainerIndex &TextContainer, const ColorRGBA &Color, float X, float Y); void RenderGameTimer(); void RenderPauseNotification(); void RenderSuddenDeath(); + void RenderScoreHud(); + int m_LastLocalClientId = -1; + void RenderSpectatorHud(); void RenderWarmupTimer(); void RenderLocalTime(float x); @@ -80,11 +104,12 @@ class CHud : public CComponent virtual void OnReset() override; virtual void OnRender() override; virtual void OnInit() override; + virtual void OnNewSnapshot() override; // DDRace virtual void OnMessage(int MsgType, void *pRawMsg) override; - void RenderNinjaBarPos(float x, const float y, const float width, const float height, float Progress, float Alpha = 1.0f); + void RenderNinjaBarPos(float x, const float y, const float Width, const float Height, float Progress, float Alpha = 1.0f); private: void RenderRecord(); @@ -131,6 +156,8 @@ class CHud : public CComponent int m_DummyHammerOffset; int m_DummyCopyOffset; int m_PracticeModeOffset; + int m_Team0ModeOffset; + int m_LockModeOffset; }; #endif diff --git a/src/game/client/components/infomessages.cpp b/src/game/client/components/infomessages.cpp index d461d67051..243072568a 100644 --- a/src/game/client/components/infomessages.cpp +++ b/src/game/client/components/infomessages.cpp @@ -14,11 +14,14 @@ #include #include +static constexpr float ROW_HEIGHT = 46.0f; +static constexpr float FONT_SIZE = 36.0f; + void CInfoMessages::OnWindowResize() { for(auto &InfoMsg : m_aInfoMsgs) { - DeleteTextContainers(&InfoMsg); + DeleteTextContainers(InfoMsg); } } @@ -28,16 +31,16 @@ void CInfoMessages::OnReset() for(auto &InfoMsg : m_aInfoMsgs) { InfoMsg.m_Tick = -100000; - DeleteTextContainers(&InfoMsg); + DeleteTextContainers(InfoMsg); } } -void CInfoMessages::DeleteTextContainers(CInfoMsg *pInfoMsg) +void CInfoMessages::DeleteTextContainers(CInfoMsg &InfoMsg) { - TextRender()->DeleteTextContainer(pInfoMsg->m_VictimTextContainerIndex); - TextRender()->DeleteTextContainer(pInfoMsg->m_KillerTextContainerIndex); - TextRender()->DeleteTextContainer(pInfoMsg->m_DiffTextContainerIndex); - TextRender()->DeleteTextContainer(pInfoMsg->m_TimeTextContainerIndex); + TextRender()->DeleteTextContainer(InfoMsg.m_VictimTextContainerIndex); + TextRender()->DeleteTextContainer(InfoMsg.m_KillerTextContainerIndex); + TextRender()->DeleteTextContainer(InfoMsg.m_DiffTextContainerIndex); + TextRender()->DeleteTextContainer(InfoMsg.m_TimeTextContainerIndex); } void CInfoMessages::OnInit() @@ -65,87 +68,119 @@ void CInfoMessages::OnInit() Graphics()->QuadContainerUpload(m_SpriteQuadContainerIndex); } -void CInfoMessages::AddInfoMsg(EType Type, CInfoMsg NewMsg) +CInfoMessages::CInfoMsg CInfoMessages::CreateInfoMsg(EType Type) { - NewMsg.m_Type = Type; - NewMsg.m_Tick = Client()->GameTick(g_Config.m_ClDummy); + CInfoMsg InfoMsg; + InfoMsg.m_Type = Type; + InfoMsg.m_Tick = Client()->GameTick(g_Config.m_ClDummy); - m_InfoMsgCurrent = (m_InfoMsgCurrent + 1) % MAX_INFOMSGS; - DeleteTextContainers(&m_aInfoMsgs[m_InfoMsgCurrent]); - m_aInfoMsgs[m_InfoMsgCurrent] = NewMsg; + for(int i = 0; i < MAX_KILLMSG_TEAM_MEMBERS; i++) + { + InfoMsg.m_aVictimIds[i] = -1; + InfoMsg.m_aVictimRenderInfo[i].Reset(); + } + InfoMsg.m_VictimDDTeam = 0; + InfoMsg.m_aVictimName[0] = '\0'; + InfoMsg.m_VictimTextContainerIndex.Reset(); + + InfoMsg.m_KillerId = -1; + InfoMsg.m_aKillerName[0] = '\0'; + InfoMsg.m_KillerTextContainerIndex.Reset(); + InfoMsg.m_KillerRenderInfo.Reset(); + + InfoMsg.m_Weapon = -1; + InfoMsg.m_ModeSpecial = 0; + InfoMsg.m_FlagCarrierBlue = -1; + InfoMsg.m_TeamSize = 0; + + InfoMsg.m_Diff = 0; + InfoMsg.m_aTimeText[0] = '\0'; + InfoMsg.m_aDiffText[0] = '\0'; + InfoMsg.m_TimeTextContainerIndex.Reset(); + InfoMsg.m_DiffTextContainerIndex.Reset(); + InfoMsg.m_RecordPersonal = false; + return InfoMsg; } -void CInfoMessages::CreateNamesIfNotCreated(CInfoMsg *pInfoMsg) +void CInfoMessages::AddInfoMsg(const CInfoMsg &InfoMsg) { - const float FontSize = 36.0f; - if(!pInfoMsg->m_VictimTextContainerIndex.Valid() && pInfoMsg->m_aVictimName[0] != 0) + if(InfoMsg.m_KillerId >= 0 && !InfoMsg.m_KillerRenderInfo.Valid()) + return; + for(int i = 0; i < InfoMsg.m_TeamSize; i++) { - pInfoMsg->m_VictimTextWidth = TextRender()->TextWidth(FontSize, pInfoMsg->m_aVictimName); + if(InfoMsg.m_aVictimIds[i] < 0 || !InfoMsg.m_aVictimRenderInfo[i].Valid()) + return; + } - CTextCursor Cursor; - TextRender()->SetCursor(&Cursor, 0, 0, FontSize, TEXTFLAG_RENDER); - Cursor.m_LineWidth = -1; + const float Height = 1.5f * 400.0f * 3.0f; + const float Width = Height * Graphics()->ScreenAspect(); + + float ScreenX0, ScreenY0, ScreenX1, ScreenY1; + Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); + Graphics()->MapScreen(0, 0, Width, Height); + + m_InfoMsgCurrent = (m_InfoMsgCurrent + 1) % MAX_INFOMSGS; + DeleteTextContainers(m_aInfoMsgs[m_InfoMsgCurrent]); + m_aInfoMsgs[m_InfoMsgCurrent] = InfoMsg; + CreateTextContainersIfNotCreated(m_aInfoMsgs[m_InfoMsgCurrent]); - unsigned Color = g_Config.m_ClKillMessageNormalColor; - if(pInfoMsg->m_aVictimIds[0] == m_pClient->m_Snap.m_LocalClientID) + Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); +} + +void CInfoMessages::CreateTextContainersIfNotCreated(CInfoMsg &InfoMsg) +{ + const auto &&NameColor = [&](int ClientId) -> ColorRGBA { + unsigned Color; + if(ClientId == m_pClient->m_Snap.m_LocalClientId) { Color = g_Config.m_ClKillMessageHighlightColor; } - TextRender()->TextColor(color_cast(ColorHSLA(Color))); + else + { + Color = g_Config.m_ClKillMessageNormalColor; + } + return color_cast(ColorHSLA(Color)); + }; - TextRender()->CreateTextContainer(pInfoMsg->m_VictimTextContainerIndex, &Cursor, pInfoMsg->m_aVictimName); + if(!InfoMsg.m_VictimTextContainerIndex.Valid() && InfoMsg.m_aVictimName[0] != '\0') + { + CTextCursor Cursor; + TextRender()->SetCursor(&Cursor, 0, 0, FONT_SIZE, TEXTFLAG_RENDER); + TextRender()->TextColor(NameColor(InfoMsg.m_aVictimIds[0])); + TextRender()->CreateTextContainer(InfoMsg.m_VictimTextContainerIndex, &Cursor, InfoMsg.m_aVictimName); } - if(!pInfoMsg->m_KillerTextContainerIndex.Valid() && pInfoMsg->m_aKillerName[0] != 0) + if(!InfoMsg.m_KillerTextContainerIndex.Valid() && InfoMsg.m_aKillerName[0] != '\0') { - pInfoMsg->m_KillerTextWidth = TextRender()->TextWidth(FontSize, pInfoMsg->m_aKillerName); - CTextCursor Cursor; - TextRender()->SetCursor(&Cursor, 0, 0, FontSize, TEXTFLAG_RENDER); - Cursor.m_LineWidth = -1; - - unsigned Color = g_Config.m_ClKillMessageNormalColor; - if(pInfoMsg->m_KillerID == m_pClient->m_Snap.m_LocalClientID) - { - Color = g_Config.m_ClKillMessageHighlightColor; - } - TextRender()->TextColor(color_cast(ColorHSLA(Color))); - - TextRender()->CreateTextContainer(pInfoMsg->m_KillerTextContainerIndex, &Cursor, pInfoMsg->m_aKillerName); + TextRender()->SetCursor(&Cursor, 0, 0, FONT_SIZE, TEXTFLAG_RENDER); + TextRender()->TextColor(NameColor(InfoMsg.m_KillerId)); + TextRender()->CreateTextContainer(InfoMsg.m_KillerTextContainerIndex, &Cursor, InfoMsg.m_aKillerName); } - TextRender()->TextColor(TextRender()->DefaultTextColor()); -} -void CInfoMessages::CreateFinishTextContainersIfNotCreated(CInfoMsg *pInfoMsg) -{ - const float FontSize = 36.0f; - if(!pInfoMsg->m_DiffTextContainerIndex.Valid() && pInfoMsg->m_aDiffText[0] != 0) + if(!InfoMsg.m_DiffTextContainerIndex.Valid() && InfoMsg.m_aDiffText[0] != '\0') { - pInfoMsg->m_DiffTextWidth = TextRender()->TextWidth(FontSize, pInfoMsg->m_aDiffText); CTextCursor Cursor; - TextRender()->SetCursor(&Cursor, 0, 0, FontSize, TEXTFLAG_RENDER); - Cursor.m_LineWidth = -1; + TextRender()->SetCursor(&Cursor, 0, 0, FONT_SIZE, TEXTFLAG_RENDER); - if(pInfoMsg->m_Diff > 0) - TextRender()->TextColor(1.0f, 0.5f, 0.5f, 1); // red - else if(pInfoMsg->m_Diff < 0) - TextRender()->TextColor(0.5f, 1.0f, 0.5f, 1); // green + if(InfoMsg.m_Diff > 0) + TextRender()->TextColor(1.0f, 0.5f, 0.5f, 1.0f); // red + else if(InfoMsg.m_Diff < 0) + TextRender()->TextColor(0.5f, 1.0f, 0.5f, 1.0f); // green else TextRender()->TextColor(TextRender()->DefaultTextColor()); - TextRender()->CreateTextContainer(pInfoMsg->m_DiffTextContainerIndex, &Cursor, pInfoMsg->m_aDiffText); + TextRender()->CreateTextContainer(InfoMsg.m_DiffTextContainerIndex, &Cursor, InfoMsg.m_aDiffText); } - if(!pInfoMsg->m_TimeTextContainerIndex.Valid() && pInfoMsg->m_aTimeText[0] != 0) + + if(!InfoMsg.m_TimeTextContainerIndex.Valid() && InfoMsg.m_aTimeText[0] != '\0') { - pInfoMsg->m_TimeTextWidth = TextRender()->TextWidth(FontSize, pInfoMsg->m_aTimeText); CTextCursor Cursor; - TextRender()->SetCursor(&Cursor, 0, 0, FontSize, TEXTFLAG_RENDER); - Cursor.m_LineWidth = -1; - + TextRender()->SetCursor(&Cursor, 0, 0, FONT_SIZE, TEXTFLAG_RENDER); TextRender()->TextColor(TextRender()->DefaultTextColor()); - - TextRender()->CreateTextContainer(pInfoMsg->m_TimeTextContainerIndex, &Cursor, pInfoMsg->m_aTimeText); + TextRender()->CreateTextContainer(InfoMsg.m_TimeTextContainerIndex, &Cursor, InfoMsg.m_aTimeText); } + TextRender()->TextColor(TextRender()->DefaultTextColor()); } @@ -154,400 +189,288 @@ void CInfoMessages::OnMessage(int MsgType, void *pRawMsg) if(m_pClient->m_SuppressEvents) return; - if(MsgType == NETMSGTYPE_SV_KILLMSGTEAM && g_Config.m_ClShowKillMessages) + switch(MsgType) { - CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg; - - CInfoMsg Kill{}; - - std::vector> vStrongWeakSorted; - for(int i = 0; i < MAX_CLIENTS; i++) - { - if(m_pClient->m_Teams.Team(i) == pMsg->m_Team) - { - CCharacter *pChr = m_pClient->m_GameWorld.GetCharacterByID(i); - vStrongWeakSorted.emplace_back(i, pMsg->m_First == i ? MAX_CLIENTS : pChr ? pChr->GetStrongWeakID() : 0); - } - } - - std::stable_sort(vStrongWeakSorted.begin(), vStrongWeakSorted.end(), [](auto &Left, auto &Right) { return Left.second > Right.second; }); - - Kill.m_TeamSize = vStrongWeakSorted.size(); - if(Kill.m_TeamSize > MAX_KILLMSG_TEAM_MEMBERS) - Kill.m_TeamSize = MAX_KILLMSG_TEAM_MEMBERS; - - Kill.m_VictimDDTeam = pMsg->m_Team; - for(int i = 0; i < Kill.m_TeamSize; i++) - { - if(m_pClient->m_aClients[vStrongWeakSorted[i].first].m_Active) - { - Kill.m_aVictimIds[i] = vStrongWeakSorted[i].first; - Kill.m_aVictimRenderInfo[i] = m_pClient->m_aClients[vStrongWeakSorted[i].first].m_RenderInfo; - } - else - { - Kill.m_aVictimIds[i] = -1; - Kill.m_aVictimRenderInfo[i].Reset(); - } - } - for(int i = Kill.m_TeamSize; i < MAX_KILLMSG_TEAM_MEMBERS; i++) - { - Kill.m_aVictimIds[i] = -1; - Kill.m_aVictimRenderInfo[i].Reset(); - } - str_format(Kill.m_aVictimName, sizeof(Kill.m_aVictimName), Localize("Team %d"), pMsg->m_Team); - - Kill.m_KillerID = -1; - Kill.m_aKillerName[0] = '\0'; - Kill.m_KillerRenderInfo.Reset(); - - Kill.m_Weapon = -1; - Kill.m_ModeSpecial = 0; - - Kill.m_VictimTextWidth = Kill.m_KillerTextWidth = 0.f; - - float Height = 400 * 3.0f; - float Width = Height * Graphics()->ScreenAspect(); - - float ScreenX0, ScreenY0, ScreenX1, ScreenY1; - Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); - Graphics()->MapScreen(0, 0, Width * 1.5f, Height * 1.5f); - - CreateNamesIfNotCreated(&Kill); - - bool KillMsgValid = true; - for(int i = 0; i < Kill.m_TeamSize; i++) - { - KillMsgValid = KillMsgValid && Kill.m_aVictimIds[i] >= 0 && ((Kill.m_aVictimRenderInfo[i].m_CustomColoredSkin && Kill.m_aVictimRenderInfo[i].m_ColorableRenderSkin.m_Body.IsValid()) || (!Kill.m_aVictimRenderInfo[i].m_CustomColoredSkin && Kill.m_aVictimRenderInfo[i].m_OriginalRenderSkin.m_Body.IsValid())); - } - - if(KillMsgValid) - { - AddInfoMsg(EType::TYPE_KILL, Kill); - } - else - { - DeleteTextContainers(&Kill); - } - - Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); + case NETMSGTYPE_SV_KILLMSGTEAM: + OnTeamKillMessage(static_cast(pRawMsg)); + break; + case NETMSGTYPE_SV_KILLMSG: + OnKillMessage(static_cast(pRawMsg)); + break; + case NETMSGTYPE_SV_RACEFINISH: + OnRaceFinishMessage(static_cast(pRawMsg)); + break; } +} - if(MsgType == NETMSGTYPE_SV_KILLMSG && g_Config.m_ClShowKillMessages) +void CInfoMessages::OnTeamKillMessage(const CNetMsg_Sv_KillMsgTeam *pMsg) +{ + std::vector> vStrongWeakSorted; + for(int i = 0; i < MAX_CLIENTS; i++) { - CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg; - - CInfoMsg Kill{}; - - Kill.m_TeamSize = 1; - Kill.m_aVictimIds[0] = pMsg->m_Victim; - if(Kill.m_aVictimIds[0] >= 0 && Kill.m_aVictimIds[0] < MAX_CLIENTS) - { - Kill.m_VictimDDTeam = m_pClient->m_Teams.Team(Kill.m_aVictimIds[0]); - str_copy(Kill.m_aVictimName, m_pClient->m_aClients[Kill.m_aVictimIds[0]].m_aName); - Kill.m_aVictimRenderInfo[0] = m_pClient->m_aClients[Kill.m_aVictimIds[0]].m_RenderInfo; - } - else - { - Kill.m_VictimDDTeam = 0; - Kill.m_aVictimName[0] = '\0'; - } - for(int i = Kill.m_TeamSize; i < MAX_KILLMSG_TEAM_MEMBERS; i++) - { - Kill.m_aVictimIds[i] = -1; - Kill.m_aVictimRenderInfo[i].Reset(); - } - - Kill.m_KillerID = pMsg->m_Killer; - if(Kill.m_KillerID >= 0 && Kill.m_KillerID < MAX_CLIENTS) - { - str_copy(Kill.m_aKillerName, m_pClient->m_aClients[Kill.m_KillerID].m_aName); - Kill.m_KillerRenderInfo = m_pClient->m_aClients[Kill.m_KillerID].m_RenderInfo; - } - else + if(m_pClient->m_Teams.Team(i) == pMsg->m_Team) { - Kill.m_aKillerName[0] = '\0'; - Kill.m_KillerRenderInfo.Reset(); + CCharacter *pChr = m_pClient->m_GameWorld.GetCharacterById(i); + vStrongWeakSorted.emplace_back(i, pMsg->m_First == i ? MAX_CLIENTS : pChr ? pChr->GetStrongWeakId() : 0); } + } + std::stable_sort(vStrongWeakSorted.begin(), vStrongWeakSorted.end(), [](auto &Left, auto &Right) { return Left.second > Right.second; }); - Kill.m_Weapon = pMsg->m_Weapon; - Kill.m_ModeSpecial = pMsg->m_ModeSpecial; - - Kill.m_FlagCarrierBlue = m_pClient->m_Snap.m_pGameDataObj ? m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue : -1; - - Kill.m_VictimTextWidth = Kill.m_KillerTextWidth = 0.f; - - float Height = 400 * 3.0f; - float Width = Height * Graphics()->ScreenAspect(); - - float ScreenX0, ScreenY0, ScreenX1, ScreenY1; - Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); - Graphics()->MapScreen(0, 0, Width * 1.5f, Height * 1.5f); - - CreateNamesIfNotCreated(&Kill); + CInfoMsg Kill = CreateInfoMsg(TYPE_KILL); + Kill.m_TeamSize = minimum(vStrongWeakSorted.size(), MAX_KILLMSG_TEAM_MEMBERS); - bool KillMsgValid = (Kill.m_aVictimRenderInfo[0].m_CustomColoredSkin && Kill.m_aVictimRenderInfo[0].m_ColorableRenderSkin.m_Body.IsValid()) || (!Kill.m_aVictimRenderInfo[0].m_CustomColoredSkin && Kill.m_aVictimRenderInfo[0].m_OriginalRenderSkin.m_Body.IsValid()); - KillMsgValid &= Kill.m_KillerID == -1 || ((Kill.m_KillerRenderInfo.m_CustomColoredSkin && Kill.m_KillerRenderInfo.m_ColorableRenderSkin.m_Body.IsValid()) || (!Kill.m_KillerRenderInfo.m_CustomColoredSkin && Kill.m_KillerRenderInfo.m_OriginalRenderSkin.m_Body.IsValid())); - if(KillMsgValid) - { - AddInfoMsg(EType::TYPE_KILL, Kill); - } - else + Kill.m_VictimDDTeam = pMsg->m_Team; + for(int i = 0; i < Kill.m_TeamSize; i++) + { + if(m_pClient->m_aClients[vStrongWeakSorted[i].first].m_Active) { - DeleteTextContainers(&Kill); + Kill.m_aVictimIds[i] = vStrongWeakSorted[i].first; + Kill.m_aVictimRenderInfo[i] = m_pClient->m_aClients[vStrongWeakSorted[i].first].m_RenderInfo; } - - Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } + str_format(Kill.m_aVictimName, sizeof(Kill.m_aVictimName), Localize("Team %d"), pMsg->m_Team); - if(MsgType == NETMSGTYPE_SV_RACEFINISH && g_Config.m_ClShowFinishMessages) - { - CNetMsg_Sv_RaceFinish *pMsg = (CNetMsg_Sv_RaceFinish *)pRawMsg; - - char aBuf[256]; - - CInfoMsg Finish; - Finish.m_TeamSize = 1; - Finish.m_aVictimIds[0] = pMsg->m_ClientID; - Finish.m_VictimDDTeam = m_pClient->m_Teams.Team(Finish.m_aVictimIds[0]); - str_copy(Finish.m_aVictimName, m_pClient->m_aClients[Finish.m_aVictimIds[0]].m_aName); - Finish.m_aVictimRenderInfo[0] = m_pClient->m_aClients[pMsg->m_ClientID].m_RenderInfo; + AddInfoMsg(Kill); +} - Finish.m_aKillerName[0] = '\0'; +void CInfoMessages::OnKillMessage(const CNetMsg_Sv_KillMsg *pMsg) +{ + CInfoMsg Kill = CreateInfoMsg(TYPE_KILL); - Finish.m_Diff = pMsg->m_Diff; - Finish.m_RecordPersonal = (pMsg->m_RecordPersonal || pMsg->m_RecordServer); + Kill.m_TeamSize = 1; + Kill.m_aVictimIds[0] = pMsg->m_Victim; + Kill.m_VictimDDTeam = m_pClient->m_Teams.Team(Kill.m_aVictimIds[0]); + str_copy(Kill.m_aVictimName, m_pClient->m_aClients[Kill.m_aVictimIds[0]].m_aName); + Kill.m_aVictimRenderInfo[0] = m_pClient->m_aClients[Kill.m_aVictimIds[0]].m_RenderInfo; - // diff time text - if(Finish.m_Diff) - { - if(Finish.m_Diff < 0) - { - str_time_float(-Finish.m_Diff / 1000.0f, TIME_HOURS_CENTISECS, aBuf, sizeof(aBuf)); - str_format(Finish.m_aDiffText, sizeof(Finish.m_aDiffText), "(-%s)", aBuf); - } - else - { - str_time_float(Finish.m_Diff / 1000.0f, TIME_HOURS_CENTISECS, aBuf, sizeof(aBuf)); - str_format(Finish.m_aDiffText, sizeof(Finish.m_aDiffText), "(+%s)", aBuf); - } - } - else - { - Finish.m_aDiffText[0] = '\0'; - } + Kill.m_KillerId = pMsg->m_Killer; + str_copy(Kill.m_aKillerName, m_pClient->m_aClients[Kill.m_KillerId].m_aName); + Kill.m_KillerRenderInfo = m_pClient->m_aClients[Kill.m_KillerId].m_RenderInfo; - // finish time text - str_time_float(pMsg->m_Time / 1000.0f, TIME_HOURS_CENTISECS, Finish.m_aTimeText, sizeof(Finish.m_aTimeText)); + Kill.m_Weapon = pMsg->m_Weapon; + Kill.m_ModeSpecial = pMsg->m_ModeSpecial; + Kill.m_FlagCarrierBlue = m_pClient->m_Snap.m_pGameDataObj ? m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue : -1; - float ScreenX0, ScreenY0, ScreenX1, ScreenY1; - float Height = 400 * 3.0f; - float Width = Height * Graphics()->ScreenAspect(); - Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); - Graphics()->MapScreen(0, 0, Width * 1.5f, Height * 1.5f); + AddInfoMsg(Kill); +} - CreateNamesIfNotCreated(&Finish); - CreateFinishTextContainersIfNotCreated(&Finish); +void CInfoMessages::OnRaceFinishMessage(const CNetMsg_Sv_RaceFinish *pMsg) +{ + CInfoMsg Finish = CreateInfoMsg(TYPE_FINISH); - AddInfoMsg(EType::TYPE_FINISH, Finish); + Finish.m_TeamSize = 1; + Finish.m_aVictimIds[0] = pMsg->m_ClientId; + Finish.m_VictimDDTeam = m_pClient->m_Teams.Team(Finish.m_aVictimIds[0]); + str_copy(Finish.m_aVictimName, m_pClient->m_aClients[Finish.m_aVictimIds[0]].m_aName); + Finish.m_aVictimRenderInfo[0] = m_pClient->m_aClients[pMsg->m_ClientId].m_RenderInfo; - Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); + Finish.m_Diff = pMsg->m_Diff; + Finish.m_RecordPersonal = pMsg->m_RecordPersonal || pMsg->m_RecordServer; + if(Finish.m_Diff) + { + char aBuf[64]; + str_time_float(absolute(Finish.m_Diff) / 1000.0f, TIME_HOURS_CENTISECS, aBuf, sizeof(aBuf)); + str_format(Finish.m_aDiffText, sizeof(Finish.m_aDiffText), "(%c%s)", Finish.m_Diff < 0 ? '-' : '+', aBuf); } + str_time_float(pMsg->m_Time / 1000.0f, TIME_HOURS_CENTISECS, Finish.m_aTimeText, sizeof(Finish.m_aTimeText)); + + AddInfoMsg(Finish); } -void CInfoMessages::RenderKillMsg(CInfoMsg *pInfoMsg, float x, float y) +void CInfoMessages::RenderKillMsg(const CInfoMsg &InfoMsg, float x, float y) { - // render victim name - x -= pInfoMsg->m_VictimTextWidth; ColorRGBA TextColor; - if(g_Config.m_ClChatTeamColors && pInfoMsg->m_VictimDDTeam) - TextColor = m_pClient->GetDDTeamColor(pInfoMsg->m_VictimDDTeam, 0.75f); + if(InfoMsg.m_VictimDDTeam) + TextColor = m_pClient->GetDDTeamColor(InfoMsg.m_VictimDDTeam, 0.75f); else TextColor = TextRender()->DefaultTextColor(); - CreateNamesIfNotCreated(pInfoMsg); - - if(pInfoMsg->m_VictimTextContainerIndex.Valid()) - TextRender()->RenderTextContainer(pInfoMsg->m_VictimTextContainerIndex, TextColor, TextRender()->DefaultTextOutlineColor(), x, y + (46.f - 36.f) / 2.f); + // render victim name + if(InfoMsg.m_VictimTextContainerIndex.Valid()) + { + x -= TextRender()->GetBoundingBoxTextContainer(InfoMsg.m_VictimTextContainerIndex).m_W; + TextRender()->RenderTextContainer(InfoMsg.m_VictimTextContainerIndex, TextColor, TextRender()->DefaultTextOutlineColor(), x, y + (ROW_HEIGHT - FONT_SIZE) / 2.0f); + } - // render victim tee + // render victim flag x -= 24.0f; - - if(m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) + if(m_pClient->m_Snap.m_pGameInfoObj && (m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) && (InfoMsg.m_ModeSpecial & 1)) { - if(pInfoMsg->m_ModeSpecial & 1) + int QuadOffset; + if(InfoMsg.m_aVictimIds[0] == InfoMsg.m_FlagCarrierBlue) { - int QuadOffset = 0; - if(pInfoMsg->m_aVictimIds[0] == pInfoMsg->m_FlagCarrierBlue) - ++QuadOffset; - - if(QuadOffset == 0) - Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagRed); - else - Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagBlue); - - Graphics()->RenderQuadContainerAsSprite(m_SpriteQuadContainerIndex, QuadOffset, x, y - 16); + Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagBlue); + QuadOffset = 0; + } + else + { + Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagRed); + QuadOffset = 1; } + Graphics()->RenderQuadContainerAsSprite(m_SpriteQuadContainerIndex, QuadOffset, x, y - 16); } - for(int j = (pInfoMsg->m_TeamSize - 1); j >= 0; j--) + // render victim tees + for(int j = (InfoMsg.m_TeamSize - 1); j >= 0; j--) { - if(pInfoMsg->m_aVictimIds[j] < 0) + if(InfoMsg.m_aVictimIds[j] < 0) continue; - const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &pInfoMsg->m_aVictimRenderInfo[j], OffsetToMid); - const vec2 TeeRenderPos = vec2(x, y + 46.0f / 2.0f + OffsetToMid.y); - RenderTools()->RenderTee(pIdleState, &pInfoMsg->m_aVictimRenderInfo[j], EMOTE_PAIN, vec2(-1, 0), TeeRenderPos); - + CRenderTools::GetRenderTeeOffsetToRenderedTee(CAnimState::GetIdle(), &InfoMsg.m_aVictimRenderInfo[j], OffsetToMid); + const vec2 TeeRenderPos = vec2(x, y + ROW_HEIGHT / 2.0f + OffsetToMid.y); + RenderTools()->RenderTee(CAnimState::GetIdle(), &InfoMsg.m_aVictimRenderInfo[j], EMOTE_PAIN, vec2(-1, 0), TeeRenderPos); x -= 44.0f; } // render weapon x -= 32.0f; - if(pInfoMsg->m_Weapon >= 0) + if(InfoMsg.m_Weapon >= 0) { - Graphics()->TextureSet(GameClient()->m_GameSkin.m_aSpriteWeapons[pInfoMsg->m_Weapon]); - Graphics()->RenderQuadContainerAsSprite(m_SpriteQuadContainerIndex, 4 + pInfoMsg->m_Weapon, x, y + 28); + Graphics()->TextureSet(GameClient()->m_GameSkin.m_aSpriteWeapons[InfoMsg.m_Weapon]); + Graphics()->RenderQuadContainerAsSprite(m_SpriteQuadContainerIndex, 4 + InfoMsg.m_Weapon, x, y + 28); } x -= 52.0f; - if(pInfoMsg->m_aVictimIds[0] != pInfoMsg->m_KillerID) + // render killer (only if different from victim) + if(InfoMsg.m_aVictimIds[0] != InfoMsg.m_KillerId) { - if(m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) + // render killer flag + if(m_pClient->m_Snap.m_pGameInfoObj && (m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) && (InfoMsg.m_ModeSpecial & 2)) { - if(pInfoMsg->m_ModeSpecial & 2) + int QuadOffset; + if(InfoMsg.m_KillerId == InfoMsg.m_FlagCarrierBlue) { - int QuadOffset = 2; - if(pInfoMsg->m_KillerID == pInfoMsg->m_FlagCarrierBlue) - ++QuadOffset; - - if(QuadOffset == 2) - Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagRed); - else - Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagBlue); - - Graphics()->RenderQuadContainerAsSprite(m_SpriteQuadContainerIndex, QuadOffset, x - 56, y - 16); + Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagBlue); + QuadOffset = 2; } + else + { + Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagRed); + QuadOffset = 3; + } + Graphics()->RenderQuadContainerAsSprite(m_SpriteQuadContainerIndex, QuadOffset, x - 56, y - 16); } // render killer tee x -= 24.0f; - - if(pInfoMsg->m_KillerID >= 0) + if(InfoMsg.m_KillerId >= 0) { - const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &pInfoMsg->m_KillerRenderInfo, OffsetToMid); - const vec2 TeeRenderPos = vec2(x, y + 46.0f / 2.0f + OffsetToMid.y); - RenderTools()->RenderTee(pIdleState, &pInfoMsg->m_KillerRenderInfo, EMOTE_ANGRY, vec2(1, 0), TeeRenderPos); + CRenderTools::GetRenderTeeOffsetToRenderedTee(CAnimState::GetIdle(), &InfoMsg.m_KillerRenderInfo, OffsetToMid); + const vec2 TeeRenderPos = vec2(x, y + ROW_HEIGHT / 2.0f + OffsetToMid.y); + RenderTools()->RenderTee(CAnimState::GetIdle(), &InfoMsg.m_KillerRenderInfo, EMOTE_ANGRY, vec2(1, 0), TeeRenderPos); } - x -= 32.0f; // render killer name - x -= pInfoMsg->m_KillerTextWidth; - - if(pInfoMsg->m_KillerTextContainerIndex.Valid()) - TextRender()->RenderTextContainer(pInfoMsg->m_KillerTextContainerIndex, TextColor, TextRender()->DefaultTextOutlineColor(), x, y + (46.f - 36.f) / 2.f); + if(InfoMsg.m_KillerTextContainerIndex.Valid()) + { + x -= TextRender()->GetBoundingBoxTextContainer(InfoMsg.m_KillerTextContainerIndex).m_W; + TextRender()->RenderTextContainer(InfoMsg.m_KillerTextContainerIndex, TextColor, TextRender()->DefaultTextOutlineColor(), x, y + (ROW_HEIGHT - FONT_SIZE) / 2.0f); + } } } -void CInfoMessages::RenderFinishMsg(CInfoMsg *pInfoMsg, float x, float y) +void CInfoMessages::RenderFinishMsg(const CInfoMsg &InfoMsg, float x, float y) { // render time diff - CreateFinishTextContainersIfNotCreated(pInfoMsg); - - if(pInfoMsg->m_Diff) + if(InfoMsg.m_DiffTextContainerIndex.Valid()) { - x -= pInfoMsg->m_DiffTextWidth; - - if(pInfoMsg->m_DiffTextContainerIndex.Valid()) - TextRender()->RenderTextContainer(pInfoMsg->m_DiffTextContainerIndex, TextRender()->DefaultTextColor(), TextRender()->DefaultTextOutlineColor(), x, y + (46.f - 36.f) / 2.f); + x -= TextRender()->GetBoundingBoxTextContainer(InfoMsg.m_DiffTextContainerIndex).m_W; + TextRender()->RenderTextContainer(InfoMsg.m_DiffTextContainerIndex, TextRender()->DefaultTextColor(), TextRender()->DefaultTextOutlineColor(), x, y + (ROW_HEIGHT - FONT_SIZE) / 2.0f); } // render time - x -= pInfoMsg->m_TimeTextWidth; - - if(pInfoMsg->m_TimeTextContainerIndex.Valid()) - TextRender()->RenderTextContainer(pInfoMsg->m_TimeTextContainerIndex, TextRender()->DefaultTextColor(), TextRender()->DefaultTextOutlineColor(), x, y + (46.f - 36.f) / 2.f); + if(InfoMsg.m_TimeTextContainerIndex.Valid()) + { + x -= TextRender()->GetBoundingBoxTextContainer(InfoMsg.m_TimeTextContainerIndex).m_W; + TextRender()->RenderTextContainer(InfoMsg.m_TimeTextContainerIndex, TextRender()->DefaultTextColor(), TextRender()->DefaultTextOutlineColor(), x, y + (ROW_HEIGHT - FONT_SIZE) / 2.0f); + } // render flag - x -= 52.0f; - + const float FlagSize = 52.0f; + x -= FlagSize; Graphics()->TextureSet(g_pData->m_aImages[IMAGE_RACEFLAG].m_Id); Graphics()->QuadsBegin(); - IGraphics::CQuadItem QuadItem(x, y, 52, 52); + IGraphics::CQuadItem QuadItem(x, y, FlagSize, FlagSize); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); // render victim name - ColorRGBA TextColor; - x -= pInfoMsg->m_VictimTextWidth; - if(g_Config.m_ClChatTeamColors && pInfoMsg->m_VictimDDTeam) - TextColor = m_pClient->GetDDTeamColor(pInfoMsg->m_VictimDDTeam, 0.75f); - else - TextColor = TextRender()->DefaultTextColor(); - - CreateNamesIfNotCreated(pInfoMsg); - - if(pInfoMsg->m_VictimTextContainerIndex.Valid()) - TextRender()->RenderTextContainer(pInfoMsg->m_VictimTextContainerIndex, TextColor, TextRender()->DefaultTextOutlineColor(), x, y + (46.f - 36.f) / 2.f); + if(InfoMsg.m_VictimTextContainerIndex.Valid()) + { + x -= TextRender()->GetBoundingBoxTextContainer(InfoMsg.m_VictimTextContainerIndex).m_W; + ColorRGBA TextColor; + if(InfoMsg.m_VictimDDTeam) + TextColor = m_pClient->GetDDTeamColor(InfoMsg.m_VictimDDTeam, 0.75f); + else + TextColor = TextRender()->DefaultTextColor(); + TextRender()->RenderTextContainer(InfoMsg.m_VictimTextContainerIndex, TextColor, TextRender()->DefaultTextOutlineColor(), x, y + (ROW_HEIGHT - FONT_SIZE) / 2.0f); + } // render victim tee x -= 24.0f; - - const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &pInfoMsg->m_aVictimRenderInfo[0], OffsetToMid); - const vec2 TeeRenderPos = vec2(x, y + 46.0f / 2.0f + OffsetToMid.y); - const int Emote = pInfoMsg->m_RecordPersonal ? EMOTE_HAPPY : EMOTE_NORMAL; - RenderTools()->RenderTee(pIdleState, &pInfoMsg->m_aVictimRenderInfo[0], Emote, vec2(-1, 0), TeeRenderPos); + CRenderTools::GetRenderTeeOffsetToRenderedTee(CAnimState::GetIdle(), &InfoMsg.m_aVictimRenderInfo[0], OffsetToMid); + const vec2 TeeRenderPos = vec2(x, y + ROW_HEIGHT / 2.0f + OffsetToMid.y); + const int Emote = InfoMsg.m_RecordPersonal ? EMOTE_HAPPY : EMOTE_NORMAL; + RenderTools()->RenderTee(CAnimState::GetIdle(), &InfoMsg.m_aVictimRenderInfo[0], Emote, vec2(-1, 0), TeeRenderPos); } void CInfoMessages::OnRender() { - float Height = 400 * 3.0f; - float Width = Height * Graphics()->ScreenAspect(); + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; - Graphics()->MapScreen(0, 0, Width * 1.5f, Height * 1.5f); - Graphics()->SetColor(1.f, 1.f, 1.f, 1.f); + const float Height = 1.5f * 400.0f * 3.0f; + const float Width = Height * Graphics()->ScreenAspect(); - float StartX = Width * 1.5f - 10.0f; - float y = 30.0f + 100.0f * ((g_Config.m_ClShowfps ? 1 : 0) + (g_Config.m_ClShowpred && Client()->State() != IClient::STATE_DEMOPLAYBACK)); + Graphics()->MapScreen(0, 0, Width, Height); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + int Showfps = g_Config.m_ClShowfps; +#if defined(CONF_VIDEORECORDER) + if(IVideo::Current()) + Showfps = 0; +#endif + const float StartX = Width - 10.0f; + const float StartY = 30.0f + (Showfps ? 100.0f : 0.0f) + (g_Config.m_ClShowpred && Client()->State() != IClient::STATE_DEMOPLAYBACK ? 100.0f : 0.0f); + + float y = StartY; for(int i = 1; i <= MAX_INFOMSGS; i++) { - CInfoMsg *pInfoMsg = &m_aInfoMsgs[(m_InfoMsgCurrent + i) % MAX_INFOMSGS]; - if(Client()->GameTick(g_Config.m_ClDummy) > pInfoMsg->m_Tick + Client()->GameTickSpeed() * 10) + CInfoMsg &InfoMsg = m_aInfoMsgs[(m_InfoMsgCurrent + i) % MAX_INFOMSGS]; + if(Client()->GameTick(g_Config.m_ClDummy) > InfoMsg.m_Tick + Client()->GameTickSpeed() * 10) continue; - if(pInfoMsg->m_Type == EType::TYPE_KILL && g_Config.m_ClShowKillMessages) + CreateTextContainersIfNotCreated(InfoMsg); + + if(InfoMsg.m_Type == EType::TYPE_KILL && g_Config.m_ClShowKillMessages) { - RenderKillMsg(pInfoMsg, StartX, y); - y += 46.0f; + RenderKillMsg(InfoMsg, StartX, y); + y += ROW_HEIGHT; } - else if(pInfoMsg->m_Type == EType::TYPE_FINISH && g_Config.m_ClShowFinishMessages) + else if(InfoMsg.m_Type == EType::TYPE_FINISH && g_Config.m_ClShowFinishMessages) { - RenderFinishMsg(pInfoMsg, StartX, y); - y += 46.0f; + RenderFinishMsg(InfoMsg, StartX, y); + y += ROW_HEIGHT; } } } -void CInfoMessages::RefindSkins() +void CInfoMessages::OnRefreshSkins() { for(auto &InfoMsg : m_aInfoMsgs) { InfoMsg.m_KillerRenderInfo.Reset(); - if(InfoMsg.m_KillerID >= 0) + if(InfoMsg.m_KillerId >= 0) { - const CGameClient::CClientData &Client = GameClient()->m_aClients[InfoMsg.m_KillerID]; + const CGameClient::CClientData &Client = GameClient()->m_aClients[InfoMsg.m_KillerId]; if(Client.m_Active && Client.m_aSkinName[0] != '\0') InfoMsg.m_KillerRenderInfo = Client.m_RenderInfo; else - InfoMsg.m_KillerID = -1; + InfoMsg.m_KillerId = -1; } for(int i = 0; i < MAX_KILLMSG_TEAM_MEMBERS; i++) diff --git a/src/game/client/components/infomessages.h b/src/game/client/components/infomessages.h index 680c46c651..03d953db8c 100644 --- a/src/game/client/components/infomessages.h +++ b/src/game/client/components/infomessages.h @@ -2,12 +2,14 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #ifndef GAME_CLIENT_COMPONENTS_INFOMESSAGES_H #define GAME_CLIENT_COMPONENTS_INFOMESSAGES_H +#include #include #include class CInfoMessages : public CComponent { int m_SpriteQuadContainerIndex; + enum { MAX_INFOMSGS = 5, @@ -20,8 +22,6 @@ class CInfoMessages : public CComponent TYPE_FINISH, }; -public: - // info messages struct CInfoMsg { EType m_Type; @@ -31,12 +31,10 @@ class CInfoMessages : public CComponent int m_VictimDDTeam; char m_aVictimName[64]; STextContainerIndex m_VictimTextContainerIndex; - float m_VictimTextWidth; CTeeRenderInfo m_aVictimRenderInfo[MAX_KILLMSG_TEAM_MEMBERS]; - int m_KillerID; + int m_KillerId; char m_aKillerName[64]; STextContainerIndex m_KillerTextContainerIndex; - float m_KillerTextWidth; CTeeRenderInfo m_KillerRenderInfo; // kill msg @@ -51,33 +49,32 @@ class CInfoMessages : public CComponent char m_aDiffText[32]; STextContainerIndex m_TimeTextContainerIndex; STextContainerIndex m_DiffTextContainerIndex; - float m_TimeTextWidth; - float m_DiffTextWidth; bool m_RecordPersonal; }; -private: - void AddInfoMsg(EType Type, CInfoMsg NewMsg); - void RenderKillMsg(CInfoMsg *pInfoMsg, float x, float y); - void RenderFinishMsg(CInfoMsg *pInfoMsg, float x, float y); + CInfoMsg m_aInfoMsgs[MAX_INFOMSGS]; + int m_InfoMsgCurrent; - void CreateNamesIfNotCreated(CInfoMsg *pInfoMsg); - void CreateFinishTextContainersIfNotCreated(CInfoMsg *pInfoMsg); + CInfoMsg CreateInfoMsg(EType Type); + void AddInfoMsg(const CInfoMsg &InfoMsg); + void RenderKillMsg(const CInfoMsg &InfoMsg, float x, float y); + void RenderFinishMsg(const CInfoMsg &InfoMsg, float x, float y); - void DeleteTextContainers(CInfoMsg *pInfoMsg); + void OnTeamKillMessage(const struct CNetMsg_Sv_KillMsgTeam *pMsg); + void OnKillMessage(const struct CNetMsg_Sv_KillMsg *pMsg); + void OnRaceFinishMessage(const struct CNetMsg_Sv_RaceFinish *pMsg); -public: - CInfoMsg m_aInfoMsgs[MAX_INFOMSGS]; - int m_InfoMsgCurrent; + void CreateTextContainersIfNotCreated(CInfoMsg &InfoMsg); + void DeleteTextContainers(CInfoMsg &InfoMsg); +public: virtual int Sizeof() const override { return sizeof(*this); } virtual void OnWindowResize() override; + virtual void OnRefreshSkins() override; virtual void OnReset() override; virtual void OnRender() override; virtual void OnMessage(int MsgType, void *pRawMsg) override; virtual void OnInit() override; - - void RefindSkins(); }; #endif diff --git a/src/game/client/components/items.cpp b/src/game/client/components/items.cpp index d3f8b21dd3..d58dea1827 100644 --- a/src/game/client/components/items.cpp +++ b/src/game/client/components/items.cpp @@ -23,7 +23,7 @@ #include "items.h" -void CItems::RenderProjectile(const CProjectileData *pCurrent, int ItemID) +void CItems::RenderProjectile(const CProjectileData *pCurrent, int ItemId) { int CurWeapon = clamp(pCurrent->m_Type, 0, NUM_WEAPONS - 1); @@ -50,7 +50,7 @@ void CItems::RenderProjectile(const CProjectileData *pCurrent, int ItemID) bool LocalPlayerInGame = false; if(m_pClient->m_Snap.m_pLocalInfo) - LocalPlayerInGame = m_pClient->m_aClients[m_pClient->m_Snap.m_pLocalInfo->m_ClientID].m_Team != TEAM_SPECTATORS; + LocalPlayerInGame = m_pClient->m_aClients[m_pClient->m_Snap.m_pLocalInfo->m_ClientId].m_Team != TEAM_SPECTATORS; static float s_LastGameTickTime = Client()->GameTickTime(g_Config.m_ClDummy); if(m_pClient->m_Snap.m_pGameInfoObj && !(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)) @@ -117,7 +117,7 @@ void CItems::RenderProjectile(const CProjectileData *pCurrent, int ItemID) s_Time += LocalTime() - s_LastLocalTime; } - Graphics()->QuadsSetRotation(s_Time * pi * 2 * 2 + ItemID); + Graphics()->QuadsSetRotation(s_Time * pi * 2 * 2 + ItemId); s_LastLocalTime = LocalTime(); } else @@ -383,7 +383,7 @@ void CItems::OnRender() continue; CProjectileData Data = pProj->GetData(); - RenderProjectile(&Data, pProj->GetID()); + RenderProjectile(&Data, pProj->GetId()); } for(CEntity *pEnt = GameClient()->m_PredictedWorld.FindFirst(CGameWorld::ENTTYPE_LASER); pEnt; pEnt = pEnt->NextEntity()) { @@ -400,7 +400,7 @@ void CItems::OnRender() if(pPickup->InDDNetTile()) { - if(auto *pPrev = (CPickup *)GameClient()->m_PrevPredictedWorld.GetEntity(pPickup->GetID(), CGameWorld::ENTTYPE_PICKUP)) + if(auto *pPrev = (CPickup *)GameClient()->m_PrevPredictedWorld.GetEntity(pPickup->GetId(), CGameWorld::ENTTYPE_PICKUP)) { CNetObj_Pickup Data, Prev; pPickup->FillInfo(&Data); @@ -414,7 +414,7 @@ void CItems::OnRender() for(const CSnapEntities &Ent : m_pClient->SnapEntities()) { const IClient::CSnapItem Item = Ent.m_Item; - const void *pData = Ent.m_pData; + const void *pData = Item.m_pData; const CNetObj_EntityEx *pEntEx = Ent.m_pDataEx; if(Item.m_Type == NETOBJTYPE_PROJECTILE || Item.m_Type == NETOBJTYPE_DDRACEPROJECTILE || Item.m_Type == NETOBJTYPE_DDNETPROJECTILE) @@ -426,13 +426,13 @@ void CItems::OnRender() if(UsePredicted) { - if(auto *pProj = (CProjectile *)GameClient()->m_GameWorld.FindMatch(Item.m_ID, Item.m_Type, pData)) + if(auto *pProj = (CProjectile *)GameClient()->m_GameWorld.FindMatch(Item.m_Id, Item.m_Type, pData)) { bool IsOtherTeam = m_pClient->IsOtherTeam(pProj->GetOwner()); if(pProj->m_LastRenderTick <= 0 && (pProj->m_Type != WEAPON_SHOTGUN || (!pProj->m_Freeze && !pProj->m_Explosive)) // skip ddrace shotgun bullets && (pProj->m_Type == WEAPON_SHOTGUN || absolute(length(pProj->m_Direction) - 1.f) < 0.02f) // workaround to skip grenades on ball mod && (pProj->GetOwner() < 0 || !GameClient()->m_aClients[pProj->GetOwner()].m_IsPredictedLocal || IsOtherTeam) // skip locally predicted projectiles - && !Client()->SnapFindItem(IClient::SNAP_PREV, Item.m_Type, Item.m_ID)) + && !Client()->SnapFindItem(IClient::SNAP_PREV, Item.m_Type, Item.m_Id)) { ReconstructSmokeTrail(&Data, pProj->m_DestroyTick); } @@ -441,7 +441,7 @@ void CItems::OnRender() continue; } } - RenderProjectile(&Data, Item.m_ID); + RenderProjectile(&Data, Item.m_Id); } else if(Item.m_Type == NETOBJTYPE_PICKUP || Item.m_Type == NETOBJTYPE_DDNETPICKUP) { @@ -452,11 +452,11 @@ void CItems::OnRender() continue; if(UsePredicted) { - auto *pPickup = (CPickup *)GameClient()->m_GameWorld.FindMatch(Item.m_ID, Item.m_Type, pData); + auto *pPickup = (CPickup *)GameClient()->m_GameWorld.FindMatch(Item.m_Id, Item.m_Type, pData); if(pPickup && pPickup->InDDNetTile()) continue; } - const void *pPrev = Client()->SnapFindItem(IClient::SNAP_PREV, Item.m_Type, Item.m_ID); + const void *pPrev = Client()->SnapFindItem(IClient::SNAP_PREV, Item.m_Type, Item.m_Id); if(pPrev) RenderPickup((const CNetObj_Pickup *)pPrev, (const CNetObj_Pickup *)pData); } @@ -464,7 +464,7 @@ void CItems::OnRender() { if(UsePredicted) { - auto *pLaser = dynamic_cast(GameClient()->m_GameWorld.FindMatch(Item.m_ID, Item.m_Type, pData)); + auto *pLaser = dynamic_cast(GameClient()->m_GameWorld.FindMatch(Item.m_Id, Item.m_Type, pData)); if(pLaser && pLaser->GetOwner() >= 0 && GameClient()->m_aClients[pLaser->GetOwner()].m_IsPredictedLocal) continue; } @@ -523,16 +523,15 @@ void CItems::OnRender() // render flag for(int i = 0; i < Num; i++) { - IClient::CSnapItem Item; - const void *pData = Client()->SnapGetItem(IClient::SNAP_CURRENT, i, &Item); + const IClient::CSnapItem Item = Client()->SnapGetItem(IClient::SNAP_CURRENT, i); if(Item.m_Type == NETOBJTYPE_FLAG) { - const void *pPrev = Client()->SnapFindItem(IClient::SNAP_PREV, Item.m_Type, Item.m_ID); + const void *pPrev = Client()->SnapFindItem(IClient::SNAP_PREV, Item.m_Type, Item.m_Id); if(pPrev) { - const void *pPrevGameData = Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_GAMEDATA, m_pClient->m_Snap.m_GameDataSnapID); - RenderFlag(static_cast(pPrev), static_cast(pData), + const void *pPrevGameData = Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_GAMEDATA, m_pClient->m_Snap.m_GameDataSnapId); + RenderFlag(static_cast(pPrev), static_cast(Item.m_pData), static_cast(pPrevGameData), m_pClient->m_Snap.m_pGameDataObj); } } @@ -599,7 +598,7 @@ void CItems::ReconstructSmokeTrail(const CProjectileData *pCurrent, int DestroyT bool LocalPlayerInGame = false; if(m_pClient->m_Snap.m_pLocalInfo) - LocalPlayerInGame = m_pClient->m_aClients[m_pClient->m_Snap.m_pLocalInfo->m_ClientID].m_Team != TEAM_SPECTATORS; + LocalPlayerInGame = m_pClient->m_aClients[m_pClient->m_Snap.m_pLocalInfo->m_ClientId].m_Team != TEAM_SPECTATORS; if(!m_pClient->AntiPingGunfire() || !LocalPlayerInGame) return; if(Client()->PredGameTick(g_Config.m_ClDummy) == pCurrent->m_StartTick) diff --git a/src/game/client/components/items.h b/src/game/client/components/items.h index d7fef4d972..b9d4a5664a 100644 --- a/src/game/client/components/items.h +++ b/src/game/client/components/items.h @@ -10,7 +10,7 @@ class CLaserData; class CItems : public CComponent { - void RenderProjectile(const CProjectileData *pCurrent, int ItemID); + void RenderProjectile(const CProjectileData *pCurrent, int ItemId); void RenderPickup(const CNetObj_Pickup *pPrev, const CNetObj_Pickup *pCurrent, bool IsPredicted = false); void RenderFlag(const CNetObj_Flag *pPrev, const CNetObj_Flag *pCurrent, const CNetObj_GameData *pPrevGameData, const CNetObj_GameData *pCurGameData); void RenderLaser(const CLaserData *pCurrent, bool IsPredicted = false); diff --git a/src/game/client/components/mapimages.cpp b/src/game/client/components/mapimages.cpp index 9a4733f161..4a34c5f25c 100644 --- a/src/game/client/components/mapimages.cpp +++ b/src/game/client/components/mapimages.cpp @@ -109,7 +109,6 @@ void CMapImages::OnMapLoadImpl(class CLayers *pLayers, IMap *pMap) { const int LoadFlag = (((m_aTextureUsedByTileOrQuadLayerFlag[i] & 1) != 0) ? TextureLoadFlag : 0) | (((m_aTextureUsedByTileOrQuadLayerFlag[i] & 2) != 0) ? 0 : (Graphics()->HasTextureArraysSupport() ? IGraphics::TEXLOAD_NO_2D_TEXTURE : 0)); const CMapItemImage_v2 *pImg = (CMapItemImage_v2 *)pMap->GetItem(Start + i); - const CImageInfo::EImageFormat Format = pImg->m_Version < CMapItemImage_v2::CURRENT_VERSION ? CImageInfo::FORMAT_RGBA : CImageInfo::ImageFormatFromInt(pImg->m_Format); const char *pName = pMap->GetDataString(pImg->m_ImageName); if(pName == nullptr || pName[0] == '\0') @@ -123,18 +122,38 @@ void CMapImages::OnMapLoadImpl(class CLayers *pLayers, IMap *pMap) pName = "(error)"; } + if(pImg->m_Version > 1 && pImg->m_MustBe1 != 1) + { + log_error("mapimages", "Failed to load map image %d '%s': invalid map image type.", i, pName); + ShowWarning = true; + continue; + } + if(pImg->m_External) { char aPath[IO_MAX_PATH_LENGTH]; - str_format(aPath, sizeof(aPath), "mapres/%s.png", pName); + bool Translated = false; + if(Client()->IsSixup()) + { + Translated = + !str_comp(pName, "grass_doodads") || + !str_comp(pName, "grass_main") || + !str_comp(pName, "winter_main") || + !str_comp(pName, "generic_unhookable"); + } + str_format(aPath, sizeof(aPath), "mapres/%s%s.png", pName, Translated ? "_0.7" : ""); m_aTextures[i] = Graphics()->LoadTexture(aPath, IStorage::TYPE_ALL, LoadFlag); } - else if(Format == CImageInfo::FORMAT_RGBA) + else { - void *pData = pMap->GetData(pImg->m_ImageData); + CImageInfo ImageInfo; + ImageInfo.m_Width = pImg->m_Width; + ImageInfo.m_Height = pImg->m_Height; + ImageInfo.m_Format = CImageInfo::FORMAT_RGBA; + ImageInfo.m_pData = static_cast(pMap->GetData(pImg->m_ImageData)); char aTexName[IO_MAX_PATH_LENGTH]; str_format(aTexName, sizeof(aTexName), "embedded: %s", pName); - m_aTextures[i] = Graphics()->LoadTextureRaw(pImg->m_Width, pImg->m_Height, Format, pData, LoadFlag, aTexName); + m_aTextures[i] = Graphics()->LoadTextureRaw(ImageInfo, LoadFlag, aTexName); pMap->UnloadData(pImg->m_ImageData); } pMap->UnloadData(pImg->m_ImageName); @@ -158,194 +177,139 @@ void CMapImages::LoadBackground(class CLayers *pLayers, class IMap *pMap) OnMapLoadImpl(pLayers, pMap); } -bool CMapImages::HasFrontLayer(EMapImageModType ModType) +static EMapImageModType GetEntitiesModType(const CGameInfo &GameInfo) { - return ModType == MAP_IMAGE_MOD_TYPE_DDNET || ModType == MAP_IMAGE_MOD_TYPE_DDRACE; -} - -bool CMapImages::HasSpeedupLayer(EMapImageModType ModType) -{ - return ModType == MAP_IMAGE_MOD_TYPE_DDNET || ModType == MAP_IMAGE_MOD_TYPE_DDRACE; -} - -bool CMapImages::HasSwitchLayer(EMapImageModType ModType) -{ - return ModType == MAP_IMAGE_MOD_TYPE_DDNET || ModType == MAP_IMAGE_MOD_TYPE_DDRACE; + if(GameInfo.m_EntitiesFDDrace) + return MAP_IMAGE_MOD_TYPE_FDDRACE; + else if(GameInfo.m_EntitiesDDNet) + return MAP_IMAGE_MOD_TYPE_DDNET; + else if(GameInfo.m_EntitiesDDRace) + return MAP_IMAGE_MOD_TYPE_DDRACE; + else if(GameInfo.m_EntitiesRace) + return MAP_IMAGE_MOD_TYPE_RACE; + else if(GameInfo.m_EntitiesBW) + return MAP_IMAGE_MOD_TYPE_BLOCKWORLDS; + else if(GameInfo.m_EntitiesFNG) + return MAP_IMAGE_MOD_TYPE_FNG; + else if(GameInfo.m_EntitiesVanilla) + return MAP_IMAGE_MOD_TYPE_VANILLA; + else + return MAP_IMAGE_MOD_TYPE_DDNET; } -bool CMapImages::HasTeleLayer(EMapImageModType ModType) +static bool IsValidTile(int LayerType, bool EntitiesAreMasked, EMapImageModType EntitiesModType, int TileIndex) { - return ModType == MAP_IMAGE_MOD_TYPE_DDNET || ModType == MAP_IMAGE_MOD_TYPE_DDRACE; -} + if(TileIndex == TILE_AIR) + return false; + if(!EntitiesAreMasked) + return true; -bool CMapImages::HasTuneLayer(EMapImageModType ModType) -{ - return ModType == MAP_IMAGE_MOD_TYPE_DDNET || ModType == MAP_IMAGE_MOD_TYPE_DDRACE; + if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET || EntitiesModType == MAP_IMAGE_MOD_TYPE_DDRACE) + { + if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET || TileIndex != TILE_BOOST) + { + if(LayerType == MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH && + !IsValidGameTile(TileIndex) && + !IsValidFrontTile(TileIndex) && + !IsValidSpeedupTile(TileIndex) && + !IsValidTeleTile(TileIndex) && + !IsValidTuneTile(TileIndex)) + { + return false; + } + else if(LayerType == MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH && + !IsValidSwitchTile(TileIndex)) + { + return false; + } + } + } + else if(EntitiesModType == MAP_IMAGE_MOD_TYPE_RACE && IsCreditsTile(TileIndex)) + { + return false; + } + else if(EntitiesModType == MAP_IMAGE_MOD_TYPE_FNG && IsCreditsTile(TileIndex)) + { + return false; + } + else if(EntitiesModType == MAP_IMAGE_MOD_TYPE_VANILLA && IsCreditsTile(TileIndex)) + { + return false; + } + return true; } IGraphics::CTextureHandle CMapImages::GetEntities(EMapImageEntityLayerType EntityLayerType) { - EMapImageModType EntitiesModType = MAP_IMAGE_MOD_TYPE_DDNET; - bool EntitiesAreMasked = !GameClient()->m_GameInfo.m_DontMaskEntities; - - if(GameClient()->m_GameInfo.m_EntitiesFDDrace) - EntitiesModType = MAP_IMAGE_MOD_TYPE_FDDRACE; - else if(GameClient()->m_GameInfo.m_EntitiesDDNet) - EntitiesModType = MAP_IMAGE_MOD_TYPE_DDNET; - else if(GameClient()->m_GameInfo.m_EntitiesDDRace) - EntitiesModType = MAP_IMAGE_MOD_TYPE_DDRACE; - else if(GameClient()->m_GameInfo.m_EntitiesRace) - EntitiesModType = MAP_IMAGE_MOD_TYPE_RACE; - else if(GameClient()->m_GameInfo.m_EntitiesBW) - EntitiesModType = MAP_IMAGE_MOD_TYPE_BLOCKWORLDS; - else if(GameClient()->m_GameInfo.m_EntitiesFNG) - EntitiesModType = MAP_IMAGE_MOD_TYPE_FNG; - else if(GameClient()->m_GameInfo.m_EntitiesVanilla) - EntitiesModType = MAP_IMAGE_MOD_TYPE_VANILLA; + const bool EntitiesAreMasked = !GameClient()->m_GameInfo.m_DontMaskEntities; + const EMapImageModType EntitiesModType = GetEntitiesModType(GameClient()->m_GameInfo); if(!m_aEntitiesIsLoaded[(EntitiesModType * 2) + (int)EntitiesAreMasked]) { m_aEntitiesIsLoaded[(EntitiesModType * 2) + (int)EntitiesAreMasked] = true; - // any mod that does not mask, will get all layers unmasked - bool WasUnknown = !EntitiesAreMasked; - - char aPath[64]; - str_format(aPath, sizeof(aPath), "%s/%s.png", m_aEntitiesPath, gs_apModEntitiesNames[EntitiesModType]); - - bool GameTypeHasFrontLayer = HasFrontLayer(EntitiesModType) || WasUnknown; - bool GameTypeHasSpeedupLayer = HasSpeedupLayer(EntitiesModType) || WasUnknown; - bool GameTypeHasSwitchLayer = HasSwitchLayer(EntitiesModType) || WasUnknown; - bool GameTypeHasTeleLayer = HasTeleLayer(EntitiesModType) || WasUnknown; - bool GameTypeHasTuneLayer = HasTuneLayer(EntitiesModType) || WasUnknown; - int TextureLoadFlag = 0; if(Graphics()->HasTextureArraysSupport()) TextureLoadFlag = (Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE) | IGraphics::TEXLOAD_NO_2D_TEXTURE; CImageInfo ImgInfo; - bool ImagePNGLoaded = false; - if(Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL)) - ImagePNGLoaded = true; - else - { - bool TryDefault = true; - // try as single ddnet replacement - if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET) - { - str_format(aPath, sizeof(aPath), "%s.png", m_aEntitiesPath); - if(Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL)) - { - ImagePNGLoaded = true; - TryDefault = false; - } - } + char aPath[IO_MAX_PATH_LENGTH]; + str_format(aPath, sizeof(aPath), "%s/%s.png", m_aEntitiesPath, gs_apModEntitiesNames[EntitiesModType]); + Graphics()->LoadPng(ImgInfo, aPath, IStorage::TYPE_ALL); - if(!ImagePNGLoaded && TryDefault) - { - // try default - str_format(aPath, sizeof(aPath), "editor/entities_clear/%s.png", gs_apModEntitiesNames[EntitiesModType]); - if(Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL)) - { - ImagePNGLoaded = true; - } - } + // try as single ddnet replacement + if(ImgInfo.m_pData == nullptr && EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET) + { + str_format(aPath, sizeof(aPath), "%s.png", m_aEntitiesPath); + Graphics()->LoadPng(ImgInfo, aPath, IStorage::TYPE_ALL); } - if(ImagePNGLoaded && ImgInfo.m_Width > 0 && ImgInfo.m_Height > 0) + // try default + if(ImgInfo.m_pData == nullptr) { - const size_t PixelSize = ImgInfo.PixelSize(); - const size_t BuildImageSize = (size_t)ImgInfo.m_Width * ImgInfo.m_Height * PixelSize; + str_format(aPath, sizeof(aPath), "editor/entities_clear/%s.png", gs_apModEntitiesNames[EntitiesModType]); + Graphics()->LoadPng(ImgInfo, aPath, IStorage::TYPE_ALL); + } - uint8_t *pTmpImgData = (uint8_t *)ImgInfo.m_pData; - uint8_t *pBuildImgData = (uint8_t *)malloc(BuildImageSize); + if(ImgInfo.m_pData != nullptr) + { + CImageInfo BuildImageInfo; + BuildImageInfo.m_Width = ImgInfo.m_Width; + BuildImageInfo.m_Height = ImgInfo.m_Height; + BuildImageInfo.m_Format = ImgInfo.m_Format; + BuildImageInfo.m_pData = static_cast(malloc(BuildImageInfo.DataSize())); // build game layer - for(int n = 0; n < MAP_IMAGE_ENTITY_LAYER_TYPE_COUNT; ++n) + for(int LayerType = 0; LayerType < MAP_IMAGE_ENTITY_LAYER_TYPE_COUNT; ++LayerType) { - bool BuildThisLayer = true; - if(n == MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH && !GameTypeHasFrontLayer && - !GameTypeHasSpeedupLayer && !GameTypeHasTeleLayer && !GameTypeHasTuneLayer) - BuildThisLayer = false; - else if(n == MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH && !GameTypeHasSwitchLayer) - BuildThisLayer = false; + dbg_assert(!m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][LayerType].IsValid(), "entities texture already loaded when it should not be"); - dbg_assert(!m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][n].IsValid(), "entities texture already loaded when it should not be"); + // set everything transparent + mem_zero(BuildImageInfo.m_pData, BuildImageInfo.DataSize()); - if(BuildThisLayer) + for(int i = 0; i < 256; ++i) { - // set everything transparent - mem_zero(pBuildImgData, BuildImageSize); - - for(int i = 0; i < 256; ++i) + int TileIndex = i; + if(IsValidTile(LayerType, EntitiesAreMasked, EntitiesModType, TileIndex)) { - bool ValidTile = i != 0; - int TileIndex = i; - if(EntitiesAreMasked) + if(LayerType == MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH && TileIndex == TILE_SWITCHTIMEDOPEN) { - if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET || EntitiesModType == MAP_IMAGE_MOD_TYPE_DDRACE) - { - if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET || TileIndex != TILE_BOOST) - { - if(n == MAP_IMAGE_ENTITY_LAYER_TYPE_ALL_EXCEPT_SWITCH && !IsValidGameTile((int)TileIndex) && !IsValidFrontTile((int)TileIndex) && !IsValidSpeedupTile((int)TileIndex) && - !IsValidTeleTile((int)TileIndex) && !IsValidTuneTile((int)TileIndex)) - ValidTile = false; - else if(n == MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH) - { - if(!IsValidSwitchTile((int)TileIndex)) - ValidTile = false; - } - } - } - else if((EntitiesModType == MAP_IMAGE_MOD_TYPE_RACE) && IsCreditsTile((int)TileIndex)) - { - ValidTile = false; - } - else if((EntitiesModType == MAP_IMAGE_MOD_TYPE_FNG) && IsCreditsTile((int)TileIndex)) - { - ValidTile = false; - } - else if((EntitiesModType == MAP_IMAGE_MOD_TYPE_VANILLA) && IsCreditsTile((int)TileIndex)) - { - ValidTile = false; - } + TileIndex = 8; } - if(EntitiesModType == MAP_IMAGE_MOD_TYPE_DDNET || EntitiesModType == MAP_IMAGE_MOD_TYPE_DDRACE) - { - if(n == MAP_IMAGE_ENTITY_LAYER_TYPE_SWITCH && TileIndex == TILE_SWITCHTIMEDOPEN) - TileIndex = 8; - } - - int X = TileIndex % 16; - int Y = TileIndex / 16; - - int CopyWidth = ImgInfo.m_Width / 16; - int CopyHeight = ImgInfo.m_Height / 16; - if(ValidTile) - { - Graphics()->CopyTextureBufferSub(pBuildImgData, pTmpImgData, ImgInfo.m_Width, ImgInfo.m_Height, PixelSize, (size_t)X * CopyWidth, (size_t)Y * CopyHeight, CopyWidth, CopyHeight); - } + const size_t CopyWidth = ImgInfo.m_Width / 16; + const size_t CopyHeight = ImgInfo.m_Height / 16; + const size_t OffsetX = (size_t)(TileIndex % 16) * CopyWidth; + const size_t OffsetY = (size_t)(TileIndex / 16) * CopyHeight; + BuildImageInfo.CopyRectFrom(ImgInfo, OffsetX, OffsetY, CopyWidth, CopyHeight, OffsetX, OffsetY); } - - m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][n] = Graphics()->LoadTextureRaw(ImgInfo.m_Width, ImgInfo.m_Height, ImgInfo.m_Format, pBuildImgData, TextureLoadFlag, aPath); } - else - { - if(!m_TransparentTexture.IsValid()) - { - // set everything transparent - mem_zero(pBuildImgData, BuildImageSize); - m_TransparentTexture = Graphics()->LoadTextureRaw(ImgInfo.m_Width, ImgInfo.m_Height, ImgInfo.m_Format, pBuildImgData, TextureLoadFlag, aPath); - } - m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][n] = m_TransparentTexture; - } + m_aaEntitiesTextures[(EntitiesModType * 2) + (int)EntitiesAreMasked][LayerType] = Graphics()->LoadTextureRaw(BuildImageInfo, TextureLoadFlag, aPath); } - free(pBuildImgData); - - Graphics()->FreePNG(&ImgInfo); + BuildImageInfo.Free(); + ImgInfo.Free(); } } @@ -358,10 +322,8 @@ IGraphics::CTextureHandle CMapImages::GetSpeedupArrow() { int TextureLoadFlag = (Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE) | IGraphics::TEXLOAD_NO_2D_TEXTURE; m_SpeedupArrowTexture = Graphics()->LoadTexture("editor/speed_arrow_array.png", IStorage::TYPE_ALL, TextureLoadFlag); - m_SpeedupArrowIsLoaded = true; } - return m_SpeedupArrowTexture; } @@ -389,18 +351,20 @@ void CMapImages::ChangeEntitiesPath(const char *pPath) str_format(m_aEntitiesPath, sizeof(m_aEntitiesPath), "assets/entities/%s", pPath); } - for(int i = 0; i < MAP_IMAGE_MOD_TYPE_COUNT * 2; ++i) + for(int ModType = 0; ModType < MAP_IMAGE_MOD_TYPE_COUNT * 2; ++ModType) { - if(m_aEntitiesIsLoaded[i]) + if(m_aEntitiesIsLoaded[ModType]) { - for(int n = 0; n < MAP_IMAGE_ENTITY_LAYER_TYPE_COUNT; ++n) + for(int LayerType = 0; LayerType < MAP_IMAGE_ENTITY_LAYER_TYPE_COUNT; ++LayerType) { - if(m_aaEntitiesTextures[i][n].IsValid()) - Graphics()->UnloadTexture(&(m_aaEntitiesTextures[i][n])); - m_aaEntitiesTextures[i][n] = IGraphics::CTextureHandle(); + if(m_aaEntitiesTextures[ModType][LayerType].IsValid()) + { + Graphics()->UnloadTexture(&(m_aaEntitiesTextures[ModType][LayerType])); + } + m_aaEntitiesTextures[ModType][LayerType] = IGraphics::CTextureHandle(); } - m_aEntitiesIsLoaded[i] = false; + m_aEntitiesIsLoaded[ModType] = false; } } } @@ -434,24 +398,21 @@ int CMapImages::GetTextureScale() const IGraphics::CTextureHandle CMapImages::UploadEntityLayerText(int TextureSize, int MaxWidth, int YOffset) { - const size_t Width = 1024; - const size_t Height = 1024; - const size_t PixelSize = CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA); + CImageInfo TextImage; + TextImage.m_Width = 1024; + TextImage.m_Height = 1024; + TextImage.m_Format = CImageInfo::FORMAT_RGBA; + TextImage.m_pData = static_cast(calloc(TextImage.DataSize(), sizeof(uint8_t))); - void *pMem = calloc(Width * Height * PixelSize, 1); - - UpdateEntityLayerText(pMem, PixelSize, Width, Height, TextureSize, MaxWidth, YOffset, 0); - UpdateEntityLayerText(pMem, PixelSize, Width, Height, TextureSize, MaxWidth, YOffset, 1); - UpdateEntityLayerText(pMem, PixelSize, Width, Height, TextureSize, MaxWidth, YOffset, 2, 255); + UpdateEntityLayerText(TextImage, TextureSize, MaxWidth, YOffset, 0); + UpdateEntityLayerText(TextImage, TextureSize, MaxWidth, YOffset, 1); + UpdateEntityLayerText(TextImage, TextureSize, MaxWidth, YOffset, 2, 255); const int TextureLoadFlag = (Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE) | IGraphics::TEXLOAD_NO_2D_TEXTURE; - IGraphics::CTextureHandle Texture = Graphics()->LoadTextureRaw(Width, Height, CImageInfo::FORMAT_RGBA, pMem, TextureLoadFlag); - free(pMem); - - return Texture; + return Graphics()->LoadTextureRawMove(TextImage, TextureLoadFlag); } -void CMapImages::UpdateEntityLayerText(void *pTexBuffer, size_t PixelSize, size_t TexWidth, size_t TexHeight, int TextureSize, int MaxWidth, int YOffset, int NumbersPower, int MaxNumber) +void CMapImages::UpdateEntityLayerText(CImageInfo &TextImage, int TextureSize, int MaxWidth, int YOffset, int NumbersPower, int MaxNumber) { char aBuf[4]; int DigitsCount = NumbersPower + 1; @@ -461,7 +422,7 @@ void CMapImages::UpdateEntityLayerText(void *pTexBuffer, size_t PixelSize, size_ if(MaxNumber == -1) MaxNumber = CurrentNumber * 10 - 1; - str_from_int(CurrentNumber, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", CurrentNumber); int CurrentNumberSuitableFontSize = TextRender()->AdjustFontSize(aBuf, DigitsCount, TextureSize, MaxWidth); int UniversalSuitableFontSize = CurrentNumberSuitableFontSize * 0.92f; // should be smoothed enough to fit any digits combination @@ -470,7 +431,7 @@ void CMapImages::UpdateEntityLayerText(void *pTexBuffer, size_t PixelSize, size_ for(; CurrentNumber <= MaxNumber; ++CurrentNumber) { - str_from_int(CurrentNumber, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", CurrentNumber); float x = (CurrentNumber % 16) * 64; float y = (CurrentNumber / 16) * 64; @@ -478,7 +439,7 @@ void CMapImages::UpdateEntityLayerText(void *pTexBuffer, size_t PixelSize, size_ int ApproximateTextWidth = TextRender()->CalculateTextWidth(aBuf, DigitsCount, 0, UniversalSuitableFontSize); int XOffSet = (MaxWidth - clamp(ApproximateTextWidth, 0, MaxWidth)) / 2; - TextRender()->UploadEntityLayerText(pTexBuffer, PixelSize, TexWidth, TexHeight, (TexWidth / 16) - XOffSet, (TexHeight / 16) - YOffset, aBuf, DigitsCount, x + XOffSet, y + YOffset, UniversalSuitableFontSize); + TextRender()->UploadEntityLayerText(TextImage, (TextImage.m_Width / 16) - XOffSet, (TextImage.m_Height / 16) - YOffset, aBuf, DigitsCount, x + XOffSet, y + YOffset, UniversalSuitableFontSize); } } diff --git a/src/game/client/components/mapimages.h b/src/game/client/components/mapimages.h index 66bd83df56..a866b0d1b8 100644 --- a/src/game/client/components/mapimages.h +++ b/src/game/client/components/mapimages.h @@ -42,12 +42,6 @@ class CMapImages : public CComponent char m_aEntitiesPath[IO_MAX_PATH_LENGTH]; - bool HasFrontLayer(EMapImageModType ModType); - bool HasSpeedupLayer(EMapImageModType ModType); - bool HasSwitchLayer(EMapImageModType ModType); - bool HasTeleLayer(EMapImageModType ModType); - bool HasTuneLayer(EMapImageModType ModType); - public: CMapImages(); CMapImages(int TextureSize); @@ -82,12 +76,11 @@ class CMapImages : public CComponent IGraphics::CTextureHandle m_OverlayBottomTexture; IGraphics::CTextureHandle m_OverlayTopTexture; IGraphics::CTextureHandle m_OverlayCenterTexture; - IGraphics::CTextureHandle m_TransparentTexture; int m_TextureScale; void InitOverlayTextures(); IGraphics::CTextureHandle UploadEntityLayerText(int TextureSize, int MaxWidth, int YOffset); - void UpdateEntityLayerText(void *pTexBuffer, size_t PixelSize, size_t TexWidth, size_t TexHeight, int TextureSize, int MaxWidth, int YOffset, int NumbersPower, int MaxNumber = -1); + void UpdateEntityLayerText(CImageInfo &TextImage, int TextureSize, int MaxWidth, int YOffset, int NumbersPower, int MaxNumber = -1); }; #endif diff --git a/src/game/client/components/maplayers.cpp b/src/game/client/components/maplayers.cpp index 191e613a9f..5d8176266a 100644 --- a/src/game/client/components/maplayers.cpp +++ b/src/game/client/components/maplayers.cpp @@ -56,18 +56,19 @@ void CMapLayers::EnvelopeUpdate() } } -void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels, void *pUser) +void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser) { CMapLayers *pThis = (CMapLayers *)pUser; - Channels = ColorRGBA(); int EnvStart, EnvNum; pThis->m_pLayers->Map()->GetType(MAPITEMTYPE_ENVELOPE, &EnvStart, &EnvNum); - if(Env < 0 || Env >= EnvNum) return; const CMapItemEnvelope *pItem = (CMapItemEnvelope *)pThis->m_pLayers->Map()->GetItem(EnvStart + Env); + if(pItem->m_Channels <= 0) + return; + Channels = minimum(Channels, pItem->m_Channels, CEnvPoint::MAX_CHANNELS); CMapBasedEnvelopePointAccess EnvelopePoints(pThis->m_pLayers->Map()); EnvelopePoints.SetPointsRange(pItem->m_StartPoint, pItem->m_NumPoints); @@ -114,7 +115,7 @@ void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels MinTick * TickToNanoSeconds; } } - CRenderTools::RenderEvalEnvelope(&EnvelopePoints, 4, s_Time + (int64_t)TimeOffsetMillis * std::chrono::nanoseconds(1ms), Channels); + CRenderTools::RenderEvalEnvelope(&EnvelopePoints, s_Time + (int64_t)TimeOffsetMillis * std::chrono::nanoseconds(1ms), Result, Channels); } else { @@ -139,7 +140,7 @@ void CMapLayers::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels s_Time += CurTime - s_LastLocalTime; s_LastLocalTime = CurTime; } - CRenderTools::RenderEvalEnvelope(&EnvelopePoints, 4, s_Time + std::chrono::nanoseconds(std::chrono::milliseconds(TimeOffsetMillis)), Channels); + CRenderTools::RenderEvalEnvelope(&EnvelopePoints, s_Time + std::chrono::nanoseconds(std::chrono::milliseconds(TimeOffsetMillis)), Result, Channels); } } @@ -581,7 +582,7 @@ void CMapLayers::OnMapLoad() Flags = 0; if(CurOverlay == 1) { - if(IsTeleTileNumberUsed(Index)) + if(IsTeleTileNumberUsedAny(Index)) Index = ((CTeleTile *)pTiles)[y * pTMap->m_Width + x].m_Number; else Index = 0; @@ -812,36 +813,36 @@ void CMapLayers::OnMapLoad() CQuad *pQuad = &pQuads[i]; for(int j = 0; j < 4; ++j) { - int QuadIDX = j; + int QuadIdX = j; if(j == 2) - QuadIDX = 3; + QuadIdX = 3; else if(j == 3) - QuadIDX = 2; + QuadIdX = 2; if(!Textured) { // ignore the conversion for the position coordinates - vtmpQuads[i].m_aVertices[j].m_X = (pQuad->m_aPoints[QuadIDX].x); - vtmpQuads[i].m_aVertices[j].m_Y = (pQuad->m_aPoints[QuadIDX].y); + vtmpQuads[i].m_aVertices[j].m_X = (pQuad->m_aPoints[QuadIdX].x); + vtmpQuads[i].m_aVertices[j].m_Y = (pQuad->m_aPoints[QuadIdX].y); vtmpQuads[i].m_aVertices[j].m_CenterX = (pQuad->m_aPoints[4].x); vtmpQuads[i].m_aVertices[j].m_CenterY = (pQuad->m_aPoints[4].y); - vtmpQuads[i].m_aVertices[j].m_R = (unsigned char)pQuad->m_aColors[QuadIDX].r; - vtmpQuads[i].m_aVertices[j].m_G = (unsigned char)pQuad->m_aColors[QuadIDX].g; - vtmpQuads[i].m_aVertices[j].m_B = (unsigned char)pQuad->m_aColors[QuadIDX].b; - vtmpQuads[i].m_aVertices[j].m_A = (unsigned char)pQuad->m_aColors[QuadIDX].a; + vtmpQuads[i].m_aVertices[j].m_R = (unsigned char)pQuad->m_aColors[QuadIdX].r; + vtmpQuads[i].m_aVertices[j].m_G = (unsigned char)pQuad->m_aColors[QuadIdX].g; + vtmpQuads[i].m_aVertices[j].m_B = (unsigned char)pQuad->m_aColors[QuadIdX].b; + vtmpQuads[i].m_aVertices[j].m_A = (unsigned char)pQuad->m_aColors[QuadIdX].a; } else { // ignore the conversion for the position coordinates - vtmpQuadsTextured[i].m_aVertices[j].m_X = (pQuad->m_aPoints[QuadIDX].x); - vtmpQuadsTextured[i].m_aVertices[j].m_Y = (pQuad->m_aPoints[QuadIDX].y); + vtmpQuadsTextured[i].m_aVertices[j].m_X = (pQuad->m_aPoints[QuadIdX].x); + vtmpQuadsTextured[i].m_aVertices[j].m_Y = (pQuad->m_aPoints[QuadIdX].y); vtmpQuadsTextured[i].m_aVertices[j].m_CenterX = (pQuad->m_aPoints[4].x); vtmpQuadsTextured[i].m_aVertices[j].m_CenterY = (pQuad->m_aPoints[4].y); - vtmpQuadsTextured[i].m_aVertices[j].m_U = fx2f(pQuad->m_aTexcoords[QuadIDX].x); - vtmpQuadsTextured[i].m_aVertices[j].m_V = fx2f(pQuad->m_aTexcoords[QuadIDX].y); - vtmpQuadsTextured[i].m_aVertices[j].m_R = (unsigned char)pQuad->m_aColors[QuadIDX].r; - vtmpQuadsTextured[i].m_aVertices[j].m_G = (unsigned char)pQuad->m_aColors[QuadIDX].g; - vtmpQuadsTextured[i].m_aVertices[j].m_B = (unsigned char)pQuad->m_aColors[QuadIDX].b; - vtmpQuadsTextured[i].m_aVertices[j].m_A = (unsigned char)pQuad->m_aColors[QuadIDX].a; + vtmpQuadsTextured[i].m_aVertices[j].m_U = fx2f(pQuad->m_aTexcoords[QuadIdX].x); + vtmpQuadsTextured[i].m_aVertices[j].m_V = fx2f(pQuad->m_aTexcoords[QuadIdX].y); + vtmpQuadsTextured[i].m_aVertices[j].m_R = (unsigned char)pQuad->m_aColors[QuadIdX].r; + vtmpQuadsTextured[i].m_aVertices[j].m_G = (unsigned char)pQuad->m_aColors[QuadIdX].g; + vtmpQuadsTextured[i].m_aVertices[j].m_B = (unsigned char)pQuad->m_aColors[QuadIdX].b; + vtmpQuadsTextured[i].m_aVertices[j].m_A = (unsigned char)pQuad->m_aColors[QuadIdX].a; } } } @@ -901,7 +902,7 @@ void CMapLayers::OnMapLoad() } } -void CMapLayers::RenderTileLayer(int LayerIndex, ColorRGBA &Color, CMapItemLayerTilemap *pTileLayer, CMapItemGroup *pGroup) +void CMapLayers::RenderTileLayer(int LayerIndex, const ColorRGBA &Color, CMapItemLayerTilemap *pTileLayer, CMapItemGroup *pGroup) { STileLayerVisuals &Visuals = *m_vpTileLayerVisuals[LayerIndex]; if(Visuals.m_BufferContainerIndex == -1) @@ -910,12 +911,6 @@ void CMapLayers::RenderTileLayer(int LayerIndex, ColorRGBA &Color, CMapItemLayer float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); - ColorRGBA Channels(1.f, 1.f, 1.f, 1.f); - if(pTileLayer->m_ColorEnv >= 0) - { - EnvelopeEval(pTileLayer->m_ColorEnvOffset, pTileLayer->m_ColorEnv, Channels, this); - } - int BorderX0, BorderY0, BorderX1, BorderY1; bool DrawBorder = false; @@ -985,11 +980,6 @@ void CMapLayers::RenderTileLayer(int LayerIndex, ColorRGBA &Color, CMapItemLayer } } - Color.x *= Channels.r; - Color.y *= Channels.g; - Color.z *= Channels.b; - Color.w *= Channels.a; - int DrawCount = s_vpIndexOffsets.size(); if(DrawCount != 0) { @@ -1277,27 +1267,11 @@ void CMapLayers::RenderQuadLayer(int LayerIndex, CMapItemLayerQuads *pQuadLayer, { CQuad *pQuad = &pQuads[i]; - ColorRGBA Color(1.f, 1.f, 1.f, 1.f); - if(pQuad->m_ColorEnv >= 0) - { - EnvelopeEval(pQuad->m_ColorEnvOffset, pQuad->m_ColorEnv, Color, this); - } - - float OffsetX = 0; - float OffsetY = 0; - float Rot = 0; - - if(pQuad->m_PosEnv >= 0) - { - ColorRGBA Channels; - EnvelopeEval(pQuad->m_PosEnvOffset, pQuad->m_PosEnv, Channels, this); - OffsetX = Channels.r; - OffsetY = Channels.g; - Rot = Channels.b / 180.0f * pi; - } + ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + EnvelopeEval(pQuad->m_ColorEnvOffset, pQuad->m_ColorEnv, Color, 4, this); - const bool IsFullyTransparent = Color.a <= 0; - bool NeedsFlush = QuadsRenderCount == gs_GraphicsMaxQuadsRenderCount || IsFullyTransparent; + const bool IsFullyTransparent = Color.a <= 0.0f; + const bool NeedsFlush = QuadsRenderCount == gs_GraphicsMaxQuadsRenderCount || IsFullyTransparent; if(NeedsFlush) { @@ -1314,11 +1288,14 @@ void CMapLayers::RenderQuadLayer(int LayerIndex, CMapItemLayerQuads *pQuadLayer, if(!IsFullyTransparent) { + ColorRGBA Position = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + EnvelopeEval(pQuad->m_PosEnvOffset, pQuad->m_PosEnv, Position, 3, this); + SQuadRenderInfo &QInfo = s_vQuadRenderInfo[QuadsRenderCount++]; QInfo.m_Color = Color; - QInfo.m_Offsets.x = OffsetX; - QInfo.m_Offsets.y = OffsetY; - QInfo.m_Rotation = Rot; + QInfo.m_Offsets.x = Position.r; + QInfo.m_Offsets.y = Position.g; + QInfo.m_Rotation = Position.b / 180.0f * pi; } } Graphics()->RenderQuadLayer(Visuals.m_BufferContainerIndex, s_vQuadRenderInfo.data(), QuadsRenderCount, CurQuadOffset); @@ -1638,17 +1615,23 @@ void CMapLayers::OnRender() if(Size >= (size_t)pTMap->m_Width * pTMap->m_Height * sizeof(CTile)) { - ColorRGBA Color = ColorRGBA(pTMap->m_Color.r / 255.0f, pTMap->m_Color.g / 255.0f, pTMap->m_Color.b / 255.0f, pTMap->m_Color.a / 255.0f); + ColorRGBA Color = IsGameLayer ? ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f) : ColorRGBA(pTMap->m_Color.r / 255.0f, pTMap->m_Color.g / 255.0f, pTMap->m_Color.b / 255.0f, pTMap->m_Color.a / 255.0f); if(IsGameLayer && EntityOverlayVal) - Color = ColorRGBA(pTMap->m_Color.r / 255.0f, pTMap->m_Color.g / 255.0f, pTMap->m_Color.b / 255.0f, pTMap->m_Color.a / 255.0f * EntityOverlayVal / 100.0f); - else if(!IsGameLayer && EntityOverlayVal && !(m_Type == TYPE_BACKGROUND_FORCE)) - Color = ColorRGBA(pTMap->m_Color.r / 255.0f, pTMap->m_Color.g / 255.0f, pTMap->m_Color.b / 255.0f, pTMap->m_Color.a / 255.0f * (100 - EntityOverlayVal) / 100.0f); + Color.a *= EntityOverlayVal / 100.0f; + else if(!IsGameLayer && EntityOverlayVal && m_Type != TYPE_BACKGROUND_FORCE) + Color.a *= (100 - EntityOverlayVal) / 100.0f; + + if(!IsGameLayer) + { + ColorRGBA ColorEnv = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + EnvelopeEval(pTMap->m_ColorEnvOffset, pTMap->m_ColorEnv, ColorEnv, 4, this); + Color = Color.Multiply(ColorEnv); + } + if(!Graphics()->IsTileBufferingEnabled()) { Graphics()->BlendNone(); - RenderTools()->RenderTilemap(pTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, Color, TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_OPAQUE, - EnvelopeEval, this, pTMap->m_ColorEnv, pTMap->m_ColorEnvOffset); - + RenderTools()->RenderTilemap(pTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, Color, TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_OPAQUE); Graphics()->BlendNormal(); // draw kill tiles outside the entity clipping rectangle @@ -1657,15 +1640,12 @@ void CMapLayers::OnRender() // slow blinking to hint that it's not a part of the map double Seconds = time_get() / (double)time_freq(); ColorRGBA ColorHint = ColorRGBA(1.0f, 1.0f, 1.0f, 0.3 + 0.7 * (1 + std::sin(2 * (double)pi * Seconds / 3)) / 2); - RenderTools()->RenderTileRectangle(-201, -201, pTMap->m_Width + 402, pTMap->m_Height + 402, 0, TILE_DEATH, // display air inside, death outside - 32.0f, Color.v4() * ColorHint.v4(), TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_TRANSPARENT, - EnvelopeEval, this, pTMap->m_ColorEnv, pTMap->m_ColorEnvOffset); + 32.0f, Color.Multiply(ColorHint), TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_TRANSPARENT); } - RenderTools()->RenderTilemap(pTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, Color, TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_TRANSPARENT, - EnvelopeEval, this, pTMap->m_ColorEnv, pTMap->m_ColorEnvOffset); + RenderTools()->RenderTilemap(pTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, Color, TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_TRANSPARENT); } else { @@ -1676,9 +1656,7 @@ void CMapLayers::OnRender() // slow blinking to hint that it's not a part of the map double Seconds = time_get() / (double)time_freq(); ColorRGBA ColorHint = ColorRGBA(1.0f, 1.0f, 1.0f, 0.3 + 0.7 * (1.0 + std::sin(2 * (double)pi * Seconds / 3)) / 2); - - ColorRGBA ColorKill(Color.x * ColorHint.x, Color.y * ColorHint.y, Color.z * ColorHint.z, Color.w * ColorHint.w); - RenderKillTileBorder(TileLayerCounter - 1, ColorKill, pTMap, pGroup); + RenderKillTileBorder(TileLayerCounter - 1, Color.Multiply(ColorHint), pTMap, pGroup); } RenderTileLayer(TileLayerCounter - 1, Color, pTMap, pGroup); } @@ -1699,8 +1677,6 @@ void CMapLayers::OnRender() { if(!Graphics()->IsQuadBufferingEnabled()) { - //Graphics()->BlendNone(); - //RenderTools()->ForceRenderQuads(pQuads, pQLayer->m_NumQuads, LAYERRENDERFLAG_OPAQUE, EnvelopeEval, this, 1.f); Graphics()->BlendNormal(); RenderTools()->ForceRenderQuads(pQuads, pQLayer->m_NumQuads, LAYERRENDERFLAG_TRANSPARENT, EnvelopeEval, this, 1.f); } @@ -1714,8 +1690,6 @@ void CMapLayers::OnRender() { if(!Graphics()->IsQuadBufferingEnabled()) { - //Graphics()->BlendNone(); - //RenderTools()->RenderQuads(pQuads, pQLayer->m_NumQuads, LAYERRENDERFLAG_OPAQUE, EnvelopeEval, this); Graphics()->BlendNormal(); RenderTools()->RenderQuads(pQuads, pQLayer->m_NumQuads, LAYERRENDERFLAG_TRANSPARENT, EnvelopeEval, this); } @@ -1736,15 +1710,13 @@ void CMapLayers::OnRender() if(Size >= (size_t)pTMap->m_Width * pTMap->m_Height * sizeof(CTile)) { - ColorRGBA Color = ColorRGBA(pTMap->m_Color.r / 255.0f, pTMap->m_Color.g / 255.0f, pTMap->m_Color.b / 255.0f, pTMap->m_Color.a / 255.0f * EntityOverlayVal / 100.0f); + const ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, EntityOverlayVal / 100.0f); if(!Graphics()->IsTileBufferingEnabled()) { Graphics()->BlendNone(); - RenderTools()->RenderTilemap(pFrontTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, Color, TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_OPAQUE, - EnvelopeEval, this, pTMap->m_ColorEnv, pTMap->m_ColorEnvOffset); + RenderTools()->RenderTilemap(pFrontTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, Color, TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_OPAQUE); Graphics()->BlendNormal(); - RenderTools()->RenderTilemap(pFrontTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, Color, TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_TRANSPARENT, - EnvelopeEval, this, pTMap->m_ColorEnv, pTMap->m_ColorEnvOffset); + RenderTools()->RenderTilemap(pFrontTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, Color, TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_TRANSPARENT); } else { @@ -1763,7 +1735,7 @@ void CMapLayers::OnRender() if(Size >= (size_t)pTMap->m_Width * pTMap->m_Height * sizeof(CSwitchTile)) { - ColorRGBA Color = ColorRGBA(pTMap->m_Color.r / 255.0f, pTMap->m_Color.g / 255.0f, pTMap->m_Color.b / 255.0f, pTMap->m_Color.a / 255.0f * EntityOverlayVal / 100.0f); + const ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, EntityOverlayVal / 100.0f); if(!Graphics()->IsTileBufferingEnabled()) { Graphics()->BlendNone(); @@ -1778,9 +1750,9 @@ void CMapLayers::OnRender() RenderTileLayer(TileLayerCounter - 3, Color, pTMap, pGroup); if(g_Config.m_ClTextEntities) { - Graphics()->TextureSet(m_pImages->GetOverlayBottom()); - RenderTileLayer(TileLayerCounter - 2, Color, pTMap, pGroup); Graphics()->TextureSet(m_pImages->GetOverlayTop()); + RenderTileLayer(TileLayerCounter - 2, Color, pTMap, pGroup); + Graphics()->TextureSet(m_pImages->GetOverlayBottom()); RenderTileLayer(TileLayerCounter - 1, Color, pTMap, pGroup); } } @@ -1796,7 +1768,7 @@ void CMapLayers::OnRender() if(Size >= (size_t)pTMap->m_Width * pTMap->m_Height * sizeof(CTeleTile)) { - ColorRGBA Color = ColorRGBA(pTMap->m_Color.r / 255.0f, pTMap->m_Color.g / 255.0f, pTMap->m_Color.b / 255.0f, pTMap->m_Color.a / 255.0f * EntityOverlayVal / 100.0f); + const ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, EntityOverlayVal / 100.0f); if(!Graphics()->IsTileBufferingEnabled()) { Graphics()->BlendNone(); @@ -1827,7 +1799,7 @@ void CMapLayers::OnRender() if(Size >= (size_t)pTMap->m_Width * pTMap->m_Height * sizeof(CSpeedupTile)) { - ColorRGBA Color = ColorRGBA(pTMap->m_Color.r / 255.0f, pTMap->m_Color.g / 255.0f, pTMap->m_Color.b / 255.0f, pTMap->m_Color.a / 255.0f * EntityOverlayVal / 100.0f); + const ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, EntityOverlayVal / 100.0f); if(!Graphics()->IsTileBufferingEnabled()) { Graphics()->BlendNone(); @@ -1866,14 +1838,13 @@ void CMapLayers::OnRender() if(Size >= (size_t)pTMap->m_Width * pTMap->m_Height * sizeof(CTuneTile)) { - ColorRGBA Color = ColorRGBA(pTMap->m_Color.r / 255.0f, pTMap->m_Color.g / 255.0f, pTMap->m_Color.b / 255.0f, pTMap->m_Color.a / 255.0f * EntityOverlayVal / 100.0f); + const ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, EntityOverlayVal / 100.0f); if(!Graphics()->IsTileBufferingEnabled()) { Graphics()->BlendNone(); RenderTools()->RenderTunemap(pTuneTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, Color, TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_OPAQUE); Graphics()->BlendNormal(); RenderTools()->RenderTunemap(pTuneTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, Color, TILERENDERFLAG_EXTEND | LAYERRENDERFLAG_TRANSPARENT); - //RenderTools()->RenderTuneOverlay(pTuneTiles, pTMap->m_Width, pTMap->m_Height, 32.0f, EntityOverlayVal/100.0f); } else { diff --git a/src/game/client/components/maplayers.h b/src/game/client/components/maplayers.h index dbc4f05136..7e1d47ce9f 100644 --- a/src/game/client/components/maplayers.h +++ b/src/game/client/components/maplayers.h @@ -35,6 +35,8 @@ class CMapLayers : public CComponent int m_LastLocalTick; bool m_EnvelopeUpdate; + bool m_OnlineOnly; + struct STileLayerVisuals { STileLayerVisuals() : @@ -150,15 +152,14 @@ class CMapLayers : public CComponent virtual void OnRender() override; virtual void OnMapLoad() override; - void RenderTileLayer(int LayerIndex, ColorRGBA &Color, CMapItemLayerTilemap *pTileLayer, CMapItemGroup *pGroup); + void RenderTileLayer(int LayerIndex, const ColorRGBA &Color, CMapItemLayerTilemap *pTileLayer, CMapItemGroup *pGroup); void RenderTileBorder(int LayerIndex, const ColorRGBA &Color, CMapItemLayerTilemap *pTileLayer, CMapItemGroup *pGroup, int BorderX0, int BorderY0, int BorderX1, int BorderY1); void RenderKillTileBorder(int LayerIndex, const ColorRGBA &Color, CMapItemLayerTilemap *pTileLayer, CMapItemGroup *pGroup); void RenderQuadLayer(int LayerIndex, CMapItemLayerQuads *pQuadLayer, CMapItemGroup *pGroup, bool ForceRender = false); void EnvelopeUpdate(); - static void EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels, void *pUser); - bool m_OnlineOnly; + static void EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser); }; #endif diff --git a/src/game/client/components/mapsounds.cpp b/src/game/client/components/mapsounds.cpp index 9f7c0b93e3..fd65b9d4b2 100644 --- a/src/game/client/components/mapsounds.cpp +++ b/src/game/client/components/mapsounds.cpp @@ -17,12 +17,31 @@ CMapSounds::CMapSounds() m_Count = 0; } +void CMapSounds::Play(int Channel, int SoundId) +{ + if(SoundId < 0 || SoundId >= m_Count) + return; + + m_pClient->m_Sounds.PlaySample(Channel, m_aSounds[SoundId], 0, 1.0f); +} + +void CMapSounds::PlayAt(int Channel, int SoundId, vec2 Position) +{ + if(SoundId < 0 || SoundId >= m_Count) + return; + + m_pClient->m_Sounds.PlaySampleAt(Channel, m_aSounds[SoundId], 0, 1.0f, Position); +} + void CMapSounds::OnMapLoad() { IMap *pMap = Kernel()->RequestInterface(); Clear(); + if(!Sound()->IsSoundEnabled()) + return; + // load samples int Start; pMap->GetType(MAPITEMTYPE_SOUND, &Start, &m_Count); @@ -51,8 +70,9 @@ void CMapSounds::OnMapLoad() } else { - void *pData = pMap->GetData(pSound->m_SoundData); - m_aSounds[i] = Sound()->LoadOpusFromMem(pData, pSound->m_SoundDataSize); + const int SoundDataSize = pMap->GetDataSize(pSound->m_SoundData); + const void *pData = pMap->GetData(pSound->m_SoundData); + m_aSounds[i] = Sound()->LoadOpusFromMem(pData, SoundDataSize); pMap->UnloadData(pSound->m_SoundData); } ShowWarning = ShowWarning || m_aSounds[i] == -1; @@ -63,7 +83,6 @@ void CMapSounds::OnMapLoad() } // enqueue sound sources - m_vSourceQueue.clear(); for(int g = 0; g < Layers()->NumGroups(); g++) { CMapItemGroup *pGroup = Layers()->GetGroup(g); @@ -98,6 +117,7 @@ void CMapSounds::OnMapLoad() CSourceQueueEntry Source; Source.m_Sound = pSoundLayer->m_Sound; Source.m_pSource = &pSources[i]; + Source.m_HighDetail = pLayer->m_Flags & LAYERFLAG_DETAIL; if(!Source.m_pSource || Source.m_Sound < 0 || Source.m_Sound >= m_Count) continue; @@ -127,7 +147,7 @@ void CMapSounds::OnRender() Client()->IntraGameTick(g_Config.m_ClDummy)); } float Offset = s_Time - Source.m_pSource->m_TimeDelay; - if(!DemoPlayerPaused && Offset >= 0.0f && g_Config.m_SndEnable) + if(!DemoPlayerPaused && Offset >= 0.0f && g_Config.m_SndEnable && (g_Config.m_GfxHighDetail || !Source.m_HighDetail)) { if(Source.m_Voice.IsValid()) { @@ -143,7 +163,7 @@ void CMapSounds::OnRender() if(!Source.m_pSource->m_Pan) Flags |= ISound::FLAG_NO_PANNING; - Source.m_Voice = m_pClient->m_Sounds.PlaySampleAt(CSounds::CHN_MAPSOUND, m_aSounds[Source.m_Sound], 1.0f, vec2(fx2f(Source.m_pSource->m_Position.x), fx2f(Source.m_pSource->m_Position.y)), Flags); + Source.m_Voice = m_pClient->m_Sounds.PlaySampleAt(CSounds::CHN_MAPSOUND, m_aSounds[Source.m_Sound], Flags, 1.0f, vec2(fx2f(Source.m_pSource->m_Position.x), fx2f(Source.m_pSource->m_Position.y))); Sound()->SetVoiceTimeOffset(Source.m_Voice, Offset); Sound()->SetVoiceFalloff(Source.m_Voice, Source.m_pSource->m_Falloff / 255.0f); switch(Source.m_pSource->m_Shape.m_Type) @@ -207,18 +227,11 @@ void CMapSounds::OnRender() if(!Voice.m_Voice.IsValid()) continue; - float OffsetX = 0, OffsetY = 0; + ColorRGBA Position = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + CMapLayers::EnvelopeEval(Voice.m_pSource->m_PosEnvOffset, Voice.m_pSource->m_PosEnv, Position, 2, &m_pClient->m_MapLayersBackground); - if(Voice.m_pSource->m_PosEnv >= 0) - { - ColorRGBA Channels; - CMapLayers::EnvelopeEval(Voice.m_pSource->m_PosEnvOffset, Voice.m_pSource->m_PosEnv, Channels, &m_pClient->m_MapLayersBackground); - OffsetX = Channels.r; - OffsetY = Channels.g; - } - - float x = fx2f(Voice.m_pSource->m_Position.x) + OffsetX; - float y = fx2f(Voice.m_pSource->m_Position.y) + OffsetY; + float x = fx2f(Voice.m_pSource->m_Position.x) + Position.r; + float y = fx2f(Voice.m_pSource->m_Position.y) + Position.g; x += Center.x * (1.0f - pGroup->m_ParallaxX / 100.0f); y += Center.y * (1.0f - pGroup->m_ParallaxY / 100.0f); @@ -226,15 +239,13 @@ void CMapSounds::OnRender() x -= pGroup->m_OffsetX; y -= pGroup->m_OffsetY; - Sound()->SetVoiceLocation(Voice.m_Voice, x, y); + Sound()->SetVoicePosition(Voice.m_Voice, vec2(x, y)); - if(Voice.m_pSource->m_SoundEnv >= 0) + ColorRGBA Volume = ColorRGBA(1.0f, 0.0f, 0.0f, 0.0f); + CMapLayers::EnvelopeEval(Voice.m_pSource->m_SoundEnvOffset, Voice.m_pSource->m_SoundEnv, Volume, 1, &m_pClient->m_MapLayersBackground); + if(Volume.r < 1.0f) { - ColorRGBA Channels; - CMapLayers::EnvelopeEval(Voice.m_pSource->m_SoundEnvOffset, Voice.m_pSource->m_SoundEnv, Channels, &m_pClient->m_MapLayersBackground); - float Volume = clamp(Channels.r, 0.0f, 1.0f); - - Sound()->SetVoiceVolume(Voice.m_Voice, Volume); + Sound()->SetVoiceVolume(Voice.m_Voice, Volume.r); } } } @@ -246,6 +257,7 @@ void CMapSounds::OnRender() void CMapSounds::Clear() { // unload all samples + m_vSourceQueue.clear(); for(int i = 0; i < m_Count; i++) { Sound()->UnloadSample(m_aSounds[i]); diff --git a/src/game/client/components/mapsounds.h b/src/game/client/components/mapsounds.h index afdd7353fb..ffca022e5d 100644 --- a/src/game/client/components/mapsounds.h +++ b/src/game/client/components/mapsounds.h @@ -18,6 +18,7 @@ class CMapSounds : public CComponent struct CSourceQueueEntry { int m_Sound; + bool m_HighDetail; ISound::CVoiceHandle m_Voice; CSoundSource *m_pSource; @@ -32,6 +33,9 @@ class CMapSounds : public CComponent CMapSounds(); virtual int Sizeof() const override { return sizeof(*this); } + void Play(int Channel, int SoundId); + void PlayAt(int Channel, int SoundId, vec2 Position); + virtual void OnMapLoad() override; virtual void OnRender() override; virtual void OnStateChange(int NewState, int OldState) override; diff --git a/src/game/client/components/menu_background.cpp b/src/game/client/components/menu_background.cpp index 7a6ee7b783..a92f271f5e 100644 --- a/src/game/client/components/menu_background.cpp +++ b/src/game/client/components/menu_background.cpp @@ -307,15 +307,14 @@ bool CMenuBackground::Render() return false; m_Camera.m_Zoom = 0.7f; - static vec2 Dir = vec2(1.0f, 0.0f); float DistToCenter = distance(m_Camera.m_Center, m_RotationCenter); if(!m_ChangedPosition && absolute(DistToCenter - (float)g_Config.m_ClRotationRadius) <= 0.5f) { // do little rotation float RotPerTick = 360.0f / (float)g_Config.m_ClRotationSpeed * clamp(Client()->RenderFrameTime(), 0.0f, 0.1f); - Dir = rotate(Dir, RotPerTick); - m_Camera.m_Center = m_RotationCenter + Dir * (float)g_Config.m_ClRotationRadius; + m_CurrentDirection = rotate(m_CurrentDirection, RotPerTick); + m_Camera.m_Center = m_RotationCenter + m_CurrentDirection * (float)g_Config.m_ClRotationRadius; } else { @@ -328,16 +327,16 @@ bool CMenuBackground::Render() vec2 TargetPos = m_RotationCenter + DirToCenter * (float)g_Config.m_ClRotationRadius; float Distance = distance(m_AnimationStartPos, TargetPos); if(Distance > 0.001f) - Dir = normalize(m_AnimationStartPos - TargetPos); + m_CurrentDirection = normalize(m_AnimationStartPos - TargetPos); else - Dir = vec2(1, 0); + m_CurrentDirection = vec2(1.0f, 0.0f); // move time m_MoveTime += clamp(Client()->RenderFrameTime(), 0.0f, 0.1f) * g_Config.m_ClCameraSpeed / 10.0f; float XVal = 1 - m_MoveTime; XVal = std::pow(XVal, 7.0f); - m_Camera.m_Center = TargetPos + Dir * (XVal * Distance); + m_Camera.m_Center = TargetPos + m_CurrentDirection * (XVal * Distance); if(m_CurrentPosition < 0) { m_AnimationStartPos = m_Camera.m_Center; diff --git a/src/game/client/components/menu_background.h b/src/game/client/components/menu_background.h index 5cc7e2066b..a2f3791e6a 100644 --- a/src/game/client/components/menu_background.h +++ b/src/game/client/components/menu_background.h @@ -61,13 +61,14 @@ class CMenuBackground : public CBackground POS_BROWSER_CUSTOM1, POS_BROWSER_CUSTOM2, POS_BROWSER_CUSTOM3, + POS_BROWSER_CUSTOM4, POS_RESERVED0, POS_RESERVED1, POS_RESERVED2, NUM_POS, - POS_BROWSER_CUSTOM_NUM = (POS_BROWSER_CUSTOM3 - POS_BROWSER_CUSTOM0) + 1, + POS_BROWSER_CUSTOM_NUM = (POS_BROWSER_CUSTOM4 - POS_BROWSER_CUSTOM0) + 1, POS_SETTINGS_RESERVED_NUM = (POS_SETTINGS_RESERVED1 - POS_SETTINGS_RESERVED0) + 1, POS_RESERVED_NUM = (POS_RESERVED2 - POS_RESERVED0) + 1, }; @@ -84,6 +85,7 @@ class CMenuBackground : public CBackground vec2 m_RotationCenter; std::array m_aPositions; int m_CurrentPosition; + vec2 m_CurrentDirection = vec2(1.0f, 0.0f); vec2 m_AnimationStartPos; bool m_ChangedPosition; float m_MoveTime; diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index 8cd07433c3..0c0f07f8f2 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -56,10 +57,8 @@ float CMenus::ms_ListheaderHeight = 17.0f; CMenus::CMenus() { m_Popup = POPUP_NONE; - m_ActivePage = PAGE_INTERNET; m_MenuPage = 0; m_GamePage = PAGE_GAME; - m_JoinTutorial = false; m_NeedRestartGraphics = false; m_NeedRestartSound = false; @@ -100,7 +99,7 @@ CMenus::CMenus() m_PasswordInput.SetHidden(true); } -int CMenus::DoButton_Toggle(const void *pID, int Checked, const CUIRect *pRect, bool Active) +int CMenus::DoButton_Toggle(const void *pId, int Checked, const CUIRect *pRect, bool Active) { Graphics()->TextureSet(g_pData->m_aImages[IMAGE_GUIBUTTONS].m_Id); Graphics()->QuadsBegin(); @@ -109,7 +108,7 @@ int CMenus::DoButton_Toggle(const void *pID, int Checked, const CUIRect *pRect, RenderTools()->SelectSprite(Checked ? SPRITE_GUIBUTTON_ON : SPRITE_GUIBUTTON_OFF); IGraphics::CQuadItem QuadItem(pRect->x, pRect->y, pRect->w, pRect->h); Graphics()->QuadsDrawTL(&QuadItem, 1); - if(UI()->HotItem() == pID && Active) + if(Ui()->HotItem() == pId && Active) { RenderTools()->SelectSprite(SPRITE_GUIBUTTON_HOVER); QuadItem = IGraphics::CQuadItem(pRect->x, pRect->y, pRect->w, pRect->h); @@ -117,7 +116,7 @@ int CMenus::DoButton_Toggle(const void *pID, int Checked, const CUIRect *pRect, } Graphics()->QuadsEnd(); - return Active ? UI()->DoButtonLogic(pID, Checked, pRect) : 0; + return Active ? Ui()->DoButtonLogic(pId, Checked, pRect) : 0; } int CMenus::DoButton_Menu(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, const char *pImageName, int Corners, float Rounding, float FontFactor, ColorRGBA Color) @@ -126,7 +125,7 @@ int CMenus::DoButton_Menu(CButtonContainer *pButtonContainer, const char *pText, if(Checked) Color = ColorRGBA(0.6f, 0.6f, 0.6f, 0.5f); - Color.a *= UI()->ButtonColorMul(pButtonContainer); + Color.a *= Ui()->ButtonColorMul(pButtonContainer); pRect->Draw(Color, Corners, Rounding); @@ -139,7 +138,7 @@ int CMenus::DoButton_Menu(CButtonContainer *pButtonContainer, const char *pText, const CMenuImage *pImage = FindMenuImage(pImageName); if(pImage) { - Graphics()->TextureSet(UI()->HotItem() == pButtonContainer ? pImage->m_OrgTexture : pImage->m_GreyTexture); + Graphics()->TextureSet(Ui()->HotItem() == pButtonContainer ? pImage->m_OrgTexture : pImage->m_GreyTexture); Graphics()->WrapClamp(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); @@ -152,14 +151,14 @@ int CMenus::DoButton_Menu(CButtonContainer *pButtonContainer, const char *pText, Text.HMargin(pRect->h >= 20.0f ? 2.0f : 1.0f, &Text); Text.HMargin((Text.h * FontFactor) / 2.0f, &Text); - UI()->DoLabel(&Text, pText, Text.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + Ui()->DoLabel(&Text, pText, Text.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); - return UI()->DoButtonLogic(pButtonContainer, Checked, pRect); + return Ui()->DoButtonLogic(pButtonContainer, Checked, pRect); } -int CMenus::DoButton_MenuTab(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, SUIAnimator *pAnimator, const ColorRGBA *pDefaultColor, const ColorRGBA *pActiveColor, const ColorRGBA *pHoverColor, float EdgeRounding) +int CMenus::DoButton_MenuTab(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, SUIAnimator *pAnimator, const ColorRGBA *pDefaultColor, const ColorRGBA *pActiveColor, const ColorRGBA *pHoverColor, float EdgeRounding, const SCommunityIcon *pCommunityIcon) { - const bool MouseInside = UI()->HotItem() == pButtonContainer; + const bool MouseInside = Ui()->HotItem() == pButtonContainer; CUIRect Rect = *pRect; if(pAnimator != NULL) @@ -230,14 +229,23 @@ int CMenus::DoButton_MenuTab(CButtonContainer *pButtonContainer, const char *pTe } } - CUIRect Temp; - Rect.HMargin(2.0f, &Temp); - UI()->DoLabel(&Temp, pText, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + if(pCommunityIcon) + { + CUIRect CommunityIcon; + Rect.Margin(2.0f, &CommunityIcon); + RenderCommunityIcon(pCommunityIcon, CommunityIcon, true); + } + else + { + CUIRect Label; + Rect.HMargin(2.0f, &Label); + Ui()->DoLabel(&Label, pText, Label.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); + } - return UI()->DoButtonLogic(pButtonContainer, Checked, pRect); + return Ui()->DoButtonLogic(pButtonContainer, Checked, pRect); } -int CMenus::DoButton_GridHeader(const void *pID, const char *pText, int Checked, const CUIRect *pRect) +int CMenus::DoButton_GridHeader(const void *pId, const char *pText, int Checked, const CUIRect *pRect) { if(Checked == 2) pRect->Draw(ColorRGBA(1, 0.98f, 0.5f, 0.55f), IGraphics::CORNER_T, 5.0f); @@ -246,34 +254,52 @@ int CMenus::DoButton_GridHeader(const void *pID, const char *pText, int Checked, CUIRect Temp; pRect->VSplitLeft(5.0f, nullptr, &Temp); - UI()->DoLabel(&Temp, pText, pRect->h * CUI::ms_FontmodHeight, TEXTALIGN_ML); - return UI()->DoButtonLogic(pID, Checked, pRect); + Ui()->DoLabel(&Temp, pText, pRect->h * CUi::ms_FontmodHeight, TEXTALIGN_ML); + return Ui()->DoButtonLogic(pId, Checked, pRect); +} + +int CMenus::DoButton_Favorite(const void *pButtonId, const void *pParentId, bool Checked, const CUIRect *pRect) +{ + if(Checked || (pParentId != nullptr && Ui()->HotItem() == pParentId) || Ui()->HotItem() == pButtonId) + { + TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); + const float Alpha = Ui()->HotItem() == pButtonId ? 0.2f : 0.0f; + TextRender()->TextColor(Checked ? ColorRGBA(1.0f, 0.85f, 0.3f, 0.8f + Alpha) : ColorRGBA(0.5f, 0.5f, 0.5f, 0.8f + Alpha)); + SLabelProperties Props; + Props.m_MaxWidth = pRect->w; + Ui()->DoLabel(pRect, FONT_ICON_STAR, 12.0f, TEXTALIGN_MC, Props); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); + } + return Ui()->DoButtonLogic(pButtonId, 0, pRect); } -int CMenus::DoButton_CheckBox_Common(const void *pID, const char *pText, const char *pBoxText, const CUIRect *pRect) +int CMenus::DoButton_CheckBox_Common(const void *pId, const char *pText, const char *pBoxText, const CUIRect *pRect) { CUIRect Box, Label; pRect->VSplitLeft(pRect->h, &Box, &Label); Label.VSplitLeft(5.0f, nullptr, &Label); Box.Margin(2.0f, &Box); - Box.Draw(ColorRGBA(1, 1, 1, 0.25f * UI()->ButtonColorMul(pID)), IGraphics::CORNER_ALL, 3.0f); + Box.Draw(ColorRGBA(1, 1, 1, 0.25f * Ui()->ButtonColorMul(pId)), IGraphics::CORNER_ALL, 3.0f); const bool Checkable = *pBoxText == 'X'; if(Checkable) { TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT); TextRender()->SetFontPreset(EFontPreset::ICON_FONT); - UI()->DoLabel(&Box, FONT_ICON_XMARK, Box.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + Ui()->DoLabel(&Box, FONT_ICON_XMARK, Box.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); } else - UI()->DoLabel(&Box, pBoxText, Box.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + Ui()->DoLabel(&Box, pBoxText, Box.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); TextRender()->SetRenderFlags(0); - UI()->DoLabel(&Label, pText, Box.h * CUI::ms_FontmodHeight, TEXTALIGN_ML); + Ui()->DoLabel(&Label, pText, Box.h * CUi::ms_FontmodHeight, TEXTALIGN_ML); - return UI()->DoButtonLogic(pID, 0, pRect); + return Ui()->DoButtonLogic(pId, 0, pRect); } void CMenus::DoLaserPreview(const CUIRect *pRect, const ColorHSLA LaserOutlineColor, const ColorHSLA LaserInnerColor, const int LaserType) @@ -347,37 +373,37 @@ void CMenus::DoLaserPreview(const CUIRect *pRect, const ColorHSLA LaserOutlineCo } } -ColorHSLA CMenus::DoLine_ColorPicker(CButtonContainer *pResetID, const float LineSize, const float LabelSize, const float BottomMargin, CUIRect *pMainRect, const char *pText, unsigned int *pColorValue, const ColorRGBA DefaultColor, bool CheckBoxSpacing, int *pCheckBoxValue, bool Alpha) +ColorHSLA CMenus::DoLine_ColorPicker(CButtonContainer *pResetId, const float LineSize, const float LabelSize, const float BottomMargin, CUIRect *pMainRect, const char *pText, unsigned int *pColorValue, const ColorRGBA DefaultColor, bool CheckBoxSpacing, int *pCheckBoxValue, bool Alpha) { CUIRect Section, ColorPickerButton, ResetButton, Label; pMainRect->HSplitTop(LineSize, &Section, pMainRect); pMainRect->HSplitTop(BottomMargin, nullptr, pMainRect); - if(CheckBoxSpacing || pCheckBoxValue != nullptr) - { - CUIRect CheckBox; - Section.VSplitLeft(Section.h, &CheckBox, &Section); - if(pCheckBoxValue != nullptr) - { - CheckBox.Margin(2.0f, &CheckBox); - if(DoButton_CheckBox(pCheckBoxValue, "", *pCheckBoxValue, &CheckBox)) - *pCheckBoxValue ^= 1; - } - Section.VSplitLeft(5.0f, nullptr, &Section); - } - - Section.VSplitMid(&Label, &Section, 4.0f); Section.VSplitRight(60.0f, &Section, &ResetButton); Section.VSplitRight(8.0f, &Section, nullptr); Section.VSplitRight(Section.h, &Section, &ColorPickerButton); + Section.VSplitRight(8.0f, &Label, nullptr); - UI()->DoLabel(&Label, pText, LabelSize, TEXTALIGN_ML); + if(pCheckBoxValue != nullptr) + { + Label.Margin(2.0f, &Label); + if(DoButton_CheckBox(pCheckBoxValue, pText, *pCheckBoxValue, &Label)) + *pCheckBoxValue ^= 1; + } + else if(CheckBoxSpacing) + { + Label.VSplitLeft(Label.h + 5.0f, nullptr, &Label); + } + if(pCheckBoxValue == nullptr) + { + Ui()->DoLabel(&Label, pText, LabelSize, TEXTALIGN_ML); + } - ColorHSLA PickedColor = DoButton_ColorPicker(&ColorPickerButton, pColorValue, Alpha); + const ColorHSLA PickedColor = DoButton_ColorPicker(&ColorPickerButton, pColorValue, Alpha); ResetButton.HMargin(2.0f, &ResetButton); - if(DoButton_Menu(pResetID, Localize("Reset"), 0, &ResetButton, nullptr, IGraphics::CORNER_ALL, 4.0f, 0.1f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f))) + if(DoButton_Menu(pResetId, Localize("Reset"), 0, &ResetButton, nullptr, IGraphics::CORNER_ALL, 4.0f, 0.1f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f))) { *pColorValue = color_cast(DefaultColor).Pack(Alpha); } @@ -390,7 +416,7 @@ ColorHSLA CMenus::DoButton_ColorPicker(const CUIRect *pRect, unsigned int *pHsla ColorHSLA HslaColor = ColorHSLA(*pHslaColor, Alpha); ColorRGBA Outline = ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f); - Outline.a *= UI()->ButtonColorMul(pHslaColor); + Outline.a *= Ui()->ButtonColorMul(pHslaColor); CUIRect Rect; pRect->Margin(3.0f, &Rect); @@ -398,17 +424,17 @@ ColorHSLA CMenus::DoButton_ColorPicker(const CUIRect *pRect, unsigned int *pHsla pRect->Draw(Outline, IGraphics::CORNER_ALL, 4.0f); Rect.Draw(color_cast(HslaColor), IGraphics::CORNER_ALL, 4.0f); - static CUI::SColorPickerPopupContext s_ColorPickerPopupContext; - if(UI()->DoButtonLogic(pHslaColor, 0, pRect)) + static CUi::SColorPickerPopupContext s_ColorPickerPopupContext; + if(Ui()->DoButtonLogic(pHslaColor, 0, pRect)) { s_ColorPickerPopupContext.m_pHslaColor = pHslaColor; s_ColorPickerPopupContext.m_HslaColor = HslaColor; s_ColorPickerPopupContext.m_HsvaColor = color_cast(HslaColor); s_ColorPickerPopupContext.m_RgbaColor = color_cast(s_ColorPickerPopupContext.m_HsvaColor); s_ColorPickerPopupContext.m_Alpha = Alpha; - UI()->ShowPopupColorPicker(UI()->MouseX(), UI()->MouseY(), &s_ColorPickerPopupContext); + Ui()->ShowPopupColorPicker(Ui()->MouseX(), Ui()->MouseY(), &s_ColorPickerPopupContext); } - else if(UI()->IsPopupOpen(&s_ColorPickerPopupContext) && s_ColorPickerPopupContext.m_pHslaColor == pHslaColor) + else if(Ui()->IsPopupOpen(&s_ColorPickerPopupContext) && s_ColorPickerPopupContext.m_pHslaColor == pHslaColor) { HslaColor = color_cast(s_ColorPickerPopupContext.m_HsvaColor); } @@ -416,12 +442,12 @@ ColorHSLA CMenus::DoButton_ColorPicker(const CUIRect *pRect, unsigned int *pHsla return HslaColor; } -int CMenus::DoButton_CheckBoxAutoVMarginAndSet(const void *pID, const char *pText, int *pValue, CUIRect *pRect, float VMargin) +int CMenus::DoButton_CheckBoxAutoVMarginAndSet(const void *pId, const char *pText, int *pValue, CUIRect *pRect, float VMargin) { CUIRect CheckBoxRect; pRect->HSplitTop(VMargin, &CheckBoxRect, pRect); - int Logic = DoButton_CheckBox_Common(pID, pText, *pValue ? "X" : "", &CheckBoxRect); + int Logic = DoButton_CheckBox_Common(pId, pText, *pValue ? "X" : "", &CheckBoxRect); if(Logic) *pValue ^= 1; @@ -429,109 +455,148 @@ int CMenus::DoButton_CheckBoxAutoVMarginAndSet(const void *pID, const char *pTex return Logic; } -int CMenus::DoButton_CheckBox(const void *pID, const char *pText, int Checked, const CUIRect *pRect) +int CMenus::DoButton_CheckBox(const void *pId, const char *pText, int Checked, const CUIRect *pRect) { - return DoButton_CheckBox_Common(pID, pText, Checked ? "X" : "", pRect); + return DoButton_CheckBox_Common(pId, pText, Checked ? "X" : "", pRect); } -int CMenus::DoButton_CheckBox_Number(const void *pID, const char *pText, int Checked, const CUIRect *pRect) +int CMenus::DoButton_CheckBox_Number(const void *pId, const char *pText, int Checked, const CUIRect *pRect) { char aBuf[16]; - str_from_int(Checked, aBuf); - return DoButton_CheckBox_Common(pID, pText, aBuf, pRect); + str_format(aBuf, sizeof(aBuf), "%d", Checked); + return DoButton_CheckBox_Common(pId, pText, aBuf, pRect); } -int CMenus::DoKeyReader(const void *pID, const CUIRect *pRect, int Key, int ModifierCombination, int *pNewModifierCombination) +int CMenus::DoKeyReader(const void *pId, const CUIRect *pRect, int Key, int ModifierCombination, int *pNewModifierCombination) { - // process - static const void *s_pGrabbedID = nullptr; - static bool s_MouseReleased = true; - static int s_ButtonUsed = 0; - const bool Inside = UI()->MouseHovered(pRect); int NewKey = Key; *pNewModifierCombination = ModifierCombination; - if(!UI()->MouseButton(0) && !UI()->MouseButton(1) && s_pGrabbedID == pID) - s_MouseReleased = true; - - if(UI()->CheckActiveItem(pID)) + const int ButtonResult = Ui()->DoButtonLogic(pId, 0, pRect); + if(ButtonResult == 1) { - if(m_Binder.m_GotKey) - { - // abort with escape key - if(m_Binder.m_Key.m_Key != KEY_ESCAPE) - { - NewKey = m_Binder.m_Key.m_Key; - *pNewModifierCombination = m_Binder.m_ModifierCombination; - } - m_Binder.m_GotKey = false; - UI()->SetActiveItem(nullptr); - s_MouseReleased = false; - s_pGrabbedID = pID; - } - - if(s_ButtonUsed == 1 && !UI()->MouseButton(1)) - { - if(Inside) - NewKey = 0; - UI()->SetActiveItem(nullptr); - } + m_Binder.m_pKeyReaderId = pId; + m_Binder.m_TakeKey = true; + m_Binder.m_GotKey = false; } - else if(UI()->HotItem() == pID) + else if(ButtonResult == 2) { - if(s_MouseReleased) - { - if(UI()->MouseButton(0)) - { - m_Binder.m_TakeKey = true; - m_Binder.m_GotKey = false; - UI()->SetActiveItem(pID); - s_ButtonUsed = 0; - } + NewKey = 0; + *pNewModifierCombination = CBinds::MODIFIER_NONE; + } - if(UI()->MouseButton(1)) - { - UI()->SetActiveItem(pID); - s_ButtonUsed = 1; - } + if(m_Binder.m_pKeyReaderId == pId && m_Binder.m_GotKey) + { + // abort with escape key + if(m_Binder.m_Key.m_Key != KEY_ESCAPE) + { + NewKey = m_Binder.m_Key.m_Key; + *pNewModifierCombination = m_Binder.m_ModifierCombination; } + m_Binder.m_pKeyReaderId = nullptr; + m_Binder.m_GotKey = false; + Ui()->SetActiveItem(nullptr); } - if(Inside) - UI()->SetHotItem(pID); - char aBuf[64]; - if(UI()->CheckActiveItem(pID) && s_ButtonUsed == 0) + if(m_Binder.m_pKeyReaderId == pId && m_Binder.m_TakeKey) str_copy(aBuf, Localize("Press a key…")); else if(NewKey == 0) aBuf[0] = '\0'; else - str_format(aBuf, sizeof(aBuf), "%s%s", CBinds::GetKeyBindModifiersName(*pNewModifierCombination), Input()->KeyName(NewKey)); + { + char aModifiers[128]; + CBinds::GetKeyBindModifiersName(*pNewModifierCombination, aModifiers, sizeof(aModifiers)); + str_format(aBuf, sizeof(aBuf), "%s%s", aModifiers, Input()->KeyName(NewKey)); + } - const ColorRGBA Color = UI()->CheckActiveItem(pID) && m_Binder.m_TakeKey ? ColorRGBA(0.0f, 1.0f, 0.0f, 0.4f) : ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * UI()->ButtonColorMul(pID)); + const ColorRGBA Color = m_Binder.m_pKeyReaderId == pId && m_Binder.m_TakeKey ? ColorRGBA(0.0f, 1.0f, 0.0f, 0.4f) : ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * Ui()->ButtonColorMul(pId)); pRect->Draw(Color, IGraphics::CORNER_ALL, 5.0f); CUIRect Temp; pRect->HMargin(1.0f, &Temp); - UI()->DoLabel(&Temp, aBuf, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + Ui()->DoLabel(&Temp, aBuf, Temp.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); return NewKey; } -int CMenus::RenderMenubar(CUIRect r) +void CMenus::RenderMenubar(CUIRect Box, IClient::EClientState ClientState) { - CUIRect Box = r; CUIRect Button; - m_ActivePage = m_MenuPage; int NewPage = -1; + int ActivePage = -1; + if(ClientState == IClient::STATE_OFFLINE) + { + ActivePage = m_MenuPage; + } + else if(ClientState == IClient::STATE_ONLINE) + { + ActivePage = m_GamePage; + } + else + { + dbg_assert(false, "Client state invalid for RenderMenubar"); + } + + // First render buttons aligned from right side so remaining + // width is known when rendering buttons from left side. + TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); + + Box.VSplitRight(33.0f, &Box, &Button); + static CButtonContainer s_QuitButton; + ColorRGBA QuitColor(1, 0, 0, 0.5f); + if(DoButton_MenuTab(&s_QuitButton, FONT_ICON_POWER_OFF, 0, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_QUIT], nullptr, nullptr, &QuitColor, 10.0f)) + { + if(m_pClient->Editor()->HasUnsavedData() || (Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0)) + { + m_Popup = POPUP_QUIT; + } + else + { + Client()->Quit(); + } + } + GameClient()->m_Tooltips.DoToolTip(&s_QuitButton, &Button, Localize("Quit")); + + Box.VSplitRight(10.0f, &Box, nullptr); + Box.VSplitRight(33.0f, &Box, &Button); + static CButtonContainer s_SettingsButton; + if(DoButton_MenuTab(&s_SettingsButton, FONT_ICON_GEAR, ActivePage == PAGE_SETTINGS, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_SETTINGS])) + { + NewPage = PAGE_SETTINGS; + } + GameClient()->m_Tooltips.DoToolTip(&s_SettingsButton, &Button, Localize("Settings")); + + Box.VSplitRight(10.0f, &Box, nullptr); + Box.VSplitRight(33.0f, &Box, &Button); + static CButtonContainer s_EditorButton; + if(DoButton_MenuTab(&s_EditorButton, FONT_ICON_PEN_TO_SQUARE, 0, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_EDITOR])) + { + g_Config.m_ClEditor = 1; + } + GameClient()->m_Tooltips.DoToolTip(&s_EditorButton, &Button, Localize("Editor")); + + if(ClientState == IClient::STATE_OFFLINE) + { + Box.VSplitRight(10.0f, &Box, nullptr); + Box.VSplitRight(33.0f, &Box, &Button); + static CButtonContainer s_DemoButton; + if(DoButton_MenuTab(&s_DemoButton, FONT_ICON_CLAPPERBOARD, ActivePage == PAGE_DEMOS, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_DEMOBUTTON])) + { + NewPage = PAGE_DEMOS; + } + GameClient()->m_Tooltips.DoToolTip(&s_DemoButton, &Button, Localize("Demos")); + } + + Box.VSplitRight(10.0f, &Box, nullptr); - if(Client()->State() != IClient::STATE_OFFLINE) - m_ActivePage = m_GamePage; + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - if(Client()->State() == IClient::STATE_OFFLINE) + if(ClientState == IClient::STATE_OFFLINE) { Box.VSplitLeft(33.0f, &Button, &Box); - static CButtonContainer s_StartButton; TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); @@ -551,8 +616,8 @@ int CMenus::RenderMenubar(CUIRect r) ColorRGBA HomeButtonColorAlert(0, 1, 0, 0.25f); ColorRGBA HomeButtonColorAlertHover(0, 1, 0, 0.5f); - ColorRGBA *pHomeButtonColor = NULL; - ColorRGBA *pHomeButtonColorHover = NULL; + ColorRGBA *pHomeButtonColor = nullptr; + ColorRGBA *pHomeButtonColorHover = nullptr; const char *pHomeScreenButtonLabel = FONT_ICON_HOUSE; if(GotNewsOrUpdate) @@ -562,247 +627,192 @@ int CMenus::RenderMenubar(CUIRect r) pHomeButtonColorHover = &HomeButtonColorAlertHover; } + static CButtonContainer s_StartButton; if(DoButton_MenuTab(&s_StartButton, pHomeScreenButtonLabel, false, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_HOME], pHomeButtonColor, pHomeButtonColor, pHomeButtonColorHover, 10.0f)) { m_ShowStart = true; } + GameClient()->m_Tooltips.DoToolTip(&s_StartButton, &Button, Localize("Main menu")); - TextRender()->SetRenderFlags(0); - TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - - Box.VSplitLeft(10.0f, 0, &Box); + const float BrowserButtonWidth = 75.0f; + Box.VSplitLeft(10.0f, nullptr, &Box); + Box.VSplitLeft(BrowserButtonWidth, &Button, &Box); + static CButtonContainer s_InternetButton; + if(DoButton_MenuTab(&s_InternetButton, FONT_ICON_EARTH_AMERICAS, ActivePage == PAGE_INTERNET, &Button, IGraphics::CORNER_T, &m_aAnimatorsBigPage[BIG_TAB_INTERNET])) + { + NewPage = PAGE_INTERNET; + } + GameClient()->m_Tooltips.DoToolTip(&s_InternetButton, &Button, Localize("Internet")); - // offline menus - if(m_ActivePage == PAGE_NEWS) + Box.VSplitLeft(BrowserButtonWidth, &Button, &Box); + static CButtonContainer s_LanButton; + if(DoButton_MenuTab(&s_LanButton, FONT_ICON_NETWORK_WIRED, ActivePage == PAGE_LAN, &Button, IGraphics::CORNER_T, &m_aAnimatorsBigPage[BIG_TAB_LAN])) { - Box.VSplitLeft(100.0f, &Button, &Box); - static CButtonContainer s_NewsButton; - if(DoButton_MenuTab(&s_NewsButton, Localize("Hall of fame"), m_ActivePage == PAGE_NEWS, &Button, IGraphics::CORNER_T, &m_aAnimatorsBigPage[BIG_TAB_NEWS])) - { - NewPage = PAGE_NEWS; - } + NewPage = PAGE_LAN; } - else if(m_ActivePage == PAGE_DEMOS) + GameClient()->m_Tooltips.DoToolTip(&s_LanButton, &Button, Localize("LAN")); + + Box.VSplitLeft(BrowserButtonWidth, &Button, &Box); + static CButtonContainer s_FavoritesButton; + if(DoButton_MenuTab(&s_FavoritesButton, FONT_ICON_STAR, ActivePage == PAGE_FAVORITES, &Button, IGraphics::CORNER_T, &m_aAnimatorsBigPage[BIG_TAB_FAVORITES])) { - Box.VSplitLeft(100.0f, &Button, &Box); - static CButtonContainer s_DemosButton; - if(DoButton_MenuTab(&s_DemosButton, Localize("Demos"), m_ActivePage == PAGE_DEMOS, &Button, IGraphics::CORNER_T, &m_aAnimatorsBigPage[BIG_TAB_DEMOS])) - { - DemolistPopulate(); - NewPage = PAGE_DEMOS; - } + NewPage = PAGE_FAVORITES; } - else + GameClient()->m_Tooltips.DoToolTip(&s_FavoritesButton, &Button, Localize("Favorites")); + + int MaxPage = PAGE_FAVORITES + ServerBrowser()->FavoriteCommunities().size(); + if( + !Ui()->IsPopupOpen() && + CLineInput::GetActiveInput() == nullptr && + (g_Config.m_UiPage >= PAGE_INTERNET && g_Config.m_UiPage <= MaxPage) && + (m_MenuPage >= PAGE_INTERNET && m_MenuPage <= PAGE_FAVORITE_COMMUNITY_5)) { - Box.VSplitLeft(100.0f, &Button, &Box); - static CButtonContainer s_InternetButton; - if(DoButton_MenuTab(&s_InternetButton, Localize("Internet"), m_ActivePage == PAGE_INTERNET, &Button, IGraphics::CORNER_T, &m_aAnimatorsBigPage[BIG_TAB_INTERNET])) + if(Input()->KeyPress(KEY_RIGHT)) { - if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_INTERNET) - { - if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_FAVORITES) - Client()->RequestDDNetInfo(); - ServerBrowser()->Refresh(IServerBrowser::TYPE_INTERNET); - } - NewPage = PAGE_INTERNET; + NewPage = g_Config.m_UiPage + 1; + if(NewPage > MaxPage) + NewPage = PAGE_INTERNET; } - - Box.VSplitLeft(100.0f, &Button, &Box); - static CButtonContainer s_LanButton; - if(DoButton_MenuTab(&s_LanButton, Localize("LAN"), m_ActivePage == PAGE_LAN, &Button, IGraphics::CORNER_T, &m_aAnimatorsBigPage[BIG_TAB_LAN])) + if(Input()->KeyPress(KEY_LEFT)) { - if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_LAN) - ServerBrowser()->Refresh(IServerBrowser::TYPE_LAN); - NewPage = PAGE_LAN; + NewPage = g_Config.m_UiPage - 1; + if(NewPage < PAGE_INTERNET) + NewPage = MaxPage; } + } - Box.VSplitLeft(100.0f, &Button, &Box); - static CButtonContainer s_FavoritesButton; - if(DoButton_MenuTab(&s_FavoritesButton, Localize("Favorites"), m_ActivePage == PAGE_FAVORITES, &Button, IGraphics::CORNER_T, &m_aAnimatorsBigPage[BIG_TAB_FAVORITES])) + size_t FavoriteCommunityIndex = 0; + static CButtonContainer s_aFavoriteCommunityButtons[5]; + static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)PAGE_FAVORITE_COMMUNITY_5 - PAGE_FAVORITE_COMMUNITY_1 + 1); + static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)BIT_TAB_FAVORITE_COMMUNITY_5 - BIT_TAB_FAVORITE_COMMUNITY_1 + 1); + static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)IServerBrowser::TYPE_FAVORITE_COMMUNITY_5 - IServerBrowser::TYPE_FAVORITE_COMMUNITY_1 + 1); + for(const CCommunity *pCommunity : ServerBrowser()->FavoriteCommunities()) + { + if(Box.w < BrowserButtonWidth) + break; + Box.VSplitLeft(BrowserButtonWidth, &Button, &Box); + const int Page = PAGE_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex; + if(DoButton_MenuTab(&s_aFavoriteCommunityButtons[FavoriteCommunityIndex], FONT_ICON_ELLIPSIS, ActivePage == Page, &Button, IGraphics::CORNER_T, &m_aAnimatorsBigPage[BIT_TAB_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex], nullptr, nullptr, nullptr, 10.0f, FindCommunityIcon(pCommunity->Id()))) { - if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_FAVORITES) - { - if(ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_INTERNET) - Client()->RequestDDNetInfo(); - ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITES); - } - NewPage = PAGE_FAVORITES; + NewPage = Page; } + GameClient()->m_Tooltips.DoToolTip(&s_aFavoriteCommunityButtons[FavoriteCommunityIndex], &Button, pCommunity->Name()); - //Box.VSplitLeft(100.0f, &Button, &Box); - //static CButtonContainer s_StatsButton; - //if(DoButton_MenuTab(&s_StatsButton, Localize("DDRace Stats"), m_ActivePage == PAGE_STATS, &Button, IGraphics::CORNER_T)) - //{ - // NewPage = PAGE_STATS; - //} + ++FavoriteCommunityIndex; + if(FavoriteCommunityIndex >= std::size(s_aFavoriteCommunityButtons)) + break; } + + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); } else { // online menus Box.VSplitLeft(90.0f, &Button, &Box); static CButtonContainer s_GameButton; - if(DoButton_MenuTab(&s_GameButton, Localize("Game"), m_ActivePage == PAGE_GAME, &Button, IGraphics::CORNER_TL)) + if(DoButton_MenuTab(&s_GameButton, Localize("Game"), ActivePage == PAGE_GAME, &Button, IGraphics::CORNER_TL)) NewPage = PAGE_GAME; Box.VSplitLeft(90.0f, &Button, &Box); static CButtonContainer s_PlayersButton; - if(DoButton_MenuTab(&s_PlayersButton, Localize("Players"), m_ActivePage == PAGE_PLAYERS, &Button, 0)) + if(DoButton_MenuTab(&s_PlayersButton, Localize("Players"), ActivePage == PAGE_PLAYERS, &Button, IGraphics::CORNER_NONE)) NewPage = PAGE_PLAYERS; Box.VSplitLeft(130.0f, &Button, &Box); static CButtonContainer s_ServerInfoButton; - if(DoButton_MenuTab(&s_ServerInfoButton, Localize("Server info"), m_ActivePage == PAGE_SERVER_INFO, &Button, 0)) + if(DoButton_MenuTab(&s_ServerInfoButton, Localize("Server info"), ActivePage == PAGE_SERVER_INFO, &Button, IGraphics::CORNER_NONE)) NewPage = PAGE_SERVER_INFO; Box.VSplitLeft(90.0f, &Button, &Box); static CButtonContainer s_NetworkButton; - if(DoButton_MenuTab(&s_NetworkButton, Localize("Browser"), m_ActivePage == PAGE_NETWORK, &Button, 0)) + if(DoButton_MenuTab(&s_NetworkButton, Localize("Browser"), ActivePage == PAGE_NETWORK, &Button, IGraphics::CORNER_NONE)) NewPage = PAGE_NETWORK; + if(GameClient()->m_GameInfo.m_Race) { + Box.VSplitLeft(90.0f, &Button, &Box); static CButtonContainer s_GhostButton; - if(GameClient()->m_GameInfo.m_Race) - { - Box.VSplitLeft(90.0f, &Button, &Box); - if(DoButton_MenuTab(&s_GhostButton, Localize("Ghost"), m_ActivePage == PAGE_GHOST, &Button, 0)) - NewPage = PAGE_GHOST; - } + if(DoButton_MenuTab(&s_GhostButton, Localize("Ghost"), ActivePage == PAGE_GHOST, &Button, IGraphics::CORNER_NONE)) + NewPage = PAGE_GHOST; } Box.VSplitLeft(100.0f, &Button, &Box); + Box.VSplitLeft(4.0f, nullptr, &Box); static CButtonContainer s_CallVoteButton; - if(DoButton_MenuTab(&s_CallVoteButton, Localize("Call vote"), m_ActivePage == PAGE_CALLVOTE, &Button, 0)) + if(DoButton_MenuTab(&s_CallVoteButton, Localize("Call vote"), ActivePage == PAGE_CALLVOTE, &Button, IGraphics::CORNER_TR)) { NewPage = PAGE_CALLVOTE; m_ControlPageOpening = true; } - Box.VSplitLeft(90.0f, &Button, &Box); - Box.VSplitLeft(4.0f, 0, &Box); - static CButtonContainer s_StatsButton; - if(DoButton_MenuTab(&s_StatsButton, Localize("Stats"), m_ActivePage == PAGE_STATS, &Button, IGraphics::CORNER_TR)) - { - NewPage = PAGE_STATS; - } - } - - TextRender()->SetFontPreset(EFontPreset::ICON_FONT); - TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - - Box.VSplitRight(33.0f, &Box, &Button); - static CButtonContainer s_QuitButton; - ColorRGBA QuitColor(1, 0, 0, 0.5f); - if(DoButton_MenuTab(&s_QuitButton, FONT_ICON_POWER_OFF, 0, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_QUIT], nullptr, nullptr, &QuitColor, 10.0f)) - { - if(m_pClient->Editor()->HasUnsavedData() || (Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0)) - { - m_Popup = POPUP_QUIT; - } - else - { - Client()->Quit(); - } - } - - Box.VSplitRight(10.0f, &Box, &Button); - Box.VSplitRight(33.0f, &Box, &Button); - static CButtonContainer s_SettingsButton; - - if(DoButton_MenuTab(&s_SettingsButton, FONT_ICON_GEAR, m_ActivePage == PAGE_SETTINGS, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_SETTINGS], nullptr, nullptr, nullptr, 10.0f)) - NewPage = PAGE_SETTINGS; - - Box.VSplitRight(10.0f, &Box, &Button); - Box.VSplitRight(33.0f, &Box, &Button); - static CButtonContainer s_EditorButton; - if(DoButton_MenuTab(&s_EditorButton, FONT_ICON_PEN_TO_SQUARE, 0, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_EDITOR], nullptr, nullptr, nullptr, 10.0f)) - { - g_Config.m_ClEditor = 1; - } - - if(Client()->State() == IClient::STATE_OFFLINE) - { - Box.VSplitRight(10.0f, &Box, &Button); - Box.VSplitRight(33.0f, &Box, &Button); - static CButtonContainer s_DemoButton; - - if(DoButton_MenuTab(&s_DemoButton, FONT_ICON_CLAPPERBOARD, m_ActivePage == PAGE_DEMOS, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_DEMOBUTTON], nullptr, nullptr, nullptr, 10.0f)) - NewPage = PAGE_DEMOS; - - Box.VSplitRight(10.0f, &Box, &Button); - Box.VSplitRight(33.0f, &Box, &Button); - static CButtonContainer s_ServerButton; - - if(DoButton_MenuTab(&s_ServerButton, FONT_ICON_EARTH_AMERICAS, m_ActivePage == g_Config.m_UiPage, &Button, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_SERVER], nullptr, nullptr, nullptr, 10.0f)) - NewPage = g_Config.m_UiPage; } - TextRender()->SetRenderFlags(0); - TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - if(NewPage != -1) { - if(Client()->State() == IClient::STATE_OFFLINE) + if(ClientState == IClient::STATE_OFFLINE) SetMenuPage(NewPage); else m_GamePage = NewPage; } - - return 0; } void CMenus::RenderLoading(const char *pCaption, const char *pContent, int IncreaseCounter, bool RenderLoadingBar, bool RenderMenuBackgroundMap) { // TODO: not supported right now due to separate render thread - static std::chrono::nanoseconds s_LastLoadRender{0}; - const int CurLoadRenderCount = m_LoadCurrent; - m_LoadCurrent += IncreaseCounter; - const float Percent = CurLoadRenderCount / (float)m_LoadTotal; + const int CurLoadRenderCount = m_LoadingState.m_Current; + m_LoadingState.m_Current += IncreaseCounter; // make sure that we don't render for each little thing we load // because that will slow down loading if we have vsync - if(time_get_nanoseconds() - s_LastLoadRender < std::chrono::nanoseconds(1s) / 60l) + const std::chrono::nanoseconds Now = time_get_nanoseconds(); + if(Now - m_LoadingState.m_LastRender < std::chrono::nanoseconds(1s) / 60l) return; - s_LastLoadRender = time_get_nanoseconds(); + m_LoadingState.m_LastRender = Now; // need up date this here to get correct ms_GuiColor = color_cast(ColorHSLA(g_Config.m_UiColor, true)); - UI()->MapScreen(); + Ui()->MapScreen(); - if(!RenderMenuBackgroundMap || !m_pBackground->Render()) + if(!RenderMenuBackgroundMap || !GameClient()->m_MenuBackground.Render()) { RenderBackground(); } - CUIRect Box = *UI()->Screen(); - Box.Margin(160.0f, &Box); + CUIRect Box; + Ui()->Screen()->Margin(160.0f, &Box); Graphics()->BlendNormal(); - Graphics()->TextureClear(); - Box.Draw(ColorRGBA{0, 0, 0, 0.50f}, IGraphics::CORNER_ALL, 15.0f); + Box.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 15.0f); + Box.Margin(20.0f, &Box); - CUIRect Part; - Box.HSplitTop(20.f, nullptr, &Box); - Box.HSplitTop(24.f, &Part, &Box); - Part.VMargin(20.f, &Part); - UI()->DoLabel(&Part, pCaption, 24.f, TEXTALIGN_MC); + CUIRect Label; + Box.HSplitTop(24.0f, &Label, &Box); + Ui()->DoLabel(&Label, pCaption, 24.0f, TEXTALIGN_MC); - Box.HSplitTop(20.f, nullptr, &Box); - Box.HSplitTop(24.f, &Part, &Box); - Part.VMargin(20.f, &Part); - UI()->DoLabel(&Part, pContent, 20.0f, TEXTALIGN_MC); + Box.HSplitTop(20.0f, nullptr, &Box); + Box.HSplitTop(24.0f, &Label, &Box); + Ui()->DoLabel(&Label, pContent, 20.0f, TEXTALIGN_MC); if(RenderLoadingBar) - Graphics()->DrawRect(Box.x + 40, Box.y + Box.h - 75, (Box.w - 80) * Percent, 25, ColorRGBA(1.0f, 1.0f, 1.0f, 0.75f), IGraphics::CORNER_ALL, 5.0f); + { + CUIRect ProgressBar; + Box.HSplitBottom(30.0f, &Box, nullptr); + Box.HSplitBottom(25.0f, &Box, &ProgressBar); + ProgressBar.VMargin(20.0f, &ProgressBar); + Ui()->RenderProgressBar(ProgressBar, CurLoadRenderCount / (float)m_LoadingState.m_Total); + } Client()->UpdateAndSwap(); } void CMenus::RenderNews(CUIRect MainView) { + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_NEWS); + g_Config.m_UiUnreadNews = false; MainView.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f); @@ -821,12 +831,12 @@ void CMenus::RenderNews(CUIRect MainView) { MainView.HSplitTop(30.0f, &Label, &MainView); aLine[Len - 1] = '\0'; - UI()->DoLabel(&Label, aLine + 1, 20.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, aLine + 1, 20.0f, TEXTALIGN_ML); } else { MainView.HSplitTop(20.0f, &Label, &MainView); - UI()->DoLabel(&Label, aLine, 15.f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, aLine, 15.f, TEXTALIGN_ML); } } } @@ -836,22 +846,43 @@ void CMenus::OnInit() if(g_Config.m_ClShowWelcome) { m_Popup = POPUP_LANGUAGE; - str_copy(g_Config.m_BrFilterExcludeCommunities, "*ddnet"); + m_CreateDefaultFavoriteCommunities = true; } + + if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 && + (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= ServerBrowser()->FavoriteCommunities().size()) + { + // Reset page to internet when there is no favorite community for this page. + g_Config.m_UiPage = PAGE_INTERNET; + } + if(g_Config.m_ClSkipStartMenu) + { m_ShowStart = false; + } - m_RefreshButton.Init(UI(), -1); - m_ConnectButton.Init(UI(), -1); + SetMenuPage(g_Config.m_UiPage); + + m_RefreshButton.Init(Ui(), -1); + m_ConnectButton.Init(Ui(), -1); Console()->Chain("add_favorite", ConchainFavoritesUpdate, this); Console()->Chain("remove_favorite", ConchainFavoritesUpdate, this); Console()->Chain("add_friend", ConchainFriendlistUpdate, this); Console()->Chain("remove_friend", ConchainFriendlistUpdate, this); - Console()->Chain("br_filter_exclude_communities", ConchainCommunitiesUpdate, this); + + Console()->Chain("add_excluded_community", ConchainCommunitiesUpdate, this); + Console()->Chain("remove_excluded_community", ConchainCommunitiesUpdate, this); + Console()->Chain("add_excluded_country", ConchainCommunitiesUpdate, this); + Console()->Chain("remove_excluded_country", ConchainCommunitiesUpdate, this); + Console()->Chain("add_excluded_type", ConchainCommunitiesUpdate, this); + Console()->Chain("remove_excluded_type", ConchainCommunitiesUpdate, this); + + Console()->Chain("ui_page", ConchainUiPageUpdate, this); Console()->Chain("snd_enable", ConchainUpdateMusicState, this); Console()->Chain("snd_enable_music", ConchainUpdateMusicState, this); + Console()->Chain("cl_background_entities", ConchainBackgroundEntities, this); Console()->Chain("cl_assets_entities", ConchainAssetsEntities, this); Console()->Chain("cl_asset_game", ConchainAssetGame, this); @@ -864,10 +895,10 @@ void CMenus::OnInit() // setup load amount const int NumMenuImages = 5; - m_LoadCurrent = 0; - m_LoadTotal = g_pData->m_NumImages + NumMenuImages + GameClient()->ComponentCount(); + m_LoadingState.m_Current = 0; + m_LoadingState.m_Total = g_pData->m_NumImages + NumMenuImages + GameClient()->ComponentCount(); if(!g_Config.m_ClThreadsoundloading) - m_LoadTotal += g_pData->m_NumSounds; + m_LoadingState.m_Total += g_pData->m_NumSounds; m_IsInit = true; @@ -887,6 +918,17 @@ void CMenus::OnConsoleInit() Console()->Register("remove_favorite_skin", "s[skin_name]", CFGFLAG_CLIENT, Con_RemFavoriteSkin, this, "Remove a skin from the favorites"); } +void CMenus::ConchainBackgroundEntities(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) +{ + pfnCallback(pResult, pCallbackUserData); + if(pResult->NumArguments()) + { + CMenus *pSelf = (CMenus *)pUserData; + if(str_comp(g_Config.m_ClBackgroundEntities, pSelf->m_pClient->m_Background.MapName()) != 0) + pSelf->m_pClient->m_Background.LoadBackground(); + } +} + void CMenus::ConchainUpdateMusicState(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) { pfnCallback(pResult, pCallbackUserData); @@ -907,7 +949,7 @@ void CMenus::UpdateMusicState() void CMenus::PopupMessage(const char *pTitle, const char *pMessage, const char *pButtonLabel, int NextPopup, FPopupButtonCallback pfnButtonCallback) { // reset active item - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); str_copy(m_aPopupTitle, pTitle); str_copy(m_aPopupMessage, pMessage); @@ -921,7 +963,7 @@ void CMenus::PopupConfirm(const char *pTitle, const char *pMessage, const char * FPopupButtonCallback pfnConfirmButtonCallback, int ConfirmNextPopup, FPopupButtonCallback pfnCancelButtonCallback, int CancelNextPopup) { // reset active item - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); str_copy(m_aPopupTitle, pTitle); str_copy(m_aPopupMessage, pMessage); @@ -943,7 +985,7 @@ void CMenus::PopupWarning(const char *pTopic, const char *pBody, const char *pBu dbg_msg(pTopic, "%s", BodyStr.c_str()); // reset active item - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); str_copy(m_aMessageTopic, pTopic); str_copy(m_aMessageBody, pBody); @@ -960,29 +1002,61 @@ bool CMenus::CanDisplayWarning() const return m_Popup == POPUP_NONE; } -int CMenus::Render() +void CMenus::Render() { - if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_Popup == POPUP_NONE) - return 0; - - CUIRect Screen = *UI()->Screen(); - UI()->MapScreen(); - UI()->ResetMouseSlow(); + Ui()->MapScreen(); + Ui()->ResetMouseSlow(); static int s_Frame = 0; if(s_Frame == 0) { - SetMenuPage(g_Config.m_UiPage); + RefreshBrowserTab(true); s_Frame++; } else if(s_Frame == 1) { UpdateMusicState(); - RefreshBrowserTab(g_Config.m_UiPage); s_Frame++; } + else + { + UpdateCommunityIcons(); + } + + if(ServerBrowser()->DDNetInfoAvailable()) + { + // Initially add DDNet as favorite community and select its tab. + // This must be delayed until the DDNet info is available. + if(m_CreateDefaultFavoriteCommunities) + { + m_CreateDefaultFavoriteCommunities = false; + if(ServerBrowser()->Community(IServerBrowser::COMMUNITY_DDNET) != nullptr) + { + ServerBrowser()->FavoriteCommunitiesFilter().Clear(); + ServerBrowser()->FavoriteCommunitiesFilter().Add(IServerBrowser::COMMUNITY_DDNET); + SetMenuPage(PAGE_FAVORITE_COMMUNITY_1); + ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITE_COMMUNITY_1); + } + } + + if(m_JoinTutorial && m_Popup == POPUP_NONE && !ServerBrowser()->IsGettingServerlist()) + { + m_JoinTutorial = false; + // This is only reached on first launch, when the DDNet community tab has been created and + // activated by default, so the server info for the tutorial server should be available. + const char *pAddr = ServerBrowser()->GetTutorialServer(); + if(pAddr) + { + Client()->Connect(pAddr); + } + } + } + + // Determine the client state once before rendering because it can change + // while rendering which causes frames with broken user interface. + const IClient::EClientState ClientState = Client()->State(); - if(Client()->State() == IClient::STATE_ONLINE || Client()->State() == IClient::STATE_DEMOPLAYBACK) + if(ClientState == IClient::STATE_ONLINE || ClientState == IClient::STATE_DEMOPLAYBACK) { ms_ColorTabbarInactive = ms_ColorTabbarInactiveIngame; ms_ColorTabbarActive = ms_ColorTabbarActiveIngame; @@ -990,7 +1064,7 @@ int CMenus::Render() } else { - if(!m_pBackground->Render()) + if(!GameClient()->m_MenuBackground.Render()) { RenderBackground(); } @@ -999,808 +1073,961 @@ int CMenus::Render() ms_ColorTabbarHover = ms_ColorTabbarHoverOutgame; } - CUIRect TabBar; - CUIRect MainView; - - // some margin around the screen - Screen.Margin(10.0f, &Screen); - - static bool s_SoundCheck = false; - if(!s_SoundCheck && m_Popup == POPUP_NONE) + CUIRect Screen = *Ui()->Screen(); + if(Client()->State() != IClient::STATE_DEMOPLAYBACK || m_Popup != POPUP_NONE) { - if(Client()->SoundInitFailed()) - PopupMessage(Localize("Sound error"), Localize("The audio device couldn't be initialised."), Localize("Ok")); - s_SoundCheck = true; + Screen.Margin(10.0f, &Screen); } - if(m_Popup == POPUP_NONE) + switch(ClientState) { - if(m_JoinTutorial && !Client()->InfoTaskRunning() && !ServerBrowser()->IsGettingServerlist()) + case IClient::STATE_QUITTING: + case IClient::STATE_RESTARTING: + // Render nothing except menu background. This should not happen for more than one frame. + return; + + case IClient::STATE_CONNECTING: + RenderPopupConnecting(Screen); + break; + + case IClient::STATE_LOADING: + RenderPopupLoading(Screen); + break; + + case IClient::STATE_OFFLINE: + if(m_Popup != POPUP_NONE) { - m_JoinTutorial = false; - const char *pAddr = ServerBrowser()->GetTutorialServer(); - if(pAddr) - Client()->Connect(pAddr); + RenderPopupFullscreen(Screen); } - if(m_ShowStart && Client()->State() == IClient::STATE_OFFLINE) + else if(m_ShowStart) { - m_pBackground->ChangePosition(CMenuBackground::POS_START); RenderStartMenu(Screen); } else { + CUIRect TabBar, MainView; Screen.HSplitTop(24.0f, &TabBar, &MainView); - // render news - if(m_MenuPage < PAGE_NEWS || m_MenuPage > PAGE_SETTINGS || (Client()->State() == IClient::STATE_OFFLINE && m_MenuPage >= PAGE_GAME && m_MenuPage <= PAGE_CALLVOTE)) - { - ServerBrowser()->Refresh(IServerBrowser::TYPE_INTERNET); - SetMenuPage(PAGE_INTERNET); - } - - // render current page - if(Client()->State() != IClient::STATE_OFFLINE) - { - if(m_GamePage == PAGE_GAME) - { - RenderGame(MainView); - RenderIngameHint(); - } - else if(m_GamePage == PAGE_PLAYERS) - { - RenderPlayers(MainView); - } - else if(m_GamePage == PAGE_SERVER_INFO) - { - RenderServerInfo(MainView); - } - else if(m_GamePage == PAGE_NETWORK) - { - RenderInGameNetwork(MainView); - } - else if(m_GamePage == PAGE_GHOST) - { - RenderGhost(MainView); - } - else if(m_GamePage == PAGE_CALLVOTE) - { - RenderServerControl(MainView); - } - else if(m_GamePage == PAGE_STATS) - { - RenderStats(MainView); - } - else if(m_GamePage == PAGE_SETTINGS) - { - RenderSettings(MainView); - } - } - else if(m_MenuPage == PAGE_NEWS) + if(m_MenuPage == PAGE_NEWS) { - m_pBackground->ChangePosition(CMenuBackground::POS_NEWS); RenderNews(MainView); } - else if(m_MenuPage == PAGE_INTERNET) + else if(m_MenuPage >= PAGE_INTERNET && m_MenuPage <= PAGE_FAVORITE_COMMUNITY_5) { - m_pBackground->ChangePosition(CMenuBackground::POS_BROWSER_INTERNET); - RenderServerbrowser(MainView); - } - else if(m_MenuPage == PAGE_LAN) - { - m_pBackground->ChangePosition(CMenuBackground::POS_BROWSER_LAN); RenderServerbrowser(MainView); } else if(m_MenuPage == PAGE_DEMOS) { - m_pBackground->ChangePosition(CMenuBackground::POS_DEMOS); RenderDemoBrowser(MainView); } - else if(m_MenuPage == PAGE_FAVORITES) - { - m_pBackground->ChangePosition(CMenuBackground::POS_BROWSER_FAVORITES); - RenderServerbrowser(MainView); - } else if(m_MenuPage == PAGE_SETTINGS) + { RenderSettings(MainView); - - // do tab bar - RenderMenubar(TabBar); + } + else + { + dbg_assert(false, "m_MenuPage invalid"); + } + + RenderMenubar(TabBar, ClientState); } - } - else - { - char aBuf[1536]; - const char *pTitle = ""; - const char *pExtraText = ""; - const char *pButtonText = ""; - bool TopAlign = false; - bool UseIpLabel = false; + break; - ColorRGBA BgColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f); - if(m_Popup == POPUP_MESSAGE || m_Popup == POPUP_CONFIRM) + case IClient::STATE_ONLINE: + if(m_Popup != POPUP_NONE) { - pTitle = m_aPopupTitle; - pExtraText = m_aPopupMessage; - TopAlign = true; + RenderPopupFullscreen(Screen); } - else if(m_Popup == POPUP_CONNECTING) + else { - pTitle = Localize("Connecting to"); - UseIpLabel = true; - pButtonText = Localize("Abort"); - if(Client()->State() == IClient::STATE_CONNECTING && time_get() - Client()->StateStartTime() > time_freq()) + CUIRect TabBar, MainView; + Screen.HSplitTop(24.0f, &TabBar, &MainView); + + if(m_GamePage == PAGE_GAME) { - int Connectivity = Client()->UdpConnectivity(Client()->ConnectNetTypes()); - switch(Connectivity) - { - case IClient::CONNECTIVITY_UNKNOWN: - break; - case IClient::CONNECTIVITY_CHECKING: - pExtraText = Localize("Trying to determine UDP connectivity..."); - break; - case IClient::CONNECTIVITY_UNREACHABLE: - pExtraText = Localize("UDP seems to be filtered."); - break; - case IClient::CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES: - pExtraText = Localize("UDP and TCP IP addresses seem to be different. Try disabling VPN, proxy or network accelerators."); - break; - case IClient::CONNECTIVITY_REACHABLE: - pExtraText = Localize("No answer from server yet."); - break; - } + RenderGame(MainView); + RenderIngameHint(); } - else if(Client()->MapDownloadTotalsize() > 0) + else if(m_GamePage == PAGE_PLAYERS) { - str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Downloading map"), Client()->MapDownloadName()); - pTitle = aBuf; - UseIpLabel = false; + RenderPlayers(MainView); } - else if(Client()->State() == IClient::STATE_LOADING) + else if(m_GamePage == PAGE_SERVER_INFO) { - UseIpLabel = false; - if(Client()->LoadingStateDetail() == IClient::LOADING_STATE_DETAIL_INITIAL) - { - pTitle = Localize("Connected"); - pExtraText = Localize("Getting game info"); - } - else if(Client()->LoadingStateDetail() == IClient::LOADING_STATE_DETAIL_LOADING_MAP) - { - pTitle = Localize("Connected"); - pExtraText = Localize("Loading map file from storage"); - } - else if(Client()->LoadingStateDetail() == IClient::LOADING_STATE_DETAIL_SENDING_READY) - { - pTitle = Localize("Connected"); - pExtraText = Localize("Requesting to join the game"); - } - else if(Client()->LoadingStateDetail() == IClient::LOADING_STATE_DETAIL_GETTING_READY) - { - pTitle = Localize("Connected"); - pExtraText = Localize("Sending initial client info"); - } + RenderServerInfo(MainView); } - } - else if(m_Popup == POPUP_DISCONNECTED) - { - pTitle = Localize("Disconnected"); - pExtraText = Client()->ErrorString(); - pButtonText = Localize("Ok"); - if(Client()->ReconnectTime() > 0) + else if(m_GamePage == PAGE_NETWORK) + { + RenderInGameNetwork(MainView); + } + else if(m_GamePage == PAGE_GHOST) + { + RenderGhost(MainView); + } + else if(m_GamePage == PAGE_CALLVOTE) + { + RenderServerControl(MainView); + } + else if(m_GamePage == PAGE_SETTINGS) + { + RenderSettings(MainView); + } + else { - str_format(aBuf, sizeof(aBuf), Localize("Reconnect in %d sec"), (int)((Client()->ReconnectTime() - time_get()) / time_freq())); - pTitle = Client()->ErrorString(); - pExtraText = aBuf; - pButtonText = Localize("Abort"); + dbg_assert(false, "m_GamePage invalid"); } + + RenderMenubar(TabBar, ClientState); } - else if(m_Popup == POPUP_RENAME_DEMO) + break; + + case IClient::STATE_DEMOPLAYBACK: + if(m_Popup != POPUP_NONE) { - dbg_assert(m_DemolistSelectedIndex >= 0, "m_DemolistSelectedIndex invalid for POPUP_RENAME_DEMO"); - pTitle = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize("Rename folder") : Localize("Rename demo"); + RenderPopupFullscreen(Screen); } -#if defined(CONF_VIDEORECORDER) - else if(m_Popup == POPUP_RENDER_DEMO) + else { - pTitle = Localize("Render demo"); + RenderDemoPlayer(Screen); } - else if(m_Popup == POPUP_RENDER_DONE) + break; + } + + Ui()->RenderPopupMenus(); + + // Prevent UI elements from being hovered while a key reader is active + if(m_Binder.m_TakeKey) + { + Ui()->SetHotItem(nullptr); + } + + // Handle this escape hotkey after popup menus + if(!m_ShowStart && ClientState == IClient::STATE_OFFLINE && Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + m_ShowStart = true; + } +} + +void CMenus::RenderPopupFullscreen(CUIRect Screen) +{ + char aBuf[1536]; + const char *pTitle = ""; + const char *pExtraText = ""; + const char *pButtonText = ""; + bool TopAlign = false; + + ColorRGBA BgColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f); + if(m_Popup == POPUP_MESSAGE || m_Popup == POPUP_CONFIRM) + { + pTitle = m_aPopupTitle; + pExtraText = m_aPopupMessage; + TopAlign = true; + } + else if(m_Popup == POPUP_DISCONNECTED) + { + pTitle = Localize("Disconnected"); + pExtraText = Client()->ErrorString(); + pButtonText = Localize("Ok"); + if(Client()->ReconnectTime() > 0) { - pTitle = Localize("Render complete"); + str_format(aBuf, sizeof(aBuf), Localize("Reconnect in %d sec"), (int)((Client()->ReconnectTime() - time_get()) / time_freq()) + 1); + pTitle = Client()->ErrorString(); + pExtraText = aBuf; + pButtonText = Localize("Abort"); } + } + else if(m_Popup == POPUP_RENAME_DEMO) + { + dbg_assert(m_DemolistSelectedIndex >= 0, "m_DemolistSelectedIndex invalid for POPUP_RENAME_DEMO"); + pTitle = m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize("Rename folder") : Localize("Rename demo"); + } +#if defined(CONF_VIDEORECORDER) + else if(m_Popup == POPUP_RENDER_DEMO) + { + pTitle = Localize("Render demo"); + } + else if(m_Popup == POPUP_RENDER_DONE) + { + pTitle = Localize("Render complete"); + } #endif - else if(m_Popup == POPUP_PASSWORD) - { - pTitle = Localize("Password incorrect"); - pButtonText = Localize("Try again"); - } - else if(m_Popup == POPUP_QUIT) - { - pTitle = Localize("Quit"); - pExtraText = Localize("Are you sure that you want to quit?"); - } - else if(m_Popup == POPUP_FIRST_LAUNCH) + else if(m_Popup == POPUP_PASSWORD) + { + pTitle = Localize("Password incorrect"); + pButtonText = Localize("Try again"); + } + else if(m_Popup == POPUP_RESTART) + { + pTitle = Localize("Restart"); + pExtraText = Localize("Are you sure that you want to restart?"); + } + else if(m_Popup == POPUP_QUIT) + { + pTitle = Localize("Quit"); + pExtraText = Localize("Are you sure that you want to quit?"); + } + else if(m_Popup == POPUP_FIRST_LAUNCH) + { + pTitle = Localize("Welcome to DDNet"); + str_format(aBuf, sizeof(aBuf), "%s\n\n%s\n\n%s\n\n%s", + Localize("DDraceNetwork is a cooperative online game where the goal is for you and your group of tees to reach the finish line of the map. As a newcomer you should start on Novice servers, which host the easiest maps. Consider the ping to choose a server close to you."), + Localize("Use k key to kill (restart), q to pause and watch other players. See settings for other key binds."), + Localize("It's recommended that you check the settings to adjust them to your liking before joining a server."), + Localize("Please enter your nickname below.")); + pExtraText = aBuf; + pButtonText = Localize("Ok"); + TopAlign = true; + } + else if(m_Popup == POPUP_POINTS) + { + pTitle = Localize("Existing Player"); + if(Client()->Points() > 50) { - pTitle = Localize("Welcome to DDNet"); - str_format(aBuf, sizeof(aBuf), "%s\n\n%s\n\n%s\n\n%s", - Localize("DDraceNetwork is a cooperative online game where the goal is for you and your group of tees to reach the finish line of the map. As a newcomer you should start on Novice servers, which host the easiest maps. Consider the ping to choose a server close to you."), - Localize("Use k key to kill (restart), q to pause and watch other players. See settings for other key binds."), - Localize("It's recommended that you check the settings to adjust them to your liking before joining a server."), - Localize("Please enter your nickname below.")); + str_format(aBuf, sizeof(aBuf), Localize("Your nickname '%s' is already used (%d points). Do you still want to use it?"), Client()->PlayerName(), Client()->Points()); pExtraText = aBuf; - pButtonText = Localize("Ok"); TopAlign = true; } - else if(m_Popup == POPUP_POINTS) + else if(Client()->Points() >= 0) { - pTitle = Localize("Existing Player"); - if(Client()->Points() > 50) - { - str_format(aBuf, sizeof(aBuf), Localize("Your nickname '%s' is already used (%d points). Do you still want to use it?"), Client()->PlayerName(), Client()->Points()); - pExtraText = aBuf; - TopAlign = true; - } - else if(Client()->Points() >= 0) - { - m_Popup = POPUP_NONE; - } - else - { - pExtraText = Localize("Checking for existing player with your name"); - } + m_Popup = POPUP_NONE; } - else if(m_Popup == POPUP_WARNING) + else { - BgColor = ColorRGBA(0.5f, 0.0f, 0.0f, 0.7f); - pTitle = m_aMessageTopic; - pExtraText = m_aMessageBody; - pButtonText = m_aMessageButton; - TopAlign = true; + pExtraText = Localize("Checking for existing player with your name"); } + } + else if(m_Popup == POPUP_WARNING) + { + BgColor = ColorRGBA(0.5f, 0.0f, 0.0f, 0.7f); + pTitle = m_aMessageTopic; + pExtraText = m_aMessageBody; + pButtonText = m_aMessageButton; + TopAlign = true; + } + else if(m_Popup == POPUP_SAVE_SKIN) + { + pTitle = Localize("Save skin"); + pExtraText = Localize("Are you sure you want to save your skin? If a skin with this name already exists, it will be replaced."); + } - CUIRect Box, Part; - Box = Screen; - if(m_Popup != POPUP_FIRST_LAUNCH) - Box.Margin(150.0f, &Box); + CUIRect Box, Part; + Box = Screen; + if(m_Popup != POPUP_FIRST_LAUNCH) + Box.Margin(150.0f, &Box); - // render the box - Box.Draw(BgColor, IGraphics::CORNER_ALL, 15.0f); + // render the box + Box.Draw(BgColor, IGraphics::CORNER_ALL, 15.0f); - Box.HSplitTop(20.f, &Part, &Box); - Box.HSplitTop(24.f, &Part, &Box); - Part.VMargin(20.f, &Part); - SLabelProperties Props; - Props.m_MaxWidth = (int)Part.w; + Box.HSplitTop(20.f, &Part, &Box); + Box.HSplitTop(24.f, &Part, &Box); + Part.VMargin(20.f, &Part); + SLabelProperties Props; + Props.m_MaxWidth = (int)Part.w; - if(TextRender()->TextWidth(24.f, pTitle, -1, -1.0f) > Part.w) - UI()->DoLabel(&Part, pTitle, 24.f, TEXTALIGN_ML, Props); - else - UI()->DoLabel(&Part, pTitle, 24.f, TEXTALIGN_MC); + if(TextRender()->TextWidth(24.f, pTitle, -1, -1.0f) > Part.w) + Ui()->DoLabel(&Part, pTitle, 24.f, TEXTALIGN_ML, Props); + else + Ui()->DoLabel(&Part, pTitle, 24.f, TEXTALIGN_MC); - Box.HSplitTop(20.f, &Part, &Box); - Box.HSplitTop(24.f, &Part, &Box); - Part.VMargin(20.f, &Part); + Box.HSplitTop(20.f, &Part, &Box); + Box.HSplitTop(24.f, &Part, &Box); + Part.VMargin(20.f, &Part); - float FontSize = m_Popup == POPUP_FIRST_LAUNCH ? 16.0f : 20.f; + float FontSize = m_Popup == POPUP_FIRST_LAUNCH ? 16.0f : 20.f; - if(UseIpLabel) - { - SLabelProperties IpLabelProps; - IpLabelProps.m_MaxWidth = Part.w; - IpLabelProps.m_EllipsisAtEnd = true; - UI()->DoLabel(&Part, Client()->ConnectAddressString(), FontSize, TEXTALIGN_MC, IpLabelProps); - Box.HSplitTop(20.f, &Part, &Box); - Box.HSplitTop(24.f, &Part, &Box); - } + Props.m_MaxWidth = (int)Part.w; + if(TopAlign) + Ui()->DoLabel(&Part, pExtraText, FontSize, TEXTALIGN_TL, Props); + else if(TextRender()->TextWidth(FontSize, pExtraText, -1, -1.0f) > Part.w) + Ui()->DoLabel(&Part, pExtraText, FontSize, TEXTALIGN_ML, Props); + else + Ui()->DoLabel(&Part, pExtraText, FontSize, TEXTALIGN_MC); - Props.m_MaxWidth = (int)Part.w; - if(TopAlign) - UI()->DoLabel(&Part, pExtraText, FontSize, TEXTALIGN_TL, Props); - else if(TextRender()->TextWidth(FontSize, pExtraText, -1, -1.0f) > Part.w) - UI()->DoLabel(&Part, pExtraText, FontSize, TEXTALIGN_ML, Props); - else - UI()->DoLabel(&Part, pExtraText, FontSize, TEXTALIGN_MC); + if(m_Popup == POPUP_MESSAGE || m_Popup == POPUP_CONFIRM) + { + CUIRect ButtonBar; + Box.HSplitBottom(20.0f, &Box, nullptr); + Box.HSplitBottom(24.0f, &Box, &ButtonBar); + ButtonBar.VMargin(100.0f, &ButtonBar); - if(m_Popup == POPUP_MESSAGE || m_Popup == POPUP_CONFIRM) + if(m_Popup == POPUP_MESSAGE) { - CUIRect ButtonBar; - Box.HSplitBottom(20.0f, &Box, nullptr); - Box.HSplitBottom(24.0f, &Box, &ButtonBar); - ButtonBar.VMargin(100.0f, &ButtonBar); - - if(m_Popup == POPUP_MESSAGE) + static CButtonContainer s_ButtonConfirm; + if(DoButton_Menu(&s_ButtonConfirm, m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, 0, &ButtonBar) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) { - static CButtonContainer s_ButtonConfirm; - if(DoButton_Menu(&s_ButtonConfirm, m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, 0, &ButtonBar) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) - { - m_Popup = m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup; - (this->*m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback)(); - } + m_Popup = m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup; + (this->*m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback)(); } - else if(m_Popup == POPUP_CONFIRM) - { - CUIRect CancelButton, ConfirmButton; - ButtonBar.VSplitMid(&CancelButton, &ConfirmButton, 40.0f); + } + else if(m_Popup == POPUP_CONFIRM) + { + CUIRect CancelButton, ConfirmButton; + ButtonBar.VSplitMid(&CancelButton, &ConfirmButton, 40.0f); - static CButtonContainer s_ButtonCancel; - if(DoButton_Menu(&s_ButtonCancel, m_aPopupButtons[BUTTON_CANCEL].m_aLabel, 0, &CancelButton) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) - { - m_Popup = m_aPopupButtons[BUTTON_CANCEL].m_NextPopup; - (this->*m_aPopupButtons[BUTTON_CANCEL].m_pfnCallback)(); - } + static CButtonContainer s_ButtonCancel; + if(DoButton_Menu(&s_ButtonCancel, m_aPopupButtons[BUTTON_CANCEL].m_aLabel, 0, &CancelButton) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + m_Popup = m_aPopupButtons[BUTTON_CANCEL].m_NextPopup; + (this->*m_aPopupButtons[BUTTON_CANCEL].m_pfnCallback)(); + } - static CButtonContainer s_ButtonConfirm; - if(DoButton_Menu(&s_ButtonConfirm, m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, 0, &ConfirmButton) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) - { - m_Popup = m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup; - (this->*m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback)(); - } + static CButtonContainer s_ButtonConfirm; + if(DoButton_Menu(&s_ButtonConfirm, m_aPopupButtons[BUTTON_CONFIRM].m_aLabel, 0, &ConfirmButton) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) + { + m_Popup = m_aPopupButtons[BUTTON_CONFIRM].m_NextPopup; + (this->*m_aPopupButtons[BUTTON_CONFIRM].m_pfnCallback)(); } } - else if(m_Popup == POPUP_QUIT) + } + else if(m_Popup == POPUP_QUIT || m_Popup == POPUP_RESTART) + { + CUIRect Yes, No; + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); + + // additional info + Box.VMargin(20.f, &Box); + if(m_pClient->Editor()->HasUnsavedData()) { - CUIRect Yes, No; - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); + str_format(aBuf, sizeof(aBuf), "%s\n\n%s", Localize("There's an unsaved map in the editor, you might want to save it."), Localize("Continue anyway?")); + Props.m_MaxWidth = Part.w - 20.0f; + Ui()->DoLabel(&Box, aBuf, 20.f, TEXTALIGN_ML, Props); + } - // additional info - Box.VMargin(20.f, &Box); - if(m_pClient->Editor()->HasUnsavedData()) - { - str_format(aBuf, sizeof(aBuf), "%s\n%s", Localize("There's an unsaved map in the editor, you might want to save it before you quit the game."), Localize("Quit anyway?")); - Props.m_MaxWidth = Part.w - 20.0f; - UI()->DoLabel(&Box, aBuf, 20.f, TEXTALIGN_ML, Props); - } + // buttons + Part.VMargin(80.0f, &Part); + Part.VSplitMid(&No, &Yes); + Yes.VMargin(20.0f, &Yes); + No.VMargin(20.0f, &No); - // buttons - Part.VMargin(80.0f, &Part); - Part.VSplitMid(&No, &Yes); - Yes.VMargin(20.0f, &Yes); - No.VMargin(20.0f, &No); + static CButtonContainer s_ButtonAbort; + if(DoButton_Menu(&s_ButtonAbort, Localize("No"), 0, &No) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + m_Popup = POPUP_NONE; - static CButtonContainer s_ButtonAbort; - if(DoButton_Menu(&s_ButtonAbort, Localize("No"), 0, &No) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) + static CButtonContainer s_ButtonTryAgain; + if(DoButton_Menu(&s_ButtonTryAgain, Localize("Yes"), 0, &Yes) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) + { + if(m_Popup == POPUP_RESTART) + { m_Popup = POPUP_NONE; - - static CButtonContainer s_ButtonTryAgain; - if(DoButton_Menu(&s_ButtonTryAgain, Localize("Yes"), 0, &Yes) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + Client()->Restart(); + } + else { m_Popup = POPUP_NONE; Client()->Quit(); } } - else if(m_Popup == POPUP_PASSWORD) - { - CUIRect Label, TextBox, TryAgain, Abort; + } + else if(m_Popup == POPUP_PASSWORD) + { + CUIRect AddressLabel, Address, Icon, Name, Label, TextBox, TryAgain, Abort; - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); - Part.VMargin(80.0f, &Part); + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); + Part.VMargin(80.0f, &Part); - Part.VSplitMid(&Abort, &TryAgain); + Part.VSplitMid(&Abort, &TryAgain); - TryAgain.VMargin(20.0f, &TryAgain); - Abort.VMargin(20.0f, &Abort); + TryAgain.VMargin(20.0f, &TryAgain); + Abort.VMargin(20.0f, &Abort); - static CButtonContainer s_ButtonAbort; - if(DoButton_Menu(&s_ButtonAbort, Localize("Abort"), 0, &Abort) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) - m_Popup = POPUP_NONE; + static CButtonContainer s_ButtonAbort; + if(DoButton_Menu(&s_ButtonAbort, Localize("Abort"), 0, &Abort) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + m_Popup = POPUP_NONE; - static CButtonContainer s_ButtonTryAgain; - if(DoButton_Menu(&s_ButtonTryAgain, Localize("Try again"), 0, &TryAgain) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) - { - Client()->Connect(g_Config.m_UiServerAddress, g_Config.m_Password); - } + char aAddr[NETADDR_MAXSTRSIZE]; + net_addr_str(&Client()->ServerAddress(), aAddr, sizeof(aAddr), true); - Box.HSplitBottom(60.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); + static CButtonContainer s_ButtonTryAgain; + if(DoButton_Menu(&s_ButtonTryAgain, Localize("Try again"), 0, &TryAgain) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) + Client()->Connect(aAddr, g_Config.m_Password); - Part.VSplitLeft(60.0f, 0, &Label); - Label.VSplitLeft(100.0f, 0, &TextBox); - TextBox.VSplitLeft(20.0f, 0, &TextBox); - TextBox.VSplitRight(60.0f, &TextBox, 0); - UI()->DoLabel(&Label, Localize("Password"), 18.0f, TEXTALIGN_ML); - UI()->DoClearableEditBox(&m_PasswordInput, &TextBox, 12.0f); - } - else if(m_Popup == POPUP_CONNECTING) - { - Box = Screen; - Box.Margin(150.0f, &Box); - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); - Part.VMargin(120.0f, &Part); + Box.HSplitBottom(32.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); - static CButtonContainer s_Button; - if(DoButton_Menu(&s_Button, pButtonText, 0, &Part) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) - { - Client()->Disconnect(); - m_Popup = POPUP_NONE; - RefreshBrowserTab(g_Config.m_UiPage); - } + Part.VSplitLeft(60.0f, 0, &Label); + Label.VSplitLeft(100.0f, 0, &TextBox); + TextBox.VSplitLeft(20.0f, 0, &TextBox); + TextBox.VSplitRight(60.0f, &TextBox, 0); + Ui()->DoLabel(&Label, Localize("Password"), 18.0f, TEXTALIGN_ML); + Ui()->DoClearableEditBox(&m_PasswordInput, &TextBox, 12.0f); - if(Client()->MapDownloadTotalsize() > 0) - { - int64_t Now = time_get(); - if(Now - m_DownloadLastCheckTime >= time_freq()) - { - if(m_DownloadLastCheckSize > Client()->MapDownloadAmount()) - { - // map downloaded restarted - m_DownloadLastCheckSize = 0; - } + Box.HSplitBottom(32.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); - // update download speed - float Diff = (Client()->MapDownloadAmount() - m_DownloadLastCheckSize) / ((int)((Now - m_DownloadLastCheckTime) / time_freq())); - float StartDiff = m_DownloadLastCheckSize - 0.0f; - if(StartDiff + Diff > 0.0f) - m_DownloadSpeed = (Diff / (StartDiff + Diff)) * (Diff / 1.0f) + (StartDiff / (Diff + StartDiff)) * m_DownloadSpeed; - else - m_DownloadSpeed = 0.0f; - m_DownloadLastCheckTime = Now; - m_DownloadLastCheckSize = Client()->MapDownloadAmount(); - } + Part.VSplitLeft(60.0f, 0, &AddressLabel); + AddressLabel.VSplitLeft(100.0f, 0, &Address); + Address.VSplitLeft(20.0f, 0, &Address); + Ui()->DoLabel(&AddressLabel, Localize("Address"), 18.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Address, aAddr, 18.0f, TEXTALIGN_ML); - Box.HSplitTop(64.f, 0, &Box); - Box.HSplitTop(24.f, &Part, &Box); - str_format(aBuf, sizeof(aBuf), "%d/%d KiB (%.1f KiB/s)", Client()->MapDownloadAmount() / 1024, Client()->MapDownloadTotalsize() / 1024, m_DownloadSpeed / 1024.0f); - UI()->DoLabel(&Part, aBuf, 20.f, TEXTALIGN_MC); + Box.HSplitBottom(32.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); - // time left - int TimeLeft = maximum(1, m_DownloadSpeed > 0.0f ? static_cast((Client()->MapDownloadTotalsize() - Client()->MapDownloadAmount()) / m_DownloadSpeed) : 1); - if(TimeLeft >= 60) - { - TimeLeft /= 60; - str_format(aBuf, sizeof(aBuf), TimeLeft == 1 ? Localize("%i minute left") : Localize("%i minutes left"), TimeLeft); - } - else - { - str_format(aBuf, sizeof(aBuf), TimeLeft == 1 ? Localize("%i second left") : Localize("%i seconds left"), TimeLeft); - } - Box.HSplitTop(20.f, 0, &Box); - Box.HSplitTop(24.f, &Part, &Box); - UI()->DoLabel(&Part, aBuf, 20.f, TEXTALIGN_MC); - - // progress bar - Box.HSplitTop(20.f, 0, &Box); - Box.HSplitTop(24.f, &Part, &Box); - Part.VMargin(40.0f, &Part); - Part.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 5.0f); - Part.w = maximum(10.0f, (Part.w * Client()->MapDownloadAmount()) / Client()->MapDownloadTotalsize()); - Part.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), IGraphics::CORNER_ALL, 5.0f); - } - } - else if(m_Popup == POPUP_LANGUAGE) + const CServerBrowser::CServerEntry *pEntry = ServerBrowser()->Find(Client()->ServerAddress()); + if(pEntry != nullptr && pEntry->m_GotInfo) { - CUIRect Button; - Screen.Margin(150.0f, &Box); - Box.HSplitTop(20.0f, nullptr, &Box); - Box.HSplitBottom(20.0f, &Box, nullptr); - Box.HSplitBottom(24.0f, &Box, &Button); - Box.HSplitBottom(20.0f, &Box, nullptr); - Box.VMargin(20.0f, &Box); - const bool Activated = RenderLanguageSelection(Box); - Button.VMargin(120.0f, &Button); + Part.VSplitLeft(60.0f, 0, &Icon); + Icon.VSplitLeft(100.0f, 0, &Name); + Icon.VSplitLeft(80.0f, &Icon, 0); + Name.VSplitLeft(20.0f, 0, &Name); + + const SCommunityIcon *pIcon = FindCommunityIcon(pEntry->m_Info.m_aCommunityId); + if(pIcon != nullptr) + RenderCommunityIcon(pIcon, Icon, true); + else + Ui()->DoLabel(&Icon, Localize("Name"), 18.0f, TEXTALIGN_ML); - static CButtonContainer s_Button; - if(DoButton_Menu(&s_Button, Localize("Ok"), 0, &Button) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || Activated) - m_Popup = POPUP_FIRST_LAUNCH; + Ui()->DoLabel(&Name, pEntry->m_Info.m_aName, 18.0f, TEXTALIGN_ML); } - else if(m_Popup == POPUP_RENAME_DEMO) - { - CUIRect Label, TextBox, Ok, Abort; + } + else if(m_Popup == POPUP_LANGUAGE) + { + CUIRect Button; + Screen.Margin(150.0f, &Box); + Box.HSplitTop(20.0f, nullptr, &Box); + Box.HSplitBottom(20.0f, &Box, nullptr); + Box.HSplitBottom(24.0f, &Box, &Button); + Box.HSplitBottom(20.0f, &Box, nullptr); + Box.VMargin(20.0f, &Box); + const bool Activated = RenderLanguageSelection(Box); + Button.VMargin(120.0f, &Button); + + static CButtonContainer s_Button; + if(DoButton_Menu(&s_Button, Localize("Ok"), 0, &Button) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER) || Activated) + m_Popup = POPUP_FIRST_LAUNCH; + } + else if(m_Popup == POPUP_RENAME_DEMO) + { + CUIRect Label, TextBox, Ok, Abort; - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); - Part.VMargin(80.0f, &Part); + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); + Part.VMargin(80.0f, &Part); - Part.VSplitMid(&Abort, &Ok); + Part.VSplitMid(&Abort, &Ok); - Ok.VMargin(20.0f, &Ok); - Abort.VMargin(20.0f, &Abort); + Ok.VMargin(20.0f, &Ok); + Abort.VMargin(20.0f, &Abort); - static CButtonContainer s_ButtonAbort; - if(DoButton_Menu(&s_ButtonAbort, Localize("Abort"), 0, &Abort) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) - m_Popup = POPUP_NONE; + static CButtonContainer s_ButtonAbort; + if(DoButton_Menu(&s_ButtonAbort, Localize("Abort"), 0, &Abort) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + m_Popup = POPUP_NONE; - static CButtonContainer s_ButtonOk; - if(DoButton_Menu(&s_ButtonOk, Localize("Ok"), 0, &Ok) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + static CButtonContainer s_ButtonOk; + if(DoButton_Menu(&s_ButtonOk, Localize("Ok"), 0, &Ok) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) + { + m_Popup = POPUP_NONE; + // rename demo + char aBufOld[IO_MAX_PATH_LENGTH]; + str_format(aBufOld, sizeof(aBufOld), "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename); + char aBufNew[IO_MAX_PATH_LENGTH]; + str_format(aBufNew, sizeof(aBufNew), "%s/%s", m_aCurrentDemoFolder, m_DemoRenameInput.GetString()); + if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir && !str_endswith(aBufNew, ".demo")) + str_append(aBufNew, ".demo"); + + if(Storage()->FileExists(aBufNew, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType)) { - m_Popup = POPUP_NONE; - // rename demo - char aBufOld[IO_MAX_PATH_LENGTH]; - str_format(aBufOld, sizeof(aBufOld), "%s/%s", m_aCurrentDemoFolder, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename); - char aBufNew[IO_MAX_PATH_LENGTH]; - str_format(aBufNew, sizeof(aBufNew), "%s/%s", m_aCurrentDemoFolder, m_DemoRenameInput.GetString()); - if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir && !str_endswith(aBufNew, ".demo")) - str_append(aBufNew, ".demo"); - - if(Storage()->FileExists(aBufNew, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType)) - { - PopupMessage(Localize("Error"), Localize("A demo with this name already exists"), Localize("Ok"), POPUP_RENAME_DEMO); - } - else if(Storage()->FolderExists(aBufNew, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType)) - { - PopupMessage(Localize("Error"), Localize("A folder with this name already exists"), Localize("Ok"), POPUP_RENAME_DEMO); - } - else if(Storage()->RenameFile(aBufOld, aBufNew, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType)) - { - str_copy(m_aCurrentDemoSelectionName, m_DemoRenameInput.GetString()); - if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) - fs_split_file_extension(m_DemoRenameInput.GetString(), m_aCurrentDemoSelectionName, sizeof(m_aCurrentDemoSelectionName)); - DemolistPopulate(); - DemolistOnUpdate(false); - } - else - { - PopupMessage(Localize("Error"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize("Unable to rename the folder") : Localize("Unable to rename the demo"), Localize("Ok"), POPUP_RENAME_DEMO); - } + PopupMessage(Localize("Error"), Localize("A demo with this name already exists"), Localize("Ok"), POPUP_RENAME_DEMO); + } + else if(Storage()->FolderExists(aBufNew, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType)) + { + PopupMessage(Localize("Error"), Localize("A folder with this name already exists"), Localize("Ok"), POPUP_RENAME_DEMO); + } + else if(Storage()->RenameFile(aBufOld, aBufNew, m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType)) + { + str_copy(m_aCurrentDemoSelectionName, m_DemoRenameInput.GetString()); + if(!m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) + fs_split_file_extension(m_DemoRenameInput.GetString(), m_aCurrentDemoSelectionName, sizeof(m_aCurrentDemoSelectionName)); + DemolistPopulate(); + DemolistOnUpdate(false); + } + else + { + PopupMessage(Localize("Error"), m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir ? Localize("Unable to rename the folder") : Localize("Unable to rename the demo"), Localize("Ok"), POPUP_RENAME_DEMO); } + } - Box.HSplitBottom(60.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); + Box.HSplitBottom(60.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); - Part.VSplitLeft(60.0f, 0, &Label); - Label.VSplitLeft(120.0f, 0, &TextBox); - TextBox.VSplitLeft(20.0f, 0, &TextBox); - TextBox.VSplitRight(60.0f, &TextBox, 0); - UI()->DoLabel(&Label, Localize("New name:"), 18.0f, TEXTALIGN_ML); - UI()->DoEditBox(&m_DemoRenameInput, &TextBox, 12.0f); - } + Part.VSplitLeft(60.0f, 0, &Label); + Label.VSplitLeft(120.0f, 0, &TextBox); + TextBox.VSplitLeft(20.0f, 0, &TextBox); + TextBox.VSplitRight(60.0f, &TextBox, 0); + Ui()->DoLabel(&Label, Localize("New name:"), 18.0f, TEXTALIGN_ML); + Ui()->DoEditBox(&m_DemoRenameInput, &TextBox, 12.0f); + } #if defined(CONF_VIDEORECORDER) - else if(m_Popup == POPUP_RENDER_DEMO) - { - CUIRect Row, Ok, Abort; - Box.VMargin(60.0f, &Box); - Box.HMargin(20.0f, &Box); - Box.HSplitBottom(24.0f, &Box, &Row); - Box.HSplitBottom(40.0f, &Box, nullptr); - Row.VMargin(40.0f, &Row); - Row.VSplitMid(&Abort, &Ok, 40.0f); - - static CButtonContainer s_ButtonAbort; - if(DoButton_Menu(&s_ButtonAbort, Localize("Abort"), 0, &Abort) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) - { - m_DemoRenderInput.Clear(); - m_Popup = POPUP_NONE; - } + else if(m_Popup == POPUP_RENDER_DEMO) + { + CUIRect Row, Ok, Abort; + Box.VMargin(60.0f, &Box); + Box.HMargin(20.0f, &Box); + Box.HSplitBottom(24.0f, &Box, &Row); + Box.HSplitBottom(40.0f, &Box, nullptr); + Row.VMargin(40.0f, &Row); + Row.VSplitMid(&Abort, &Ok, 40.0f); + + static CButtonContainer s_ButtonAbort; + if(DoButton_Menu(&s_ButtonAbort, Localize("Abort"), 0, &Abort) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + m_DemoRenderInput.Clear(); + m_Popup = POPUP_NONE; + } - static CButtonContainer s_ButtonOk; - if(DoButton_Menu(&s_ButtonOk, Localize("Ok"), 0, &Ok) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + static CButtonContainer s_ButtonOk; + if(DoButton_Menu(&s_ButtonOk, Localize("Ok"), 0, &Ok) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) + { + m_Popup = POPUP_NONE; + // render video + char aVideoPath[IO_MAX_PATH_LENGTH]; + str_format(aVideoPath, sizeof(aVideoPath), "videos/%s", m_DemoRenderInput.GetString()); + if(!str_endswith(aVideoPath, ".mp4")) + str_append(aVideoPath, ".mp4"); + if(Storage()->FolderExists(aVideoPath, IStorage::TYPE_SAVE)) { - m_Popup = POPUP_NONE; - // render video - char aVideoPath[IO_MAX_PATH_LENGTH]; - str_format(aVideoPath, sizeof(aVideoPath), "videos/%s", m_DemoRenderInput.GetString()); - if(!str_endswith(aVideoPath, ".mp4")) - str_append(aVideoPath, ".mp4"); - if(Storage()->FolderExists(aVideoPath, IStorage::TYPE_SAVE)) - { - PopupMessage(Localize("Error"), Localize("A folder with this name already exists"), Localize("Ok"), POPUP_RENDER_DEMO); - } - else if(Storage()->FileExists(aVideoPath, IStorage::TYPE_SAVE)) - { - char aMessage[128 + IO_MAX_PATH_LENGTH]; - str_format(aMessage, sizeof(aMessage), Localize("File '%s' already exists, do you want to overwrite it?"), m_DemoRenderInput.GetString()); - PopupConfirm(Localize("Replace video"), aMessage, Localize("Yes"), Localize("No"), &CMenus::PopupConfirmDemoReplaceVideo, POPUP_NONE, &CMenus::DefaultButtonCallback, POPUP_RENDER_DEMO); - } - else - { - PopupConfirmDemoReplaceVideo(); - } + PopupMessage(Localize("Error"), Localize("A folder with this name already exists"), Localize("Ok"), POPUP_RENDER_DEMO); } - - CUIRect ShowChatCheckbox, UseSoundsCheckbox; - Box.HSplitBottom(20.0f, &Box, &Row); - Box.HSplitBottom(10.0f, &Box, nullptr); - Row.VSplitMid(&ShowChatCheckbox, &UseSoundsCheckbox, 20.0f); - - if(DoButton_CheckBox(&g_Config.m_ClVideoShowChat, Localize("Show chat"), g_Config.m_ClVideoShowChat, &ShowChatCheckbox)) - g_Config.m_ClVideoShowChat ^= 1; - - if(DoButton_CheckBox(&g_Config.m_ClVideoSndEnable, Localize("Use sounds"), g_Config.m_ClVideoSndEnable, &UseSoundsCheckbox)) - g_Config.m_ClVideoSndEnable ^= 1; - - CUIRect ShowHudButton; - Box.HSplitBottom(20.0f, &Box, &Row); - Row.VSplitMid(&Row, &ShowHudButton, 20.0f); - - if(DoButton_CheckBox(&g_Config.m_ClVideoShowhud, Localize("Show ingame HUD"), g_Config.m_ClVideoShowhud, &ShowHudButton)) - g_Config.m_ClVideoShowhud ^= 1; - - // slowdown - CUIRect SlowDownButton; - Row.VSplitLeft(20.0f, &SlowDownButton, &Row); - Row.VSplitLeft(5.0f, nullptr, &Row); - static CButtonContainer s_SlowDownButton; - if(DoButton_FontIcon(&s_SlowDownButton, FONT_ICON_BACKWARD, 0, &SlowDownButton, IGraphics::CORNER_ALL)) - m_Speed = clamp(m_Speed - 1, 0, (int)(g_DemoSpeeds - 1)); - - // paused - CUIRect PausedButton; - Row.VSplitLeft(20.0f, &PausedButton, &Row); - Row.VSplitLeft(5.0f, nullptr, &Row); - static CButtonContainer s_PausedButton; - if(DoButton_FontIcon(&s_PausedButton, FONT_ICON_PAUSE, 0, &PausedButton, IGraphics::CORNER_ALL)) - m_StartPaused ^= 1; - - // fastforward - CUIRect FastForwardButton; - Row.VSplitLeft(20.0f, &FastForwardButton, &Row); - Row.VSplitLeft(8.0f, nullptr, &Row); - static CButtonContainer s_FastForwardButton; - if(DoButton_FontIcon(&s_FastForwardButton, FONT_ICON_FORWARD, 0, &FastForwardButton, IGraphics::CORNER_ALL)) - m_Speed = clamp(m_Speed + 1, 0, (int)(g_DemoSpeeds - 1)); - - // speed meter - char aBuffer[128]; - const char *pPaused = m_StartPaused ? Localize("(paused)") : ""; - str_format(aBuffer, sizeof(aBuffer), "%s: ×%g %s", Localize("Speed"), g_aSpeeds[m_Speed], pPaused); - UI()->DoLabel(&Row, aBuffer, 12.8f, TEXTALIGN_ML); - - Box.HSplitBottom(16.0f, &Box, nullptr); - Box.HSplitBottom(24.0f, &Box, &Row); - - CUIRect Label, TextBox; - Row.VSplitLeft(110.0f, &Label, &TextBox); - TextBox.VSplitLeft(10.0f, nullptr, &TextBox); - UI()->DoLabel(&Label, Localize("Video name:"), 12.8f, TEXTALIGN_ML); - UI()->DoEditBox(&m_DemoRenderInput, &TextBox, 12.8f); - } - else if(m_Popup == POPUP_RENDER_DONE) - { - CUIRect Ok, OpenFolder; - - char aFilePath[IO_MAX_PATH_LENGTH]; - char aSaveFolder[IO_MAX_PATH_LENGTH]; - Storage()->GetCompletePath(IStorage::TYPE_SAVE, "videos", aSaveFolder, sizeof(aSaveFolder)); - str_format(aFilePath, sizeof(aFilePath), "%s/%s.mp4", aSaveFolder, m_DemoRenderInput.GetString()); - - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); - Part.VMargin(80.0f, &Part); - - Part.VSplitMid(&OpenFolder, &Ok); - - Ok.VMargin(20.0f, &Ok); - OpenFolder.VMargin(20.0f, &OpenFolder); - - static CButtonContainer s_ButtonOpenFolder; - if(DoButton_Menu(&s_ButtonOpenFolder, Localize("Videos directory"), 0, &OpenFolder)) + else if(Storage()->FileExists(aVideoPath, IStorage::TYPE_SAVE)) { - if(!open_file(aSaveFolder)) - { - dbg_msg("menus", "couldn't open file '%s'", aSaveFolder); - } + char aMessage[128 + IO_MAX_PATH_LENGTH]; + str_format(aMessage, sizeof(aMessage), Localize("File '%s' already exists, do you want to overwrite it?"), m_DemoRenderInput.GetString()); + PopupConfirm(Localize("Replace video"), aMessage, Localize("Yes"), Localize("No"), &CMenus::PopupConfirmDemoReplaceVideo, POPUP_NONE, &CMenus::DefaultButtonCallback, POPUP_RENDER_DEMO); } - - static CButtonContainer s_ButtonOk; - if(DoButton_Menu(&s_ButtonOk, Localize("Ok"), 0, &Ok) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + else { - m_Popup = POPUP_NONE; - m_DemoRenderInput.Clear(); + PopupConfirmDemoReplaceVideo(); } + } + + CUIRect ShowChatCheckbox, UseSoundsCheckbox; + Box.HSplitBottom(20.0f, &Box, &Row); + Box.HSplitBottom(10.0f, &Box, nullptr); + Row.VSplitMid(&ShowChatCheckbox, &UseSoundsCheckbox, 20.0f); + + if(DoButton_CheckBox(&g_Config.m_ClVideoShowChat, Localize("Show chat"), g_Config.m_ClVideoShowChat, &ShowChatCheckbox)) + g_Config.m_ClVideoShowChat ^= 1; + + if(DoButton_CheckBox(&g_Config.m_ClVideoSndEnable, Localize("Use sounds"), g_Config.m_ClVideoSndEnable, &UseSoundsCheckbox)) + g_Config.m_ClVideoSndEnable ^= 1; + + CUIRect ShowHudButton; + Box.HSplitBottom(20.0f, &Box, &Row); + Row.VSplitMid(&Row, &ShowHudButton, 20.0f); + + if(DoButton_CheckBox(&g_Config.m_ClVideoShowhud, Localize("Show ingame HUD"), g_Config.m_ClVideoShowhud, &ShowHudButton)) + g_Config.m_ClVideoShowhud ^= 1; + + // slowdown + CUIRect SlowDownButton; + Row.VSplitLeft(20.0f, &SlowDownButton, &Row); + Row.VSplitLeft(5.0f, nullptr, &Row); + static CButtonContainer s_SlowDownButton; + if(DoButton_FontIcon(&s_SlowDownButton, FONT_ICON_BACKWARD, 0, &SlowDownButton, IGraphics::CORNER_ALL)) + m_Speed = clamp(m_Speed - 1, 0, (int)(g_DemoSpeeds - 1)); + + // paused + CUIRect PausedButton; + Row.VSplitLeft(20.0f, &PausedButton, &Row); + Row.VSplitLeft(5.0f, nullptr, &Row); + static CButtonContainer s_PausedButton; + if(DoButton_FontIcon(&s_PausedButton, FONT_ICON_PAUSE, 0, &PausedButton, IGraphics::CORNER_ALL)) + m_StartPaused ^= 1; + + // fastforward + CUIRect FastForwardButton; + Row.VSplitLeft(20.0f, &FastForwardButton, &Row); + Row.VSplitLeft(8.0f, nullptr, &Row); + static CButtonContainer s_FastForwardButton; + if(DoButton_FontIcon(&s_FastForwardButton, FONT_ICON_FORWARD, 0, &FastForwardButton, IGraphics::CORNER_ALL)) + m_Speed = clamp(m_Speed + 1, 0, (int)(g_DemoSpeeds - 1)); + + // speed meter + char aBuffer[128]; + const char *pPaused = m_StartPaused ? Localize("(paused)") : ""; + str_format(aBuffer, sizeof(aBuffer), "%s: ×%g %s", Localize("Speed"), g_aSpeeds[m_Speed], pPaused); + Ui()->DoLabel(&Row, aBuffer, 12.8f, TEXTALIGN_ML); + Box.HSplitBottom(16.0f, &Box, nullptr); + Box.HSplitBottom(24.0f, &Box, &Row); + + CUIRect Label, TextBox; + Row.VSplitLeft(110.0f, &Label, &TextBox); + TextBox.VSplitLeft(10.0f, nullptr, &TextBox); + Ui()->DoLabel(&Label, Localize("Video name:"), 12.8f, TEXTALIGN_ML); + Ui()->DoEditBox(&m_DemoRenderInput, &TextBox, 12.8f); + } + else if(m_Popup == POPUP_RENDER_DONE) + { + CUIRect Ok, OpenFolder; + + char aFilePath[IO_MAX_PATH_LENGTH]; + char aSaveFolder[IO_MAX_PATH_LENGTH]; + Storage()->GetCompletePath(IStorage::TYPE_SAVE, "videos", aSaveFolder, sizeof(aSaveFolder)); + str_format(aFilePath, sizeof(aFilePath), "%s/%s.mp4", aSaveFolder, m_DemoRenderInput.GetString()); + + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); + Part.VMargin(80.0f, &Part); - Box.HSplitBottom(160.f, &Box, &Part); - Part.VMargin(20.0f, &Part); + Part.VSplitMid(&OpenFolder, &Ok); - str_format(aBuf, sizeof(aBuf), Localize("Video was saved to '%s'"), aFilePath); + Ok.VMargin(20.0f, &Ok); + OpenFolder.VMargin(20.0f, &OpenFolder); - SLabelProperties MessageProps; - MessageProps.m_MaxWidth = (int)Part.w; - UI()->DoLabel(&Part, aBuf, 18.0f, TEXTALIGN_TL, MessageProps); + static CButtonContainer s_ButtonOpenFolder; + if(DoButton_Menu(&s_ButtonOpenFolder, Localize("Videos directory"), 0, &OpenFolder)) + { + Client()->ViewFile(aSaveFolder); + } + + static CButtonContainer s_ButtonOk; + if(DoButton_Menu(&s_ButtonOk, Localize("Ok"), 0, &Ok) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) + { + m_Popup = POPUP_NONE; + m_DemoRenderInput.Clear(); } + + Box.HSplitBottom(160.f, &Box, &Part); + Part.VMargin(20.0f, &Part); + + str_format(aBuf, sizeof(aBuf), Localize("Video was saved to '%s'"), aFilePath); + + SLabelProperties MessageProps; + MessageProps.m_MaxWidth = (int)Part.w; + Ui()->DoLabel(&Part, aBuf, 18.0f, TEXTALIGN_TL, MessageProps); + } #endif - else if(m_Popup == POPUP_FIRST_LAUNCH) + else if(m_Popup == POPUP_FIRST_LAUNCH) + { + CUIRect Label, TextBox, Skip, Join; + + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); + Part.VMargin(80.0f, &Part); + Part.VSplitMid(&Skip, &Join); + Skip.VMargin(20.0f, &Skip); + Join.VMargin(20.0f, &Join); + + static CButtonContainer s_JoinTutorialButton; + if(DoButton_Menu(&s_JoinTutorialButton, Localize("Join Tutorial Server"), 0, &Join) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) { - CUIRect Label, TextBox, Skip, Join; + m_JoinTutorial = true; + Client()->RequestDDNetInfo(); + m_Popup = g_Config.m_BrIndicateFinished ? POPUP_POINTS : POPUP_NONE; + } - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); - Part.VMargin(80.0f, &Part); - Part.VSplitMid(&Skip, &Join); - Skip.VMargin(20.0f, &Skip); - Join.VMargin(20.0f, &Join); + static CButtonContainer s_SkipTutorialButton; + if(DoButton_Menu(&s_SkipTutorialButton, Localize("Skip Tutorial"), 0, &Skip) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + m_JoinTutorial = false; + Client()->RequestDDNetInfo(); + m_Popup = g_Config.m_BrIndicateFinished ? POPUP_POINTS : POPUP_NONE; + } - static CButtonContainer s_JoinTutorialButton; - if(DoButton_Menu(&s_JoinTutorialButton, Localize("Join Tutorial Server"), 0, &Join) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) - { - m_JoinTutorial = true; - Client()->RequestDDNetInfo(); - m_Popup = g_Config.m_BrIndicateFinished ? POPUP_POINTS : POPUP_NONE; - } + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); - static CButtonContainer s_SkipTutorialButton; - if(DoButton_Menu(&s_SkipTutorialButton, Localize("Skip Tutorial"), 0, &Skip) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) - { - m_JoinTutorial = false; - Client()->RequestDDNetInfo(); - m_Popup = g_Config.m_BrIndicateFinished ? POPUP_POINTS : POPUP_NONE; - } + Part.VSplitLeft(30.0f, 0, &Part); + str_format(aBuf, sizeof(aBuf), "%s\n(%s)", + Localize("Show DDNet map finishes in server browser"), + Localize("transmits your player name to info.ddnet.org")); - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); + if(DoButton_CheckBox(&g_Config.m_BrIndicateFinished, aBuf, g_Config.m_BrIndicateFinished, &Part)) + g_Config.m_BrIndicateFinished ^= 1; - Part.VSplitLeft(30.0f, 0, &Part); - str_format(aBuf, sizeof(aBuf), "%s\n(%s)", - Localize("Show DDNet map finishes in server browser"), - Localize("transmits your player name to info.ddnet.org")); + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); - if(DoButton_CheckBox(&g_Config.m_BrIndicateFinished, aBuf, g_Config.m_BrIndicateFinished, &Part)) - g_Config.m_BrIndicateFinished ^= 1; + Part.VSplitLeft(60.0f, 0, &Label); + Label.VSplitLeft(100.0f, 0, &TextBox); + TextBox.VSplitLeft(20.0f, 0, &TextBox); + TextBox.VSplitRight(60.0f, &TextBox, 0); + Ui()->DoLabel(&Label, Localize("Nickname"), 16.0f, TEXTALIGN_ML); + static CLineInput s_PlayerNameInput(g_Config.m_PlayerName, sizeof(g_Config.m_PlayerName)); + s_PlayerNameInput.SetEmptyText(Client()->PlayerName()); + Ui()->DoEditBox(&s_PlayerNameInput, &TextBox, 12.0f); + } + else if(m_Popup == POPUP_POINTS) + { + CUIRect Yes, No; - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); + Part.VMargin(80.0f, &Part); - Part.VSplitLeft(60.0f, 0, &Label); - Label.VSplitLeft(100.0f, 0, &TextBox); - TextBox.VSplitLeft(20.0f, 0, &TextBox); - TextBox.VSplitRight(60.0f, &TextBox, 0); - UI()->DoLabel(&Label, Localize("Nickname"), 16.0f, TEXTALIGN_ML); - static CLineInput s_PlayerNameInput(g_Config.m_PlayerName, sizeof(g_Config.m_PlayerName)); - s_PlayerNameInput.SetEmptyText(Client()->PlayerName()); - UI()->DoEditBox(&s_PlayerNameInput, &TextBox, 12.0f); - } - else if(m_Popup == POPUP_POINTS) + Part.VSplitMid(&No, &Yes); + + Yes.VMargin(20.0f, &Yes); + No.VMargin(20.0f, &No); + + static CButtonContainer s_ButtonNo; + if(DoButton_Menu(&s_ButtonNo, Localize("No"), 0, &No) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + m_Popup = POPUP_FIRST_LAUNCH; + + static CButtonContainer s_ButtonYes; + if(DoButton_Menu(&s_ButtonYes, Localize("Yes"), 0, &Yes) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) + m_Popup = POPUP_NONE; + } + else if(m_Popup == POPUP_WARNING) + { + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); + Part.VMargin(120.0f, &Part); + + static CButtonContainer s_Button; + if(DoButton_Menu(&s_Button, pButtonText, 0, &Part) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER) || (m_PopupWarningDuration > 0s && time_get_nanoseconds() - m_PopupWarningLastTime >= m_PopupWarningDuration)) { - CUIRect Yes, No; + m_Popup = POPUP_NONE; + SetActive(false); + } + } + else if(m_Popup == POPUP_SAVE_SKIN) + { + CUIRect Label, TextBox, Yes, No; - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); - Part.VMargin(80.0f, &Part); + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); + Part.VMargin(80.0f, &Part); - Part.VSplitMid(&No, &Yes); + Part.VSplitMid(&No, &Yes); - Yes.VMargin(20.0f, &Yes); - No.VMargin(20.0f, &No); + Yes.VMargin(20.0f, &Yes); + No.VMargin(20.0f, &No); - static CButtonContainer s_ButtonNo; - if(DoButton_Menu(&s_ButtonNo, Localize("No"), 0, &No) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) - m_Popup = POPUP_FIRST_LAUNCH; + static CButtonContainer s_ButtonNo; + if(DoButton_Menu(&s_ButtonNo, Localize("No"), 0, &No) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + m_Popup = POPUP_NONE; - static CButtonContainer s_ButtonYes; - if(DoButton_Menu(&s_ButtonYes, Localize("Yes"), 0, &Yes) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) - m_Popup = POPUP_NONE; - } - else if(m_Popup == POPUP_WARNING) + static CButtonContainer s_ButtonYes; + if(DoButton_Menu(&s_ButtonYes, Localize("Yes"), m_SkinNameInput.IsEmpty() ? 1 : 0, &Yes) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) { - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); - Part.VMargin(120.0f, &Part); - - static CButtonContainer s_Button; - if(DoButton_Menu(&s_Button, pButtonText, 0, &Part) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || (m_PopupWarningDuration > 0s && time_get_nanoseconds() - m_PopupWarningLastTime >= m_PopupWarningDuration)) + if(m_SkinNameInput.GetLength()) { - m_Popup = POPUP_NONE; - SetActive(false); + if(m_SkinNameInput.GetString()[0] != 'x' && m_SkinNameInput.GetString()[1] != '_') + { + if(m_pClient->m_Skins7.SaveSkinfile(m_SkinNameInput.GetString(), m_Dummy)) + { + m_Popup = POPUP_NONE; + m_SkinListNeedsUpdate = true; + } + else + PopupMessage(Localize("Error"), Localize("Unable to save the skin"), Localize("Ok"), POPUP_SAVE_SKIN); + } + else + PopupMessage(Localize("Error"), Localize("Unable to save the skin with a reserved name"), Localize("Ok"), POPUP_SAVE_SKIN); } } - else + + Box.HSplitBottom(60.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); + + Part.VMargin(60.0f, &Label); + Label.VSplitLeft(100.0f, &Label, &TextBox); + TextBox.VSplitLeft(20.0f, nullptr, &TextBox); + Ui()->DoLabel(&Label, Localize("Name"), 18.0f, TEXTALIGN_ML); + Ui()->DoClearableEditBox(&m_SkinNameInput, &TextBox, 12.0f); + } + else + { + Box.HSplitBottom(20.f, &Box, &Part); + Box.HSplitBottom(24.f, &Box, &Part); + Part.VMargin(120.0f, &Part); + + static CButtonContainer s_Button; + if(DoButton_Menu(&s_Button, pButtonText, 0, &Part) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER)) + { + if(m_Popup == POPUP_DISCONNECTED && Client()->ReconnectTime() > 0) + Client()->SetReconnectTime(0); + m_Popup = POPUP_NONE; + } + } + + if(m_Popup == POPUP_NONE) + Ui()->SetActiveItem(nullptr); +} + +void CMenus::RenderPopupConnecting(CUIRect Screen) +{ + const float FontSize = 20.0f; + + CUIRect Box, Label; + Screen.Margin(150.0f, &Box); + Box.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 15.0f); + Box.Margin(20.0f, &Box); + + Box.HSplitTop(24.0f, &Label, &Box); + Ui()->DoLabel(&Label, Localize("Connecting to"), 24.0f, TEXTALIGN_MC); + + Box.HSplitTop(20.0f, nullptr, &Box); + Box.HSplitTop(24.0f, &Label, &Box); + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + Props.m_EllipsisAtEnd = true; + Ui()->DoLabel(&Label, Client()->ConnectAddressString(), FontSize, TEXTALIGN_MC, Props); + + if(time_get() - Client()->StateStartTime() > time_freq()) + { + const char *pConnectivityLabel = ""; + switch(Client()->UdpConnectivity(Client()->ConnectNetTypes())) + { + case IClient::CONNECTIVITY_UNKNOWN: + break; + case IClient::CONNECTIVITY_CHECKING: + pConnectivityLabel = Localize("Trying to determine UDP connectivity…"); + break; + case IClient::CONNECTIVITY_UNREACHABLE: + pConnectivityLabel = Localize("UDP seems to be filtered."); + break; + case IClient::CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES: + pConnectivityLabel = Localize("UDP and TCP IP addresses seem to be different. Try disabling VPN, proxy or network accelerators."); + break; + case IClient::CONNECTIVITY_REACHABLE: + pConnectivityLabel = Localize("No answer from server yet."); + break; + } + if(pConnectivityLabel[0] != '\0') { - Box.HSplitBottom(20.f, &Box, &Part); - Box.HSplitBottom(24.f, &Box, &Part); - Part.VMargin(120.0f, &Part); + Box.HSplitTop(20.0f, nullptr, &Box); + Box.HSplitTop(24.0f, &Label, &Box); + SLabelProperties ConnectivityLabelProps; + ConnectivityLabelProps.m_MaxWidth = Label.w; + if(TextRender()->TextWidth(FontSize, pConnectivityLabel) > Label.w) + Ui()->DoLabel(&Label, pConnectivityLabel, FontSize, TEXTALIGN_ML, ConnectivityLabelProps); + else + Ui()->DoLabel(&Label, pConnectivityLabel, FontSize, TEXTALIGN_MC); + } + } + + CUIRect Button; + Box.HSplitBottom(24.0f, &Box, &Button); + Button.VMargin(100.0f, &Button); - static CButtonContainer s_Button; - if(DoButton_Menu(&s_Button, pButtonText, 0, &Part) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER)) + static CButtonContainer s_Button; + if(DoButton_Menu(&s_Button, Localize("Abort"), 0, &Button) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + Client()->Disconnect(); + Ui()->SetActiveItem(nullptr); + RefreshBrowserTab(true); + } +} + +void CMenus::RenderPopupLoading(CUIRect Screen) +{ + char aTitle[256]; + char aLabel1[128]; + char aLabel2[128]; + if(Client()->MapDownloadTotalsize() > 0) + { + const int64_t Now = time_get(); + if(Now - m_DownloadLastCheckTime >= time_freq()) + { + if(m_DownloadLastCheckSize > Client()->MapDownloadAmount()) { - if(m_Popup == POPUP_DISCONNECTED && Client()->ReconnectTime() > 0) - Client()->SetReconnectTime(0); - m_Popup = POPUP_NONE; + // map downloaded restarted + m_DownloadLastCheckSize = 0; } + + // update download speed + const float Diff = (Client()->MapDownloadAmount() - m_DownloadLastCheckSize) / ((int)((Now - m_DownloadLastCheckTime) / time_freq())); + const float StartDiff = m_DownloadLastCheckSize - 0.0f; + if(StartDiff + Diff > 0.0f) + m_DownloadSpeed = (Diff / (StartDiff + Diff)) * (Diff / 1.0f) + (StartDiff / (Diff + StartDiff)) * m_DownloadSpeed; + else + m_DownloadSpeed = 0.0f; + m_DownloadLastCheckTime = Now; + m_DownloadLastCheckSize = Client()->MapDownloadAmount(); } - if(m_Popup == POPUP_NONE) - UI()->SetActiveItem(nullptr); + str_format(aTitle, sizeof(aTitle), "%s: %s", Localize("Downloading map"), Client()->MapDownloadName()); + + str_format(aLabel1, sizeof(aLabel1), Localize("%d/%d KiB (%.1f KiB/s)"), Client()->MapDownloadAmount() / 1024, Client()->MapDownloadTotalsize() / 1024, m_DownloadSpeed / 1024.0f); + + const int SecondsLeft = maximum(1, m_DownloadSpeed > 0.0f ? static_cast((Client()->MapDownloadTotalsize() - Client()->MapDownloadAmount()) / m_DownloadSpeed) : 1); + const int MinutesLeft = SecondsLeft / 60; + if(MinutesLeft > 0) + { + str_format(aLabel2, sizeof(aLabel2), MinutesLeft == 1 ? Localize("%i minute left") : Localize("%i minutes left"), MinutesLeft); + } + else + { + str_format(aLabel2, sizeof(aLabel2), SecondsLeft == 1 ? Localize("%i second left") : Localize("%i seconds left"), SecondsLeft); + } + } + else + { + str_copy(aTitle, Localize("Connected")); + switch(Client()->LoadingStateDetail()) + { + case IClient::LOADING_STATE_DETAIL_INITIAL: + str_copy(aLabel1, Localize("Getting game info")); + break; + case IClient::LOADING_STATE_DETAIL_LOADING_MAP: + str_copy(aLabel1, Localize("Loading map file from storage")); + break; + case IClient::LOADING_STATE_DETAIL_LOADING_DEMO: + str_copy(aLabel1, Localize("Loading demo file from storage")); + break; + case IClient::LOADING_STATE_DETAIL_SENDING_READY: + str_copy(aLabel1, Localize("Requesting to join the game")); + break; + case IClient::LOADING_STATE_DETAIL_GETTING_READY: + str_copy(aLabel1, Localize("Sending initial client info")); + break; + default: + dbg_assert(false, "Invalid loading state for RenderPopupLoading"); + break; + } + aLabel2[0] = '\0'; } - UI()->RenderPopupMenus(); + const float FontSize = 20.0f; - // Handle this escape hotkey after popup menus - if(!m_ShowStart && Client()->State() == IClient::STATE_OFFLINE && UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) + CUIRect Box, Label; + Screen.Margin(150.0f, &Box); + Box.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 15.0f); + Box.Margin(20.0f, &Box); + + Box.HSplitTop(24.0f, &Label, &Box); + Ui()->DoLabel(&Label, aTitle, 24.0f, TEXTALIGN_MC); + + Box.HSplitTop(20.0f, nullptr, &Box); + Box.HSplitTop(24.0f, &Label, &Box); + Ui()->DoLabel(&Label, aLabel1, FontSize, TEXTALIGN_MC); + + if(aLabel2[0] != '\0') { - m_ShowStart = true; + Box.HSplitTop(20.0f, nullptr, &Box); + Box.HSplitTop(24.0f, &Label, &Box); + SLabelProperties ExtraTextProps; + ExtraTextProps.m_MaxWidth = Label.w; + if(TextRender()->TextWidth(FontSize, aLabel2) > Label.w) + Ui()->DoLabel(&Label, aLabel2, FontSize, TEXTALIGN_ML, ExtraTextProps); + else + Ui()->DoLabel(&Label, aLabel2, FontSize, TEXTALIGN_MC); } - return 0; + if(Client()->MapDownloadTotalsize() > 0) + { + CUIRect ProgressBar; + Box.HSplitTop(20.0f, nullptr, &Box); + Box.HSplitTop(24.0f, &ProgressBar, &Box); + ProgressBar.VMargin(20.0f, &ProgressBar); + Ui()->RenderProgressBar(ProgressBar, Client()->MapDownloadAmount() / (float)Client()->MapDownloadTotalsize()); + } + + CUIRect Button; + Box.HSplitBottom(24.0f, &Box, &Button); + Button.VMargin(100.0f, &Button); + + static CButtonContainer s_Button; + if(DoButton_Menu(&s_Button, Localize("Abort"), 0, &Button) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + Client()->Disconnect(); + Ui()->SetActiveItem(nullptr); + RefreshBrowserTab(true); + } } #if defined(CONF_VIDEORECORDER) @@ -1810,8 +2037,6 @@ void CMenus::PopupConfirmDemoReplaceVideo() str_format(aBuf, sizeof(aBuf), "%s/%s.demo", m_aCurrentDemoFolder, m_aCurrentDemoSelectionName); char aVideoName[IO_MAX_PATH_LENGTH]; str_copy(aVideoName, m_DemoRenderInput.GetString()); - if(!str_endswith(aVideoName, ".mp4")) - str_append(aVideoName, ".mp4"); const char *pError = Client()->DemoPlayer_Render(aBuf, m_DemolistStorageType, aVideoName, m_Speed, m_StartPaused); m_Speed = 4; m_StartPaused = false; @@ -1827,7 +2052,7 @@ void CMenus::PopupConfirmDemoReplaceVideo() void CMenus::RenderThemeSelection(CUIRect MainView) { - const std::vector &vThemes = m_pBackground->GetThemes(); + const std::vector &vThemes = GameClient()->m_MenuBackground.GetThemes(); int SelectedTheme = -1; for(int i = 0; i < (int)vThemes.size(); i++) @@ -1884,7 +2109,7 @@ void CMenus::RenderThemeSelection(CUIRect MainView) else // generic str_copy(aName, Theme.m_Name.c_str()); - UI()->DoLabel(&Label, aName, 16.0f * CUI::ms_FontmodHeight, TEXTALIGN_ML); + Ui()->DoLabel(&Label, aName, 16.0f * CUi::ms_FontmodHeight, TEXTALIGN_ML); } SelectedTheme = s_ListBox.DoEnd(); @@ -1893,7 +2118,7 @@ void CMenus::RenderThemeSelection(CUIRect MainView) { const CTheme &Theme = vThemes[SelectedTheme]; str_copy(g_Config.m_ClMenuMap, Theme.m_Name.c_str()); - m_pBackground->LoadMenuBackground(Theme.m_HasDay, Theme.m_HasNight); + GameClient()->m_MenuBackground.LoadMenuBackground(Theme.m_HasDay, Theme.m_HasNight); } } @@ -1901,8 +2126,8 @@ void CMenus::SetActive(bool Active) { if(Active != m_MenuActive) { - UI()->SetHotItem(nullptr); - UI()->SetActiveItem(nullptr); + Ui()->SetHotItem(nullptr); + Ui()->SetActiveItem(nullptr); } m_MenuActive = Active; if(!m_MenuActive) @@ -1937,6 +2162,8 @@ void CMenus::OnReset() void CMenus::OnShutdown() { KillServer(); + m_CommunityIconLoadJobs.clear(); + m_CommunityIconDownloadJobs.clear(); } bool CMenus::OnCursorMove(float x, float y, IInput::ECursorType CursorType) @@ -1944,8 +2171,8 @@ bool CMenus::OnCursorMove(float x, float y, IInput::ECursorType CursorType) if(!m_MenuActive) return false; - UI()->ConvertMouseMove(&x, &y, CursorType); - UI()->OnCursorMove(x, y); + Ui()->ConvertMouseMove(&x, &y, CursorType); + Ui()->OnCursorMove(x, y); return true; } @@ -1955,7 +2182,7 @@ bool CMenus::OnInput(const IInput::CEvent &Event) // Escape key is always handled to activate/deactivate menu if((Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_ESCAPE) || IsActive()) { - UI()->OnInput(Event); + Ui()->OnInput(Event); return true; } return false; @@ -1964,7 +2191,7 @@ bool CMenus::OnInput(const IInput::CEvent &Event) void CMenus::OnStateChange(int NewState, int OldState) { // reset active item - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); if(OldState == IClient::STATE_ONLINE || OldState == IClient::STATE_OFFLINE) TextRender()->DeleteTextContainer(m_MotdTextContainerIndex); @@ -1980,7 +2207,7 @@ void CMenus::OnStateChange(int NewState, int OldState) { m_Popup = POPUP_PASSWORD; m_PasswordInput.SelectAll(); - UI()->SetActiveItem(&m_PasswordInput); + Ui()->SetActiveItem(&m_PasswordInput); } else m_Popup = POPUP_DISCONNECTED; @@ -1988,15 +2215,10 @@ void CMenus::OnStateChange(int NewState, int OldState) } else if(NewState == IClient::STATE_LOADING) { - m_Popup = POPUP_CONNECTING; m_DownloadLastCheckTime = time_get(); m_DownloadLastCheckSize = 0; m_DownloadSpeed = 0.0f; } - else if(NewState == IClient::STATE_CONNECTING) - { - m_Popup = POPUP_CONNECTING; - } else if(NewState == IClient::STATE_ONLINE || NewState == IClient::STATE_DEMOPLAYBACK) { if(m_Popup != POPUP_WARNING) @@ -2014,17 +2236,11 @@ void CMenus::OnWindowResize() void CMenus::OnRender() { - UI()->StartCheck(); + Ui()->StartCheck(); if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) SetActive(true); - if(Client()->State() == IClient::STATE_DEMOPLAYBACK) - { - UI()->MapScreen(); - RenderDemoPlayer(*UI()->Screen()); - } - if(Client()->State() == IClient::STATE_ONLINE && m_pClient->m_ServerMode == CGameClient::SERVERMODE_PUREMOD) { Client()->Disconnect(); @@ -2034,29 +2250,38 @@ void CMenus::OnRender() if(!IsActive()) { - if(UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) + if(Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { SetActive(true); - UI()->FinishCheck(); - UI()->ClearHotkeys(); - return; + } + else if(Client()->State() != IClient::STATE_DEMOPLAYBACK) + { + Ui()->FinishCheck(); + Ui()->ClearHotkeys(); + return; + } } UpdateColors(); - UI()->Update(); + Ui()->Update(); Render(); - RenderTools()->RenderCursor(UI()->MousePos(), 24.0f); + + if(IsActive()) + { + RenderTools()->RenderCursor(Ui()->MousePos(), 24.0f); + } // render debug information if(g_Config.m_Debug) - UI()->DebugRender(); + Ui()->DebugRender(2.0f, Ui()->Screen()->h - 12.0f); - if(UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) + if(Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) SetActive(false); - UI()->FinishCheck(); - UI()->ClearHotkeys(); + Ui()->FinishCheck(); + Ui()->ClearHotkeys(); } void CMenus::UpdateColors() @@ -2106,14 +2331,16 @@ void CMenus::RenderBackground() Graphics()->QuadsBegin(); Graphics()->SetColor(0.0f, 0.0f, 0.0f, 0.045f); const float Size = 15.0f; - const float OffsetTime = std::fmod(LocalTime() * 0.15f, 2.0f); + const float OffsetTime = std::fmod(Client()->GlobalTime() * 0.15f, 2.0f); IGraphics::CQuadItem aCheckerItems[64]; size_t NumCheckerItems = 0; - for(int y = -2; y < (int)(ScreenWidth / Size); y++) + const int NumItemsWidth = std::ceil(ScreenWidth / Size); + const int NumItemsHeight = std::ceil(ScreenHeight / Size); + for(int y = -2; y < NumItemsHeight; y++) { - for(int x = -2; x < (int)(ScreenHeight / Size); x++) + for(int x = 0; x < NumItemsWidth + 4; x += 2) { - aCheckerItems[NumCheckerItems] = IGraphics::CQuadItem((x - OffsetTime) * Size * 2 + (y & 1) * Size, (y + OffsetTime) * Size, Size, Size); + aCheckerItems[NumCheckerItems] = IGraphics::CQuadItem((x - 2 * OffsetTime + (y & 1)) * Size, (y + OffsetTime) * Size, Size, Size); NumCheckerItems++; if(NumCheckerItems == std::size(aCheckerItems)) { @@ -2135,7 +2362,7 @@ void CMenus::RenderBackground() Graphics()->QuadsEnd(); // restore screen - UI()->MapScreen(); + Ui()->MapScreen(); } bool CMenus::CheckHotKey(int Key) const @@ -2145,16 +2372,16 @@ bool CMenus::CheckHotKey(int Key) const Input()->KeyIsPressed(Key) && m_pClient->m_GameConsole.IsClosed(); } -int CMenus::DoButton_CheckBox_Tristate(const void *pID, const char *pText, TRISTATE Checked, const CUIRect *pRect) +int CMenus::DoButton_CheckBox_Tristate(const void *pId, const char *pText, TRISTATE Checked, const CUIRect *pRect) { switch(Checked) { case TRISTATE::NONE: - return DoButton_CheckBox_Common(pID, pText, "", pRect); + return DoButton_CheckBox_Common(pId, pText, "", pRect); case TRISTATE::SOME: - return DoButton_CheckBox_Common(pID, pText, "O", pRect); + return DoButton_CheckBox_Common(pId, pText, "O", pRect); case TRISTATE::ALL: - return DoButton_CheckBox_Common(pID, pText, "X", pRect); + return DoButton_CheckBox_Common(pId, pText, "X", pRect); default: dbg_assert(false, "invalid tristate"); } @@ -2173,7 +2400,7 @@ int CMenus::MenuImageScan(const char *pName, int IsDir, int DirType, void *pUser str_format(aPath, sizeof(aPath), "menuimages/%s", pName); CImageInfo Info; - if(!pSelf->Graphics()->LoadPNG(&Info, aPath, DirType)) + if(!pSelf->Graphics()->LoadPng(Info, aPath, DirType)) { char aError[IO_MAX_PATH_LENGTH + 64]; str_format(aError, sizeof(aError), "Failed to load menu image from '%s'", aPath); @@ -2182,32 +2409,22 @@ int CMenus::MenuImageScan(const char *pName, int IsDir, int DirType, void *pUser } if(Info.m_Format != CImageInfo::FORMAT_RGBA) { - pSelf->Graphics()->FreePNG(&Info); + Info.Free(); char aError[IO_MAX_PATH_LENGTH + 64]; str_format(aError, sizeof(aError), "Failed to load menu image from '%s': must be an RGBA image", aPath); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "menus", aError); return 0; } - MenuImage.m_OrgTexture = pSelf->Graphics()->LoadTextureRaw(Info.m_Width, Info.m_Height, Info.m_Format, Info.m_pData, 0); + MenuImage.m_OrgTexture = pSelf->Graphics()->LoadTextureRaw(Info, 0, aPath); - // create gray scale version - unsigned char *pData = static_cast(Info.m_pData); - const size_t Step = Info.PixelSize(); - for(int i = 0; i < Info.m_Width * Info.m_Height; i++) - { - int v = (pData[i * Step] + pData[i * Step + 1] + pData[i * Step + 2]) / 3; - pData[i * Step] = v; - pData[i * Step + 1] = v; - pData[i * Step + 2] = v; - } - MenuImage.m_GreyTexture = pSelf->Graphics()->LoadTextureRaw(Info.m_Width, Info.m_Height, Info.m_Format, Info.m_pData, 0); - pSelf->Graphics()->FreePNG(&Info); + ConvertToGrayscale(Info); + MenuImage.m_GreyTexture = pSelf->Graphics()->LoadTextureRawMove(Info, 0, aPath); str_truncate(MenuImage.m_aName, sizeof(MenuImage.m_aName), pName, str_length(pName) - str_length(pExtension)); pSelf->m_vMenuImages.push_back(MenuImage); - pSelf->RenderLoading(Localize("Loading StormA Client"), Localize("Loading menu images"), 1); + pSelf->RenderLoading(Localize("Loading DDNet Client"), Localize("Loading menu images"), 1); return 0; } @@ -2222,28 +2439,69 @@ const CMenus::CMenuImage *CMenus::FindMenuImage(const char *pName) void CMenus::SetMenuPage(int NewPage) { - if(NewPage == PAGE_DDNET_LEGACY || NewPage == PAGE_KOG_LEGACY) - NewPage = PAGE_INTERNET; - + const int OldPage = m_MenuPage; m_MenuPage = NewPage; - if(NewPage >= PAGE_INTERNET && NewPage <= PAGE_FAVORITES) + if(NewPage >= PAGE_INTERNET && NewPage <= PAGE_FAVORITE_COMMUNITY_5) + { g_Config.m_UiPage = NewPage; + bool ForceRefresh = false; + if(m_ForceRefreshLanPage) + { + ForceRefresh = NewPage == PAGE_LAN; + m_ForceRefreshLanPage = false; + } + if(OldPage != NewPage || ForceRefresh) + { + RefreshBrowserTab(ForceRefresh); + } + } } -void CMenus::RefreshBrowserTab(int UiPage) +void CMenus::RefreshBrowserTab(bool Force) { - if(UiPage == PAGE_INTERNET) + if(g_Config.m_UiPage == PAGE_INTERNET) + { + if(Force || ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_INTERNET) + { + if(Force || ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN) + { + Client()->RequestDDNetInfo(); + } + ServerBrowser()->Refresh(IServerBrowser::TYPE_INTERNET); + UpdateCommunityCache(true); + } + } + else if(g_Config.m_UiPage == PAGE_LAN) { - Client()->RequestDDNetInfo(); - ServerBrowser()->Refresh(IServerBrowser::TYPE_INTERNET); + if(Force || ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_LAN) + { + ServerBrowser()->Refresh(IServerBrowser::TYPE_LAN); + UpdateCommunityCache(true); + } } - else if(UiPage == PAGE_LAN) + else if(g_Config.m_UiPage == PAGE_FAVORITES) { - ServerBrowser()->Refresh(IServerBrowser::TYPE_LAN); + if(Force || ServerBrowser()->GetCurrentType() != IServerBrowser::TYPE_FAVORITES) + { + if(Force || ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN) + { + Client()->RequestDDNetInfo(); + } + ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITES); + UpdateCommunityCache(true); + } } - else if(UiPage == PAGE_FAVORITES) + else if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5) { - Client()->RequestDDNetInfo(); - ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITES); + const int BrowserType = g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1 + IServerBrowser::TYPE_FAVORITE_COMMUNITY_1; + if(Force || ServerBrowser()->GetCurrentType() != BrowserType) + { + if(Force || ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN) + { + Client()->RequestDDNetInfo(); + } + ServerBrowser()->Refresh(BrowserType); + UpdateCommunityCache(true); + } } } diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index 98eeeb4e06..4595bd1eeb 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -17,15 +17,19 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include +#include + struct CServerProcess { PROCESS m_Process; @@ -35,6 +39,7 @@ struct CServerProcess class CMenusKeyBinder : public CComponent { public: + const void *m_pKeyReaderId; bool m_TakeKey; bool m_GotKey; IInput::CEvent m_Key; @@ -44,6 +49,14 @@ class CMenusKeyBinder : public CComponent virtual bool OnInput(const IInput::CEvent &Event) override; }; +struct SCommunityIcon +{ + char m_aCommunityId[CServerInfo::MAX_COMMUNITY_ID_LENGTH]; + SHA256_DIGEST m_Sha256; + IGraphics::CTextureHandle m_OrgTexture; + IGraphics::CTextureHandle m_GreyTexture; +}; + class CMenus : public CComponent { static ColorRGBA ms_GuiColor; @@ -58,21 +71,22 @@ class CMenus : public CComponent static ColorRGBA ms_ColorTabbarHover; int DoButton_FontIcon(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners = IGraphics::CORNER_ALL, bool Enabled = true); - int DoButton_Toggle(const void *pID, int Checked, const CUIRect *pRect, bool Active); + int DoButton_Toggle(const void *pId, int Checked, const CUIRect *pRect, bool Active); int DoButton_Menu(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, const char *pImageName = nullptr, int Corners = IGraphics::CORNER_ALL, float Rounding = 5.0f, float FontFactor = 0.0f, ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f)); - int DoButton_MenuTab(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, SUIAnimator *pAnimator = nullptr, const ColorRGBA *pDefaultColor = nullptr, const ColorRGBA *pActiveColor = nullptr, const ColorRGBA *pHoverColor = nullptr, float EdgeRounding = 10); + int DoButton_MenuTab(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, SUIAnimator *pAnimator = nullptr, const ColorRGBA *pDefaultColor = nullptr, const ColorRGBA *pActiveColor = nullptr, const ColorRGBA *pHoverColor = nullptr, float EdgeRounding = 10.0f, const SCommunityIcon *pCommunityIcon = nullptr); - int DoButton_CheckBox_Common(const void *pID, const char *pText, const char *pBoxText, const CUIRect *pRect); - int DoButton_CheckBox(const void *pID, const char *pText, int Checked, const CUIRect *pRect); - int DoButton_CheckBoxAutoVMarginAndSet(const void *pID, const char *pText, int *pValue, CUIRect *pRect, float VMargin); - int DoButton_CheckBox_Number(const void *pID, const char *pText, int Checked, const CUIRect *pRect); + int DoButton_CheckBox_Common(const void *pId, const char *pText, const char *pBoxText, const CUIRect *pRect); + int DoButton_CheckBox(const void *pId, const char *pText, int Checked, const CUIRect *pRect); + int DoButton_CheckBoxAutoVMarginAndSet(const void *pId, const char *pText, int *pValue, CUIRect *pRect, float VMargin); + int DoButton_CheckBox_Number(const void *pId, const char *pText, int Checked, const CUIRect *pRect); - ColorHSLA DoLine_ColorPicker(CButtonContainer *pResetID, float LineSize, float LabelSize, float BottomMargin, CUIRect *pMainRect, const char *pText, unsigned int *pColorValue, ColorRGBA DefaultColor, bool CheckBoxSpacing = true, int *pCheckBoxValue = nullptr, bool Alpha = false); + ColorHSLA DoLine_ColorPicker(CButtonContainer *pResetId, float LineSize, float LabelSize, float BottomMargin, CUIRect *pMainRect, const char *pText, unsigned int *pColorValue, ColorRGBA DefaultColor, bool CheckBoxSpacing = true, int *pCheckBoxValue = nullptr, bool Alpha = false); ColorHSLA DoButton_ColorPicker(const CUIRect *pRect, unsigned int *pHslaColor, bool Alpha); void DoLaserPreview(const CUIRect *pRect, ColorHSLA OutlineColor, ColorHSLA InnerColor, const int LaserType); - int DoButton_GridHeader(const void *pID, const char *pText, int Checked, const CUIRect *pRect); + int DoButton_GridHeader(const void *pId, const char *pText, int Checked, const CUIRect *pRect); + int DoButton_Favorite(const void *pButtonId, const void *pParentId, bool Checked, const CUIRect *pRect); - int DoKeyReader(const void *pID, const CUIRect *pRect, int Key, int ModifierCombination, int *pNewModifierCombination); + int DoKeyReader(const void *pId, const CUIRect *pRect, int Key, int ModifierCombination, int *pNewModifierCombination); void DoSettingsControlsButtons(int Start, int Stop, CUIRect View); @@ -80,9 +94,7 @@ class CMenus : public CComponent void DoJoystickAxisPicker(CUIRect View); void DoJoystickBar(const CUIRect *pRect, float Current, float Tolerance, bool Active); - void RefreshSkins(); - - void RandomSkin(); + bool m_SkinListNeedsUpdate = false; // menus_settings_assets.cpp public: @@ -155,10 +167,12 @@ class CMenus : public CComponent int m_MenuPage; int m_GamePage; int m_Popup; - int m_ActivePage; bool m_ShowStart; bool m_MenuActive; - bool m_JoinTutorial; + + bool m_JoinTutorial = false; + bool m_CreateDefaultFavoriteCommunities = false; + bool m_ForceRefreshLanPage = false; char m_aNextServer[256]; @@ -174,8 +188,14 @@ class CMenus : public CComponent const CMenuImage *FindMenuImage(const char *pName); // loading - int m_LoadCurrent; - int m_LoadTotal; + class CLoadingState + { + public: + std::chrono::nanoseconds m_LastRender{0}; + int m_Current; + int m_Total; + }; + CLoadingState m_LoadingState; // char m_aMessageTopic[512]; @@ -219,16 +239,21 @@ class CMenus : public CComponent static float ms_ListitemAdditionalHeight; // for settings - bool m_NeedRestartGeneral; - bool m_NeedRestartSkins; bool m_NeedRestartGraphics; bool m_NeedRestartSound; bool m_NeedRestartUpdate; - bool m_NeedRestartDDNet; bool m_NeedSendinfo; bool m_NeedSendDummyinfo; int m_SettingPlayerPage; + // 0.7 skins + bool m_CustomSkinMenu = false; + int m_TeePartSelected = protocol7::SKINPART_BODY; + const CSkins7::CSkin *m_pSelectedSkin = nullptr; + CLineInputBuffered m_SkinNameInput; + bool m_SkinPartListNeedsUpdate = false; + void PopupConfirmDeleteSkin7(); + // for map download popup int64_t m_DownloadLastCheckTime; int m_DownloadLastCheckSize; @@ -244,8 +269,6 @@ class CMenus : public CComponent CLineInputBuffered<64> m_FilterInput; bool m_ControlPageOpening; - // for stats - CLineInputBuffered m_StatsPlayerInput; // demo enum { @@ -394,11 +417,12 @@ class CMenus : public CComponent const void *ListItemId() const { return &m_aName; } const void *RemoveButtonId() const { return &m_FriendState; } const void *CommunityTooltipId() const { return &m_IsPlayer; } + const void *SkinTooltipId() const { return &m_aSkin; } bool operator<(const CFriendItem &Other) const { - const int Result = str_comp(m_aName, Other.m_aName); - return Result < 0 || (Result == 0 && str_comp(m_aClan, Other.m_aClan) < 0); + const int Result = str_comp_nocase(m_aName, Other.m_aName); + return Result < 0 || (Result == 0 && str_comp_nocase(m_aClan, Other.m_aClan) < 0); } }; @@ -413,12 +437,16 @@ class CMenus : public CComponent const CFriendItem *m_pRemoveFriend = nullptr; // found in menus.cpp - int Render(); + void Render(); + void RenderPopupFullscreen(CUIRect Screen); + void RenderPopupConnecting(CUIRect Screen); + void RenderPopupLoading(CUIRect Screen); #if defined(CONF_VIDEORECORDER) void PopupConfirmDemoReplaceVideo(); #endif - int RenderMenubar(CUIRect r); + void RenderMenubar(CUIRect Box, IClient::EClientState ClientState); void RenderNews(CUIRect MainView); + static void ConchainBackgroundEntities(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainUpdateMusicState(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); void UpdateMusicState(); @@ -442,6 +470,8 @@ class CMenus : public CComponent // found in menus_start.cpp void RenderStartMenu(CUIRect MainView); + bool m_EditorHotkeyWasPressed = true; + float m_EditorHotKeyChecktime = 0.0f; // found in menus_ingame.cpp STextContainerIndex m_MotdTextContainerIndex; @@ -452,7 +482,6 @@ class CMenus : public CComponent void RenderServerInfo(CUIRect MainView); void RenderServerInfoMotd(CUIRect Motd); void RenderServerControl(CUIRect MainView); - void RenderStats(CUIRect MainView); bool RenderServerControlKick(CUIRect MainView, bool FilterSpectators); bool RenderServerControlServer(CUIRect MainView); void RenderIngameHint(); @@ -466,6 +495,7 @@ class CMenus : public CComponent void Connect(const char *pAddress); void PopupConfirmSwitchServer(); void RenderServerbrowserFilters(CUIRect View); + void ResetServerbrowserFilters(); void RenderServerbrowserDDNetFilter(CUIRect View, IFilterList &Filter, float ItemHeight, int MaxItems, int ItemsPerRow, @@ -482,7 +512,7 @@ class CMenus : public CComponent int m_Selection; bool m_New; }; - static CUI::EPopupMenuFunctionResult PopupCountrySelection(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupCountrySelection(void *pContext, CUIRect View, bool Active); void RenderServerbrowserInfo(CUIRect View); void RenderServerbrowserInfoScoreboard(CUIRect View, const CServerInfo *pSelectedServer); void RenderServerbrowserFriends(CUIRect View); @@ -497,16 +527,7 @@ class CMenus : public CComponent static void ConchainFriendlistUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainFavoritesUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainCommunitiesUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); - struct SCommunityCache - { - int64_t m_UpdateTime = 0; - bool m_PageWithCommunities; - std::vector m_vpSelectedCommunities; - std::vector m_vpSelectableCountries; - std::vector m_vpSelectableTypes; - bool m_AnyRanksAvailable; - }; - SCommunityCache m_CommunityCache; + static void ConchainUiPageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); void UpdateCommunityCache(bool Force); // community icons @@ -518,49 +539,45 @@ class CMenus : public CComponent char m_aPath[IO_MAX_PATH_LENGTH]; int m_StorageType; bool m_Success = false; - CImageInfo m_ImageInfo; SHA256_DIGEST m_Sha256; CAbstractCommunityIconJob(CMenus *pMenus, const char *pCommunityId, int StorageType); - virtual ~CAbstractCommunityIconJob(); + virtual ~CAbstractCommunityIconJob(){}; public: const char *CommunityId() const { return m_aCommunityId; } bool Success() const { return m_Success; } - CImageInfo &&ImageInfo() { return std::move(m_ImageInfo); } - SHA256_DIGEST &&Sha256() { return std::move(m_Sha256); } + const SHA256_DIGEST &Sha256() const { return m_Sha256; } }; + class CCommunityIconLoadJob : public IJob, public CAbstractCommunityIconJob { + CImageInfo m_ImageInfo; + protected: void Run() override; public: CCommunityIconLoadJob(CMenus *pMenus, const char *pCommunityId, int StorageType); + ~CCommunityIconLoadJob(); + + CImageInfo &ImageInfo() { return m_ImageInfo; } }; + class CCommunityIconDownloadJob : public CHttpRequest, public CAbstractCommunityIconJob { - protected: - int OnCompletion(int State) override; - public: CCommunityIconDownloadJob(CMenus *pMenus, const char *pCommunityId, const char *pUrl, const SHA256_DIGEST &Sha256); }; - struct SCommunityIcon - { - char m_aCommunityId[CServerInfo::MAX_COMMUNITY_ID_LENGTH]; - SHA256_DIGEST m_Sha256; - IGraphics::CTextureHandle m_OrgTexture; - IGraphics::CTextureHandle m_GreyTexture; - }; + std::vector m_vCommunityIcons; std::deque> m_CommunityIconLoadJobs; std::deque> m_CommunityIconDownloadJobs; - int64_t m_CommunityIconsUpdateTime = 0; + SHA256_DIGEST m_CommunityIconsInfoSha256 = SHA256_ZEROED; static int CommunityIconScan(const char *pName, int IsDir, int DirType, void *pUser); const SCommunityIcon *FindCommunityIcon(const char *pCommunityId); bool LoadCommunityIconFile(const char *pPath, int DirType, CImageInfo &Info, SHA256_DIGEST &Sha256); - void LoadCommunityIconFinish(const char *pCommunityId, CImageInfo &&Info, SHA256_DIGEST &&Sha256); + void LoadCommunityIconFinish(const char *pCommunityId, CImageInfo &Info, const SHA256_DIGEST &Sha256); void RenderCommunityIcon(const SCommunityIcon *pIcon, CUIRect Rect, bool Active); void UpdateCommunityIcons(); @@ -576,9 +593,13 @@ class CMenus : public CComponent bool RenderLanguageSelection(CUIRect MainView); void RenderThemeSelection(CUIRect MainView); void RenderSettingsGeneral(CUIRect MainView); - int RenderSettingsPlayer(CUIRect MainView); + void RenderSettingsPlayer(CUIRect MainView); void RenderSettingsDummyPlayer(CUIRect MainView); void RenderSettingsTee(CUIRect MainView); + void RenderSettingsTee7(CUIRect MainView); + void RenderSettingsTeeCustom7(CUIRect MainView); + void RenderSkinSelection7(CUIRect MainView); + void RenderSkinPartSelection7(CUIRect MainView); void RenderSettingsControls(CUIRect MainView); void ResetSettingsControls(); void RenderSettingsGraphics(CUIRect MainView); @@ -586,6 +607,36 @@ class CMenus : public CComponent void RenderSettings(CUIRect MainView); void RenderSettingsCustom(CUIRect MainView); + class CMapListItem + { + public: + char m_aFilename[IO_MAX_PATH_LENGTH]; + bool m_IsDirectory; + }; + class CPopupMapPickerContext + { + public: + std::vector m_vMaps; + char m_aCurrentMapFolder[IO_MAX_PATH_LENGTH] = ""; + static int MapListFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser); + void MapListPopulate(); + CMenus *m_pMenus; + int m_Selection; + }; + + static bool CompareFilenameAscending(const CMapListItem Lhs, const CMapListItem Rhs) + { + if(str_comp(Lhs.m_aFilename, "..") == 0) + return true; + if(str_comp(Rhs.m_aFilename, "..") == 0) + return false; + if(Lhs.m_IsDirectory != Rhs.m_IsDirectory) + return Lhs.m_IsDirectory; + return str_comp_filenames(Lhs.m_aFilename, Rhs.m_aFilename) < 0; + } + + static CUi::EPopupMenuFunctionResult PopupMapPicker(void *pContext, CUIRect View, bool Active); + void SetNeedSendInfo(); void SetActive(bool Active); void UpdateColors(); @@ -594,13 +645,9 @@ class CMenus : public CComponent bool CheckHotKey(int Key) const; - class CMenuBackground *m_pBackground; - public: void RenderBackground(); - void SetMenuBackground(class CMenuBackground *pBackground) { m_pBackground = pBackground; } - static CMenusKeyBinder m_Binder; CMenus(); @@ -618,6 +665,7 @@ class CMenus : public CComponent virtual void OnStateChange(int NewState, int OldState) override; virtual void OnWindowResize() override; + virtual void OnRefreshSkins() override; virtual void OnReset() override; virtual void OnRender() override; virtual bool OnInput(const IInput::CEvent &Event) override; @@ -630,16 +678,17 @@ class CMenus : public CComponent PAGE_GAME, PAGE_PLAYERS, PAGE_SERVER_INFO, - PAGE_STATS, PAGE_CALLVOTE, PAGE_INTERNET, PAGE_LAN, PAGE_FAVORITES, - PAGE_DDNET_LEGACY, // removed, redirects to PAGE_INTERNET - PAGE_KOG_LEGACY, // removed, redirects to PAGE_INTERNET //fuckers ;c + PAGE_FAVORITE_COMMUNITY_1, + PAGE_FAVORITE_COMMUNITY_2, + PAGE_FAVORITE_COMMUNITY_3, + PAGE_FAVORITE_COMMUNITY_4, + PAGE_FAVORITE_COMMUNITY_5, PAGE_DEMOS, PAGE_SETTINGS, - PAGE_SYSTEM, PAGE_NETWORK, PAGE_GHOST, @@ -655,8 +704,6 @@ class CMenus : public CComponent SETTINGS_SOUND, SETTINGS_DDNET, SETTINGS_ASSETS, - SETTINGS_STA, - SETTINGS_PROFILES, SETTINGS_LENGTH, @@ -664,7 +711,11 @@ class CMenus : public CComponent BIG_TAB_INTERNET, BIG_TAB_LAN, BIG_TAB_FAVORITES, - BIG_TAB_STATS, + BIT_TAB_FAVORITE_COMMUNITY_1, + BIT_TAB_FAVORITE_COMMUNITY_2, + BIT_TAB_FAVORITE_COMMUNITY_3, + BIT_TAB_FAVORITE_COMMUNITY_4, + BIT_TAB_FAVORITE_COMMUNITY_5, BIG_TAB_DEMOS, BIG_TAB_LENGTH, @@ -687,7 +738,7 @@ class CMenus : public CComponent SUIAnimator m_aAnimatorsSettingsTab[SETTINGS_LENGTH]; // DDRace - int DoButton_CheckBox_Tristate(const void *pID, const char *pText, TRISTATE Checked, const CUIRect *pRect); + int DoButton_CheckBox_Tristate(const void *pId, const char *pText, TRISTATE Checked, const CUIRect *pRect); std::vector m_vDemos; std::vector m_vpFilteredDemos; void DemolistPopulate(); @@ -718,6 +769,14 @@ class CMenus : public CComponent bool HasFile() const { return m_aFilename[0]; } }; + enum + { + GHOST_SORT_NONE = -1, + GHOST_SORT_NAME, + GHOST_SORT_TIME, + GHOST_SORT_DATE, + }; + std::vector m_vGhosts; std::chrono::nanoseconds m_GhostPopulateStartTime{0}; @@ -726,8 +785,8 @@ class CMenus : public CComponent CGhostItem *GetOwnGhost(); void UpdateOwnGhost(CGhostItem Item); void DeleteGhostItem(int Index); + void SortGhostlist(); - int GetCurPopup() const { return m_Popup; } bool CanDisplayWarning() const; void PopupWarning(const char *pTopic, const char *pBody, const char *pButton, std::chrono::nanoseconds Duration); @@ -744,7 +803,6 @@ class CMenus : public CComponent POPUP_CONFIRM, // generic confirmation popup (two buttons) POPUP_FIRST_LAUNCH, POPUP_POINTS, - POPUP_CONNECTING, POPUP_DISCONNECTED, POPUP_LANGUAGE, POPUP_RENAME_DEMO, @@ -752,7 +810,9 @@ class CMenus : public CComponent POPUP_RENDER_DONE, POPUP_PASSWORD, POPUP_QUIT, + POPUP_RESTART, POPUP_WARNING, + POPUP_SAVE_SKIN, // demo player states DEMOPLAYER_NONE = 0, @@ -762,20 +822,17 @@ class CMenus : public CComponent private: static int GhostlistFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser); void SetMenuPage(int NewPage); - void RefreshBrowserTab(int UiPage); + void RefreshBrowserTab(bool Force); // found in menus_ingame.cpp void RenderInGameNetwork(CUIRect MainView); void RenderGhost(CUIRect MainView); // found in menus_settings.cpp - void RenderSettingsProfiles(CUIRect MainView); void RenderSettingsDDNet(CUIRect MainView); void RenderSettingsAppearance(CUIRect MainView); - void RenderSettingsStA(CUIRect MainView); - ColorHSLA RenderHSLScrollbars(CUIRect *pRect, unsigned int *pColor, bool Alpha = false, bool ClampedLight = false); + bool RenderHslaScrollbars(CUIRect *pRect, unsigned int *pColor, bool Alpha, float DarkestLight); CServerProcess m_ServerProcess; - void RotateLabel(CUIRect rect, void *pVoid, const char *str, float d, ETextAlignment alignment, SLabelProperties properties, int i, void *pVoid1, float d1); }; #endif diff --git a/src/game/client/components/menus_browser.cpp b/src/game/client/components/menus_browser.cpp index 34d0250a11..ac5e3328be 100644 --- a/src/game/client/components/menus_browser.cpp +++ b/src/game/client/components/menus_browser.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -30,10 +31,10 @@ static void FormatServerbrowserPing(char (&aBuffer)[N], const CServerInfo *pInfo { if(!pInfo->m_LatencyIsEstimated) { - str_from_int(pInfo->m_Latency, aBuffer); + str_format(aBuffer, sizeof(aBuffer), "%d", pInfo->m_Latency); return; } - static const char *LOCATION_NAMES[CServerInfo::NUM_LOCS] = { + static const char *const LOCATION_NAMES[CServerInfo::NUM_LOCS] = { "", // LOC_UNKNOWN Localizable("AFR"), // LOC_AFRICA Localizable("ASI"), // LOC_ASIA @@ -55,11 +56,18 @@ static ColorRGBA GetPingTextColor(int Latency) static ColorRGBA GetGametypeTextColor(const char *pGametype) { ColorHSLA HslaColor; - if(str_comp(pGametype, "DM") == 0 || str_comp(pGametype, "TDM") == 0 || str_comp(pGametype, "CTF") == 0) + if(str_comp(pGametype, "DM") == 0 || str_comp(pGametype, "TDM") == 0 || str_comp(pGametype, "CTF") == 0 || str_comp(pGametype, "LMS") == 0 || str_comp(pGametype, "LTS") == 0) HslaColor = ColorHSLA(0.33f, 1.0f, 0.75f); else if(str_find_nocase(pGametype, "catch")) HslaColor = ColorHSLA(0.17f, 1.0f, 0.75f); - else if(str_find_nocase(pGametype, "idm") || str_find_nocase(pGametype, "itdm") || str_find_nocase(pGametype, "ictf") || str_find_nocase(pGametype, "f-ddrace")) + else if(str_find_nocase(pGametype, "dm") || str_find_nocase(pGametype, "tdm") || str_find_nocase(pGametype, "ctf") || str_find_nocase(pGametype, "lms") || str_find_nocase(pGametype, "lts")) + { + if(pGametype[0] == 'i' || pGametype[0] == 'g') + HslaColor = ColorHSLA(0.0f, 1.0f, 0.75f); + else + HslaColor = ColorHSLA(0.40f, 1.0f, 0.75f); + } + else if(str_find_nocase(pGametype, "f-ddrace") || str_find_nocase(pGametype, "freeze")) HslaColor = ColorHSLA(0.0f, 1.0f, 0.75f); else if(str_find_nocase(pGametype, "fng")) HslaColor = ColorHSLA(0.83f, 1.0f, 0.75f); @@ -92,7 +100,7 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct struct SColumn { - int m_ID; + int m_Id; int m_Sort; const char *m_pCaption; int m_Direction; @@ -108,6 +116,7 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct COL_NAME, COL_GAMETYPE, COL_MAP, + COL_FRIENDS, COL_PLAYERS, COL_PING, @@ -135,7 +144,8 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct {COL_NAME, IServerBrowser::SORT_NAME, Localizable("Name"), 0, 50.0f, {0}}, {COL_GAMETYPE, IServerBrowser::SORT_GAMETYPE, Localizable("Type"), 1, 50.0f, {0}}, {COL_MAP, IServerBrowser::SORT_MAP, Localizable("Map"), 1, 120.0f + (Headers.w - 480) / 8, {0}}, - {COL_PLAYERS, IServerBrowser::SORT_NUMPLAYERS, Localizable("Players"), 1, 85.0f, {0}}, + {COL_FRIENDS, IServerBrowser::SORT_NUMFRIENDS, "", 1, 20.0f, {0}}, + {COL_PLAYERS, IServerBrowser::SORT_NUMPLAYERS, Localizable("Players"), 1, 60.0f, {0}}, {-1, -1, "", 1, 4.0f, {0}}, {COL_PING, IServerBrowser::SORT_PING, Localizable("Ping"), 1, 40.0f, {0}}, }; @@ -180,7 +190,7 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct if(PlayersOrPing && g_Config.m_BrSortOrder == 2 && (Col.m_Sort == IServerBrowser::SORT_NUMPLAYERS || Col.m_Sort == IServerBrowser::SORT_PING)) Checked = 2; - if(DoButton_GridHeader(&Col.m_ID, Localize(Col.m_pCaption), Checked, &Col.m_Rect)) + if(DoButton_GridHeader(&Col.m_Id, Localize(Col.m_pCaption), Checked, &Col.m_Rect)) { if(Col.m_Sort != -1) { @@ -191,6 +201,15 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct g_Config.m_BrSort = Col.m_Sort; } } + + if(Col.m_Id == COL_FRIENDS) + { + TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); + Ui()->DoLabel(&Col.m_Rect, FONT_ICON_HEART, 14.0f, TEXTALIGN_MC); + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); + } } const int NumServers = ServerBrowser()->NumSortedServers(); @@ -199,14 +218,39 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct // users misses it { if(!ServerBrowser()->NumServers() && ServerBrowser()->IsGettingServerlist()) - UI()->DoLabel(&View, Localize("Getting server list from master server"), 16.0f, TEXTALIGN_MC); + { + Ui()->DoLabel(&View, Localize("Getting server list from master server"), 16.0f, TEXTALIGN_MC); + } else if(!ServerBrowser()->NumServers()) - UI()->DoLabel(&View, Localize("No servers found"), 16.0f, TEXTALIGN_MC); + { + if(ServerBrowser()->GetCurrentType() == IServerBrowser::TYPE_LAN) + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), Localize("No local servers found (ports %d-%d)"), IServerBrowser::LAN_PORT_BEGIN, IServerBrowser::LAN_PORT_END); + Ui()->DoLabel(&View, aBuf, 16.0f, TEXTALIGN_MC); + } + else + { + Ui()->DoLabel(&View, Localize("No servers found"), 16.0f, TEXTALIGN_MC); + } + } else if(ServerBrowser()->NumServers() && !NumServers) - UI()->DoLabel(&View, Localize("No servers match your filter criteria"), 16.0f, TEXTALIGN_MC); + { + CUIRect Label, ResetButton; + View.HMargin((View.h - (16.0f + 18.0f + 8.0f)) / 2.0f, &Label); + Label.HSplitTop(16.0f, &Label, &ResetButton); + ResetButton.HSplitTop(8.0f, nullptr, &ResetButton); + ResetButton.VMargin((ResetButton.w - 200.0f) / 2.0f, &ResetButton); + Ui()->DoLabel(&Label, Localize("No servers match your filter criteria"), 16.0f, TEXTALIGN_MC); + static CButtonContainer s_ResetButton; + if(DoButton_Menu(&s_ResetButton, Localize("Reset filter"), 0, &ResetButton)) + { + ResetServerbrowserFilters(); + } + } } - s_ListBox.SetActive(!UI()->IsPopupOpen()); + s_ListBox.SetActive(!Ui()->IsPopupOpen()); s_ListBox.DoStart(ms_ListheaderHeight, NumServers, 1, 3, -1, &View, false); if(m_ServerBrowserShouldRevealSelection) @@ -222,7 +266,7 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); TextRender()->TextColor(TextColor); TextRender()->TextOutlineColor(TextOutlineColor); - UI()->DoLabelStreamed(UIRect, pRect, pText, FontSize, TextAlign); + Ui()->DoLabelStreamed(UIRect, pRect, pText, FontSize, TextAlign); TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor()); TextRender()->TextColor(TextRender()->DefaultTextColor()); TextRender()->SetRenderFlags(0); @@ -240,7 +284,7 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct if(vpServerBrowserUiElements[i] == nullptr) { - vpServerBrowserUiElements[i] = UI()->GetNewUIElement(NUM_UI_ELEMS); + vpServerBrowserUiElements[i] = Ui()->GetNewUIElement(NUM_UI_ELEMS); } CUIElement *pUiElement = vpServerBrowserUiElements[i]; @@ -251,8 +295,8 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct if(!ListItem.m_Visible) { // reset active item, if not visible - if(UI()->CheckActiveItem(pItem)) - UI()->SetActiveItem(nullptr); + if(Ui()->CheckActiveItem(pItem)) + Ui()->SetActiveItem(nullptr); // don't render invisible items continue; @@ -268,22 +312,22 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct Button.h = ListItem.m_Rect.h; Button.w = Col.m_Rect.w; - const int ID = Col.m_ID; - if(ID == COL_FLAG_LOCK) + const int Id = Col.m_Id; + if(Id == COL_FLAG_LOCK) { if(pItem->m_Flags & SERVER_FLAG_PASSWORD) { RenderBrowserIcons(*pUiElement->Rect(UI_ELEM_LOCK_ICON), &Button, ColorRGBA(0.75f, 0.75f, 0.75f, 1.0f), TextRender()->DefaultTextOutlineColor(), FONT_ICON_LOCK, TEXTALIGN_MC); } } - else if(ID == COL_FLAG_FAV) + else if(Id == COL_FLAG_FAV) { if(pItem->m_Favorite != TRISTATE::NONE) { - RenderBrowserIcons(*pUiElement->Rect(UI_ELEM_FAVORITE_ICON), &Button, ColorRGBA(0.94f, 0.4f, 0.4f, 1.0f), TextRender()->DefaultTextOutlineColor(), FONT_ICON_HEART, TEXTALIGN_MC); + RenderBrowserIcons(*pUiElement->Rect(UI_ELEM_FAVORITE_ICON), &Button, ColorRGBA(1.0f, 0.85f, 0.3f, 1.0f), TextRender()->DefaultTextOutlineColor(), FONT_ICON_STAR, TEXTALIGN_MC); } } - else if(ID == COL_COMMUNITY) + else if(Id == COL_COMMUNITY) { if(pCommunity != nullptr) { @@ -293,12 +337,12 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct CUIRect CommunityIcon; Button.Margin(2.0f, &CommunityIcon); RenderCommunityIcon(pIcon, CommunityIcon, true); - UI()->DoButtonLogic(&pItem->m_aCommunityId, 0, &CommunityIcon); + Ui()->DoButtonLogic(&pItem->m_aCommunityId, 0, &CommunityIcon); GameClient()->m_Tooltips.DoToolTip(&pItem->m_aCommunityId, &CommunityIcon, pCommunity->Name()); } } } - else if(ID == COL_NAME) + else if(Id == COL_NAME) { SLabelProperties Props; Props.m_MaxWidth = Button.w; @@ -307,16 +351,16 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct bool Printed = false; if(g_Config.m_BrFilterString[0] && (pItem->m_QuickSearchHit & IServerBrowser::QUICK_SERVERNAME)) Printed = PrintHighlighted(pItem->m_aName, [&](const char *pFilteredStr, const int FilterLen) { - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_NAME_1), &Button, pItem->m_aName, FontSize, TEXTALIGN_ML, Props, (int)(pFilteredStr - pItem->m_aName)); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_NAME_1), &Button, pItem->m_aName, FontSize, TEXTALIGN_ML, Props, (int)(pFilteredStr - pItem->m_aName)); TextRender()->TextColor(gs_HighlightedTextColor); - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_NAME_2), &Button, pFilteredStr, FontSize, TEXTALIGN_ML, Props, FilterLen, &pUiElement->Rect(UI_ELEM_NAME_1)->m_Cursor); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_NAME_2), &Button, pFilteredStr, FontSize, TEXTALIGN_ML, Props, FilterLen, &pUiElement->Rect(UI_ELEM_NAME_1)->m_Cursor); TextRender()->TextColor(TextRender()->DefaultTextColor()); - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_NAME_3), &Button, pFilteredStr + FilterLen, FontSize, TEXTALIGN_ML, Props, -1, &pUiElement->Rect(UI_ELEM_NAME_2)->m_Cursor); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_NAME_3), &Button, pFilteredStr + FilterLen, FontSize, TEXTALIGN_ML, Props, -1, &pUiElement->Rect(UI_ELEM_NAME_2)->m_Cursor); }); if(!Printed) - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_NAME_1), &Button, pItem->m_aName, FontSize, TEXTALIGN_ML, Props); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_NAME_1), &Button, pItem->m_aName, FontSize, TEXTALIGN_ML, Props); } - else if(ID == COL_GAMETYPE) + else if(Id == COL_GAMETYPE) { SLabelProperties Props; Props.m_MaxWidth = Button.w; @@ -326,10 +370,10 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct { TextRender()->TextColor(GetGametypeTextColor(pItem->m_aGameType)); } - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_GAMETYPE), &Button, pItem->m_aGameType, FontSize, TEXTALIGN_ML, Props); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_GAMETYPE), &Button, pItem->m_aGameType, FontSize, TEXTALIGN_ML, Props); TextRender()->TextColor(TextRender()->DefaultTextColor()); } - else if(ID == COL_MAP) + else if(Id == COL_MAP) { { CUIRect Icon; @@ -349,43 +393,41 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct bool Printed = false; if(g_Config.m_BrFilterString[0] && (pItem->m_QuickSearchHit & IServerBrowser::QUICK_MAPNAME)) Printed = PrintHighlighted(pItem->m_aMap, [&](const char *pFilteredStr, const int FilterLen) { - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_MAP_1), &Button, pItem->m_aMap, FontSize, TEXTALIGN_ML, Props, (int)(pFilteredStr - pItem->m_aMap)); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_MAP_1), &Button, pItem->m_aMap, FontSize, TEXTALIGN_ML, Props, (int)(pFilteredStr - pItem->m_aMap)); TextRender()->TextColor(gs_HighlightedTextColor); - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_MAP_2), &Button, pFilteredStr, FontSize, TEXTALIGN_ML, Props, FilterLen, &pUiElement->Rect(UI_ELEM_MAP_1)->m_Cursor); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_MAP_2), &Button, pFilteredStr, FontSize, TEXTALIGN_ML, Props, FilterLen, &pUiElement->Rect(UI_ELEM_MAP_1)->m_Cursor); TextRender()->TextColor(TextRender()->DefaultTextColor()); - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_MAP_3), &Button, pFilteredStr + FilterLen, FontSize, TEXTALIGN_ML, Props, -1, &pUiElement->Rect(UI_ELEM_MAP_2)->m_Cursor); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_MAP_3), &Button, pFilteredStr + FilterLen, FontSize, TEXTALIGN_ML, Props, -1, &pUiElement->Rect(UI_ELEM_MAP_2)->m_Cursor); }); if(!Printed) - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_MAP_1), &Button, pItem->m_aMap, FontSize, TEXTALIGN_ML, Props); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_MAP_1), &Button, pItem->m_aMap, FontSize, TEXTALIGN_ML, Props); } - else if(ID == COL_PLAYERS) + else if(Id == COL_FRIENDS) { - Button.VMargin(2.0f, &Button); if(pItem->m_FriendState != IFriends::FRIEND_NO) { - CUIRect Icon; - Button.VSplitRight(50.0f, &Icon, &Button); - Icon.Margin(2.0f, &Icon); - RenderBrowserIcons(*pUiElement->Rect(UI_ELEM_FRIEND_ICON), &Icon, ColorRGBA(0.94f, 0.4f, 0.4f, 1.0f), TextRender()->DefaultTextOutlineColor(), FONT_ICON_HEART, TEXTALIGN_MC); + RenderBrowserIcons(*pUiElement->Rect(UI_ELEM_FRIEND_ICON), &Button, ColorRGBA(0.94f, 0.4f, 0.4f, 1.0f), TextRender()->DefaultTextOutlineColor(), FONT_ICON_HEART, TEXTALIGN_MC); if(pItem->m_FriendNum > 1) { - str_from_int(pItem->m_FriendNum, aTemp); + str_format(aTemp, sizeof(aTemp), "%d", pItem->m_FriendNum); TextRender()->TextColor(0.94f, 0.8f, 0.8f, 1.0f); - UI()->DoLabel(&Icon, aTemp, 9.0f, TEXTALIGN_MC); + Ui()->DoLabel(&Button, aTemp, 9.0f, TEXTALIGN_MC); TextRender()->TextColor(TextRender()->DefaultTextColor()); } } - + } + else if(Id == COL_PLAYERS) + { str_format(aTemp, sizeof(aTemp), "%i/%i", pItem->m_NumFilteredPlayers, ServerBrowser()->Max(*pItem)); if(g_Config.m_BrFilterString[0] && (pItem->m_QuickSearchHit & IServerBrowser::QUICK_PLAYER)) { TextRender()->TextColor(gs_HighlightedTextColor); } - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_PLAYERS), &Button, aTemp, FontSize, TEXTALIGN_MR); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_PLAYERS), &Button, aTemp, FontSize, TEXTALIGN_MR); TextRender()->TextColor(TextRender()->DefaultTextColor()); } - else if(ID == COL_PING) + else if(Id == COL_PING) { Button.VMargin(4.0f, &Button); FormatServerbrowserPing(aTemp, pItem); @@ -393,7 +435,7 @@ void CMenus::RenderServerbrowserServerList(CUIRect View, bool &WasListboxItemAct { TextRender()->TextColor(GetPingTextColor(pItem->m_Latency)); } - UI()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_PING), &Button, aTemp, FontSize, TEXTALIGN_MR); + Ui()->DoLabelStreamed(*pUiElement->Rect(UI_ELEM_PING), &Button, aTemp, FontSize, TEXTALIGN_MR); TextRender()->TextColor(TextRender()->DefaultTextColor()); } } @@ -464,7 +506,7 @@ void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItem { TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - UI()->DoLabel(&QuickSearch, FONT_ICON_MAGNIFYING_GLASS, 16.0f, TEXTALIGN_ML); + Ui()->DoLabel(&QuickSearch, FONT_ICON_MAGNIFYING_GLASS, 16.0f, TEXTALIGN_ML); TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); QuickSearch.VSplitLeft(ExcludeSearchIconMax, nullptr, &QuickSearch); @@ -472,17 +514,20 @@ void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItem char aBufSearch[64]; str_format(aBufSearch, sizeof(aBufSearch), "%s:", Localize("Search")); - UI()->DoLabel(&QuickSearch, aBufSearch, 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&QuickSearch, aBufSearch, 14.0f, TEXTALIGN_ML); QuickSearch.VSplitLeft(SearchExcludeAddrStrMax, nullptr, &QuickSearch); QuickSearch.VSplitLeft(5.0f, nullptr, &QuickSearch); static CLineInput s_FilterInput(g_Config.m_BrFilterString, sizeof(g_Config.m_BrFilterString)); - if(!UI()->IsPopupOpen() && Input()->KeyPress(KEY_F) && Input()->ModifierIsPressed()) + static char s_aTooltipText[64]; + str_format(s_aTooltipText, sizeof(s_aTooltipText), "%s: \"solo; nameless tee; kobra 2\"", Localize("Example of usage")); + GameClient()->m_Tooltips.DoToolTip(&s_FilterInput, &QuickSearch, s_aTooltipText); + if(!Ui()->IsPopupOpen() && Input()->KeyPress(KEY_F) && Input()->ModifierIsPressed()) { - UI()->SetActiveItem(&s_FilterInput); + Ui()->SetActiveItem(&s_FilterInput); s_FilterInput.SelectAll(); } - if(UI()->DoClearableEditBox(&s_FilterInput, &QuickSearch, 12.0f)) + if(Ui()->DoClearableEditBox(&s_FilterInput, &QuickSearch, 12.0f)) Client()->ServerBrowserUpdate(); } @@ -490,7 +535,7 @@ void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItem { TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - UI()->DoLabel(&QuickExclude, FONT_ICON_BAN, 16.0f, TEXTALIGN_ML); + Ui()->DoLabel(&QuickExclude, FONT_ICON_BAN, 16.0f, TEXTALIGN_ML); TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); QuickExclude.VSplitLeft(ExcludeSearchIconMax, nullptr, &QuickExclude); @@ -498,17 +543,20 @@ void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItem char aBufExclude[64]; str_format(aBufExclude, sizeof(aBufExclude), "%s:", Localize("Exclude")); - UI()->DoLabel(&QuickExclude, aBufExclude, 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&QuickExclude, aBufExclude, 14.0f, TEXTALIGN_ML); QuickExclude.VSplitLeft(SearchExcludeAddrStrMax, nullptr, &QuickExclude); QuickExclude.VSplitLeft(5.0f, nullptr, &QuickExclude); static CLineInput s_ExcludeInput(g_Config.m_BrExcludeString, sizeof(g_Config.m_BrExcludeString)); - if(!UI()->IsPopupOpen() && Input()->KeyPress(KEY_X) && Input()->ShiftIsPressed() && Input()->ModifierIsPressed()) + static char s_aTooltipText[64]; + str_format(s_aTooltipText, sizeof(s_aTooltipText), "%s: \"CHN; [A]\"", Localize("Example of usage")); + GameClient()->m_Tooltips.DoToolTip(&s_ExcludeInput, &QuickSearch, s_aTooltipText); + if(!Ui()->IsPopupOpen() && Input()->KeyPress(KEY_X) && Input()->ShiftIsPressed() && Input()->ModifierIsPressed()) { - UI()->SetActiveItem(&s_ExcludeInput); + Ui()->SetActiveItem(&s_ExcludeInput); s_ExcludeInput.SelectAll(); } - if(UI()->DoClearableEditBox(&s_ExcludeInput, &QuickExclude, 12.0f)) + if(Ui()->DoClearableEditBox(&s_ExcludeInput, &QuickExclude, 12.0f)) Client()->ServerBrowserUpdate(); } @@ -522,13 +570,13 @@ void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItem str_format(aBuf, sizeof(aBuf), Localize("%d of %d servers"), ServerBrowser()->NumSortedServers(), ServerBrowser()->NumServers()); else str_format(aBuf, sizeof(aBuf), Localize("%d of %d server"), ServerBrowser()->NumSortedServers(), ServerBrowser()->NumServers()); - UI()->DoLabel(&ServersOnline, aBuf, 12.0f, TEXTALIGN_MR); + Ui()->DoLabel(&ServersOnline, aBuf, 12.0f, TEXTALIGN_MR); if(ServerBrowser()->NumSortedPlayers() != 1) str_format(aBuf, sizeof(aBuf), Localize("%d players"), ServerBrowser()->NumSortedPlayers()); else str_format(aBuf, sizeof(aBuf), Localize("%d player"), ServerBrowser()->NumSortedPlayers()); - UI()->DoLabel(&PlayersOnline, aBuf, 12.0f, TEXTALIGN_MR); + Ui()->DoLabel(&PlayersOnline, aBuf, 12.0f, TEXTALIGN_MR); } // address info @@ -537,9 +585,9 @@ void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItem ServerAddr.Margin(2.0f, &ServerAddr); ServerAddr.VSplitLeft(SearchExcludeAddrStrMax + 5.0f + ExcludeSearchIconMax + 5.0f, &ServerAddrLabel, &ServerAddrEditBox); - UI()->DoLabel(&ServerAddrLabel, Localize("Server address:"), 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&ServerAddrLabel, Localize("Server address:"), 14.0f, TEXTALIGN_ML); static CLineInput s_ServerAddressInput(g_Config.m_UiServerAddress, sizeof(g_Config.m_UiServerAddress)); - if(UI()->DoClearableEditBox(&s_ServerAddressInput, &ServerAddrEditBox, 12.0f)) + if(Ui()->DoClearableEditBox(&s_ServerAddressInput, &ServerAddrEditBox, 12.0f)) m_ServerBrowserShouldRevealSelection = true; } @@ -564,9 +612,9 @@ void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItem Props.m_UseIconFont = true; static CButtonContainer s_RefreshButton; - if(UI()->DoButton_Menu(m_RefreshButton, &s_RefreshButton, RefreshLabelFunc, &ButtonRefresh, Props) || (!UI()->IsPopupOpen() && (Input()->KeyPress(KEY_F5) || (Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed())))) + if(Ui()->DoButton_Menu(m_RefreshButton, &s_RefreshButton, RefreshLabelFunc, &ButtonRefresh, Props) || (!Ui()->IsPopupOpen() && (Input()->KeyPress(KEY_F5) || (Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed())))) { - RefreshBrowserTab(g_Config.m_UiPage); + RefreshBrowserTab(true); } } @@ -579,7 +627,7 @@ void CMenus::RenderServerbrowserStatusBox(CUIRect StatusBox, bool WasListboxItem Props.m_Color = ColorRGBA(0.5f, 1.0f, 0.5f, 0.5f); static CButtonContainer s_ConnectButton; - if(UI()->DoButton_Menu(m_ConnectButton, &s_ConnectButton, ConnectLabelFunc, &ButtonConnect, Props) || WasListboxItemActivated || (!UI()->IsPopupOpen() && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + if(Ui()->DoButton_Menu(m_ConnectButton, &s_ConnectButton, ConnectLabelFunc, &ButtonConnect, Props) || WasListboxItemActivated || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) { Connect(g_Config.m_UiServerAddress); } @@ -606,7 +654,7 @@ void CMenus::PopupConfirmSwitchServer() void CMenus::RenderServerbrowserFilters(CUIRect View) { const float RowHeight = 18.0f; - const float FontSize = (RowHeight - 4.0f) * CUI::ms_FontmodHeight; // based on DoButton_CheckBox + const float FontSize = (RowHeight - 4.0f) * CUi::ms_FontmodHeight; // based on DoButton_CheckBox View.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), IGraphics::CORNER_B, 4.0f); View.Margin(5.0f, &View); @@ -635,26 +683,30 @@ void CMenus::RenderServerbrowserFilters(CUIRect View) if(DoButton_CheckBox(&g_Config.m_BrFilterPw, Localize("No password"), g_Config.m_BrFilterPw, &Button)) g_Config.m_BrFilterPw ^= 1; + View.HSplitTop(RowHeight, &Button, &View); + if(DoButton_CheckBox(&g_Config.m_BrFilterLogin, Localize("No login required"), g_Config.m_BrFilterLogin, &Button)) + g_Config.m_BrFilterLogin ^= 1; + View.HSplitTop(RowHeight, &Button, &View); if(DoButton_CheckBox(&g_Config.m_BrFilterGametypeStrict, Localize("Strict gametype filter"), g_Config.m_BrFilterGametypeStrict, &Button)) g_Config.m_BrFilterGametypeStrict ^= 1; View.HSplitTop(3.0f, nullptr, &View); View.HSplitTop(RowHeight, &Button, &View); - UI()->DoLabel(&Button, Localize("Game types:"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Game types:"), FontSize, TEXTALIGN_ML); Button.VSplitRight(60.0f, nullptr, &Button); static CLineInput s_GametypeInput(g_Config.m_BrFilterGametype, sizeof(g_Config.m_BrFilterGametype)); - if(UI()->DoEditBox(&s_GametypeInput, &Button, FontSize)) + if(Ui()->DoEditBox(&s_GametypeInput, &Button, FontSize)) Client()->ServerBrowserUpdate(); // server address View.HSplitTop(6.0f, nullptr, &View); View.HSplitTop(RowHeight, &Button, &View); View.HSplitTop(6.0f, nullptr, &View); - UI()->DoLabel(&Button, Localize("Server address:"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Server address:"), FontSize, TEXTALIGN_ML); Button.VSplitRight(60.0f, nullptr, &Button); static CLineInput s_FilterServerAddressInput(g_Config.m_BrFilterServerAddress, sizeof(g_Config.m_BrFilterServerAddress)); - if(UI()->DoEditBox(&s_FilterServerAddressInput, &Button, FontSize)) + if(Ui()->DoEditBox(&s_FilterServerAddressInput, &Button, FontSize)) Client()->ServerBrowserUpdate(); // player country @@ -668,16 +720,16 @@ void CMenus::RenderServerbrowserFilters(CUIRect View) const float OldWidth = Flag.w; Flag.w = Flag.h * 2.0f; Flag.x += (OldWidth - Flag.w) / 2.0f; - m_pClient->m_CountryFlags.Render(g_Config.m_BrFilterCountryIndex, ColorRGBA(1.0f, 1.0f, 1.0f, UI()->HotItem() == &g_Config.m_BrFilterCountryIndex ? 1.0f : g_Config.m_BrFilterCountry ? 0.9f : 0.5f), Flag.x, Flag.y, Flag.w, Flag.h); + m_pClient->m_CountryFlags.Render(g_Config.m_BrFilterCountryIndex, ColorRGBA(1.0f, 1.0f, 1.0f, Ui()->HotItem() == &g_Config.m_BrFilterCountryIndex ? 1.0f : g_Config.m_BrFilterCountry ? 0.9f : 0.5f), Flag.x, Flag.y, Flag.w, Flag.h); - if(UI()->DoButtonLogic(&g_Config.m_BrFilterCountryIndex, 0, &Flag)) + if(Ui()->DoButtonLogic(&g_Config.m_BrFilterCountryIndex, 0, &Flag)) { static SPopupMenuId s_PopupCountryId; static SPopupCountrySelectionContext s_PopupCountryContext; s_PopupCountryContext.m_pMenus = this; s_PopupCountryContext.m_Selection = g_Config.m_BrFilterCountryIndex; s_PopupCountryContext.m_New = true; - UI()->DoPopupMenu(&s_PopupCountryId, Flag.x, Flag.y + Flag.h, 490, 210, &s_PopupCountryContext, PopupCountrySelection); + Ui()->DoPopupMenu(&s_PopupCountryId, Flag.x, Flag.y + Flag.h, 490, 210, &s_PopupCountryContext, PopupCountrySelection); } } @@ -685,23 +737,8 @@ void CMenus::RenderServerbrowserFilters(CUIRect View) if(DoButton_CheckBox(&g_Config.m_BrFilterConnectingPlayers, Localize("Filter connecting players"), g_Config.m_BrFilterConnectingPlayers, &Button)) g_Config.m_BrFilterConnectingPlayers ^= 1; - // community filter - if((g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES) && !ServerBrowser()->Communities().empty()) - { - CUIRect Row; - View.HSplitTop(6.0f, nullptr, &View); - View.HSplitTop(19.0f, &Row, &View); - Row.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), IGraphics::CORNER_T, 4.0f); - UI()->DoLabel(&Row, Localize("Communities"), 12.0f, TEXTALIGN_MC); - - View.HSplitTop(4.0f * 17.0f + CScrollRegion::HEIGHT_MAGIC_FIX, &Row, &View); - View.HSplitTop(3.0f, nullptr, &View); - Row.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), IGraphics::CORNER_B, 4.0f); - RenderServerbrowserCommunitiesFilter(Row); - } - // map finish filters - if(m_CommunityCache.m_AnyRanksAvailable) + if(ServerBrowser()->CommunityCache().AnyRanksAvailable()) { View.HSplitTop(RowHeight, &Button, &View); if(DoButton_CheckBox(&g_Config.m_BrIndicateFinished, Localize("Indicate map finish"), g_Config.m_BrIndicateFinished, &Button)) @@ -723,7 +760,8 @@ void CMenus::RenderServerbrowserFilters(CUIRect View) } } - if(!m_CommunityCache.m_vpSelectableCountries.empty() || !m_CommunityCache.m_vpSelectableTypes.empty()) + // countries and types filters + if(ServerBrowser()->CommunityCache().CountriesTypesFilterAvailable()) { const ColorRGBA ColorActive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f); const ColorRGBA ColorInactive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f); @@ -767,26 +805,43 @@ void CMenus::RenderServerbrowserFilters(CUIRect View) static CButtonContainer s_ResetButton; if(DoButton_Menu(&s_ResetButton, Localize("Reset filter"), 0, &ResetButton)) { - g_Config.m_BrFilterString[0] = '\0'; - g_Config.m_BrExcludeString[0] = '\0'; - g_Config.m_BrFilterFull = 0; - g_Config.m_BrFilterEmpty = 0; - g_Config.m_BrFilterSpectators = 0; - g_Config.m_BrFilterFriends = 0; - g_Config.m_BrFilterCountry = 0; - g_Config.m_BrFilterCountryIndex = -1; - g_Config.m_BrFilterPw = 0; - g_Config.m_BrFilterGametype[0] = '\0'; - g_Config.m_BrFilterGametypeStrict = 0; - g_Config.m_BrFilterConnectingPlayers = 1; - g_Config.m_BrFilterUnfinishedMap = 0; - g_Config.m_BrFilterServerAddress[0] = '\0'; - ConfigManager()->Reset("br_filter_exclude_communities"); - ConfigManager()->Reset("br_filter_exclude_countries"); - ConfigManager()->Reset("br_filter_exclude_types"); - Client()->ServerBrowserUpdate(); + ResetServerbrowserFilters(); + } +} + +void CMenus::ResetServerbrowserFilters() +{ + g_Config.m_BrFilterString[0] = '\0'; + g_Config.m_BrExcludeString[0] = '\0'; + g_Config.m_BrFilterFull = 0; + g_Config.m_BrFilterEmpty = 0; + g_Config.m_BrFilterSpectators = 0; + g_Config.m_BrFilterFriends = 0; + g_Config.m_BrFilterCountry = 0; + g_Config.m_BrFilterCountryIndex = -1; + g_Config.m_BrFilterPw = 0; + g_Config.m_BrFilterGametype[0] = '\0'; + g_Config.m_BrFilterGametypeStrict = 0; + g_Config.m_BrFilterConnectingPlayers = 1; + g_Config.m_BrFilterServerAddress[0] = '\0'; + g_Config.m_BrFilterLogin = true; + + if(g_Config.m_UiPage != PAGE_LAN) + { + if(ServerBrowser()->CommunityCache().AnyRanksAvailable()) + { + g_Config.m_BrFilterUnfinishedMap = 0; + } + if(g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES) + { + ServerBrowser()->CommunitiesFilter().Clear(); + } + ServerBrowser()->CountriesFilter().Clear(); + ServerBrowser()->TypesFilter().Clear(); UpdateCommunityCache(true); } + + Client()->ServerBrowserUpdate(); } void CMenus::RenderServerbrowserDDNetFilter(CUIRect View, @@ -823,7 +878,7 @@ void CMenus::RenderServerbrowserDDNetFilter(CUIRect View, const char *pName = GetItemName(ItemIndex); const bool Active = !Filter.Filtered(pName); - const int Click = UI()->DoButtonLogic(pItemId, 0, &Item); + const int Click = Ui()->DoButtonLogic(pItemId, 0, &Item); if(Click == 1 || Click == 2) { // left/right click to toggle filter @@ -831,17 +886,23 @@ void CMenus::RenderServerbrowserDDNetFilter(CUIRect View, { if(Click == 1) { - // Left click: when all are active, only activate one + // Left click: when all are active, only activate one and none for(int j = 0; j < MaxItems; ++j) { - if(j != ItemIndex) - Filter.Add(GetItemName(j)); + if(const char *pItemName = GetItemName(j); + j != ItemIndex && + !((&Filter == &ServerBrowser()->CountriesFilter() && str_comp(pItemName, IServerBrowser::COMMUNITY_COUNTRY_NONE) == 0) || + (&Filter == &ServerBrowser()->TypesFilter() && str_comp(pItemName, IServerBrowser::COMMUNITY_TYPE_NONE) == 0))) + Filter.Add(pItemName); } } else if(Click == 2) { // Right click: when all are active, only deactivate one - Filter.Add(GetItemName(ItemIndex)); + if(MaxItems >= 2) + { + Filter.Add(GetItemName(ItemIndex)); + } } } else @@ -849,16 +910,23 @@ void CMenus::RenderServerbrowserDDNetFilter(CUIRect View, bool AllFilteredExceptUs = true; for(int j = 0; j < MaxItems; ++j) { - if(j != ItemIndex && !Filter.Filtered(GetItemName(j))) + if(const char *pItemName = GetItemName(j); + j != ItemIndex && !Filter.Filtered(pItemName) && + !((&Filter == &ServerBrowser()->CountriesFilter() && str_comp(pItemName, IServerBrowser::COMMUNITY_COUNTRY_NONE) == 0) || + (&Filter == &ServerBrowser()->TypesFilter() && str_comp(pItemName, IServerBrowser::COMMUNITY_TYPE_NONE) == 0))) { AllFilteredExceptUs = false; break; } } - // when last one is removed, reset (re-enable all) - if(AllFilteredExceptUs) + // When last one is removed, re-enable all currently selectable items. + // Don't use Clear, to avoid enabling also currently unselectable items. + if(AllFilteredExceptUs && Active) { - Filter.Clear(); + for(int j = 0; j < MaxItems; ++j) + { + Filter.Remove(GetItemName(j)); + } } else if(Active) { @@ -876,14 +944,17 @@ void CMenus::RenderServerbrowserDDNetFilter(CUIRect View, } else if(Click == 3) { - // middle click to reset (re-enable all) - Filter.Clear(); + // middle click to reset (re-enable all currently selectable items) + for(int j = 0; j < MaxItems; ++j) + { + Filter.Remove(GetItemName(j)); + } Client()->ServerBrowserUpdate(); if(UpdateCommunityCacheOnChange) UpdateCommunityCache(true); } - if(UI()->HotItem() == pItemId && !ScrollRegion.Animating()) + if(Ui()->HotItem() == pItemId && !ScrollRegion.Animating()) Item.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.33f), IGraphics::CORNER_ALL, 2.0f); RenderItem(ItemIndex, Item, pItemId, Active); } @@ -893,11 +964,18 @@ void CMenus::RenderServerbrowserDDNetFilter(CUIRect View, void CMenus::RenderServerbrowserCommunitiesFilter(CUIRect View) { + CUIRect Tab; + View.HSplitTop(19.0f, &Tab, &View); + Tab.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), IGraphics::CORNER_T, 4.0f); + Ui()->DoLabel(&Tab, Localize("Communities"), 12.0f, TEXTALIGN_MC); + View.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f), IGraphics::CORNER_B, 4.0f); + const int MaxEntries = ServerBrowser()->Communities().size(); const int EntriesPerRow = 1; static CScrollRegion s_ScrollRegion; static std::vector s_vItemIds; + static std::vector s_vFavoriteButtonIds; const float ItemHeight = 13.0f; const float Spacing = 2.0f; @@ -909,30 +987,46 @@ void CMenus::RenderServerbrowserCommunitiesFilter(CUIRect View) return ServerBrowser()->Communities()[ItemIndex].Name(); }; const auto &&RenderItem = [&](int ItemIndex, CUIRect Item, const void *pItemId, bool Active) { - const float Alpha = (Active ? 0.9f : 0.2f) + (UI()->HotItem() == pItemId ? 0.1f : 0.0f); + const float Alpha = (Active ? 0.9f : 0.2f) + (Ui()->HotItem() == pItemId ? 0.1f : 0.0f); - CUIRect Icon, Label; + CUIRect Icon, Label, FavoriteButton; + Item.VSplitRight(Item.h, &Item, &FavoriteButton); Item.Margin(Spacing, &Item); Item.VSplitLeft(Item.h * 2.0f, &Icon, &Label); Label.VSplitLeft(Spacing, nullptr, &Label); - const SCommunityIcon *pIcon = FindCommunityIcon(GetItemName(ItemIndex)); + const char *pItemName = GetItemName(ItemIndex); + const SCommunityIcon *pIcon = FindCommunityIcon(pItemName); if(pIcon != nullptr) { RenderCommunityIcon(pIcon, Icon, Active); } TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); - UI()->DoLabel(&Label, GetItemDisplayName(ItemIndex), Label.h * CUI::ms_FontmodHeight, TEXTALIGN_ML); + Ui()->DoLabel(&Label, GetItemDisplayName(ItemIndex), Label.h * CUi::ms_FontmodHeight, TEXTALIGN_ML); TextRender()->TextColor(TextRender()->DefaultTextColor()); + + const bool Favorite = ServerBrowser()->FavoriteCommunitiesFilter().Filtered(pItemName); + if(DoButton_Favorite(&s_vFavoriteButtonIds[ItemIndex], pItemId, Favorite, &FavoriteButton)) + { + if(Favorite) + { + ServerBrowser()->FavoriteCommunitiesFilter().Remove(pItemName); + } + else + { + ServerBrowser()->FavoriteCommunitiesFilter().Add(pItemName); + } + } }; + s_vFavoriteButtonIds.resize(MaxEntries); RenderServerbrowserDDNetFilter(View, ServerBrowser()->CommunitiesFilter(), ItemHeight + 2.0f * Spacing, MaxEntries, EntriesPerRow, s_ScrollRegion, s_vItemIds, true, GetItemName, RenderItem); } void CMenus::RenderServerbrowserCountriesFilter(CUIRect View) { - const int MaxEntries = m_CommunityCache.m_vpSelectableCountries.size(); + const int MaxEntries = ServerBrowser()->CommunityCache().SelectableCountries().size(); const int EntriesPerRow = MaxEntries > 8 ? 5 : 4; static CScrollRegion s_ScrollRegion; @@ -942,14 +1036,14 @@ void CMenus::RenderServerbrowserCountriesFilter(CUIRect View) const float Spacing = 2.0f; const auto &&GetItemName = [&](int ItemIndex) { - return m_CommunityCache.m_vpSelectableCountries[ItemIndex]->Name(); + return ServerBrowser()->CommunityCache().SelectableCountries()[ItemIndex]->Name(); }; const auto &&RenderItem = [&](int ItemIndex, CUIRect Item, const void *pItemId, bool Active) { Item.Margin(Spacing, &Item); const float OldWidth = Item.w; Item.w = Item.h * 2.0f; Item.x += (OldWidth - Item.w) / 2.0f; - m_pClient->m_CountryFlags.Render(m_CommunityCache.m_vpSelectableCountries[ItemIndex]->FlagId(), ColorRGBA(1.0f, 1.0f, 1.0f, (Active ? 0.9f : 0.2f) + (UI()->HotItem() == pItemId ? 0.1f : 0.0f)), Item.x, Item.y, Item.w, Item.h); + m_pClient->m_CountryFlags.Render(ServerBrowser()->CommunityCache().SelectableCountries()[ItemIndex]->FlagId(), ColorRGBA(1.0f, 1.0f, 1.0f, (Active ? 0.9f : 0.2f) + (Ui()->HotItem() == pItemId ? 0.1f : 0.0f)), Item.x, Item.y, Item.w, Item.h); }; RenderServerbrowserDDNetFilter(View, ServerBrowser()->CountriesFilter(), ItemHeight + 2.0f * Spacing, MaxEntries, EntriesPerRow, s_ScrollRegion, s_vItemIds, false, GetItemName, RenderItem); @@ -957,7 +1051,7 @@ void CMenus::RenderServerbrowserCountriesFilter(CUIRect View) void CMenus::RenderServerbrowserTypesFilter(CUIRect View) { - const int MaxEntries = m_CommunityCache.m_vpSelectableTypes.size(); + const int MaxEntries = ServerBrowser()->CommunityCache().SelectableTypes().size(); const int EntriesPerRow = 3; static CScrollRegion s_ScrollRegion; @@ -967,19 +1061,19 @@ void CMenus::RenderServerbrowserTypesFilter(CUIRect View) const float Spacing = 2.0f; const auto &&GetItemName = [&](int ItemIndex) { - return m_CommunityCache.m_vpSelectableTypes[ItemIndex]->Name(); + return ServerBrowser()->CommunityCache().SelectableTypes()[ItemIndex]->Name(); }; const auto &&RenderItem = [&](int ItemIndex, CUIRect Item, const void *pItemId, bool Active) { Item.Margin(Spacing, &Item); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, (Active ? 0.9f : 0.2f) + (UI()->HotItem() == pItemId ? 0.1f : 0.0f)); - UI()->DoLabel(&Item, GetItemName(ItemIndex), Item.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + TextRender()->TextColor(1.0f, 1.0f, 1.0f, (Active ? 0.9f : 0.2f) + (Ui()->HotItem() == pItemId ? 0.1f : 0.0f)); + Ui()->DoLabel(&Item, GetItemName(ItemIndex), Item.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); TextRender()->TextColor(TextRender()->DefaultTextColor()); }; RenderServerbrowserDDNetFilter(View, ServerBrowser()->TypesFilter(), ItemHeight + 2.0f * Spacing, MaxEntries, EntriesPerRow, s_ScrollRegion, s_vItemIds, false, GetItemName, RenderItem); } -CUI::EPopupMenuFunctionResult CMenus::PopupCountrySelection(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CMenus::PopupCountrySelection(void *pContext, CUIRect View, bool Active) { SPopupCountrySelectionContext *pPopupContext = static_cast(pContext); CMenus *pMenus = pPopupContext->m_pMenus; @@ -1011,7 +1105,7 @@ CUI::EPopupMenuFunctionResult CMenus::PopupCountrySelection(void *pContext, CUIR FlagRect.x += (OldWidth - FlagRect.w) / 2.0f; pMenus->m_pClient->m_CountryFlags.Render(pEntry->m_CountryCode, ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f), FlagRect.x, FlagRect.y, FlagRect.w, FlagRect.h); - pMenus->UI()->DoLabel(&Label, pEntry->m_aCountryCodeString, 10.0f, TEXTALIGN_MC); + pMenus->Ui()->DoLabel(&Label, pEntry->m_aCountryCodeString, 10.0f, TEXTALIGN_MC); } const int NewSelected = s_ListBox.DoEnd(); @@ -1021,10 +1115,10 @@ CUI::EPopupMenuFunctionResult CMenus::PopupCountrySelection(void *pContext, CUIR g_Config.m_BrFilterCountry = 1; g_Config.m_BrFilterCountryIndex = pPopupContext->m_Selection; pMenus->Client()->ServerBrowserUpdate(); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } void CMenus::RenderServerbrowserInfo(CUIRect View) @@ -1032,7 +1126,7 @@ void CMenus::RenderServerbrowserInfo(CUIRect View) const CServerInfo *pSelectedServer = ServerBrowser()->SortedGet(m_SelectedIndex); const float RowHeight = 18.0f; - const float FontSize = (RowHeight - 4.0f) * CUI::ms_FontmodHeight; // based on DoButton_CheckBox + const float FontSize = (RowHeight - 4.0f) * CUi::ms_FontmodHeight; // based on DoButton_CheckBox CUIRect ServerDetails, Scoreboard; View.HSplitTop(4.0f * 15.0f + RowHeight + 2.0f * 5.0f + 2.0f * 2.0f, &ServerDetails, &Scoreboard); @@ -1050,7 +1144,13 @@ void CMenus::RenderServerbrowserInfo(CUIRect View) if(DoButton_Menu(&s_CopyButton, Localize("Copy info"), 0, &Button)) { char aInfo[256]; - pSelectedServer->InfoToString(aInfo, sizeof(aInfo)); + str_format( + aInfo, + sizeof(aInfo), + "%s\n" + "Address: ddnet://%s\n", + pSelectedServer->m_aName, + pSelectedServer->m_aAddress); Input()->SetClipboardText(aInfo); } } @@ -1094,30 +1194,30 @@ void CMenus::RenderServerbrowserInfo(CUIRect View) ServerDetails.VSplitLeft(80.0f, &LeftColumn, &RightColumn); LeftColumn.HSplitTop(15.0f, &Row, &LeftColumn); - UI()->DoLabel(&Row, Localize("Version"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Row, Localize("Version"), FontSize, TEXTALIGN_ML); RightColumn.HSplitTop(15.0f, &Row, &RightColumn); - UI()->DoLabel(&Row, pSelectedServer->m_aVersion, FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Row, pSelectedServer->m_aVersion, FontSize, TEXTALIGN_ML); LeftColumn.HSplitTop(15.0f, &Row, &LeftColumn); - UI()->DoLabel(&Row, Localize("Game type"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Row, Localize("Game type"), FontSize, TEXTALIGN_ML); RightColumn.HSplitTop(15.0f, &Row, &RightColumn); - UI()->DoLabel(&Row, pSelectedServer->m_aGameType, FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Row, pSelectedServer->m_aGameType, FontSize, TEXTALIGN_ML); LeftColumn.HSplitTop(15.0f, &Row, &LeftColumn); - UI()->DoLabel(&Row, Localize("Ping"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Row, Localize("Ping"), FontSize, TEXTALIGN_ML); char aTemp[16]; FormatServerbrowserPing(aTemp, pSelectedServer); RightColumn.HSplitTop(15.0f, &Row, &RightColumn); - UI()->DoLabel(&Row, aTemp, FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Row, aTemp, FontSize, TEXTALIGN_ML); RenderServerbrowserInfoScoreboard(Scoreboard, pSelectedServer); } else { - UI()->DoLabel(&ServerDetails, Localize("No server selected"), FontSize, TEXTALIGN_MC); + Ui()->DoLabel(&ServerDetails, Localize("No server selected"), FontSize, TEXTALIGN_MC); } } @@ -1185,7 +1285,7 @@ void CMenus::RenderServerbrowserInfoScoreboard(CUIRect View, const CServerInfo * } else if(pSelectedServer->m_ClientScoreKind == CServerInfo::CLIENT_SCORE_KIND_POINTS) { - str_from_int(CurrentClient.m_Score, aTemp); + str_format(aTemp, sizeof(aTemp), "%d", CurrentClient.m_Score); } else { @@ -1214,7 +1314,7 @@ void CMenus::RenderServerbrowserInfoScoreboard(CUIRect View, const CServerInfo * } } - UI()->DoLabel(&Score, aTemp, FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Score, aTemp, FontSize, TEXTALIGN_ML); // render tee if available if(CurrentClient.m_aSkin[0] != '\0') @@ -1222,9 +1322,11 @@ void CMenus::RenderServerbrowserInfoScoreboard(CUIRect View, const CServerInfo * const CTeeRenderInfo TeeInfo = GetTeeRenderInfo(vec2(Skin.w, Skin.h), CurrentClient.m_aSkin, CurrentClient.m_CustomSkinColors, CurrentClient.m_CustomSkinColorBody, CurrentClient.m_CustomSkinColorFeet); const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); + CRenderTools::GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); const vec2 TeeRenderPos = vec2(Skin.x + TeeInfo.m_Size / 2.0f, Skin.y + Skin.h / 2.0f + OffsetToMid.y); RenderTools()->RenderTee(pIdleState, &TeeInfo, CurrentClient.m_Afk ? EMOTE_BLINK : EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos); + Ui()->DoButtonLogic(&CurrentClient.m_aSkin, 0, &Skin); + GameClient()->m_Tooltips.DoToolTip(&CurrentClient.m_aSkin, &Skin, CurrentClient.m_aSkin); } // name @@ -1345,12 +1447,12 @@ void CMenus::RenderServerbrowserFriends(CUIRect View) CUIRect Header, GroupIcon, GroupLabel; List.HSplitTop(ms_ListheaderHeight, &Header, &List); s_ScrollRegion.AddRect(Header); - Header.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, UI()->HotItem() == &s_aListExtended[FriendType] ? 0.4f : 0.25f), IGraphics::CORNER_ALL, 5.0f); + Header.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, Ui()->HotItem() == &s_aListExtended[FriendType] ? 0.4f : 0.25f), IGraphics::CORNER_ALL, 5.0f); Header.VSplitLeft(Header.h, &GroupIcon, &GroupLabel); GroupIcon.Margin(2.0f, &GroupIcon); TextRender()->SetFontPreset(EFontPreset::ICON_FONT); - TextRender()->TextColor(UI()->HotItem() == &s_aListExtended[FriendType] ? TextRender()->DefaultTextColor() : ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f)); - UI()->DoLabel(&GroupIcon, s_aListExtended[FriendType] ? FONT_ICON_SQUARE_MINUS : FONT_ICON_SQUARE_PLUS, GroupIcon.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + TextRender()->TextColor(Ui()->HotItem() == &s_aListExtended[FriendType] ? TextRender()->DefaultTextColor() : ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f)); + Ui()->DoLabel(&GroupIcon, s_aListExtended[FriendType] ? FONT_ICON_SQUARE_MINUS : FONT_ICON_SQUARE_PLUS, GroupIcon.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); TextRender()->TextColor(TextRender()->DefaultTextColor()); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); switch(FriendType) @@ -1368,8 +1470,8 @@ void CMenus::RenderServerbrowserFriends(CUIRect View) dbg_assert(false, "FriendType invalid"); break; } - UI()->DoLabel(&GroupLabel, aBuf, FontSize, TEXTALIGN_ML); - if(UI()->DoButtonLogic(&s_aListExtended[FriendType], 0, &Header)) + Ui()->DoLabel(&GroupLabel, aBuf, FontSize, TEXTALIGN_ML); + if(Ui()->DoButtonLogic(&s_aListExtended[FriendType], 0, &Header)) { s_aListExtended[FriendType] = !s_aListExtended[FriendType]; } @@ -1393,8 +1495,9 @@ void CMenus::RenderServerbrowserFriends(CUIRect View) if(s_ScrollRegion.RectClipped(Rect)) continue; - const bool Inside = UI()->HotItem() == Friend.ListItemId() || UI()->HotItem() == Friend.RemoveButtonId() || UI()->HotItem() == Friend.CommunityTooltipId(); - bool ButtonResult = UI()->DoButtonLogic(Friend.ListItemId(), 0, &Rect); + const bool Inside = Ui()->HotItem() == Friend.ListItemId() || Ui()->HotItem() == Friend.RemoveButtonId() || Ui()->HotItem() == Friend.CommunityTooltipId() || Ui()->HotItem() == Friend.SkinTooltipId(); + int ButtonResult = Ui()->DoButtonLogic(Friend.ListItemId(), 0, &Rect); + if(Friend.ServerInfo()) { GameClient()->m_Tooltips.DoToolTip(Friend.ListItemId(), &Rect, Localize("Click to select server. Double click to join your friend.")); @@ -1414,26 +1517,27 @@ void CMenus::RenderServerbrowserFriends(CUIRect View) Rect.HSplitTop(11.0f + 10.0f, &Rect, nullptr); // tee + CUIRect Skin; + Rect.VSplitLeft(Rect.h, &Skin, &Rect); + Rect.VSplitLeft(2.0f, nullptr, &Rect); if(Friend.Skin()[0] != '\0') { - CUIRect Skin; - Rect.VSplitLeft(Rect.h, &Skin, &Rect); - Rect.VSplitLeft(2.0f, nullptr, &Rect); - const CTeeRenderInfo TeeInfo = GetTeeRenderInfo(vec2(Skin.w, Skin.h), Friend.Skin(), Friend.CustomSkinColors(), Friend.CustomSkinColorBody(), Friend.CustomSkinColorFeet()); const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); + CRenderTools::GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); const vec2 TeeRenderPos = vec2(Skin.x + Skin.w / 2.0f, Skin.y + Skin.h * 0.55f + OffsetToMid.y); RenderTools()->RenderTee(pIdleState, &TeeInfo, Friend.IsAfk() ? EMOTE_BLINK : EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos); + Ui()->DoButtonLogic(Friend.SkinTooltipId(), 0, &Skin); + GameClient()->m_Tooltips.DoToolTip(Friend.SkinTooltipId(), &Skin, Friend.Skin()); } Rect.HSplitTop(11.0f, &NameLabel, &ClanLabel); // name - UI()->DoLabel(&NameLabel, Friend.Name(), FontSize - 1.0f, TEXTALIGN_ML); + Ui()->DoLabel(&NameLabel, Friend.Name(), FontSize - 1.0f, TEXTALIGN_ML); // clan - UI()->DoLabel(&ClanLabel, Friend.Clan(), FontSize - 2.0f, TEXTALIGN_ML); + Ui()->DoLabel(&ClanLabel, Friend.Clan(), FontSize - 2.0f, TEXTALIGN_ML); // server info if(Friend.ServerInfo()) @@ -1449,7 +1553,7 @@ void CMenus::RenderServerbrowserFriends(CUIRect View) InfoLabel.VSplitLeft(21.0f, &CommunityIcon, &InfoLabel); InfoLabel.VSplitLeft(2.0f, nullptr, &InfoLabel); RenderCommunityIcon(pIcon, CommunityIcon, true); - UI()->DoButtonLogic(Friend.CommunityTooltipId(), 0, &CommunityIcon); + Ui()->DoButtonLogic(Friend.CommunityTooltipId(), 0, &CommunityIcon); GameClient()->m_Tooltips.DoToolTip(Friend.CommunityTooltipId(), &CommunityIcon, pCommunity->Name()); } } @@ -1461,23 +1565,23 @@ void CMenus::RenderServerbrowserFriends(CUIRect View) str_format(aBuf, sizeof(aBuf), "%s | %s | %s", Friend.ServerInfo()->m_aMap, Friend.ServerInfo()->m_aGameType, aLatency); else str_format(aBuf, sizeof(aBuf), "%s | %s", Friend.ServerInfo()->m_aMap, Friend.ServerInfo()->m_aGameType); - UI()->DoLabel(&InfoLabel, aBuf, FontSize - 2.0f, TEXTALIGN_ML); + Ui()->DoLabel(&InfoLabel, aBuf, FontSize - 2.0f, TEXTALIGN_ML); } // remove button if(Inside) { - TextRender()->TextColor(UI()->HotItem() == Friend.RemoveButtonId() ? TextRender()->DefaultTextColor() : ColorRGBA(0.4f, 0.4f, 0.4f, 1.0f)); + TextRender()->TextColor(Ui()->HotItem() == Friend.RemoveButtonId() ? TextRender()->DefaultTextColor() : ColorRGBA(0.4f, 0.4f, 0.4f, 1.0f)); TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - UI()->DoLabel(&RemoveButton, FONT_ICON_TRASH, RemoveButton.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + Ui()->DoLabel(&RemoveButton, FONT_ICON_TRASH, RemoveButton.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); TextRender()->TextColor(TextRender()->DefaultTextColor()); - if(UI()->DoButtonLogic(Friend.RemoveButtonId(), 0, &RemoveButton)) + if(Ui()->DoButtonLogic(Friend.RemoveButtonId(), 0, &RemoveButton)) { m_pRemoveFriend = &Friend; - ButtonResult = false; + ButtonResult = 0; } GameClient()->m_Tooltips.DoToolTip(Friend.RemoveButtonId(), &RemoveButton, Friend.FriendState() == IFriends::FRIEND_PLAYER ? Localize("Click to remove this player from your friends list.") : Localize("Click to remove this clan from your friends list.")); } @@ -1487,7 +1591,7 @@ void CMenus::RenderServerbrowserFriends(CUIRect View) { str_copy(g_Config.m_UiServerAddress, Friend.ServerInfo()->m_aAddress); m_ServerBrowserShouldRevealSelection = true; - if(Input()->MouseDoubleClick()) + if(ButtonResult == 1 && Ui()->DoDoubleClickLogic(Friend.ListItemId())) { Connect(g_Config.m_UiServerAddress); } @@ -1499,7 +1603,7 @@ void CMenus::RenderServerbrowserFriends(CUIRect View) CUIRect Label; List.HSplitTop(12.0f, &Label, &List); s_ScrollRegion.AddRect(Label); - UI()->DoLabel(&Label, Localize("None"), Label.h * CUI::ms_FontmodHeight, TEXTALIGN_ML); + Ui()->DoLabel(&Label, Localize("None"), Label.h * CUi::ms_FontmodHeight, TEXTALIGN_ML); } } @@ -1529,18 +1633,18 @@ void CMenus::RenderServerbrowserFriends(CUIRect View) ServerFriends.HSplitTop(18.0f, &Button, &ServerFriends); str_format(aBuf, sizeof(aBuf), "%s:", Localize("Name")); - UI()->DoLabel(&Button, aBuf, FontSize + 2.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Button, aBuf, FontSize + 2.0f, TEXTALIGN_ML); Button.VSplitLeft(80.0f, nullptr, &Button); static CLineInputBuffered s_NameInput; - UI()->DoEditBox(&s_NameInput, &Button, FontSize + 2.0f); + Ui()->DoEditBox(&s_NameInput, &Button, FontSize + 2.0f); ServerFriends.HSplitTop(3.0f, nullptr, &ServerFriends); ServerFriends.HSplitTop(18.0f, &Button, &ServerFriends); str_format(aBuf, sizeof(aBuf), "%s:", Localize("Clan")); - UI()->DoLabel(&Button, aBuf, FontSize + 2.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Button, aBuf, FontSize + 2.0f, TEXTALIGN_ML); Button.VSplitLeft(80.0f, nullptr, &Button); static CLineInputBuffered s_ClanInput; - UI()->DoEditBox(&s_ClanInput, &Button, FontSize + 2.0f); + Ui()->DoEditBox(&s_ClanInput, &Button, FontSize + 2.0f); ServerFriends.HSplitTop(3.0f, nullptr, &ServerFriends); ServerFriends.HSplitTop(18.0f, &Button, &ServerFriends); @@ -1586,7 +1690,7 @@ void CMenus::RenderServerbrowserTabBar(CUIRect TabBar) const ColorRGBA ColorActive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f); const ColorRGBA ColorInactive = ColorRGBA(0.0f, 0.0f, 0.0f, 0.15f); - if(!UI()->IsPopupOpen() && UI()->ConsumeHotkey(CUI::HOTKEY_TAB)) + if(!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(CUi::HOTKEY_TAB)) { const int Direction = Input()->ShiftIsPressed() ? -1 : 1; g_Config.m_UiToolboxPage = (g_Config.m_UiToolboxPage + NUM_UI_TOOLBOX_PAGES + Direction) % NUM_UI_TOOLBOX_PAGES; @@ -1597,15 +1701,24 @@ void CMenus::RenderServerbrowserTabBar(CUIRect TabBar) static CButtonContainer s_FilterTabButton; if(DoButton_MenuTab(&s_FilterTabButton, FONT_ICON_LIST_UL, g_Config.m_UiToolboxPage == UI_TOOLBOX_PAGE_FILTERS, &FilterTabButton, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_BROWSER_FILTER], &ColorInactive, &ColorActive)) + { g_Config.m_UiToolboxPage = UI_TOOLBOX_PAGE_FILTERS; + } + GameClient()->m_Tooltips.DoToolTip(&s_FilterTabButton, &FilterTabButton, Localize("Server filter")); static CButtonContainer s_InfoTabButton; if(DoButton_MenuTab(&s_InfoTabButton, FONT_ICON_INFO, g_Config.m_UiToolboxPage == UI_TOOLBOX_PAGE_INFO, &InfoTabButton, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_BROWSER_INFO], &ColorInactive, &ColorActive)) + { g_Config.m_UiToolboxPage = UI_TOOLBOX_PAGE_INFO; + } + GameClient()->m_Tooltips.DoToolTip(&s_InfoTabButton, &InfoTabButton, Localize("Server info")); static CButtonContainer s_FriendsTabButton; if(DoButton_MenuTab(&s_FriendsTabButton, FONT_ICON_HEART, g_Config.m_UiToolboxPage == UI_TOOLBOX_PAGE_FRIENDS, &FriendsTabButton, IGraphics::CORNER_T, &m_aAnimatorsSmallPage[SMALL_TAB_BROWSER_FRIENDS], &ColorInactive, &ColorActive)) + { g_Config.m_UiToolboxPage = UI_TOOLBOX_PAGE_FRIENDS; + } + GameClient()->m_Tooltips.DoToolTip(&s_FriendsTabButton, &FriendsTabButton, Localize("Friends")); TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); @@ -1635,17 +1748,38 @@ void CMenus::RenderServerbrowserToolBox(CUIRect ToolBox) void CMenus::RenderServerbrowser(CUIRect MainView) { UpdateCommunityCache(false); - UpdateCommunityIcons(); + + switch(g_Config.m_UiPage) + { + case PAGE_INTERNET: + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_BROWSER_INTERNET); + break; + case PAGE_LAN: + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_BROWSER_LAN); + break; + case PAGE_FAVORITES: + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_BROWSER_FAVORITES); + break; + case PAGE_FAVORITE_COMMUNITY_1: + case PAGE_FAVORITE_COMMUNITY_2: + case PAGE_FAVORITE_COMMUNITY_3: + case PAGE_FAVORITE_COMMUNITY_4: + case PAGE_FAVORITE_COMMUNITY_5: + GameClient()->m_MenuBackground.ChangePosition(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1 + CMenuBackground::POS_BROWSER_CUSTOM0); + break; + default: + dbg_assert(false, "ui_page invalid for RenderServerbrowser"); + } /* - +-----------------+ +--tabs--+ - | | | | - | | | | - | server list | | tool | - | | | box | - | | | | - +-----------------+ | | - status box +--------+ + +---------------------------+ +---communities---+ + | | | | + | | +------tabs-------+ + | server list | | | + | | | tool | + | | | box | + +---------------------------+ | | + status box +-----------------+ */ CUIRect ServerList, StatusBox, ToolBox, TabBar; @@ -1653,6 +1787,15 @@ void CMenus::RenderServerbrowser(CUIRect MainView) MainView.Margin(10.0f, &MainView); MainView.VSplitRight(205.0f, &ServerList, &ToolBox); ServerList.VSplitRight(5.0f, &ServerList, nullptr); + + if((g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES) && !ServerBrowser()->Communities().empty()) + { + CUIRect CommunityFilter; + ToolBox.HSplitTop(19.0f + 4.0f * 17.0f + CScrollRegion::HEIGHT_MAGIC_FIX, &CommunityFilter, &ToolBox); + ToolBox.HSplitTop(8.0f, nullptr, &ToolBox); + RenderServerbrowserCommunitiesFilter(CommunityFilter); + } + ToolBox.HSplitTop(24.0f, &TabBar, &ToolBox); ServerList.HSplitBottom(65.0f, &ServerList, &StatusBox); @@ -1669,21 +1812,24 @@ bool CMenus::PrintHighlighted(const char *pName, F &&PrintFn) { const char *pStr = g_Config.m_BrFilterString; char aFilterStr[sizeof(g_Config.m_BrFilterString)]; + char aFilterStrTrimmed[sizeof(g_Config.m_BrFilterString)]; while((pStr = str_next_token(pStr, IServerBrowser::SEARCH_EXCLUDE_TOKEN, aFilterStr, sizeof(aFilterStr)))) { + str_copy(aFilterStrTrimmed, str_utf8_skip_whitespaces(aFilterStr)); + str_utf8_trim_right(aFilterStrTrimmed); // highlight the parts that matches const char *pFilteredStr; - int FilterLen = str_length(aFilterStr); - if(aFilterStr[0] == '"' && aFilterStr[FilterLen - 1] == '"') + int FilterLen = str_length(aFilterStrTrimmed); + if(aFilterStrTrimmed[0] == '"' && aFilterStrTrimmed[FilterLen - 1] == '"') { - aFilterStr[FilterLen - 1] = '\0'; - pFilteredStr = str_comp(pName, &aFilterStr[1]) == 0 ? pName : nullptr; + aFilterStrTrimmed[FilterLen - 1] = '\0'; + pFilteredStr = str_comp(pName, &aFilterStrTrimmed[1]) == 0 ? pName : nullptr; FilterLen -= 2; } else { const char *pFilteredStrEnd; - pFilteredStr = str_utf8_find_nocase(pName, aFilterStr, &pFilteredStrEnd); + pFilteredStr = str_utf8_find_nocase(pName, aFilterStrTrimmed, &pFilteredStrEnd); if(pFilteredStr != nullptr && pFilteredStrEnd != nullptr) FilterLen = pFilteredStrEnd - pFilteredStr; } @@ -1698,17 +1844,13 @@ bool CMenus::PrintHighlighted(const char *pName, F &&PrintFn) CTeeRenderInfo CMenus::GetTeeRenderInfo(vec2 Size, const char *pSkinName, bool CustomSkinColors, int CustomSkinColorBody, int CustomSkinColorFeet) const { - const CSkin *pSkin = m_pClient->m_Skins.Find(pSkinName); - CTeeRenderInfo TeeInfo; - TeeInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin; - TeeInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin; - TeeInfo.m_SkinMetrics = pSkin->m_Metrics; + TeeInfo.Apply(m_pClient->m_Skins.Find(pSkinName)); TeeInfo.m_CustomColoredSkin = CustomSkinColors; if(CustomSkinColors) { - TeeInfo.m_ColorBody = color_cast(ColorHSLA(CustomSkinColorBody).UnclampLighting()); - TeeInfo.m_ColorFeet = color_cast(ColorHSLA(CustomSkinColorFeet).UnclampLighting()); + TeeInfo.m_ColorBody = color_cast(ColorHSLA(CustomSkinColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT)); + TeeInfo.m_ColorFeet = color_cast(ColorHSLA(CustomSkinColorFeet).UnclampLighting(ColorHSLA::DARKEST_LGT)); } else { @@ -1741,57 +1883,44 @@ void CMenus::ConchainCommunitiesUpdate(IConsole::IResult *pResult, void *pUserDa { pfnCallback(pResult, pCallbackUserData); CMenus *pThis = static_cast(pUserData); - if(pResult->NumArguments() >= 1 && (g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES)) + if(pResult->NumArguments() >= 1 && (g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES || (g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5))) { - pThis->ServerBrowser()->CleanFilters(); pThis->UpdateCommunityCache(true); pThis->Client()->ServerBrowserUpdate(); } } -void CMenus::UpdateCommunityCache(bool Force) +void CMenus::ConchainUiPageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) { - const bool PageWithCommunities = g_Config.m_UiPage == PAGE_INTERNET || g_Config.m_UiPage == PAGE_FAVORITES; - if(!Force && m_CommunityCache.m_UpdateTime != 0 && m_CommunityCache.m_UpdateTime == ServerBrowser()->DDNetInfoUpdateTime() && m_CommunityCache.m_PageWithCommunities == PageWithCommunities) - return; - - m_CommunityCache.m_UpdateTime = ServerBrowser()->DDNetInfoUpdateTime(); - m_CommunityCache.m_PageWithCommunities = PageWithCommunities; - - if(m_CommunityCache.m_PageWithCommunities) - m_CommunityCache.m_vpSelectedCommunities = ServerBrowser()->SelectedCommunities(); - else - m_CommunityCache.m_vpSelectedCommunities.clear(); - - m_CommunityCache.m_vpSelectableCountries.clear(); - m_CommunityCache.m_vpSelectableTypes.clear(); - for(const CCommunity *pCommunity : m_CommunityCache.m_vpSelectedCommunities) + pfnCallback(pResult, pCallbackUserData); + CMenus *pThis = static_cast(pUserData); + if(pResult->NumArguments() >= 1) { - for(const auto &Country : pCommunity->Countries()) + if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 && + (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= pThis->ServerBrowser()->FavoriteCommunities().size()) { - const auto ExistingCountry = std::find_if(m_CommunityCache.m_vpSelectableCountries.begin(), m_CommunityCache.m_vpSelectableCountries.end(), [&](const CCommunityCountry *pOther) { - return str_comp(Country.Name(), pOther->Name()) == 0 && Country.FlagId() == pOther->FlagId(); - }); - if(ExistingCountry == m_CommunityCache.m_vpSelectableCountries.end()) - { - m_CommunityCache.m_vpSelectableCountries.push_back(&Country); - } - } - for(const auto &Type : pCommunity->Types()) - { - const auto ExistingType = std::find_if(m_CommunityCache.m_vpSelectableTypes.begin(), m_CommunityCache.m_vpSelectableTypes.end(), [&](const CCommunityType *pOther) { - return str_comp(Type.Name(), pOther->Name()) == 0; - }); - if(ExistingType == m_CommunityCache.m_vpSelectableTypes.end()) - { - m_CommunityCache.m_vpSelectableTypes.push_back(&Type); - } + // Reset page to internet when there is no favorite community for this page. + g_Config.m_UiPage = PAGE_INTERNET; } + + pThis->SetMenuPage(g_Config.m_UiPage); } +} - m_CommunityCache.m_AnyRanksAvailable = std::any_of(m_CommunityCache.m_vpSelectedCommunities.begin(), m_CommunityCache.m_vpSelectedCommunities.end(), [](const CCommunity *pCommunity) { - return pCommunity->HasRanks(); - }); +void CMenus::UpdateCommunityCache(bool Force) +{ + if(g_Config.m_UiPage >= PAGE_FAVORITE_COMMUNITY_1 && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 && + (size_t)(g_Config.m_UiPage - PAGE_FAVORITE_COMMUNITY_1) >= ServerBrowser()->FavoriteCommunities().size()) + { + // Reset page to internet when there is no favorite community for this page, + // i.e. when favorite community is removed via console while the page is open. + // This also updates the community cache because the page is changed. + SetMenuPage(PAGE_INTERNET); + } + else + { + ServerBrowser()->CommunityCache().Update(Force); + } } CMenus::CAbstractCommunityIconJob::CAbstractCommunityIconJob(CMenus *pMenus, const char *pCommunityId, int StorageType) : @@ -1802,25 +1931,6 @@ CMenus::CAbstractCommunityIconJob::CAbstractCommunityIconJob(CMenus *pMenus, con str_format(m_aPath, sizeof(m_aPath), "communityicons/%s.png", pCommunityId); } -CMenus::CAbstractCommunityIconJob::~CAbstractCommunityIconJob() -{ - free(m_ImageInfo.m_pData); - m_ImageInfo.m_pData = nullptr; -} - -int CMenus::CCommunityIconDownloadJob::OnCompletion(int State) -{ - State = CHttpRequest::OnCompletion(State); - if(State == HTTP_DONE) - { - if(m_pMenus->LoadCommunityIconFile(Dest(), IStorage::TYPE_SAVE, m_ImageInfo, m_Sha256)) - m_Success = true; - else - State = HTTP_ERROR; - } - return State; -} - CMenus::CCommunityIconDownloadJob::CCommunityIconDownloadJob(CMenus *pMenus, const char *pCommunityId, const char *pUrl, const SHA256_DIGEST &Sha256) : CHttpRequest(pUrl), CAbstractCommunityIconJob(pMenus, pCommunityId, IStorage::TYPE_SAVE) @@ -1839,6 +1949,12 @@ void CMenus::CCommunityIconLoadJob::Run() CMenus::CCommunityIconLoadJob::CCommunityIconLoadJob(CMenus *pMenus, const char *pCommunityId, int StorageType) : CAbstractCommunityIconJob(pMenus, pCommunityId, StorageType) { + Abortable(true); +} + +CMenus::CCommunityIconLoadJob::~CCommunityIconLoadJob() +{ + m_ImageInfo.Free(); } int CMenus::CommunityIconScan(const char *pName, int IsDir, int DirType, void *pUser) @@ -1857,7 +1973,7 @@ int CMenus::CommunityIconScan(const char *pName, int IsDir, int DirType, void *p return 0; } -const CMenus::SCommunityIcon *CMenus::FindCommunityIcon(const char *pCommunityId) +const SCommunityIcon *CMenus::FindCommunityIcon(const char *pCommunityId) { auto Icon = std::find_if(m_vCommunityIcons.begin(), m_vCommunityIcons.end(), [pCommunityId](const SCommunityIcon &Element) { return str_comp(Element.m_aCommunityId, pCommunityId) == 0; @@ -1868,7 +1984,7 @@ const CMenus::SCommunityIcon *CMenus::FindCommunityIcon(const char *pCommunityId bool CMenus::LoadCommunityIconFile(const char *pPath, int DirType, CImageInfo &Info, SHA256_DIGEST &Sha256) { char aError[IO_MAX_PATH_LENGTH + 128]; - if(!Graphics()->LoadPNG(&Info, pPath, DirType)) + if(!Graphics()->LoadPng(Info, pPath, DirType)) { str_format(aError, sizeof(aError), "Failed to load community icon from '%s'", pPath); Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "menus/browser", aError); @@ -1876,14 +1992,14 @@ bool CMenus::LoadCommunityIconFile(const char *pPath, int DirType, CImageInfo &I } if(Info.m_Format != CImageInfo::FORMAT_RGBA) { - Graphics()->FreePNG(&Info); + Info.Free(); str_format(aError, sizeof(aError), "Failed to load community icon from '%s': must be an RGBA image", pPath); Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "menus/browser", aError); return false; } if(!Storage()->CalculateHashes(pPath, DirType, &Sha256)) { - Graphics()->FreePNG(&Info); + Info.Free(); str_format(aError, sizeof(aError), "Failed to load community icon from '%s': could not calculate hash", pPath); Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "menus/browser", aError); return false; @@ -1891,25 +2007,15 @@ bool CMenus::LoadCommunityIconFile(const char *pPath, int DirType, CImageInfo &I return true; } -void CMenus::LoadCommunityIconFinish(const char *pCommunityId, CImageInfo &&Info, SHA256_DIGEST &&Sha256) +void CMenus::LoadCommunityIconFinish(const char *pCommunityId, CImageInfo &Info, const SHA256_DIGEST &Sha256) { SCommunityIcon CommunityIcon; str_copy(CommunityIcon.m_aCommunityId, pCommunityId); CommunityIcon.m_Sha256 = Sha256; - CommunityIcon.m_OrgTexture = Graphics()->LoadTextureRaw(Info.m_Width, Info.m_Height, Info.m_Format, Info.m_pData, 0); + CommunityIcon.m_OrgTexture = Graphics()->LoadTextureRaw(Info, 0, pCommunityId); - // create gray scale version - unsigned char *pData = static_cast(Info.m_pData); - const size_t Step = Info.PixelSize(); - for(int i = 0; i < Info.m_Width * Info.m_Height; i++) - { - int v = (pData[i * Step] + pData[i * Step + 1] + pData[i * Step + 2]) / 3; - pData[i * Step] = v; - pData[i * Step + 1] = v; - pData[i * Step + 2] = v; - } - CommunityIcon.m_GreyTexture = Graphics()->LoadTextureRaw(Info.m_Width, Info.m_Height, Info.m_Format, Info.m_pData, 0); - Graphics()->FreePNG(&Info); + ConvertToGrayscale(Info); + CommunityIcon.m_GreyTexture = Graphics()->LoadTextureRawMove(Info, 0, pCommunityId); auto ExistingIcon = std::find_if(m_vCommunityIcons.begin(), m_vCommunityIcons.end(), [pCommunityId](const SCommunityIcon &Element) { return str_comp(Element.m_aCommunityId, pCommunityId) == 0; @@ -1948,7 +2054,7 @@ void CMenus::UpdateCommunityIcons() if(!m_CommunityIconLoadJobs.empty()) { std::shared_ptr pJob = m_CommunityIconLoadJobs.front(); - if(pJob->Status() == IJob::STATE_DONE) + if(pJob->Done()) { if(pJob->Success()) LoadCommunityIconFinish(pJob->CommunityId(), pJob->ImageInfo(), pJob->Sha256()); @@ -1964,18 +2070,22 @@ void CMenus::UpdateCommunityIcons() if(!m_CommunityIconDownloadJobs.empty()) { std::shared_ptr pJob = m_CommunityIconDownloadJobs.front(); - if(pJob->Status() == IJob::STATE_DONE) + if(pJob->Done()) { - if(pJob->Success()) - LoadCommunityIconFinish(pJob->CommunityId(), pJob->ImageInfo(), pJob->Sha256()); + if(pJob->State() == EHttpState::DONE) + { + std::shared_ptr pLoadJob = std::make_shared(this, pJob->CommunityId(), IStorage::TYPE_SAVE); + Engine()->AddJob(pLoadJob); + m_CommunityIconLoadJobs.push_back(pLoadJob); + } m_CommunityIconDownloadJobs.pop_front(); } } // Rescan for changed communities only when necessary - if(m_CommunityIconsUpdateTime != 0 && m_CommunityIconsUpdateTime == ServerBrowser()->DDNetInfoUpdateTime()) + if(!ServerBrowser()->DDNetInfoAvailable() || (m_CommunityIconsInfoSha256 != SHA256_ZEROED && m_CommunityIconsInfoSha256 == ServerBrowser()->DDNetInfoSha256())) return; - m_CommunityIconsUpdateTime = ServerBrowser()->DDNetInfoUpdateTime(); + m_CommunityIconsInfoSha256 = ServerBrowser()->DDNetInfoSha256(); // Remove icons for removed communities auto RemovalIterator = m_vCommunityIcons.begin(); @@ -2007,7 +2117,7 @@ void CMenus::UpdateCommunityIcons() if(pExistingDownload == m_CommunityIconDownloadJobs.end() && (ExistingIcon == m_vCommunityIcons.end() || ExistingIcon->m_Sha256 != Community.IconSha256())) { std::shared_ptr pJob = std::make_shared(this, Community.Id(), Community.IconUrl(), Community.IconSha256()); - Engine()->AddJob(pJob); + Http()->Run(pJob); m_CommunityIconDownloadJobs.push_back(pJob); } } diff --git a/src/game/client/components/menus_demo.cpp b/src/game/client/components/menus_demo.cpp index 506af175ed..9da978a1f3 100644 --- a/src/game/client/components/menus_demo.cpp +++ b/src/game/client/components/menus_demo.cpp @@ -30,7 +30,7 @@ using namespace std::chrono_literals; int CMenus::DoButton_FontIcon(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, int Corners, bool Enabled) { - pRect->Draw(ColorRGBA(1.0f, 1.0f, 1.0f, (Checked ? 0.10f : 0.5f) * UI()->ButtonColorMul(pButtonContainer)), Corners, 5.0f); + pRect->Draw(ColorRGBA(1.0f, 1.0f, 1.0f, (Checked ? 0.10f : 0.5f) * Ui()->ButtonColorMul(pButtonContainer)), Corners, 5.0f); TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING); @@ -38,13 +38,13 @@ int CMenus::DoButton_FontIcon(CButtonContainer *pButtonContainer, const char *pT TextRender()->TextColor(TextRender()->DefaultTextColor()); CUIRect Temp; pRect->HMargin(2.0f, &Temp); - UI()->DoLabel(&Temp, pText, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + Ui()->DoLabel(&Temp, pText, Temp.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); if(!Enabled) { TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f)); TextRender()->TextOutlineColor(ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f)); - UI()->DoLabel(&Temp, FONT_ICON_SLASH, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + Ui()->DoLabel(&Temp, FONT_ICON_SLASH, Temp.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor()); TextRender()->TextColor(TextRender()->DefaultTextColor()); } @@ -52,7 +52,7 @@ int CMenus::DoButton_FontIcon(CButtonContainer *pButtonContainer, const char *pT TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - return UI()->DoButtonLogic(pButtonContainer, Checked, pRect); + return Ui()->DoButtonLogic(pButtonContainer, Checked, pRect); } bool CMenus::DemoFilterChat(const void *pData, int Size, void *pUser) @@ -78,6 +78,7 @@ void CMenus::HandleDemoSeeking(float PositionToSeek, float TimeToSeek) if((PositionToSeek >= 0.0f && PositionToSeek <= 1.0f) || TimeToSeek != 0.0f) { m_pClient->m_Chat.Reset(); + m_pClient->m_DamageInd.OnReset(); m_pClient->m_InfoMessages.OnReset(); m_pClient->m_Particles.OnReset(); m_pClient->m_Sounds.OnReset(); @@ -169,17 +170,17 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) // handle keyboard shortcuts independent of active menu float PositionToSeek = -1.0f; float TimeToSeek = 0.0f; - if(m_pClient->m_GameConsole.IsClosed() && m_DemoPlayerState == DEMOPLAYER_NONE && g_Config.m_ClDemoKeyboardShortcuts && !UI()->IsPopupOpen()) + if(m_pClient->m_GameConsole.IsClosed() && m_DemoPlayerState == DEMOPLAYER_NONE && g_Config.m_ClDemoKeyboardShortcuts && !Ui()->IsPopupOpen()) { // increase/decrease speed if(!Input()->ModifierIsPressed() && !Input()->ShiftIsPressed() && !Input()->AltIsPressed()) { - if(Input()->KeyPress(KEY_MOUSE_WHEEL_UP) || Input()->KeyPress(KEY_UP)) + if(Input()->KeyPress(KEY_UP) || (m_MenuActive && Input()->KeyPress(KEY_MOUSE_WHEEL_UP))) { DemoPlayer()->AdjustSpeedIndex(+1); UpdateLastSpeedChange(); } - else if(Input()->KeyPress(KEY_MOUSE_WHEEL_DOWN) || Input()->KeyPress(KEY_DOWN)) + else if(Input()->KeyPress(KEY_DOWN) || (m_MenuActive && Input()->KeyPress(KEY_MOUSE_WHEEL_DOWN))) { DemoPlayer()->AdjustSpeedIndex(-1); UpdateLastSpeedChange(); @@ -271,7 +272,7 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor().WithMultipliedAlpha(Alpha)); TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING); - UI()->DoLabel(UI()->Screen(), pInfo->m_Paused ? FONT_ICON_PAUSE : FONT_ICON_PLAY, 36.0f + Time * 12.0f, TEXTALIGN_MC); + Ui()->DoLabel(Ui()->Screen(), pInfo->m_Paused ? FONT_ICON_PAUSE : FONT_ICON_PLAY, 36.0f + Time * 12.0f, TEXTALIGN_MC); TextRender()->TextColor(TextRender()->DefaultTextColor()); TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor()); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); @@ -282,7 +283,7 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) // Render speed info if(g_Config.m_ClDemoShowSpeed && Client()->GlobalTime() - m_LastSpeedChange < 1.0f) { - CUIRect Screen = *UI()->Screen(); + CUIRect Screen = *Ui()->Screen(); char aSpeedBuf[16]; str_format(aSpeedBuf, sizeof(aSpeedBuf), "×%.2f", pInfo->m_Speed); @@ -349,18 +350,18 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) bool Clicked; bool Abrupted; - if(int Result = UI()->DoDraggableButtonLogic(&s_Operation, 8, &DemoControlsDragRect, &Clicked, &Abrupted)) + if(int Result = Ui()->DoDraggableButtonLogic(&s_Operation, 8, &DemoControlsDragRect, &Clicked, &Abrupted)) { if(s_Operation == OP_NONE && Result == 1) { - s_InitialMouse = UI()->MousePos(); + s_InitialMouse = Ui()->MousePos(); s_Operation = OP_CLICKED; } if(Clicked || Abrupted) s_Operation = OP_NONE; - if(s_Operation == OP_CLICKED && length(UI()->MousePos() - s_InitialMouse) > 5.0f) + if(s_Operation == OP_CLICKED && length(Ui()->MousePos() - s_InitialMouse) > 5.0f) { s_Operation = OP_DRAGGING; s_InitialMouse -= m_DemoControlsPositionOffset; @@ -368,7 +369,7 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) if(s_Operation == OP_DRAGGING) { - m_DemoControlsPositionOffset = UI()->MousePos() - s_InitialMouse; + m_DemoControlsPositionOffset = Ui()->MousePos() - s_InitialMouse; m_DemoControlsPositionOffset.x = clamp(m_DemoControlsPositionOffset.x, -DemoControlsOriginal.x, MainView.w - DemoControlsDragRect.w - DemoControlsOriginal.x); m_DemoControlsPositionOffset.y = clamp(m_DemoControlsPositionOffset.y, -DemoControlsOriginal.y, MainView.h - DemoControlsDragRect.h - DemoControlsOriginal.y); } @@ -379,8 +380,8 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) { const float Rounding = 5.0f; - static int s_SeekBarID = 0; - void *pId = &s_SeekBarID; + static int s_SeekBarId = 0; + void *pId = &s_SeekBarId; char aBuffer[128]; // draw seek bar @@ -413,7 +414,7 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); - IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, UI()->PixelSize(), SeekBar.h); + IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); } @@ -426,7 +427,7 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 0.0f, 0.0f, 1.0f); - IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, UI()->PixelSize(), SeekBar.h); + IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); } @@ -438,7 +439,7 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(1.0f, 0.0f, 0.0f, 1.0f); - IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, UI()->PixelSize(), SeekBar.h); + IGraphics::CQuadItem QuadItem(2 * Rounding + SeekBar.x + (SeekBar.w - 2 * Rounding) * Ratio, SeekBar.y, Ui()->PixelSize(), SeekBar.h); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); } @@ -449,19 +450,19 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) char aTotalTime[32]; str_time((int64_t)TotalTicks / Client()->GameTickSpeed() * 100, TIME_HOURS, aTotalTime, sizeof(aTotalTime)); str_format(aBuffer, sizeof(aBuffer), "%s / %s", aCurrentTime, aTotalTime); - UI()->DoLabel(&SeekBar, aBuffer, SeekBar.h * 0.70f, TEXTALIGN_MC); + Ui()->DoLabel(&SeekBar, aBuffer, SeekBar.h * 0.70f, TEXTALIGN_MC); // do the logic - const bool Inside = UI()->MouseInside(&SeekBar); + const bool Inside = Ui()->MouseInside(&SeekBar); - if(UI()->CheckActiveItem(pId)) + if(Ui()->CheckActiveItem(pId)) { - if(!UI()->MouseButton(0)) - UI()->SetActiveItem(nullptr); + if(!Ui()->MouseButton(0)) + Ui()->SetActiveItem(nullptr); else { static float s_PrevAmount = 0.0f; - float AmountSeek = clamp((UI()->MouseX() - SeekBar.x - Rounding) / (float)(SeekBar.w - 2 * Rounding), 0.0f, 1.0f); + float AmountSeek = clamp((Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), 0.0f, 1.0f); if(Input()->ShiftIsPressed()) { @@ -481,23 +482,24 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) } } } - else if(UI()->HotItem() == pId) + else if(Ui()->HotItem() == pId) { - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { - UI()->SetActiveItem(pId); - } - else - { - const int HoveredTick = (int)(clamp((UI()->MouseX() - SeekBar.x - Rounding) / (float)(SeekBar.w - 2 * Rounding), 0.0f, 1.0f) * TotalTicks); - static char s_aHoveredTime[32]; - str_time((int64_t)HoveredTick / Client()->GameTickSpeed() * 100, TIME_HOURS, s_aHoveredTime, sizeof(s_aHoveredTime)); - GameClient()->m_Tooltips.DoToolTip(pId, &SeekBar, s_aHoveredTime); + Ui()->SetActiveItem(pId); } } - if(Inside) - UI()->SetHotItem(pId); + if(Inside && !Ui()->MouseButton(0)) + Ui()->SetHotItem(pId); + + if(Ui()->HotItem() == pId) + { + const int HoveredTick = (int)(clamp((Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), 0.0f, 1.0f) * TotalTicks); + static char s_aHoveredTime[32]; + str_time((int64_t)HoveredTick / Client()->GameTickSpeed() * 100, TIME_HOURS, s_aHoveredTime, sizeof(s_aHoveredTime)); + GameClient()->m_Tooltips.DoToolTip(pId, &SeekBar, s_aHoveredTime); + } } bool IncreaseDemoSpeed = false, DecreaseDemoSpeed = false; @@ -565,10 +567,10 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) s_vpDurationNames[i] = s_vDurationNames[i].c_str(); } - static CUI::SDropDownState s_SkipDurationDropDownState; + static CUi::SDropDownState s_SkipDurationDropDownState; static CScrollRegion s_SkipDurationDropDownScrollRegion; s_SkipDurationDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_SkipDurationDropDownScrollRegion; - s_SkipDurationIndex = UI()->DoDropDown(&Button, s_SkipDurationIndex, s_vpDurationNames.data(), NumDurationLabels, s_SkipDurationDropDownState); + s_SkipDurationIndex = Ui()->DoDropDown(&Button, s_SkipDurationIndex, s_vpDurationNames.data(), NumDurationLabels, s_SkipDurationDropDownState); GameClient()->m_Tooltips.DoToolTip(&s_SkipDurationDropDownState.m_ButtonContainer, &Button, Localize("Change the skip duration")); } @@ -642,7 +644,7 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) ButtonBar.VSplitLeft(Margins * 12, &SpeedBar, &ButtonBar); char aBuffer[64]; str_format(aBuffer, sizeof(aBuffer), "×%g", pInfo->m_Speed); - UI()->DoLabel(&SpeedBar, aBuffer, Button.h * 0.7f, TEXTALIGN_MC); + Ui()->DoLabel(&SpeedBar, aBuffer, Button.h * 0.7f, TEXTALIGN_MC); // slice begin button ButtonBar.VSplitLeft(ButtonbarHeight, &Button, &ButtonBar); @@ -691,7 +693,7 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) char aDemoName[IO_MAX_PATH_LENGTH]; DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName)); m_DemoSliceInput.Set(aDemoName); - UI()->SetActiveItem(&m_DemoSliceInput); + Ui()->SetActiveItem(&m_DemoSliceInput); m_DemoPlayerState = DEMOPLAYER_SLICE_SAVE; } GameClient()->m_Tooltips.DoToolTip(&s_SliceSaveButton, &Button, Localize("Export cut as a separate demo")); @@ -725,7 +727,7 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) Props.m_MaxWidth = NameBar.w; Props.m_EllipsisAtEnd = true; Props.m_EnableWidthCheck = false; - UI()->DoLabel(&NameBar, aBuf, Button.h * 0.5f, TEXTALIGN_ML, Props); + Ui()->DoLabel(&NameBar, aBuf, Button.h * 0.5f, TEXTALIGN_ML, Props); if(IncreaseDemoSpeed) { @@ -744,14 +746,12 @@ void CMenus::RenderDemoPlayer(CUIRect MainView) if(m_DemoPlayerState != DEMOPLAYER_NONE) { // prevent element under the active popup from being activated - UI()->SetHotItem(nullptr); + Ui()->SetHotItem(nullptr); } if(m_DemoPlayerState == DEMOPLAYER_SLICE_SAVE) { RenderDemoPlayerSliceSavePopup(MainView); } - - UI()->RenderPopupMenus(); } void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView) @@ -769,7 +769,7 @@ void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView) CUIRect Title; Box.HSplitTop(24.0f, &Title, &Box); Box.HSplitTop(20.0f, nullptr, &Box); - UI()->DoLabel(&Title, Localize("Export demo cut"), 24.0f, TEXTALIGN_MC); + Ui()->DoLabel(&Title, Localize("Export demo cut"), 24.0f, TEXTALIGN_MC); // slice times CUIRect SliceTimesBar, SliceInterval, SliceLength; @@ -786,9 +786,9 @@ void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView) str_time((RealSliceEnd - RealSliceBegin) / Client()->GameTickSpeed() * 100, TIME_HOURS, aSliceLength, sizeof(aSliceLength)); char aBuf[256]; str_format(aBuf, sizeof(aBuf), "%s: %s – %s", Localize("Cut interval"), aSliceBegin, aSliceEnd); - UI()->DoLabel(&SliceInterval, aBuf, 18.0f, TEXTALIGN_ML); + Ui()->DoLabel(&SliceInterval, aBuf, 18.0f, TEXTALIGN_ML); str_format(aBuf, sizeof(aBuf), "%s: %s", Localize("Cut length"), aSliceLength); - UI()->DoLabel(&SliceLength, aBuf, 18.0f, TEXTALIGN_ML); + Ui()->DoLabel(&SliceLength, aBuf, 18.0f, TEXTALIGN_ML); // file name CUIRect NameLabel, NameBox; @@ -796,8 +796,8 @@ void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView) Box.HSplitTop(20.0f, nullptr, &Box); NameLabel.VSplitLeft(150.0f, &NameLabel, &NameBox); NameBox.VSplitLeft(20.0f, nullptr, &NameBox); - UI()->DoLabel(&NameLabel, Localize("New name:"), 18.0f, TEXTALIGN_ML); - UI()->DoEditBox(&m_DemoSliceInput, &NameBox, 12.0f); + Ui()->DoLabel(&NameLabel, Localize("New name:"), 18.0f, TEXTALIGN_ML); + Ui()->DoEditBox(&m_DemoSliceInput, &NameBox, 12.0f); // remove chat checkbox static int s_RemoveChat = 0; @@ -824,26 +824,28 @@ void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView) ButtonBar.VSplitMid(&AbortButton, &OkButton, 40.0f); static CButtonContainer s_ButtonAbort; - if(DoButton_Menu(&s_ButtonAbort, Localize("Abort"), 0, &AbortButton) || (!UI()->IsPopupOpen() && UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE))) + if(DoButton_Menu(&s_ButtonAbort, Localize("Abort"), 0, &AbortButton) || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE))) m_DemoPlayerState = DEMOPLAYER_NONE; - static CUI::SConfirmPopupContext s_ConfirmPopupContext; + static CUi::SConfirmPopupContext s_ConfirmPopupContext; static CButtonContainer s_ButtonOk; - if(DoButton_Menu(&s_ButtonOk, Localize("Ok"), 0, &OkButton) || (!UI()->IsPopupOpen() && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + if(DoButton_Menu(&s_ButtonOk, Localize("Ok"), 0, &OkButton) || (!Ui()->IsPopupOpen() && Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) { + if(str_endswith(m_DemoSliceInput.GetString(), ".demo")) + { + char aNameWithoutExt[IO_MAX_PATH_LENGTH]; + fs_split_file_extension(m_DemoSliceInput.GetString(), aNameWithoutExt, sizeof(aNameWithoutExt)); + m_DemoSliceInput.Set(aNameWithoutExt); + } + char aDemoName[IO_MAX_PATH_LENGTH]; - char aNameWithoutExt[IO_MAX_PATH_LENGTH]; DemoPlayer()->GetDemoName(aDemoName, sizeof(aDemoName)); - - fs_split_file_extension(m_DemoSliceInput.GetString(), aNameWithoutExt, sizeof(aNameWithoutExt)); - m_DemoSliceInput.Set(aNameWithoutExt); - if(str_comp(aDemoName, m_DemoSliceInput.GetString()) == 0) { - static CUI::SMessagePopupContext s_MessagePopupContext; + static CUi::SMessagePopupContext s_MessagePopupContext; s_MessagePopupContext.ErrorColor(); str_copy(s_MessagePopupContext.m_aMessage, Localize("Please use a different filename")); - UI()->ShowPopupMessage(UI()->MouseX(), OkButton.y + OkButton.h + 5.0f, &s_MessagePopupContext); + Ui()->ShowPopupMessage(Ui()->MouseX(), OkButton.y + OkButton.h + 5.0f, &s_MessagePopupContext); } else { @@ -854,14 +856,14 @@ void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView) s_ConfirmPopupContext.Reset(); s_ConfirmPopupContext.YesNoButtons(); str_copy(s_ConfirmPopupContext.m_aMessage, Localize("File already exists, do you want to overwrite it?")); - UI()->ShowPopupConfirm(UI()->MouseX(), OkButton.y + OkButton.h + 5.0f, &s_ConfirmPopupContext); + Ui()->ShowPopupConfirm(Ui()->MouseX(), OkButton.y + OkButton.h + 5.0f, &s_ConfirmPopupContext); } else - s_ConfirmPopupContext.m_Result = CUI::SConfirmPopupContext::CONFIRMED; + s_ConfirmPopupContext.m_Result = CUi::SConfirmPopupContext::CONFIRMED; } } - if(s_ConfirmPopupContext.m_Result == CUI::SConfirmPopupContext::CONFIRMED) + if(s_ConfirmPopupContext.m_Result == CUi::SConfirmPopupContext::CONFIRMED) { char aPath[IO_MAX_PATH_LENGTH]; str_format(aPath, sizeof(aPath), "%s/%s.demo", m_aCurrentDemoFolder, m_DemoSliceInput.GetString()); @@ -879,13 +881,13 @@ void CMenus::RenderDemoPlayerSliceSavePopup(CUIRect MainView) m_Popup = POPUP_RENDER_DEMO; m_StartPaused = false; m_DemoRenderInput.Set(m_aCurrentDemoSelectionName); - UI()->SetActiveItem(&m_DemoRenderInput); + Ui()->SetActiveItem(&m_DemoRenderInput); if(m_DemolistStorageType != IStorage::TYPE_ALL && m_DemolistStorageType != IStorage::TYPE_SAVE) m_DemolistStorageType = IStorage::TYPE_ALL; // Select a storage type containing the sliced demo } #endif } - if(s_ConfirmPopupContext.m_Result != CUI::SConfirmPopupContext::UNSET) + if(s_ConfirmPopupContext.m_Result != CUi::SConfirmPopupContext::UNSET) { s_ConfirmPopupContext.Reset(); } @@ -1060,6 +1062,8 @@ void CMenus::FetchAllHeaders() void CMenus::RenderDemoBrowser(CUIRect MainView) { + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_DEMOS); + CUIRect ListView, DetailsView, ButtonsView; MainView.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f); MainView.Margin(10.0f, &MainView); @@ -1085,7 +1089,14 @@ void CMenus::RenderDemoBrowserList(CUIRect ListView, bool &WasListboxItemActivat #if defined(CONF_VIDEORECORDER) if(!m_DemoRenderInput.IsEmpty()) { - m_Popup = POPUP_RENDER_DONE; + if(DemoPlayer()->ErrorMessage()[0] == '\0') + { + m_Popup = POPUP_RENDER_DONE; + } + else + { + m_DemoRenderInput.Clear(); + } } #endif @@ -1212,7 +1223,7 @@ void CMenus::RenderDemoBrowserList(CUIRect ListView, bool &WasListboxItemActivat TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->TextColor(IconColor); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING); - UI()->DoLabel(&Button, pIconType, 12.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Button, pIconType, 12.0f, TEXTALIGN_ML); TextRender()->SetRenderFlags(0); TextRender()->TextColor(TextRender()->DefaultTextColor()); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); @@ -1223,19 +1234,19 @@ void CMenus::RenderDemoBrowserList(CUIRect ListView, bool &WasListboxItemActivat Props.m_MaxWidth = Button.w; Props.m_EllipsisAtEnd = true; Props.m_EnableWidthCheck = false; - UI()->DoLabel(&Button, pItem->m_aName, 12.0f, TEXTALIGN_ML, Props); + Ui()->DoLabel(&Button, pItem->m_aName, 12.0f, TEXTALIGN_ML, Props); } else if(Col.m_Id == COL_LENGTH && !pItem->m_IsDir && pItem->m_Valid) { str_time((int64_t)pItem->Length() * 100, TIME_HOURS, aBuf, sizeof(aBuf)); Button.VMargin(4.0f, &Button); - UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR); + Ui()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR); } else if(Col.m_Id == COL_DATE && !pItem->m_IsDir) { str_timestamp_ex(pItem->m_Date, aBuf, sizeof(aBuf), FORMAT_SPACE); Button.VMargin(4.0f, &Button); - UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR); + Ui()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_MR); } } } @@ -1276,7 +1287,7 @@ void CMenus::RenderDemoBrowserDetails(CUIRect DetailsView) pHeaderLabel = Localize("Invalid Demo"); else pHeaderLabel = Localize("Demo"); - UI()->DoLabel(&Header, pHeaderLabel, FontSize + 2.0f, TEXTALIGN_MC); + Ui()->DoLabel(&Header, pHeaderLabel, FontSize + 2.0f, TEXTALIGN_MC); if(pItem == nullptr || pItem->m_IsDir) return; @@ -1285,10 +1296,10 @@ void CMenus::RenderDemoBrowserDetails(CUIRect DetailsView) CUIRect Left, Right; Contents.HSplitTop(18.0f, &Left, &Contents); - UI()->DoLabel(&Left, Localize("Created"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Left, Localize("Created"), FontSize, TEXTALIGN_ML); str_timestamp_ex(pItem->m_Date, aBuf, sizeof(aBuf), FORMAT_SPACE); Contents.HSplitTop(18.0f, &Left, &Contents); - UI()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML); Contents.HSplitTop(4.0f, nullptr, &Contents); if(!pItem->m_Valid) @@ -1296,41 +1307,41 @@ void CMenus::RenderDemoBrowserDetails(CUIRect DetailsView) Contents.HSplitTop(18.0f, &Left, &Contents); Left.VSplitMid(&Left, &Right, 4.0f); - UI()->DoLabel(&Left, Localize("Type"), FontSize, TEXTALIGN_ML); - UI()->DoLabel(&Right, Localize("Version"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Left, Localize("Type"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Right, Localize("Version"), FontSize, TEXTALIGN_ML); Contents.HSplitTop(18.0f, &Left, &Contents); Left.VSplitMid(&Left, &Right, 4.0f); - UI()->DoLabel(&Left, pItem->m_Info.m_aType, FontSize - 1.0f, TEXTALIGN_ML); - str_from_int(pItem->m_Info.m_Version, aBuf); - UI()->DoLabel(&Right, aBuf, FontSize - 1.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Left, pItem->m_Info.m_aType, FontSize - 1.0f, TEXTALIGN_ML); + str_format(aBuf, sizeof(aBuf), "%d", pItem->m_Info.m_Version); + Ui()->DoLabel(&Right, aBuf, FontSize - 1.0f, TEXTALIGN_ML); Contents.HSplitTop(4.0f, nullptr, &Contents); Contents.HSplitTop(18.0f, &Left, &Contents); Left.VSplitMid(&Left, &Right, 4.0f); - UI()->DoLabel(&Left, Localize("Length"), FontSize, TEXTALIGN_ML); - UI()->DoLabel(&Right, Localize("Markers"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Left, Localize("Length"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Right, Localize("Markers"), FontSize, TEXTALIGN_ML); Contents.HSplitTop(18.0f, &Left, &Contents); Left.VSplitMid(&Left, &Right, 4.0f); str_time((int64_t)pItem->Length() * 100, TIME_HOURS, aBuf, sizeof(aBuf)); - UI()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML); - str_from_int(pItem->NumMarkers(), aBuf); - UI()->DoLabel(&Right, aBuf, FontSize - 1.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML); + str_format(aBuf, sizeof(aBuf), "%d", pItem->NumMarkers()); + Ui()->DoLabel(&Right, aBuf, FontSize - 1.0f, TEXTALIGN_ML); Contents.HSplitTop(4.0f, nullptr, &Contents); Contents.HSplitTop(18.0f, &Left, &Contents); - UI()->DoLabel(&Left, Localize("Netversion"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Left, Localize("Netversion"), FontSize, TEXTALIGN_ML); Contents.HSplitTop(18.0f, &Left, &Contents); - UI()->DoLabel(&Left, pItem->m_Info.m_aNetversion, FontSize - 1.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Left, pItem->m_Info.m_aNetversion, FontSize - 1.0f, TEXTALIGN_ML); Contents.HSplitTop(16.0f, nullptr, &Contents); Contents.HSplitTop(18.0f, &Left, &Contents); - UI()->DoLabel(&Left, Localize("Map"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Left, Localize("Map"), FontSize, TEXTALIGN_ML); Contents.HSplitTop(18.0f, &Left, &Contents); - UI()->DoLabel(&Left, pItem->m_Info.m_aMapName, FontSize - 1.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Left, pItem->m_Info.m_aMapName, FontSize - 1.0f, TEXTALIGN_ML); Contents.HSplitTop(4.0f, nullptr, &Contents); Contents.HSplitTop(18.0f, &Left, &Contents); - UI()->DoLabel(&Left, Localize("Size"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Left, Localize("Size"), FontSize, TEXTALIGN_ML); Contents.HSplitTop(18.0f, &Left, &Contents); const float Size = pItem->Size() / 1024.0f; if(Size == 0.0f) @@ -1339,13 +1350,13 @@ void CMenus::RenderDemoBrowserDetails(CUIRect DetailsView) str_format(aBuf, sizeof(aBuf), Localize("%.2f MiB"), Size / 1024.0f); else str_format(aBuf, sizeof(aBuf), Localize("%.2f KiB"), Size); - UI()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML); Contents.HSplitTop(4.0f, nullptr, &Contents); Contents.HSplitTop(18.0f, &Left, &Contents); if(pItem->m_MapInfo.m_Sha256 != SHA256_ZEROED) { - UI()->DoLabel(&Left, "SHA256", FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Left, "SHA256", FontSize, TEXTALIGN_ML); Contents.HSplitTop(18.0f, &Left, &Contents); char aSha[SHA256_MAXSTRSIZE]; sha256_str(pItem->m_MapInfo.m_Sha256, aSha, sizeof(aSha)); @@ -1353,14 +1364,14 @@ void CMenus::RenderDemoBrowserDetails(CUIRect DetailsView) Props.m_MaxWidth = Left.w; Props.m_EllipsisAtEnd = true; Props.m_EnableWidthCheck = false; - UI()->DoLabel(&Left, aSha, FontSize - 1.0f, TEXTALIGN_ML, Props); + Ui()->DoLabel(&Left, aSha, FontSize - 1.0f, TEXTALIGN_ML, Props); } else { - UI()->DoLabel(&Left, "CRC32", FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Left, "CRC32", FontSize, TEXTALIGN_ML); Contents.HSplitTop(18.0f, &Left, &Contents); str_format(aBuf, sizeof(aBuf), "%08x", pItem->m_MapInfo.m_Crc); - UI()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Left, aBuf, FontSize - 1.0f, TEXTALIGN_ML); } Contents.HSplitTop(4.0f, nullptr, &Contents); } @@ -1386,22 +1397,10 @@ void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemAc // quick search { - SetIconMode(true); - CUIRect DemoSearch, SearchIcon; + CUIRect DemoSearch; ButtonBarTop.VSplitLeft(ButtonBarBottom.h * 21.0f, &DemoSearch, &ButtonBarTop); ButtonBarTop.VSplitLeft(ButtonBarTop.h / 2.0f, nullptr, &ButtonBarTop); - DemoSearch.VSplitLeft(TextRender()->TextWidth(14.0f, FONT_ICON_MAGNIFYING_GLASS), &SearchIcon, &DemoSearch); - DemoSearch.VSplitLeft(5.0f, nullptr, &DemoSearch); - UI()->DoLabel(&SearchIcon, FONT_ICON_MAGNIFYING_GLASS, 14.0f, TEXTALIGN_ML); - SetIconMode(false); - m_DemoSearchInput.SetEmptyText(Localize("Search")); - - if(Input()->KeyPress(KEY_F) && Input()->ModifierIsPressed()) - { - UI()->SetActiveItem(&m_DemoSearchInput); - m_DemoSearchInput.SelectAll(); - } - if(UI()->DoClearableEditBox(&m_DemoSearchInput, &DemoSearch, 12.0f)) + if(Ui()->DoEditBox_Search(&m_DemoSearchInput, &DemoSearch, 14.0f, !Ui()->IsPopupOpen() && m_pClient->m_GameConsole.IsClosed())) { RefreshFilteredDemos(); DemolistOnUpdate(false); @@ -1448,10 +1447,7 @@ void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemAc { char aBuf[IO_MAX_PATH_LENGTH]; Storage()->GetCompletePath(m_DemolistSelectedIndex >= 0 ? m_vpFilteredDemos[m_DemolistSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, m_aCurrentDemoFolder[0] == '\0' ? "demos" : m_aCurrentDemoFolder, aBuf, sizeof(aBuf)); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } GameClient()->m_Tooltips.DoToolTip(&s_DemosDirectoryButton, &DemosDirectoryButton, Localize("Open the directory that contains the demo files")); } @@ -1464,7 +1460,7 @@ void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemAc ButtonBarBottom.VSplitRight(ButtonBarBottom.h, &ButtonBarBottom, nullptr); SetIconMode(true); static CButtonContainer s_PlayButton; - if(DoButton_Menu(&s_PlayButton, (m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) ? FONT_ICON_FOLDER_OPEN : FONT_ICON_PLAY, 0, &PlayButton) || WasListboxItemActivated || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || (Input()->KeyPress(KEY_P) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive())) + if(DoButton_Menu(&s_PlayButton, (m_DemolistSelectedIndex >= 0 && m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) ? FONT_ICON_FOLDER_OPEN : FONT_ICON_PLAY, 0, &PlayButton) || WasListboxItemActivated || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER) || (Input()->KeyPress(KEY_P) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive())) { SetIconMode(false); if(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_IsDir) // folder @@ -1513,7 +1509,7 @@ void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemAc } else { - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); return; } } @@ -1544,7 +1540,7 @@ void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemAc fs_split_file_extension(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, aNameWithoutExt, sizeof(aNameWithoutExt)); m_DemoRenameInput.Set(aNameWithoutExt); } - UI()->SetActiveItem(&m_DemoRenameInput); + Ui()->SetActiveItem(&m_DemoRenameInput); return; } @@ -1553,7 +1549,7 @@ void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemAc CUIRect DeleteButton; ButtonBarBottom.VSplitRight(ButtonBarBottom.h * 3.0f, &ButtonBarBottom, &DeleteButton); ButtonBarBottom.VSplitRight(ButtonBarBottom.h / 2.0f, &ButtonBarBottom, nullptr); - if(DoButton_Menu(&s_DeleteButton, FONT_ICON_TRASH, 0, &DeleteButton) || UI()->ConsumeHotkey(CUI::HOTKEY_DELETE) || (Input()->KeyPress(KEY_D) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive())) + if(DoButton_Menu(&s_DeleteButton, FONT_ICON_TRASH, 0, &DeleteButton) || Ui()->ConsumeHotkey(CUi::HOTKEY_DELETE) || (Input()->KeyPress(KEY_D) && m_pClient->m_GameConsole.IsClosed() && !m_DemoSearchInput.IsActive())) { SetIconMode(false); char aBuf[128 + IO_MAX_PATH_LENGTH]; @@ -1581,7 +1577,7 @@ void CMenus::RenderDemoBrowserButtons(CUIRect ButtonsView, bool WasListboxItemAc char aNameWithoutExt[IO_MAX_PATH_LENGTH]; fs_split_file_extension(m_vpFilteredDemos[m_DemolistSelectedIndex]->m_aFilename, aNameWithoutExt, sizeof(aNameWithoutExt)); m_DemoRenderInput.Set(aNameWithoutExt); - UI()->SetActiveItem(&m_DemoRenderInput); + Ui()->SetActiveItem(&m_DemoRenderInput); return; } SetIconMode(false); diff --git a/src/game/client/components/menus_ingame.cpp b/src/game/client/components/menus_ingame.cpp index d3b2099f95..ab511e7688 100644 --- a/src/game/client/components/menus_ingame.cpp +++ b/src/game/client/components/menus_ingame.cpp @@ -41,6 +41,7 @@ using namespace std::chrono_literals; void CMenus::RenderGame(CUIRect MainView) { CUIRect Button, ButtonBar, ButtonBar2; + bool ShowDDRaceButtons = MainView.w > 855.0f; MainView.HSplitTop(45.0f, &ButtonBar, &MainView); ButtonBar.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f); @@ -64,20 +65,25 @@ void CMenus::RenderGame(CUIRect MainView) else { Client()->Disconnect(); - RefreshBrowserTab(g_Config.m_UiPage); + RefreshBrowserTab(true); } } - ButtonBar.VSplitRight(5.0f, &ButtonBar, 0); + ButtonBar.VSplitRight(5.0f, &ButtonBar, nullptr); ButtonBar.VSplitRight(170.0f, &ButtonBar, &Button); - bool DummyConnecting = Client()->DummyConnecting(); static CButtonContainer s_DummyButton; if(!Client()->DummyAllowed()) { DoButton_Menu(&s_DummyButton, Localize("Connect Dummy"), 1, &Button); + GameClient()->m_Tooltips.DoToolTip(&s_DummyButton, &Button, Localize("Dummy is not allowed on this server")); } - else if(DummyConnecting) + else if(Client()->DummyConnectingDelayed()) + { + DoButton_Menu(&s_DummyButton, Localize("Connect Dummy"), 1, &Button); + GameClient()->m_Tooltips.DoToolTip(&s_DummyButton, &Button, Localize("Please wait…")); + } + else if(Client()->DummyConnecting()) { DoButton_Menu(&s_DummyButton, Localize("Connecting dummy"), 1, &Button); } @@ -101,38 +107,34 @@ void CMenus::RenderGame(CUIRect MainView) } } - ButtonBar.VSplitRight(5.0f, &ButtonBar, 0); + ButtonBar.VSplitRight(5.0f, &ButtonBar, nullptr); ButtonBar.VSplitRight(140.0f, &ButtonBar, &Button); - static CButtonContainer s_DemoButton; - bool Recording = DemoRecorder(RECORDER_MANUAL)->IsRecording(); + const bool Recording = DemoRecorder(RECORDER_MANUAL)->IsRecording(); if(DoButton_Menu(&s_DemoButton, Recording ? Localize("Stop record") : Localize("Record demo"), 0, &Button)) { if(!Recording) Client()->DemoRecorder_Start(Client()->GetCurrentMap(), true, RECORDER_MANUAL); else - Client()->DemoRecorder_Stop(RECORDER_MANUAL); + Client()->DemoRecorder(RECORDER_MANUAL)->Stop(IDemoRecorder::EStopMode::KEEP_FILE); } - static CButtonContainer s_SpectateButton; - static CButtonContainer s_JoinRedButton; - static CButtonContainer s_JoinBlueButton; - bool Paused = false; bool Spec = false; - if(m_pClient->m_Snap.m_LocalClientID >= 0) + if(m_pClient->m_Snap.m_LocalClientId >= 0) { - Paused = m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID].m_Paused; - Spec = m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID].m_Spec; + Paused = m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId].m_Paused; + Spec = m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId].m_Spec; } if(m_pClient->m_Snap.m_pLocalInfo && m_pClient->m_Snap.m_pGameInfoObj && !Paused && !Spec) { if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); ButtonBar.VSplitLeft(120.0f, &Button, &ButtonBar); - if(!DummyConnecting && DoButton_Menu(&s_SpectateButton, Localize("Spectate"), 0, &Button)) + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); + static CButtonContainer s_SpectateButton; + if(!Client()->DummyConnecting() && DoButton_Menu(&s_SpectateButton, Localize("Spectate"), 0, &Button)) { if(g_Config.m_ClDummy == 0 || Client()->DummyConnected()) { @@ -146,9 +148,10 @@ void CMenus::RenderGame(CUIRect MainView) { if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_RED) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); ButtonBar.VSplitLeft(120.0f, &Button, &ButtonBar); - if(!DummyConnecting && DoButton_Menu(&s_JoinRedButton, Localize("Join red"), 0, &Button)) + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); + static CButtonContainer s_JoinRedButton; + if(!Client()->DummyConnecting() && DoButton_Menu(&s_JoinRedButton, Localize("Join red"), 0, &Button)) { m_pClient->SendSwitchTeam(TEAM_RED); SetActive(false); @@ -157,9 +160,10 @@ void CMenus::RenderGame(CUIRect MainView) if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_BLUE) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); ButtonBar.VSplitLeft(120.0f, &Button, &ButtonBar); - if(!DummyConnecting && DoButton_Menu(&s_JoinBlueButton, Localize("Join blue"), 0, &Button)) + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); + static CButtonContainer s_JoinBlueButton; + if(!Client()->DummyConnecting() && DoButton_Menu(&s_JoinBlueButton, Localize("Join blue"), 0, &Button)) { m_pClient->SendSwitchTeam(TEAM_BLUE); SetActive(false); @@ -168,22 +172,23 @@ void CMenus::RenderGame(CUIRect MainView) } else { - if(m_pClient->m_Snap.m_pLocalInfo->m_Team != 0) + if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_RED) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); ButtonBar.VSplitLeft(120.0f, &Button, &ButtonBar); - if(!DummyConnecting && DoButton_Menu(&s_SpectateButton, Localize("Join game"), 0, &Button)) + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); + static CButtonContainer s_JoinGameButton; + if(!Client()->DummyConnecting() && DoButton_Menu(&s_JoinGameButton, Localize("Join game"), 0, &Button)) { - m_pClient->SendSwitchTeam(0); + m_pClient->SendSwitchTeam(TEAM_RED); SetActive(false); } } } - if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS) + if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS && (ShowDDRaceButtons || !(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS))) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); ButtonBar.VSplitLeft(65.0f, &Button, &ButtonBar); + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); static CButtonContainer s_KillButton; if(DoButton_Menu(&s_KillButton, Localize("Kill"), 0, &Button)) @@ -194,17 +199,17 @@ void CMenus::RenderGame(CUIRect MainView) } } - if(m_pClient->m_ReceivedDDNetPlayer && m_pClient->m_Snap.m_pLocalInfo && m_pClient->m_Snap.m_pGameInfoObj) + if(m_pClient->m_ReceivedDDNetPlayer && m_pClient->m_Snap.m_pLocalInfo && m_pClient->m_Snap.m_pGameInfoObj && (ShowDDRaceButtons || !(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS))) { if(m_pClient->m_Snap.m_pLocalInfo->m_Team != TEAM_SPECTATORS || Paused || Spec) { - ButtonBar.VSplitLeft(5.0f, 0, &ButtonBar); ButtonBar.VSplitLeft((!Paused && !Spec) ? 65.0f : 120.0f, &Button, &ButtonBar); + ButtonBar.VSplitLeft(5.0f, nullptr, &ButtonBar); static CButtonContainer s_PauseButton; if(DoButton_Menu(&s_PauseButton, (!Paused && !Spec) ? Localize("Pause") : Localize("Join game"), 0, &Button)) { - m_pClient->Console()->ExecuteLine("say /pause"); + Console()->ExecuteLine("say /pause"); SetActive(false); } } @@ -224,20 +229,18 @@ void CMenus::PopupConfirmDisconnectDummy() void CMenus::RenderPlayers(CUIRect MainView) { - CUIRect Button, Button2, ButtonBar, Options, Player; + CUIRect Button, Button2, ButtonBar, PlayerList, Player; MainView.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f); - // player options - MainView.Margin(10.0f, &Options); - Options.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 10.0f); - Options.Margin(10.0f, &Options); - Options.HSplitTop(50.0f, &Button, &Options); - UI()->DoLabel(&Button, Localize("Player options"), 34.0f, TEXTALIGN_ML); + // list background color + MainView.Margin(10.0f, &PlayerList); + PlayerList.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 10.0f); + PlayerList.Margin(10.0f, &PlayerList); // headline - Options.HSplitTop(34.0f, &ButtonBar, &Options); + PlayerList.HSplitTop(34.0f, &ButtonBar, &PlayerList); ButtonBar.VSplitRight(231.0f, &Player, &ButtonBar); - UI()->DoLabel(&Player, Localize("Player"), 24.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Player, Localize("Player"), 24.0f, TEXTALIGN_ML); ButtonBar.HMargin(1.0f, &ButtonBar); float Width = ButtonBar.h * 2.0f; @@ -252,37 +255,33 @@ void CMenus::RenderPlayers(CUIRect MainView) ButtonBar.VSplitLeft(Width, &Button, &ButtonBar); RenderTools()->RenderIcon(IMAGE_GUIICONS, SPRITE_GUIICON_FRIEND, &Button); - Options.HSplitTop(34.0f, &ButtonBar, &Options); - ButtonBar.VSplitRight(310.0f, &Player, &ButtonBar); - UI()->DoLabel(&Player, Localize("Blacklist"), 10.0f, TEXTALIGN_MR); - int TotalPlayers = 0; for(const auto &pInfoByName : m_pClient->m_Snap.m_apInfoByName) { if(!pInfoByName) continue; - int Index = pInfoByName->m_ClientID; + int Index = pInfoByName->m_ClientId; - if(Index == m_pClient->m_Snap.m_LocalClientID) + if(Index == m_pClient->m_Snap.m_LocalClientId) continue; TotalPlayers++; } static CListBox s_ListBox; - s_ListBox.DoStart(24.0f, TotalPlayers, 1, 3, -1, &Options); + s_ListBox.DoStart(24.0f, TotalPlayers, 1, 3, -1, &PlayerList); // options - static char s_aPlayerIDs[MAX_CLIENTS][4] = {{0}}; + static char s_aPlayerIds[MAX_CLIENTS][4] = {{0}}; for(int i = 0, Count = 0; i < MAX_CLIENTS; ++i) { if(!m_pClient->m_Snap.m_apInfoByName[i]) continue; - int Index = m_pClient->m_Snap.m_apInfoByName[i]->m_ClientID; - if(Index == m_pClient->m_Snap.m_LocalClientID) + int Index = m_pClient->m_Snap.m_apInfoByName[i]->m_ClientId; + if(Index == m_pClient->m_Snap.m_LocalClientId) continue; CGameClient::CClientData &CurrentClient = m_pClient->m_aClients[Index]; @@ -303,49 +302,34 @@ void CMenus::RenderPlayers(CUIRect MainView) Player.VSplitLeft(28.0f, &Button, &Player); CTeeRenderInfo TeeInfo = CurrentClient.m_RenderInfo; - TeeInfo.m_Size = Button.h; const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); + CRenderTools::GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); vec2 TeeRenderPos(Button.x + Button.h / 2, Button.y + Button.h / 2 + OffsetToMid.y); - RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos); + Ui()->DoButtonLogic(&s_aPlayerIds[Index][3], 0, &Button); + GameClient()->m_Tooltips.DoToolTip(&s_aPlayerIds[Index][3], &Button, CurrentClient.m_aSkinName); Player.HSplitTop(1.5f, nullptr, &Player); Player.VSplitMid(&Player, &Button); Row.VSplitRight(210.0f, &Button2, &Row); - UI()->DoLabel(&Player, CurrentClient.m_aName, 14.0f, TEXTALIGN_ML); - UI()->DoLabel(&Button, CurrentClient.m_aClan, 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Player, CurrentClient.m_aName, 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Button, CurrentClient.m_aClan, 14.0f, TEXTALIGN_ML); m_pClient->m_CountryFlags.Render(CurrentClient.m_Country, ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), Button2.x, Button2.y + Button2.h / 2.0f - 0.75f * Button2.h / 2.0f, 1.5f * Button2.h, 0.75f * Button2.h); - //blacklisting hahxd - Row.VSplitLeft(5.0f, &Row, nullptr); - // Row.VSplitLeft(Width, &Button, &Row); - Button.VSplitRight((Width - Button.h) / 4.0f, nullptr, &Button); - Button.VSplitRight(Button.h, nullptr, &Button); - - if(DoButton_CheckBox(&s_aPlayerIDs[Index][3], Localize(""), CurrentClient.m_Foe, &Button)) - { - CurrentClient.m_Foe ^= 1; - if(CurrentClient.m_Foe) - Client()->Foes()->AddFriend(CurrentClient.m_aName, CurrentClient.m_aClan); - else - Client()->Foes()->RemoveFriend(CurrentClient.m_aName, CurrentClient.m_aClan); - } - // ignore chat button Row.HMargin(2.0f, &Row); Row.VSplitLeft(Width, &Button, &Row); Button.VSplitLeft((Width - Button.h) / 4.0f, nullptr, &Button); Button.VSplitLeft(Button.h, &Button, nullptr); if(g_Config.m_ClShowChatFriends && !CurrentClient.m_Friend) - DoButton_Toggle(&s_aPlayerIDs[Index][0], 1, &Button, false); - else if(DoButton_Toggle(&s_aPlayerIDs[Index][0], CurrentClient.m_ChatIgnore, &Button, true)) + DoButton_Toggle(&s_aPlayerIds[Index][0], 1, &Button, false); + else if(DoButton_Toggle(&s_aPlayerIds[Index][0], CurrentClient.m_ChatIgnore, &Button, true)) CurrentClient.m_ChatIgnore ^= 1; // ignore emoticon button @@ -354,8 +338,8 @@ void CMenus::RenderPlayers(CUIRect MainView) Button.VSplitLeft((Width - Button.h) / 4.0f, nullptr, &Button); Button.VSplitLeft(Button.h, &Button, nullptr); if(g_Config.m_ClShowChatFriends && !CurrentClient.m_Friend) - DoButton_Toggle(&s_aPlayerIDs[Index][1], 1, &Button, false); - else if(DoButton_Toggle(&s_aPlayerIDs[Index][1], CurrentClient.m_EmoticonIgnore, &Button, true)) + DoButton_Toggle(&s_aPlayerIds[Index][1], 1, &Button, false); + else if(DoButton_Toggle(&s_aPlayerIds[Index][1], CurrentClient.m_EmoticonIgnore, &Button, true)) CurrentClient.m_EmoticonIgnore ^= 1; // friend button @@ -363,7 +347,7 @@ void CMenus::RenderPlayers(CUIRect MainView) Row.VSplitLeft(Width, &Button, &Row); Button.VSplitLeft((Width - Button.h) / 4.0f, nullptr, &Button); Button.VSplitLeft(Button.h, &Button, nullptr); - if(DoButton_Toggle(&s_aPlayerIDs[Index][2], CurrentClient.m_Friend, &Button, true)) + if(DoButton_Toggle(&s_aPlayerIds[Index][2], CurrentClient.m_Friend, &Button, true)) { if(CurrentClient.m_Friend) m_pClient->Friends()->RemoveFriend(CurrentClient.m_aName, CurrentClient.m_aClan); @@ -396,7 +380,7 @@ void CMenus::RenderServerInfo(CUIRect MainView) char aBuf[1024]; - // set view to use for all submodules + // set view to use for all sub-modules MainView.Margin(10.0f, &View); // serverinfo @@ -438,7 +422,15 @@ void CMenus::RenderServerInfo(CUIRect MainView) if(DoButton_Menu(&s_CopyButton, Localize("Copy info"), 0, &Button)) { char aInfo[256]; - CurrentServerInfo.InfoToString(aInfo, sizeof(aInfo)); + str_format( + aInfo, + sizeof(aInfo), + "%s\n" + "Address: ddnet://%s\n" + "My IGN: %s\n", + CurrentServerInfo.m_aName, + CurrentServerInfo.m_aAddress, + Client()->PlayerName()); Input()->SetClipboardText(aInfo); } } @@ -576,7 +568,7 @@ bool CMenus::RenderServerControlServer(CUIRect MainView) CUIRect Label; Item.m_Rect.VMargin(2.0f, &Label); - UI()->DoLabel(&Label, pOption->m_aDescription, 13.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, pOption->m_aDescription, 13.0f, TEXTALIGN_ML); } s_CurVoteOption = s_ListBox.DoEnd(); @@ -588,15 +580,15 @@ bool CMenus::RenderServerControlServer(CUIRect MainView) bool CMenus::RenderServerControlKick(CUIRect MainView, bool FilterSpectators) { int NumOptions = 0; - int Selected = 0; - static int aPlayerIDs[MAX_CLIENTS]; + int Selected = -1; + int aPlayerIds[MAX_CLIENTS]; for(const auto &pInfoByName : m_pClient->m_Snap.m_apInfoByName) { if(!pInfoByName) continue; - int Index = pInfoByName->m_ClientID; - if(Index == m_pClient->m_Snap.m_LocalClientID || (FilterSpectators && pInfoByName->m_Team == TEAM_SPECTATORS)) + int Index = pInfoByName->m_ClientId; + if(Index == m_pClient->m_Snap.m_LocalClientId || (FilterSpectators && pInfoByName->m_Team == TEAM_SPECTATORS)) continue; if(!str_utf8_find_nocase(m_pClient->m_aClients[Index].m_aName, m_FilterInput.GetString())) @@ -604,7 +596,8 @@ bool CMenus::RenderServerControlKick(CUIRect MainView, bool FilterSpectators) if(m_CallvoteSelectedPlayer == Index) Selected = NumOptions; - aPlayerIDs[NumOptions++] = Index; + aPlayerIds[NumOptions] = Index; + NumOptions++; } static CListBox s_ListBox; @@ -612,38 +605,45 @@ bool CMenus::RenderServerControlKick(CUIRect MainView, bool FilterSpectators) for(int i = 0; i < NumOptions; i++) { - const CListboxItem Item = s_ListBox.DoNextItem(&aPlayerIDs[i]); + const CListboxItem Item = s_ListBox.DoNextItem(&aPlayerIds[i]); if(!Item.m_Visible) continue; CUIRect TeeRect, Label; Item.m_Rect.VSplitLeft(Item.m_Rect.h, &TeeRect, &Label); - CTeeRenderInfo TeeInfo = m_pClient->m_aClients[aPlayerIDs[i]].m_RenderInfo; + CTeeRenderInfo TeeInfo = m_pClient->m_aClients[aPlayerIds[i]].m_RenderInfo; TeeInfo.m_Size = TeeRect.h; const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); + CRenderTools::GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); vec2 TeeRenderPos(TeeRect.x + TeeInfo.m_Size / 2, TeeRect.y + TeeInfo.m_Size / 2 + OffsetToMid.y); RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos); - UI()->DoLabel(&Label, m_pClient->m_aClients[aPlayerIDs[i]].m_aName, 16.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, m_pClient->m_aClients[aPlayerIds[i]].m_aName, 16.0f, TEXTALIGN_ML); } Selected = s_ListBox.DoEnd(); - m_CallvoteSelectedPlayer = Selected != -1 ? aPlayerIDs[Selected] : -1; + m_CallvoteSelectedPlayer = Selected != -1 ? aPlayerIds[Selected] : -1; return s_ListBox.WasItemActivated(); } void CMenus::RenderServerControl(CUIRect MainView) { - static int s_ControlPage = 0; + enum class EServerControlTab + { + SETTINGS, + KICKVOTE, + SPECVOTE, + }; + static EServerControlTab s_ControlPage = EServerControlTab::SETTINGS; + // render background CUIRect Bottom, RconExtension, TabBar, Button; MainView.HSplitTop(20.0f, &Bottom, &MainView); - Bottom.Draw(ms_ColorTabbarActive, 0, 10.0f); + Bottom.Draw(ms_ColorTabbarActive, IGraphics::CORNER_NONE, 0.0f); MainView.HSplitTop(20.0f, &TabBar, &MainView); MainView.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f); MainView.Margin(10.0f, &MainView); @@ -654,71 +654,59 @@ void CMenus::RenderServerControl(CUIRect MainView) // tab bar TabBar.VSplitLeft(TabBar.w / 3, &Button, &TabBar); static CButtonContainer s_Button0; - if(DoButton_MenuTab(&s_Button0, Localize("Change settings"), s_ControlPage == 0, &Button, 0)) - s_ControlPage = 0; + if(DoButton_MenuTab(&s_Button0, Localize("Change settings"), s_ControlPage == EServerControlTab::SETTINGS, &Button, IGraphics::CORNER_NONE)) + s_ControlPage = EServerControlTab::SETTINGS; TabBar.VSplitMid(&Button, &TabBar); static CButtonContainer s_Button1; - if(DoButton_MenuTab(&s_Button1, Localize("Kick player"), s_ControlPage == 1, &Button, 0)) - s_ControlPage = 1; + if(DoButton_MenuTab(&s_Button1, Localize("Kick player"), s_ControlPage == EServerControlTab::KICKVOTE, &Button, IGraphics::CORNER_NONE)) + s_ControlPage = EServerControlTab::KICKVOTE; static CButtonContainer s_Button2; - if(DoButton_MenuTab(&s_Button2, Localize("Move player to spectators"), s_ControlPage == 2, &TabBar, 0)) - s_ControlPage = 2; + if(DoButton_MenuTab(&s_Button2, Localize("Move player to spectators"), s_ControlPage == EServerControlTab::SPECVOTE, &TabBar, IGraphics::CORNER_NONE)) + s_ControlPage = EServerControlTab::SPECVOTE; // render page MainView.HSplitBottom(ms_ButtonHeight + 5 * 2, &MainView, &Bottom); Bottom.HMargin(5.0f, &Bottom); + Bottom.HSplitTop(5.0f, nullptr, &Bottom); bool Call = false; - if(s_ControlPage == 0) + if(s_ControlPage == EServerControlTab::SETTINGS) Call = RenderServerControlServer(MainView); - else if(s_ControlPage == 1) + else if(s_ControlPage == EServerControlTab::KICKVOTE) Call = RenderServerControlKick(MainView, false); - else if(s_ControlPage == 2) + else if(s_ControlPage == EServerControlTab::SPECVOTE) Call = RenderServerControlKick(MainView, true); // vote menu - CUIRect QuickSearch; // render quick search - Bottom.VSplitLeft(5.0f, 0, &Bottom); + CUIRect QuickSearch; + Bottom.VSplitLeft(5.0f, nullptr, &Bottom); Bottom.VSplitLeft(250.0f, &QuickSearch, &Bottom); - QuickSearch.HSplitTop(5.0f, 0, &QuickSearch); - TextRender()->SetFontPreset(EFontPreset::ICON_FONT); - TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - - UI()->DoLabel(&QuickSearch, FONT_ICON_MAGNIFYING_GLASS, 14.0f, TEXTALIGN_ML); - float wSearch = TextRender()->TextWidth(14.0f, FONT_ICON_MAGNIFYING_GLASS, -1, -1.0f); - TextRender()->SetRenderFlags(0); - TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - QuickSearch.VSplitLeft(wSearch, 0, &QuickSearch); - QuickSearch.VSplitLeft(5.0f, 0, &QuickSearch); - - if(m_ControlPageOpening || (Input()->KeyPress(KEY_F) && Input()->ModifierIsPressed())) + if(m_ControlPageOpening) { - UI()->SetActiveItem(&m_FilterInput); m_ControlPageOpening = false; + Ui()->SetActiveItem(&m_FilterInput); m_FilterInput.SelectAll(); } - m_FilterInput.SetEmptyText(Localize("Search")); - UI()->DoClearableEditBox(&m_FilterInput, &QuickSearch, 14.0f); + Ui()->DoEditBox_Search(&m_FilterInput, &QuickSearch, 14.0f, !Ui()->IsPopupOpen() && m_pClient->m_GameConsole.IsClosed()); // call vote Bottom.VSplitRight(10.0f, &Bottom, 0); Bottom.VSplitRight(120.0f, &Bottom, &Button); - Button.HSplitTop(5.0f, 0, &Button); static CButtonContainer s_CallVoteButton; if(DoButton_Menu(&s_CallVoteButton, Localize("Call vote"), 0, &Button) || Call) { - if(s_ControlPage == 0) + if(s_ControlPage == EServerControlTab::SETTINGS) { m_pClient->m_Voting.CallvoteOption(m_CallvoteSelectedOption, m_CallvoteReasonInput.GetString()); if(g_Config.m_UiCloseWindowAfterChangingSetting) SetActive(false); } - else if(s_ControlPage == 1) + else if(s_ControlPage == EServerControlTab::KICKVOTE) { if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS && m_pClient->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer]) @@ -727,7 +715,7 @@ void CMenus::RenderServerControl(CUIRect MainView) SetActive(false); } } - else if(s_ControlPage == 2) + else if(s_ControlPage == EServerControlTab::SPECVOTE) { if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS && m_pClient->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer]) @@ -743,17 +731,28 @@ void CMenus::RenderServerControl(CUIRect MainView) CUIRect Reason; Bottom.VSplitRight(20.0f, &Bottom, 0); Bottom.VSplitRight(200.0f, &Bottom, &Reason); - Reason.HSplitTop(5.0f, 0, &Reason); const char *pLabel = Localize("Reason:"); - UI()->DoLabel(&Reason, pLabel, 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Reason, pLabel, 14.0f, TEXTALIGN_ML); float w = TextRender()->TextWidth(14.0f, pLabel, -1, -1.0f); Reason.VSplitLeft(w + 10.0f, 0, &Reason); if(Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed()) { - UI()->SetActiveItem(&m_CallvoteReasonInput); + Ui()->SetActiveItem(&m_CallvoteReasonInput); m_CallvoteReasonInput.SelectAll(); } - UI()->DoEditBox(&m_CallvoteReasonInput, &Reason, 14.0f); + Ui()->DoEditBox(&m_CallvoteReasonInput, &Reason, 14.0f); + + // vote option loading indicator + if(s_ControlPage == EServerControlTab::SETTINGS && m_pClient->m_Voting.IsReceivingOptions()) + { + CUIRect Spinner, LoadingLabel; + Bottom.VSplitLeft(20.0f, nullptr, &Bottom); + Bottom.VSplitLeft(16.0f, &Spinner, &Bottom); + Bottom.VSplitLeft(5.0f, nullptr, &Bottom); + Bottom.VSplitRight(10.0f, &LoadingLabel, nullptr); + Ui()->RenderProgressSpinner(Spinner.Center(), 8.0f); + Ui()->DoLabel(&LoadingLabel, Localize("Loading…"), 14.0f, TEXTALIGN_ML); + } // extended features (only available when authed in rcon) if(Client()->RconAuthed()) @@ -770,9 +769,11 @@ void CMenus::RenderServerControl(CUIRect MainView) static CButtonContainer s_ForceVoteButton; if(DoButton_Menu(&s_ForceVoteButton, Localize("Force vote"), 0, &Button)) { - if(s_ControlPage == 0) + if(s_ControlPage == EServerControlTab::SETTINGS) + { m_pClient->m_Voting.CallvoteOption(m_CallvoteSelectedOption, m_CallvoteReasonInput.GetString(), true); - else if(s_ControlPage == 1) + } + else if(s_ControlPage == EServerControlTab::KICKVOTE) { if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS && m_pClient->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer]) @@ -781,7 +782,7 @@ void CMenus::RenderServerControl(CUIRect MainView) SetActive(false); } } - else if(s_ControlPage == 2) + else if(s_ControlPage == EServerControlTab::SPECVOTE) { if(m_CallvoteSelectedPlayer >= 0 && m_CallvoteSelectedPlayer < MAX_CLIENTS && m_pClient->m_Snap.m_apPlayerInfos[m_CallvoteSelectedPlayer]) @@ -793,7 +794,7 @@ void CMenus::RenderServerControl(CUIRect MainView) m_CallvoteReasonInput.Clear(); } - if(s_ControlPage == 0) + if(s_ControlPage == EServerControlTab::SETTINGS) { // remove vote Bottom.VSplitRight(10.0f, &Bottom, 0); @@ -806,10 +807,10 @@ void CMenus::RenderServerControl(CUIRect MainView) RconExtension.HSplitTop(20.0f, &Bottom, &RconExtension); Bottom.VSplitLeft(5.0f, 0, &Bottom); Bottom.VSplitLeft(250.0f, &Button, &Bottom); - UI()->DoLabel(&Button, Localize("Vote description:"), 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Vote description:"), 14.0f, TEXTALIGN_ML); Bottom.VSplitLeft(20.0f, 0, &Button); - UI()->DoLabel(&Button, Localize("Vote command:"), 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Vote command:"), 14.0f, TEXTALIGN_ML); static CLineInputBuffered s_VoteDescriptionInput; static CLineInputBuffered s_VoteCommandInput; @@ -823,10 +824,10 @@ void CMenus::RenderServerControl(CUIRect MainView) Bottom.VSplitLeft(5.0f, 0, &Bottom); Bottom.VSplitLeft(250.0f, &Button, &Bottom); - UI()->DoEditBox(&s_VoteDescriptionInput, &Button, 14.0f); + Ui()->DoEditBox(&s_VoteDescriptionInput, &Button, 14.0f); Bottom.VMargin(20.0f, &Button); - UI()->DoEditBox(&s_VoteCommandInput, &Button, 14.0f); + Ui()->DoEditBox(&s_VoteCommandInput, &Button, 14.0f); } } } @@ -840,45 +841,57 @@ void CMenus::RenderInGameNetwork(CUIRect MainView) int NewPage = g_Config.m_UiPage; - TabBar.VSplitLeft(100.0f, &Button, &TabBar); + TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); + + TabBar.VSplitLeft(75.0f, &Button, &TabBar); static CButtonContainer s_InternetButton; - if(DoButton_MenuTab(&s_InternetButton, Localize("Internet"), g_Config.m_UiPage == PAGE_INTERNET, &Button, IGraphics::CORNER_NONE)) + if(DoButton_MenuTab(&s_InternetButton, FONT_ICON_EARTH_AMERICAS, g_Config.m_UiPage == PAGE_INTERNET, &Button, IGraphics::CORNER_NONE)) { - if(g_Config.m_UiPage != PAGE_INTERNET) - { - if(g_Config.m_UiPage != PAGE_FAVORITES) - Client()->RequestDDNetInfo(); - ServerBrowser()->Refresh(IServerBrowser::TYPE_INTERNET); - } NewPage = PAGE_INTERNET; } + GameClient()->m_Tooltips.DoToolTip(&s_InternetButton, &Button, Localize("Internet")); - TabBar.VSplitLeft(80.0f, &Button, &TabBar); + TabBar.VSplitLeft(75.0f, &Button, &TabBar); static CButtonContainer s_LanButton; - if(DoButton_MenuTab(&s_LanButton, Localize("LAN"), g_Config.m_UiPage == PAGE_LAN, &Button, IGraphics::CORNER_NONE)) + if(DoButton_MenuTab(&s_LanButton, FONT_ICON_NETWORK_WIRED, g_Config.m_UiPage == PAGE_LAN, &Button, IGraphics::CORNER_NONE)) { - if(g_Config.m_UiPage != PAGE_LAN) - ServerBrowser()->Refresh(IServerBrowser::TYPE_LAN); NewPage = PAGE_LAN; } + GameClient()->m_Tooltips.DoToolTip(&s_LanButton, &Button, Localize("LAN")); - TabBar.VSplitLeft(110.0f, &Button, &TabBar); + TabBar.VSplitLeft(75.0f, &Button, &TabBar); static CButtonContainer s_FavoritesButton; - if(DoButton_MenuTab(&s_FavoritesButton, Localize("Favorites"), g_Config.m_UiPage == PAGE_FAVORITES, &Button, IGraphics::CORNER_NONE)) + if(DoButton_MenuTab(&s_FavoritesButton, FONT_ICON_STAR, g_Config.m_UiPage == PAGE_FAVORITES, &Button, IGraphics::CORNER_NONE)) + { + NewPage = PAGE_FAVORITES; + } + GameClient()->m_Tooltips.DoToolTip(&s_FavoritesButton, &Button, Localize("Favorites")); + + size_t FavoriteCommunityIndex = 0; + static CButtonContainer s_aFavoriteCommunityButtons[5]; + static_assert(std::size(s_aFavoriteCommunityButtons) == (size_t)PAGE_FAVORITE_COMMUNITY_5 - PAGE_FAVORITE_COMMUNITY_1 + 1); + for(const CCommunity *pCommunity : ServerBrowser()->FavoriteCommunities()) { - if(g_Config.m_UiPage != PAGE_FAVORITES) + TabBar.VSplitLeft(75.0f, &Button, &TabBar); + const int Page = PAGE_FAVORITE_COMMUNITY_1 + FavoriteCommunityIndex; + if(DoButton_MenuTab(&s_aFavoriteCommunityButtons[FavoriteCommunityIndex], FONT_ICON_ELLIPSIS, g_Config.m_UiPage == Page, &Button, IGraphics::CORNER_NONE, nullptr, nullptr, nullptr, nullptr, 10.0f, FindCommunityIcon(pCommunity->Id()))) { - if(g_Config.m_UiPage != PAGE_INTERNET) - Client()->RequestDDNetInfo(); - ServerBrowser()->Refresh(IServerBrowser::TYPE_FAVORITES); + NewPage = Page; } - NewPage = PAGE_FAVORITES; + GameClient()->m_Tooltips.DoToolTip(&s_aFavoriteCommunityButtons[FavoriteCommunityIndex], &Button, pCommunity->Name()); + + ++FavoriteCommunityIndex; + if(FavoriteCommunityIndex >= std::size(s_aFavoriteCommunityButtons)) + break; } + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); + if(NewPage != g_Config.m_UiPage) { - if(Client()->State() != IClient::STATE_OFFLINE) - SetMenuPage(NewPage); + SetMenuPage(NewPage); } RenderServerbrowser(MainView); @@ -920,7 +933,7 @@ void CMenus::GhostlistPopulate() m_vGhosts.clear(); m_GhostPopulateStartTime = time_get_nanoseconds(); Storage()->ListDirectoryInfo(IStorage::TYPE_ALL, m_pClient->m_Ghost.GetGhostDir(), GhostlistFetchCallback, this); - std::sort(m_vGhosts.begin(), m_vGhosts.end()); + SortGhostlist(); CGhostItem *pOwnGhost = 0; for(auto &Ghost : m_vGhosts) @@ -952,33 +965,31 @@ void CMenus::UpdateOwnGhost(CGhostItem Item) if(m_vGhosts[i].m_Own) Own = i; - if(Own != -1) + if(Own == -1) { - if(g_Config.m_ClRaceGhostSaveBest) - { - if(Item.HasFile() || !m_vGhosts[Own].HasFile()) - DeleteGhostItem(Own); - } - if(m_vGhosts[Own].m_Time > Item.m_Time) - { - Item.m_Own = true; - m_vGhosts[Own].m_Own = false; - m_vGhosts[Own].m_Slot = -1; - } - else - { - Item.m_Own = false; - Item.m_Slot = -1; - } + Item.m_Own = true; } - else + else if(g_Config.m_ClRaceGhostSaveBest && (Item.HasFile() || !m_vGhosts[Own].HasFile())) + { + Item.m_Own = true; + DeleteGhostItem(Own); + } + else if(m_vGhosts[Own].m_Time > Item.m_Time) { Item.m_Own = true; + m_vGhosts[Own].m_Own = false; + m_vGhosts[Own].m_Slot = -1; + } + else + { + Item.m_Own = false; + Item.m_Slot = -1; } Item.m_Date = std::time(0); Item.m_Failed = false; m_vGhosts.insert(std::lower_bound(m_vGhosts.begin(), m_vGhosts.end(), Item), Item); + SortGhostlist(); } void CMenus::DeleteGhostItem(int Index) @@ -988,6 +999,22 @@ void CMenus::DeleteGhostItem(int Index) m_vGhosts.erase(m_vGhosts.begin() + Index); } +void CMenus::SortGhostlist() +{ + if(g_Config.m_GhSort == GHOST_SORT_NAME) + std::stable_sort(m_vGhosts.begin(), m_vGhosts.end(), [](const CGhostItem &Left, const CGhostItem &Right) { + return g_Config.m_GhSortOrder ? (str_comp(Left.m_aPlayer, Right.m_aPlayer) > 0) : (str_comp(Left.m_aPlayer, Right.m_aPlayer) < 0); + }); + else if(g_Config.m_GhSort == GHOST_SORT_TIME) + std::stable_sort(m_vGhosts.begin(), m_vGhosts.end(), [](const CGhostItem &Left, const CGhostItem &Right) { + return g_Config.m_GhSortOrder ? (Left.m_Time > Right.m_Time) : (Left.m_Time < Right.m_Time); + }); + else if(g_Config.m_GhSort == GHOST_SORT_DATE) + std::stable_sort(m_vGhosts.begin(), m_vGhosts.end(), [](const CGhostItem &Left, const CGhostItem &Right) { + return g_Config.m_GhSortOrder ? (Left.m_Date > Right.m_Date) : (Left.m_Date < Right.m_Date); + }); +} + void CMenus::RenderGhost(CUIRect MainView) { // render background @@ -1008,13 +1035,14 @@ void CMenus::RenderGhost(CUIRect MainView) Headers.Draw(ColorRGBA(1, 1, 1, 0.25f), IGraphics::CORNER_T, 5.0f); Headers.VSplitRight(20.0f, &Headers, 0); - struct CColumn + class CColumn { + public: const char *m_pCaption; int m_Id; + int m_Sort; float m_Width; CUIRect m_Rect; - CUIRect m_Spacer; }; enum @@ -1026,11 +1054,11 @@ void CMenus::RenderGhost(CUIRect MainView) }; static CColumn s_aCols[] = { - {"", -1, 2.0f, {0}, {0}}, - {"", COL_ACTIVE, 30.0f, {0}, {0}}, - {Localizable("Name"), COL_NAME, 200.0f, {0}, {0}}, - {Localizable("Time"), COL_TIME, 90.0f, {0}, {0}}, - {Localizable("Date"), COL_DATE, 150.0f, {0}, {0}}, + {"", -1, GHOST_SORT_NONE, 2.0f, {0}}, + {"", COL_ACTIVE, GHOST_SORT_NONE, 30.0f, {0}}, + {Localizable("Name"), COL_NAME, GHOST_SORT_NAME, 200.0f, {0}}, + {Localizable("Time"), COL_TIME, GHOST_SORT_TIME, 90.0f, {0}}, + {Localizable("Date"), COL_DATE, GHOST_SORT_DATE, 150.0f, {0}}, }; int NumCols = std::size(s_aCols); @@ -1041,12 +1069,26 @@ void CMenus::RenderGhost(CUIRect MainView) Headers.VSplitLeft(s_aCols[i].m_Width, &s_aCols[i].m_Rect, &Headers); if(i + 1 < NumCols) - Headers.VSplitLeft(2, &s_aCols[i].m_Spacer, &Headers); + Headers.VSplitLeft(2, nullptr, &Headers); } // do headers - for(int i = 0; i < NumCols; i++) - DoButton_GridHeader(&s_aCols[i].m_Id, Localize(s_aCols[i].m_pCaption), 0, &s_aCols[i].m_Rect); + for(const auto &Col : s_aCols) + { + if(DoButton_GridHeader(&Col.m_Id, Localize(Col.m_pCaption), g_Config.m_GhSort == Col.m_Sort, &Col.m_Rect)) + { + if(Col.m_Sort != GHOST_SORT_NONE) + { + if(g_Config.m_GhSort == Col.m_Sort) + g_Config.m_GhSortOrder ^= 1; + else + g_Config.m_GhSortOrder = 0; + g_Config.m_GhSort = Col.m_Sort; + + SortGhostlist(); + } + } + } View.Draw(ColorRGBA(0, 0, 0, 0.15f), 0, 0); @@ -1105,19 +1147,19 @@ void CMenus::RenderGhost(CUIRect MainView) } else if(Id == COL_NAME) { - UI()->DoLabel(&Button, pGhost->m_aPlayer, 12.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Button, pGhost->m_aPlayer, 12.0f, TEXTALIGN_ML); } else if(Id == COL_TIME) { char aBuf[64]; str_time(pGhost->m_Time / 10, TIME_HOURS_CENTISECS, aBuf, sizeof(aBuf)); - UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_ML); } else if(Id == COL_DATE) { char aBuf[64]; str_timestamp_ex(pGhost->m_Date, aBuf, sizeof(aBuf), FORMAT_SPACE); - UI()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Button, aBuf, 12.0f, TEXTALIGN_ML); } } @@ -1149,10 +1191,7 @@ void CMenus::RenderGhost(CUIRect MainView) char aBuf[IO_MAX_PATH_LENGTH]; Storage()->GetCompletePath(IStorage::TYPE_SAVE, "ghosts", aBuf, sizeof(aBuf)); Storage()->CreateFolder("ghosts", IStorage::TYPE_SAVE); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } Status.VSplitLeft(5.0f, &Button, &Status); @@ -1246,6 +1285,5 @@ void CMenus::RenderIngameHint() Graphics()->MapScreen(0, 0, Width, 300); TextRender()->TextColor(1, 1, 1, 1); TextRender()->Text(5, 280, 5, Localize("Menu opened. Press Esc key again to close menu."), -1.0f); - UI()->MapScreen(); + Ui()->MapScreen(); } - diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp index 4a8a52122d..95a62cc446 100644 --- a/src/game/client/components/menus_settings.cpp +++ b/src/game/client/components/menus_settings.cpp @@ -1,18 +1,20 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include #include #include #include #include -#include -//#include +#include #include +#include #include #include -//#include -//#include -#include +#include + +#include + #include #include #include @@ -32,7 +34,7 @@ #include #include -//#include +#include #include #include #include @@ -44,9 +46,10 @@ CMenusKeyBinder CMenus::m_Binder; CMenusKeyBinder::CMenusKeyBinder() { + m_pKeyReaderId = nullptr; m_TakeKey = false; m_GotKey = false; - m_ModifierCombination = 0; + m_ModifierCombination = CBinds::MODIFIER_NONE; } bool CMenusKeyBinder::OnInput(const IInput::CEvent &Event) @@ -63,7 +66,7 @@ bool CMenusKeyBinder::OnInput(const IInput::CEvent &Event) m_ModifierCombination = CBinds::GetModifierMask(Input()); if(m_ModifierCombination == CBinds::GetModifierMaskOfKey(Event.m_Key)) { - m_ModifierCombination = 0; + m_ModifierCombination = CBinds::MODIFIER_NONE; } } return true; @@ -75,22 +78,20 @@ bool CMenusKeyBinder::OnInput(const IInput::CEvent &Event) void CMenus::RenderSettingsGeneral(CUIRect MainView) { char aBuf[128 + IO_MAX_PATH_LENGTH]; - CUIRect Label, Button, Left, Right, Game, Client; - MainView.HSplitTop(150.0f, &Game, &Client); + CUIRect Label, Button, Left, Right, Game, ClientSettings; + MainView.HSplitTop(150.0f, &Game, &ClientSettings); // game { // headline - Game.HSplitTop(20.0f, &Label, &Game); - UI()->DoLabel(&Label, Localize("Game"), 20.0f, TEXTALIGN_ML); - Game.Margin(5.0f, &Game); - Game.VSplitMid(&Left, &Right); - Left.VSplitRight(5.0f, &Left, 0); - Right.VMargin(5.0f, &Right); + Game.HSplitTop(30.0f, &Label, &Game); + Ui()->DoLabel(&Label, Localize("Game"), 20.0f, TEXTALIGN_ML); + Game.HSplitTop(5.0f, nullptr, &Game); + Game.VSplitMid(&Left, nullptr, 20.0f); // dynamic camera Left.HSplitTop(20.0f, &Button, &Left); - bool IsDyncam = g_Config.m_ClDyncam || g_Config.m_ClMouseFollowfactor > 0; + const bool IsDyncam = g_Config.m_ClDyncam || g_Config.m_ClMouseFollowfactor > 0; if(DoButton_CheckBox(&g_Config.m_ClDyncam, Localize("Dynamic Camera"), IsDyncam, &Button)) { if(IsDyncam) @@ -105,7 +106,7 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView) } // smooth dynamic camera - Left.HSplitTop(5.0f, 0, &Left); + Left.HSplitTop(5.0f, nullptr, &Left); Left.HSplitTop(20.0f, &Button, &Left); if(g_Config.m_ClDyncam) { @@ -124,13 +125,13 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView) } // weapon pickup - Left.HSplitTop(5.0f, 0, &Left); + Left.HSplitTop(5.0f, nullptr, &Left); Left.HSplitTop(20.0f, &Button, &Left); if(DoButton_CheckBox(&g_Config.m_ClAutoswitchWeapons, Localize("Switch weapon on pickup"), g_Config.m_ClAutoswitchWeapons, &Button)) g_Config.m_ClAutoswitchWeapons ^= 1; // weapon out of ammo autoswitch - Left.HSplitTop(5.0f, 0, &Left); + Left.HSplitTop(5.0f, nullptr, &Left); Left.HSplitTop(20.0f, &Button, &Left); if(DoButton_CheckBox(&g_Config.m_ClAutoswitchWeaponsOutOfAmmo, Localize("Switch weapon when out of ammo"), g_Config.m_ClAutoswitchWeaponsOutOfAmmo, &Button)) g_Config.m_ClAutoswitchWeaponsOutOfAmmo ^= 1; @@ -139,103 +140,88 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView) // client { // headline - Client.HSplitTop(20.0f, &Label, &Client); - UI()->DoLabel(&Label, Localize("Client"), 20.0f, TEXTALIGN_ML); - Client.Margin(5.0f, &Client); - Client.VSplitMid(&Left, &Right); - Left.VSplitRight(5.0f, &Left, 0); - Right.VMargin(5.0f, &Right); + ClientSettings.HSplitTop(30.0f, &Label, &ClientSettings); + Ui()->DoLabel(&Label, Localize("Client"), 20.0f, TEXTALIGN_ML); + ClientSettings.HSplitTop(5.0f, nullptr, &ClientSettings); + ClientSettings.VSplitMid(&Left, &Right, 20.0f); // skip main menu - Left.HSplitTop(5.0f, 0, &Left); Left.HSplitTop(20.0f, &Button, &Left); if(DoButton_CheckBox(&g_Config.m_ClSkipStartMenu, Localize("Skip the main menu"), g_Config.m_ClSkipStartMenu, &Button)) g_Config.m_ClSkipStartMenu ^= 1; - float SliderGroupMargin = 10.0f; - - // auto demo settings - { - Right.HSplitTop(40.0f, nullptr, &Right); - Right.HSplitTop(20.0f, &Button, &Right); - if(DoButton_CheckBox(&g_Config.m_ClAutoDemoRecord, Localize("Automatically record demos"), g_Config.m_ClAutoDemoRecord, &Button)) - g_Config.m_ClAutoDemoRecord ^= 1; - - Right.HSplitTop(2 * 20.0f, &Button, &Right); - if(g_Config.m_ClAutoDemoRecord) - UI()->DoScrollbarOption(&g_Config.m_ClAutoDemoMax, &g_Config.m_ClAutoDemoMax, &Button, Localize("Max demos"), 1, 1000, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_INFINITE | CUI::SCROLLBAR_OPTION_MULTILINE); - - Right.HSplitTop(SliderGroupMargin, nullptr, &Right); - Right.HSplitTop(20.0f, &Button, &Right); - if(DoButton_CheckBox(&g_Config.m_ClAutoScreenshot, Localize("Automatically take game over screenshot"), g_Config.m_ClAutoScreenshot, &Button)) - g_Config.m_ClAutoScreenshot ^= 1; - - Right.HSplitTop(2 * 20.0f, &Button, &Right); - if(g_Config.m_ClAutoScreenshot) - UI()->DoScrollbarOption(&g_Config.m_ClAutoScreenshotMax, &g_Config.m_ClAutoScreenshotMax, &Button, Localize("Max Screenshots"), 1, 1000, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_INFINITE | CUI::SCROLLBAR_OPTION_MULTILINE); - } - Left.HSplitTop(10.0f, nullptr, &Left); Left.HSplitTop(20.0f, &Button, &Left); - UI()->DoScrollbarOption(&g_Config.m_ClRefreshRate, &g_Config.m_ClRefreshRate, &Button, Localize("Refresh Rate"), 10, 10000, &CUI::ms_LogarithmicScrollbarScale, CUI::SCROLLBAR_OPTION_INFINITE, " Hz"); + str_copy(aBuf, " "); + str_append(aBuf, Localize("Hz", "Hertz")); + Ui()->DoScrollbarOption(&g_Config.m_ClRefreshRate, &g_Config.m_ClRefreshRate, &Button, Localize("Refresh Rate"), 10, 10000, &CUi::ms_LogarithmicScrollbarScale, CUi::SCROLLBAR_OPTION_INFINITE, aBuf); Left.HSplitTop(5.0f, nullptr, &Left); Left.HSplitTop(20.0f, &Button, &Left); - int s_LowerRefreshRate; + static int s_LowerRefreshRate; if(DoButton_CheckBox(&s_LowerRefreshRate, Localize("Save power by lowering refresh rate (higher input latency)"), g_Config.m_ClRefreshRate <= 480 && g_Config.m_ClRefreshRate != 0, &Button)) g_Config.m_ClRefreshRate = g_Config.m_ClRefreshRate > 480 || g_Config.m_ClRefreshRate == 0 ? 480 : 0; CUIRect SettingsButton; - Left.HSplitBottom(25.0f, &Left, &SettingsButton); - - SettingsButton.HSplitTop(5.0f, 0, &SettingsButton); - static CButtonContainer s_SettingsButtonID; - if(DoButton_Menu(&s_SettingsButtonID, Localize("Settings file"), 0, &SettingsButton)) + Left.HSplitBottom(20.0f, &Left, &SettingsButton); + Left.HSplitBottom(5.0f, &Left, nullptr); + static CButtonContainer s_SettingsButtonId; + if(DoButton_Menu(&s_SettingsButtonId, Localize("Settings file"), 0, &SettingsButton)) { Storage()->GetCompletePath(IStorage::TYPE_SAVE, CONFIG_FILE, aBuf, sizeof(aBuf)); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } - GameClient()->m_Tooltips.DoToolTip(&s_SettingsButtonID, &SettingsButton, Localize("Open the settings file")); + GameClient()->m_Tooltips.DoToolTip(&s_SettingsButtonId, &SettingsButton, Localize("Open the settings file")); - Left.HSplitTop(15.0f, 0, &Left); CUIRect ConfigButton; - Left.HSplitBottom(25.0f, &Left, &ConfigButton); - - ConfigButton.HSplitTop(5.0f, 0, &ConfigButton); - static CButtonContainer s_ConfigButtonID; - if(DoButton_Menu(&s_ConfigButtonID, Localize("Config directory"), 0, &ConfigButton)) + Left.HSplitBottom(20.0f, &Left, &ConfigButton); + Left.HSplitBottom(5.0f, &Left, nullptr); + static CButtonContainer s_ConfigButtonId; + if(DoButton_Menu(&s_ConfigButtonId, Localize("Config directory"), 0, &ConfigButton)) { Storage()->GetCompletePath(IStorage::TYPE_SAVE, "", aBuf, sizeof(aBuf)); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } - GameClient()->m_Tooltips.DoToolTip(&s_ConfigButtonID, &ConfigButton, Localize("Open the directory that contains the configuration and user files")); + GameClient()->m_Tooltips.DoToolTip(&s_ConfigButtonId, &ConfigButton, Localize("Open the directory that contains the configuration and user files")); - Left.HSplitTop(15.0f, 0, &Left); CUIRect DirectoryButton; - Left.HSplitBottom(25.0f, &Left, &DirectoryButton); - RenderThemeSelection(Left); - - DirectoryButton.HSplitTop(5.0f, 0, &DirectoryButton); - static CButtonContainer s_ThemesButtonID; - if(DoButton_Menu(&s_ThemesButtonID, Localize("Themes directory"), 0, &DirectoryButton)) + Left.HSplitBottom(20.0f, &Left, &DirectoryButton); + Left.HSplitBottom(5.0f, &Left, nullptr); + static CButtonContainer s_ThemesButtonId; + if(DoButton_Menu(&s_ThemesButtonId, Localize("Themes directory"), 0, &DirectoryButton)) { Storage()->GetCompletePath(IStorage::TYPE_SAVE, "themes", aBuf, sizeof(aBuf)); Storage()->CreateFolder("themes", IStorage::TYPE_SAVE); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); + } + GameClient()->m_Tooltips.DoToolTip(&s_ThemesButtonId, &DirectoryButton, Localize("Open the directory to add custom themes")); + + Left.HSplitTop(20.0f, nullptr, &Left); + RenderThemeSelection(Left); + + // auto demo settings + { + Right.HSplitTop(40.0f, nullptr, &Right); + Right.HSplitTop(20.0f, &Button, &Right); + if(DoButton_CheckBox(&g_Config.m_ClAutoDemoRecord, Localize("Automatically record demos"), g_Config.m_ClAutoDemoRecord, &Button)) + g_Config.m_ClAutoDemoRecord ^= 1; + + Right.HSplitTop(2 * 20.0f, &Button, &Right); + if(g_Config.m_ClAutoDemoRecord) + Ui()->DoScrollbarOption(&g_Config.m_ClAutoDemoMax, &g_Config.m_ClAutoDemoMax, &Button, Localize("Max demos"), 1, 1000, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_MULTILINE); + + Right.HSplitTop(10.0f, nullptr, &Right); + Right.HSplitTop(20.0f, &Button, &Right); + if(DoButton_CheckBox(&g_Config.m_ClAutoScreenshot, Localize("Automatically take game over screenshot"), g_Config.m_ClAutoScreenshot, &Button)) + g_Config.m_ClAutoScreenshot ^= 1; + + Right.HSplitTop(2 * 20.0f, &Button, &Right); + if(g_Config.m_ClAutoScreenshot) + Ui()->DoScrollbarOption(&g_Config.m_ClAutoScreenshotMax, &g_Config.m_ClAutoScreenshotMax, &Button, Localize("Max Screenshots"), 1, 1000, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_MULTILINE); } - GameClient()->m_Tooltips.DoToolTip(&s_ThemesButtonID, &DirectoryButton, Localize("Open the directory to add custom themes")); // auto statboard screenshot { - Right.HSplitTop(SliderGroupMargin, nullptr, &Right); + Right.HSplitTop(10.0f, nullptr, &Right); Right.HSplitTop(20.0f, &Button, &Right); if(DoButton_CheckBox(&g_Config.m_ClAutoStatboardScreenshot, Localize("Automatically take statboard screenshot"), g_Config.m_ClAutoStatboardScreenshot, &Button)) { @@ -244,12 +230,12 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView) Right.HSplitTop(2 * 20.0f, &Button, &Right); if(g_Config.m_ClAutoStatboardScreenshot) - UI()->DoScrollbarOption(&g_Config.m_ClAutoStatboardScreenshotMax, &g_Config.m_ClAutoStatboardScreenshotMax, &Button, Localize("Max Screenshots"), 1, 1000, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_INFINITE | CUI::SCROLLBAR_OPTION_MULTILINE); + Ui()->DoScrollbarOption(&g_Config.m_ClAutoStatboardScreenshotMax, &g_Config.m_ClAutoStatboardScreenshotMax, &Button, Localize("Max Screenshots"), 1, 1000, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_MULTILINE); } // auto statboard csv { - Right.HSplitTop(SliderGroupMargin, nullptr, &Right); + Right.HSplitTop(10.0f, nullptr, &Right); Right.HSplitTop(20.0f, &Button, &Right); if(DoButton_CheckBox(&g_Config.m_ClAutoCSV, Localize("Automatically create statboard csv"), g_Config.m_ClAutoCSV, &Button)) { @@ -258,7 +244,7 @@ void CMenus::RenderSettingsGeneral(CUIRect MainView) Right.HSplitTop(2 * 20.0f, &Button, &Right); if(g_Config.m_ClAutoCSV) - UI()->DoScrollbarOption(&g_Config.m_ClAutoCSVMax, &g_Config.m_ClAutoCSVMax, &Button, Localize("Max CSVs"), 1, 1000, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_INFINITE | CUI::SCROLLBAR_OPTION_MULTILINE); + Ui()->DoScrollbarOption(&g_Config.m_ClAutoCSVMax, &g_Config.m_ClAutoCSVMax, &Button, Localize("Max CSVs"), 1, 1000, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_MULTILINE); } } } @@ -271,120 +257,103 @@ void CMenus::SetNeedSendInfo() m_NeedSendinfo = true; } -int CMenus::RenderSettingsPlayer(CUIRect MainView) +void CMenus::RenderSettingsPlayer(CUIRect MainView) { - CUIRect Button, Label, Dummy; - MainView.HSplitTop(10.0f, 0, &MainView); - - char *pName = g_Config.m_PlayerName; - const char *pNameFallback = Client()->PlayerName(); - char *pClan = g_Config.m_PlayerClan; - int *pCountry = &g_Config.m_PlayerCountry; + CUIRect TabBar, PlayerTab, DummyTab, ChangeInfo, QuickSearch; + MainView.HSplitTop(20.0f, &TabBar, &MainView); + TabBar.VSplitMid(&TabBar, &ChangeInfo, 20.f); + TabBar.VSplitMid(&PlayerTab, &DummyTab); + MainView.HSplitTop(10.0f, nullptr, &MainView); - if(m_Dummy) + static CButtonContainer s_PlayerTabButton; + if(DoButton_MenuTab(&s_PlayerTabButton, Localize("Player"), !m_Dummy, &PlayerTab, IGraphics::CORNER_L, nullptr, nullptr, nullptr, nullptr, 4.0f)) { - pName = g_Config.m_ClDummyName; - pNameFallback = Client()->DummyName(); - pClan = g_Config.m_ClDummyClan; - pCountry = &g_Config.m_ClDummyCountry; + m_Dummy = false; } - // player name - MainView.HSplitTop(20.0f, &Button, &MainView); - Button.VSplitLeft(80.0f, &Label, &Button); - Button.VSplitLeft(150.0f, &Button, 0); - char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "%s:", Localize("Name")); - UI()->DoLabel(&Label, aBuf, 14.0f, TEXTALIGN_ML); - static CLineInput s_NameInput; - - if(strcmp(Client()->PlayerName(), "-StormAx") == 0 || strcmp(Client()->PlayerName(), "meloƞ") == 0 || strcmp(Client()->PlayerName(), "我叫芙焦") == 0 || strcmp(Client()->PlayerName(), "Mʎɹ シ") == 0 || strcmp(Client()->PlayerName(), "Cheeru") == 0 || strcmp(Client()->PlayerName(), "Mónik") == 0) + static CButtonContainer s_DummyTabButton; + if(DoButton_MenuTab(&s_DummyTabButton, Localize("Dummy"), m_Dummy, &DummyTab, IGraphics::CORNER_R, nullptr, nullptr, nullptr, nullptr, 4.0f)) { - ColorRGBA col = color_cast(ColorHSVA(round_to_int(LocalTime() * 30.f) % 255 / 255.f, 1.f, 1.f)); - TextRender()->TextColor(col); + m_Dummy = true; } - s_NameInput.SetBuffer(pName, sizeof(g_Config.m_PlayerName)); - s_NameInput.SetEmptyText(pNameFallback); - if(UI()->DoEditBox(&s_NameInput, &Button, 14.0f)) + if(Client()->State() == IClient::STATE_ONLINE && m_pClient->m_NextChangeInfo && m_pClient->m_NextChangeInfo > Client()->GameTick(g_Config.m_ClDummy)) { - SetNeedSendInfo(); + char aChangeInfo[128], aTimeLeft[32]; + str_format(aTimeLeft, sizeof(aTimeLeft), Localize("%ds left"), (m_pClient->m_NextChangeInfo - Client()->GameTick(g_Config.m_ClDummy) + Client()->GameTickSpeed() - 1) / Client()->GameTickSpeed()); + str_format(aChangeInfo, sizeof(aChangeInfo), "%s: %s", Localize("Player info change cooldown"), aTimeLeft); + Ui()->DoLabel(&ChangeInfo, aChangeInfo, 10.f, TEXTALIGN_ML); } - //Kill game when start using m_PlayerName - if(!str_comp(g_Config.m_PlayerName, "nigger")) - { - dbg_assert_imp("UR NAME SUCK.js", -1, 0, "YOU ARE FUCKING RACIST DON'T USE THIS CLIENT EVER AGAIN"); - } - if(!str_comp(g_Config.m_PlayerName, "faggot")) - { - dbg_assert_imp("UR NAME SUCK.js", -1, 0, "YOU ARE FUCKING RACIST DON'T USE THIS CLIENT EVER AGAIN"); - } - if(!str_comp(g_Config.m_PlayerName, "pingvin?")) - { - dbg_assert_imp("UR NAME SUCK.js", -1, 0, "Фу, умри русня"); - } - if(!str_comp(g_Config.m_PlayerName, "Sedonya")) - { - dbg_assert_imp("UR NAME SUCK.js", -1, 0, "Кисао привет"); - } - if(!str_comp(g_Config.m_PlayerName, "Cheeru")) - { - dbg_assert_imp("Hey!.js", -1, 0, "Привет Aйви, надеюсь хорошо поживаешь? Если все таки заинтересовалась клиент отпиши, пожалуйста, очень важноPlayerName()); + s_ClanInput.SetBuffer(g_Config.m_PlayerClan, sizeof(g_Config.m_PlayerClan)); } - - if(!str_comp(g_Config.m_PlayerName, "-StormAx")) + else { - dbg_assert_imp("FakeNameList.txt", -1, 0, "I see you tried to use developer name, :clueless:" ); + pCountry = &g_Config.m_ClDummyCountry; + s_NameInput.SetBuffer(g_Config.m_ClDummyName, sizeof(g_Config.m_ClDummyName)); + s_NameInput.SetEmptyText(Client()->DummyName()); + s_ClanInput.SetBuffer(g_Config.m_ClDummyClan, sizeof(g_Config.m_ClDummyClan)); } - if(!str_comp(g_Config.m_PlayerName, "Mʎɹ シ")) + // player name + CUIRect Button, Label; + MainView.HSplitTop(20.0f, &Button, &MainView); + Button.VSplitLeft(80.0f, &Label, &Button); + Button.VSplitLeft(150.0f, &Button, nullptr); + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "%s:", Localize("Name")); + Ui()->DoLabel(&Label, aBuf, 14.0f, TEXTALIGN_ML); + if(Ui()->DoEditBox(&s_NameInput, &Button, 14.0f)) { - dbg_assert_imp("FakeNameList.txt", -1, 0, "You was so clueless :Invalid:" ); + SetNeedSendInfo(); } -*/ + // player clan - MainView.HSplitTop(5.0f, 0, &MainView); + MainView.HSplitTop(5.0f, nullptr, &MainView); MainView.HSplitTop(20.0f, &Button, &MainView); Button.VSplitLeft(80.0f, &Label, &Button); - Button.VSplitLeft(200.0f, &Button, &Dummy); - Button.VSplitLeft(150.0f, &Button, 0); + Button.VSplitLeft(150.0f, &Button, nullptr); str_format(aBuf, sizeof(aBuf), "%s:", Localize("Clan")); - UI()->DoLabel(&Label, aBuf, 14.0f, TEXTALIGN_ML); - static CLineInput s_ClanInput; - if(strcmp(g_Config.m_PlayerClan, "Inner peace") == 0 || strcmp(g_Config.m_PlayerClan, "Vegaming") == 0) - { - ColorRGBA col = color_cast(ColorHSVA(round_to_int(LocalTime() * 15.f) % 255 / 255.f, 1.f, 1.f)); - TextRender()->TextColor(col); - } - s_ClanInput.SetBuffer(pClan, sizeof(g_Config.m_PlayerClan)); - if(UI()->DoEditBox(&s_ClanInput, &Button, 14.0f)) + Ui()->DoLabel(&Label, aBuf, 14.0f, TEXTALIGN_ML); + if(Ui()->DoEditBox(&s_ClanInput, &Button, 14.0f)) { SetNeedSendInfo(); } - if(DoButton_CheckBox(&m_Dummy, Localize("Dummy settings"), m_Dummy, &Dummy)) + // country flag selector + static CLineInputBuffered<25> s_FlagFilterInput; + + std::vector vpFilteredFlags; + for(size_t i = 0; i < m_pClient->m_CountryFlags.Num(); ++i) { - m_Dummy ^= 1; + const CCountryFlags::CCountryFlag *pEntry = m_pClient->m_CountryFlags.GetByIndex(i); + if(!str_find_nocase(pEntry->m_aCountryCodeString, s_FlagFilterInput.GetString())) + continue; + vpFilteredFlags.push_back(pEntry); } - GameClient()->m_Tooltips.DoToolTip(&m_Dummy, &Dummy, Localize("Toggle to edit your dummy settings")); - // country flag selector - MainView.HSplitTop(20.0f, 0, &MainView); + MainView.HSplitTop(10.0f, nullptr, &MainView); + MainView.HSplitBottom(20.0f, &MainView, &QuickSearch); + MainView.HSplitBottom(5.0f, &MainView, nullptr); + QuickSearch.VSplitLeft(220.0f, &QuickSearch, nullptr); + int OldSelected = -1; static CListBox s_ListBox; - s_ListBox.DoStart(50.0f, m_pClient->m_CountryFlags.Num(), 10, 3, OldSelected, &MainView); + s_ListBox.DoStart(48.0f, vpFilteredFlags.size(), 10, 3, OldSelected, &MainView); - for(size_t i = 0; i < m_pClient->m_CountryFlags.Num(); ++i) + for(size_t i = 0; i < vpFilteredFlags.size(); i++) { - const CCountryFlags::CCountryFlag *pEntry = m_pClient->m_CountryFlags.GetByIndex(i); + const CCountryFlags::CCountryFlag *pEntry = vpFilteredFlags[i]; + if(pEntry->m_CountryCode == *pCountry) OldSelected = i; @@ -396,24 +365,25 @@ int CMenus::RenderSettingsPlayer(CUIRect MainView) Item.m_Rect.Margin(5.0f, &FlagRect); FlagRect.HSplitBottom(12.0f, &FlagRect, &Label); Label.HSplitTop(2.0f, nullptr, &Label); - float OldWidth = FlagRect.w; + const float OldWidth = FlagRect.w; FlagRect.w = FlagRect.h * 2; FlagRect.x += (OldWidth - FlagRect.w) / 2.0f; m_pClient->m_CountryFlags.Render(pEntry->m_CountryCode, ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f), FlagRect.x, FlagRect.y, FlagRect.w, FlagRect.h); if(pEntry->m_Texture.IsValid()) { - UI()->DoLabel(&Label, pEntry->m_aCountryCodeString, 10.0f, TEXTALIGN_MC); + Ui()->DoLabel(&Label, pEntry->m_aCountryCodeString, 10.0f, TEXTALIGN_MC); } } const int NewSelected = s_ListBox.DoEnd(); if(OldSelected != NewSelected) { - *pCountry = m_pClient->m_CountryFlags.GetByIndex(NewSelected)->m_CountryCode; + *pCountry = vpFilteredFlags[NewSelected]->m_CountryCode; SetNeedSendInfo(); } - return 0; + + Ui()->DoEditBox_Search(&s_FlagFilterInput, &QuickSearch, 14.0f, !Ui()->IsPopupOpen() && m_pClient->m_GameConsole.IsClosed()); } struct CUISkin @@ -431,73 +401,33 @@ struct CUISkin bool operator==(const char *pOther) const { return !str_comp_nocase(m_pSkin->GetName(), pOther); } }; -void CMenus::RefreshSkins() -{ - auto SkinStartLoadTime = time_get_nanoseconds(); - m_pClient->m_Skins.Refresh([&](int) { - // if skin refreshing takes to long, swap to a loading screen - if(time_get_nanoseconds() - SkinStartLoadTime > 500ms) - { - RenderLoading(Localize("Loading skin files"), "", 0, false); - } - }); - if(Client()->State() == IClient::STATE_ONLINE || Client()->State() == IClient::STATE_DEMOPLAYBACK) - { - m_pClient->RefindSkins(); - } -} - -void CMenus::RandomSkin() +void CMenus::OnRefreshSkins() { - static const float s_aSchemes[] = {1.0f / 2.0f, 1.0f / 3.0f, 1.0f / -3.0f, 1.0f / 12.0f, 1.0f / -12.0f}; // complementary, triadic, analogous - - float GoalSat = random_float(0.3f, 1.0f); - float MaxBodyLht = 1.0f - GoalSat * GoalSat; // max allowed lightness before we start losing saturation - - ColorHSLA Body; - Body.h = random_float(); - Body.l = random_float(0.0f, MaxBodyLht); - Body.s = clamp(GoalSat * GoalSat / (1.0f - Body.l), 0.0f, 1.0f); - - ColorHSLA Feet; - Feet.h = std::fmod(Body.h + s_aSchemes[rand() % std::size(s_aSchemes)], 1.0f); - Feet.l = random_float(); - Feet.s = clamp(GoalSat * GoalSat / (1.0f - Feet.l), 0.0f, 1.0f); - - const char *pRandomSkinName = CSkins::VANILLA_SKINS[rand() % (std::size(CSkins::VANILLA_SKINS) - 2)]; // last 2 skins are x_ninja and x_spec - - char *pSkinName = !m_Dummy ? g_Config.m_ClPlayerSkin : g_Config.m_ClDummySkin; - unsigned *pColorBody = !m_Dummy ? &g_Config.m_ClPlayerColorBody : &g_Config.m_ClDummyColorBody; - unsigned *pColorFeet = !m_Dummy ? &g_Config.m_ClPlayerColorFeet : &g_Config.m_ClDummyColorFeet; - - mem_copy(pSkinName, pRandomSkinName, sizeof(g_Config.m_ClPlayerSkin)); - *pColorBody = Body.Pack(false); - *pColorFeet = Feet.Pack(false); - - SetNeedSendInfo(); + m_SkinListNeedsUpdate = true; } void CMenus::Con_AddFavoriteSkin(IConsole::IResult *pResult, void *pUserData) { auto *pSelf = (CMenus *)pUserData; - if(pResult->NumArguments() >= 1) + const char *pStr = pResult->GetString(0); + if(!CSkin::IsValidName(pStr)) { - pSelf->m_SkinFavorites.emplace(pResult->GetString(0)); - pSelf->m_SkinFavoritesChanged = true; + log_error("menus/settings", "Favorite skin name '%s' is not valid", pStr); + log_error("menus/settings", "%s", CSkin::m_aSkinNameRestrictions); + return; } + pSelf->m_SkinFavorites.emplace(pStr); + pSelf->m_SkinFavoritesChanged = true; } void CMenus::Con_RemFavoriteSkin(IConsole::IResult *pResult, void *pUserData) { auto *pSelf = (CMenus *)pUserData; - if(pResult->NumArguments() >= 1) + const auto it = pSelf->m_SkinFavorites.find(pResult->GetString(0)); + if(it != pSelf->m_SkinFavorites.end()) { - const auto it = pSelf->m_SkinFavorites.find(pResult->GetString(0)); - if(it != pSelf->m_SkinFavorites.end()) - { - pSelf->m_SkinFavorites.erase(it); - pSelf->m_SkinFavoritesChanged = true; - } + pSelf->m_SkinFavorites.erase(it); + pSelf->m_SkinFavoritesChanged = true; } } @@ -512,9 +442,6 @@ void CMenus::OnConfigSave(IConfigManager *pConfigManager) for(const auto &Entry : m_SkinFavorites) { char aBuffer[256]; - char aNameEscaped[256]; - char *pDst = aNameEscaped; - str_escape(&pDst, Entry.c_str(), aNameEscaped + std::size(aNameEscaped)); str_format(aBuffer, std::size(aBuffer), "add_favorite_skin \"%s\"", Entry.c_str()); pConfigManager->WriteLine(aBuffer); } @@ -522,84 +449,119 @@ void CMenus::OnConfigSave(IConfigManager *pConfigManager) void CMenus::RenderSettingsTee(CUIRect MainView) { - CUIRect Button, Label, Dummy, DummyLabel, SkinList, QuickSearch, QuickSearchClearButton, SkinDB, SkinPrefix, SkinPrefixLabel, DirectoryButton, RefreshButton, Eyes, EyesLabel, EyesTee, EyesRight; + CUIRect TabBar, PlayerTab, DummyTab, ChangeInfo; + MainView.HSplitTop(20.0f, &TabBar, &MainView); + TabBar.VSplitMid(&TabBar, &ChangeInfo, 20.f); + TabBar.VSplitMid(&PlayerTab, &DummyTab); + MainView.HSplitTop(10.0f, nullptr, &MainView); + + static CButtonContainer s_PlayerTabButton; + if(DoButton_MenuTab(&s_PlayerTabButton, Localize("Player"), !m_Dummy, &PlayerTab, IGraphics::CORNER_L, nullptr, nullptr, nullptr, nullptr, 4.0f)) + { + m_Dummy = false; + } - static bool s_InitSkinlist = true; - Eyes = MainView; + static CButtonContainer s_DummyTabButton; + if(DoButton_MenuTab(&s_DummyTabButton, Localize("Dummy"), m_Dummy, &DummyTab, IGraphics::CORNER_R, nullptr, nullptr, nullptr, nullptr, 4.0f)) + { + m_Dummy = true; + } - char *pSkinName = g_Config.m_ClPlayerSkin; - int *pUseCustomColor = &g_Config.m_ClPlayerUseCustomColor; - unsigned *pColorBody = &g_Config.m_ClPlayerColorBody; - unsigned *pColorFeet = &g_Config.m_ClPlayerColorFeet; + if(Client()->State() == IClient::STATE_ONLINE && m_pClient->m_NextChangeInfo && m_pClient->m_NextChangeInfo > Client()->GameTick(g_Config.m_ClDummy)) + { + char aChangeInfo[128], aTimeLeft[32]; + str_format(aTimeLeft, sizeof(aTimeLeft), Localize("%ds left"), (m_pClient->m_NextChangeInfo - Client()->GameTick(g_Config.m_ClDummy) + Client()->GameTickSpeed() - 1) / Client()->GameTickSpeed()); + str_format(aChangeInfo, sizeof(aChangeInfo), "%s: %s", Localize("Player info change cooldown"), aTimeLeft); + Ui()->DoLabel(&ChangeInfo, aChangeInfo, 10.f, TEXTALIGN_ML); + } - if(m_Dummy) + char *pSkinName; + size_t SkinNameSize; + int *pUseCustomColor; + unsigned *pColorBody; + unsigned *pColorFeet; + int *pEmote; + if(!m_Dummy) + { + pSkinName = g_Config.m_ClPlayerSkin; + SkinNameSize = sizeof(g_Config.m_ClPlayerSkin); + pUseCustomColor = &g_Config.m_ClPlayerUseCustomColor; + pColorBody = &g_Config.m_ClPlayerColorBody; + pColorFeet = &g_Config.m_ClPlayerColorFeet; + pEmote = &g_Config.m_ClPlayerDefaultEyes; + } + else { pSkinName = g_Config.m_ClDummySkin; + SkinNameSize = sizeof(g_Config.m_ClDummySkin); pUseCustomColor = &g_Config.m_ClDummyUseCustomColor; pColorBody = &g_Config.m_ClDummyColorBody; pColorFeet = &g_Config.m_ClDummyColorFeet; + pEmote = &g_Config.m_ClDummyDefaultEyes; } - MainView.HSplitTop(10.0f, &Label, &MainView); - Label.VSplitLeft(280.0f, &Label, &Dummy); - Label.VSplitLeft(230.0f, &Label, 0); - Dummy.VSplitLeft(170.0f, &Dummy, &SkinPrefix); - SkinPrefix.VSplitLeft(120.0f, &SkinPrefix, &EyesRight); - char aBuf[128 + IO_MAX_PATH_LENGTH]; - str_format(aBuf, sizeof(aBuf), "%s:", Localize("Your skin")); - UI()->DoLabel(&Label, aBuf, 14.0f, TEXTALIGN_ML); - - Dummy.HSplitTop(20.0f, &DummyLabel, &Dummy); - - if(DoButton_CheckBox(&m_Dummy, Localize("Dummy settings"), m_Dummy, &DummyLabel)) + const float EyeButtonSize = 40.0f; + const bool RenderEyesBelow = MainView.w < 750.0f; + CUIRect YourSkin, Checkboxes, SkinPrefix, Eyes, Button, Label; + MainView.HSplitTop(90.0f, &YourSkin, &MainView); + if(RenderEyesBelow) { - m_Dummy ^= 1; + YourSkin.VSplitLeft(MainView.w * 0.45f, &YourSkin, &Checkboxes); + Checkboxes.VSplitLeft(MainView.w * 0.35f, &Checkboxes, &SkinPrefix); + MainView.HSplitTop(5.0f, nullptr, &MainView); + MainView.HSplitTop(EyeButtonSize, &Eyes, &MainView); + Eyes.VSplitRight(EyeButtonSize * NUM_EMOTES + 5.0f * (NUM_EMOTES - 1), nullptr, &Eyes); } - GameClient()->m_Tooltips.DoToolTip(&m_Dummy, &DummyLabel, Localize("Toggle to edit your dummy settings")); - - Dummy.HSplitTop(20.0f, &DummyLabel, &Dummy); + else + { + YourSkin.VSplitRight(3 * EyeButtonSize + 2 * 5.0f, &YourSkin, &Eyes); + const float RemainderWidth = YourSkin.w; + YourSkin.VSplitLeft(RemainderWidth * 0.4f, &YourSkin, &Checkboxes); + Checkboxes.VSplitLeft(RemainderWidth * 0.35f, &Checkboxes, &SkinPrefix); + SkinPrefix.VSplitRight(20.0f, &SkinPrefix, nullptr); + } + YourSkin.VSplitRight(20.0f, &YourSkin, nullptr); + Checkboxes.VSplitRight(20.0f, &Checkboxes, nullptr); - if(DoButton_CheckBox(&g_Config.m_ClDownloadSkins, Localize("Download skins"), g_Config.m_ClDownloadSkins, &DummyLabel)) + // Checkboxes + Checkboxes.HSplitTop(20.0f, &Button, &Checkboxes); + if(DoButton_CheckBox(&g_Config.m_ClDownloadSkins, Localize("Download skins"), g_Config.m_ClDownloadSkins, &Button)) { g_Config.m_ClDownloadSkins ^= 1; - RefreshSkins(); - s_InitSkinlist = true; + m_pClient->RefreshSkins(); } - Dummy.HSplitTop(20.0f, &DummyLabel, &Dummy); - - if(DoButton_CheckBox(&g_Config.m_ClDownloadCommunitySkins, Localize("Download community skins"), g_Config.m_ClDownloadCommunitySkins, &DummyLabel)) + Checkboxes.HSplitTop(20.0f, &Button, &Checkboxes); + if(DoButton_CheckBox(&g_Config.m_ClDownloadCommunitySkins, Localize("Download community skins"), g_Config.m_ClDownloadCommunitySkins, &Button)) { g_Config.m_ClDownloadCommunitySkins ^= 1; - RefreshSkins(); - s_InitSkinlist = true; + m_pClient->RefreshSkins(); } - Dummy.HSplitTop(20.0f, &DummyLabel, &Dummy); - - if(DoButton_CheckBox(&g_Config.m_ClVanillaSkinsOnly, Localize("Vanilla skins only"), g_Config.m_ClVanillaSkinsOnly, &DummyLabel)) + Checkboxes.HSplitTop(20.0f, &Button, &Checkboxes); + if(DoButton_CheckBox(&g_Config.m_ClVanillaSkinsOnly, Localize("Vanilla skins only"), g_Config.m_ClVanillaSkinsOnly, &Button)) { g_Config.m_ClVanillaSkinsOnly ^= 1; - RefreshSkins(); - s_InitSkinlist = true; + m_pClient->RefreshSkins(); } - Dummy.HSplitTop(20.0f, &DummyLabel, &Dummy); - - if(DoButton_CheckBox(&g_Config.m_ClFatSkins, Localize("Fat skins (DDFat)"), g_Config.m_ClFatSkins, &DummyLabel)) + Checkboxes.HSplitTop(20.0f, &Button, &Checkboxes); + if(DoButton_CheckBox(&g_Config.m_ClFatSkins, Localize("Fat skins (DDFat)"), g_Config.m_ClFatSkins, &Button)) { g_Config.m_ClFatSkins ^= 1; } - SkinPrefix.HSplitTop(20.0f, &SkinPrefixLabel, &SkinPrefix); - UI()->DoLabel(&SkinPrefixLabel, Localize("Skin prefix"), 14.0f, TEXTALIGN_ML); + // Skin prefix + { + SkinPrefix.HSplitTop(20.0f, &Label, &SkinPrefix); + Ui()->DoLabel(&Label, Localize("Skin prefix"), 14.0f, TEXTALIGN_ML); + + SkinPrefix.HSplitTop(20.0f, &Button, &SkinPrefix); + static CLineInput s_SkinPrefixInput(g_Config.m_ClSkinPrefix, sizeof(g_Config.m_ClSkinPrefix)); + Ui()->DoClearableEditBox(&s_SkinPrefixInput, &Button, 14.0f); - SkinPrefix.HSplitTop(20.0f, &SkinPrefixLabel, &SkinPrefix); - static CLineInput s_SkinPrefixInput(g_Config.m_ClSkinPrefix, sizeof(g_Config.m_ClSkinPrefix)); - UI()->DoClearableEditBox(&s_SkinPrefixInput, &SkinPrefixLabel, 14.0f); + SkinPrefix.HSplitTop(2.0f, nullptr, &SkinPrefix); - SkinPrefix.HSplitTop(2.0f, 0, &SkinPrefix); - { static const char *s_apSkinPrefixes[] = {"kitty", "santa"}; static CButtonContainer s_aPrefixButtons[std::size(s_apSkinPrefixes)]; for(size_t i = 0; i < std::size(s_apSkinPrefixes); i++) @@ -613,21 +575,29 @@ void CMenus::RenderSettingsTee(CUIRect MainView) } } - Dummy.HSplitTop(20.0f, &DummyLabel, &Dummy); + // Player skin area + CUIRect CustomColorsButton, RandomSkinButton; + YourSkin.HSplitTop(20.0f, &Label, &YourSkin); + YourSkin.HSplitBottom(20.0f, &YourSkin, &CustomColorsButton); + CustomColorsButton.VSplitRight(30.0f, &CustomColorsButton, &RandomSkinButton); + CustomColorsButton.VSplitRight(20.0f, &CustomColorsButton, nullptr); + YourSkin.VSplitLeft(65.0f, &YourSkin, &Button); + Button.VSplitLeft(5.0f, nullptr, &Button); + Button.HMargin((Button.h - 20.0f) / 2.0f, &Button); - // note: get the skin info after the settings buttons, because they can trigger a refresh - // which invalidates the skin - // skin info + char aBuf[128 + IO_MAX_PATH_LENGTH]; + str_format(aBuf, sizeof(aBuf), "%s:", Localize("Your skin")); + Ui()->DoLabel(&Label, aBuf, 14.0f, TEXTALIGN_ML); + + // Note: get the skin info after the settings buttons, because they can trigger a refresh + // which invalidates the skin. CTeeRenderInfo OwnSkinInfo; - const CSkin *pSkin = m_pClient->m_Skins.Find(pSkinName); - OwnSkinInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin; - OwnSkinInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin; - OwnSkinInfo.m_SkinMetrics = pSkin->m_Metrics; + OwnSkinInfo.Apply(m_pClient->m_Skins.Find(pSkinName)); OwnSkinInfo.m_CustomColoredSkin = *pUseCustomColor; if(*pUseCustomColor) { - OwnSkinInfo.m_ColorBody = color_cast(ColorHSLA(*pColorBody).UnclampLighting()); - OwnSkinInfo.m_ColorFeet = color_cast(ColorHSLA(*pColorFeet).UnclampLighting()); + OwnSkinInfo.m_ColorBody = color_cast(ColorHSLA(*pColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT)); + OwnSkinInfo.m_ColorFeet = color_cast(ColorHSLA(*pColorFeet).UnclampLighting(ColorHSLA::DARKEST_LGT)); } else { @@ -636,152 +606,114 @@ void CMenus::RenderSettingsTee(CUIRect MainView) } OwnSkinInfo.m_Size = 50.0f; - MainView.HSplitTop(50.0f, &Label, &MainView); - Label.VSplitLeft(260.0f, &Label, 0); - const CAnimState *pIdleState = CAnimState::GetIdle(); - vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &OwnSkinInfo, OffsetToMid); - vec2 TeeRenderPos(Label.x + 30.0f, Label.y + Label.h / 2.0f + OffsetToMid.y); - int Emote = m_Dummy ? g_Config.m_ClDummyDefaultEyes : g_Config.m_ClPlayerDefaultEyes; - RenderTools()->RenderTee(pIdleState, &OwnSkinInfo, Emote, vec2(1, 0), TeeRenderPos); - Label.VSplitLeft(70.0f, 0, &Label); - Label.HMargin(15.0f, &Label); - - // default eyes - bool RenderEyesBelow = MainView.w < 750.0f; - if(RenderEyesBelow) - { - Eyes.VSplitLeft(190.0f, 0, &Eyes); - Eyes.HSplitTop(105.0f, 0, &Eyes); - } - else - { - Eyes = EyesRight; - if(MainView.w < 810.0f) - Eyes.VSplitRight(205.0f, 0, &Eyes); - Eyes.HSplitTop(50.0f, &Eyes, 0); - } - Eyes.HSplitTop(120.0f, &EyesLabel, &Eyes); - EyesLabel.VSplitLeft(20.0f, 0, &EyesLabel); - EyesLabel.HSplitTop(50.0f, &EyesLabel, &Eyes); - - static CButtonContainer s_aEyeButtons[6]; - for(int CurrentEyeEmote = 0; CurrentEyeEmote < 6; CurrentEyeEmote++) + // Tee { - EyesLabel.VSplitLeft(10.0f, 0, &EyesLabel); - EyesLabel.VSplitLeft(50.0f, &EyesTee, &EyesLabel); - - if(CurrentEyeEmote == 2 && !RenderEyesBelow) - { - Eyes.HSplitTop(60.0f, &EyesLabel, 0); - EyesLabel.HSplitTop(10.0f, 0, &EyesLabel); - } - float Highlight = (m_Dummy ? g_Config.m_ClDummyDefaultEyes == CurrentEyeEmote : g_Config.m_ClPlayerDefaultEyes == CurrentEyeEmote) ? 1.0f : 0.0f; - if(DoButton_Menu(&s_aEyeButtons[CurrentEyeEmote], "", 0, &EyesTee, 0, IGraphics::CORNER_ALL, 10.0f, 0.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f + Highlight * 0.25f))) - { - if(m_Dummy) - { - g_Config.m_ClDummyDefaultEyes = CurrentEyeEmote; - if(g_Config.m_ClDummy) - GameClient()->m_Emoticon.EyeEmote(CurrentEyeEmote); - } - else - { - g_Config.m_ClPlayerDefaultEyes = CurrentEyeEmote; - if(!g_Config.m_ClDummy) - GameClient()->m_Emoticon.EyeEmote(CurrentEyeEmote); - } - } - GameClient()->m_Tooltips.DoToolTip(&s_aEyeButtons[CurrentEyeEmote], &EyesTee, Localize("Choose default eyes when joining a server")); - RenderTools()->RenderTee(pIdleState, &OwnSkinInfo, CurrentEyeEmote, vec2(1, 0), vec2(EyesTee.x + 25.0f, EyesTee.y + EyesTee.h / 2.0f + OffsetToMid.y)); + vec2 OffsetToMid; + CRenderTools::GetRenderTeeOffsetToRenderedTee(CAnimState::GetIdle(), &OwnSkinInfo, OffsetToMid); + const vec2 TeeRenderPos = vec2(YourSkin.x + YourSkin.w / 2.0f, YourSkin.y + YourSkin.h / 2.0f + OffsetToMid.y); + RenderTools()->RenderTee(CAnimState::GetIdle(), &OwnSkinInfo, *pEmote, vec2(1.0f, 0.0f), TeeRenderPos); } - Label.VSplitRight(34.0f, &Label, &Button); - + // Skin name static CLineInput s_SkinInput; - s_SkinInput.SetBuffer(pSkinName, sizeof(g_Config.m_ClPlayerSkin)); + s_SkinInput.SetBuffer(pSkinName, SkinNameSize); s_SkinInput.SetEmptyText("default"); - if(UI()->DoClearableEditBox(&s_SkinInput, &Label, 14.0f)) + if(Ui()->DoClearableEditBox(&s_SkinInput, &Button, 14.0f)) { SetNeedSendInfo(); } - // random skin button - Button.VSplitRight(30.0f, 0, &Button); - static CButtonContainer s_RandomSkinButtonID; + // Random skin button + static CButtonContainer s_RandomSkinButton; static const char *s_apDice[] = {FONT_ICON_DICE_ONE, FONT_ICON_DICE_TWO, FONT_ICON_DICE_THREE, FONT_ICON_DICE_FOUR, FONT_ICON_DICE_FIVE, FONT_ICON_DICE_SIX}; static int s_CurrentDie = rand() % std::size(s_apDice); TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - if(DoButton_Menu(&s_RandomSkinButtonID, s_apDice[s_CurrentDie], 1, &Button, nullptr, IGraphics::CORNER_ALL, 5.0f, -0.2f)) + if(DoButton_Menu(&s_RandomSkinButton, s_apDice[s_CurrentDie], 0, &RandomSkinButton, nullptr, IGraphics::CORNER_ALL, 5.0f, -0.2f)) { - RandomSkin(); + GameClient()->m_Skins.RandomizeSkin(m_Dummy); + SetNeedSendInfo(); s_CurrentDie = rand() % std::size(s_apDice); } TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - GameClient()->m_Tooltips.DoToolTip(&s_RandomSkinButtonID, &Button, Localize("Create a random skin")); + GameClient()->m_Tooltips.DoToolTip(&s_RandomSkinButton, &RandomSkinButton, Localize("Create a random skin")); - // custom color selector - MainView.HSplitTop(20.0f + RenderEyesBelow * 25.0f, 0, &MainView); - MainView.HSplitTop(20.0f, &Button, &MainView); - CUIRect RandomColorsButton; - Button.VSplitLeft(150.0f, &Button, &RandomColorsButton); - static int s_CustomColorID = 0; - if(DoButton_CheckBox(&s_CustomColorID, Localize("Custom colors"), *pUseCustomColor, &Button)) + // Custom colors button + if(DoButton_CheckBox(pUseCustomColor, Localize("Custom colors"), *pUseCustomColor, &CustomColorsButton)) { *pUseCustomColor = *pUseCustomColor ? 0 : 1; SetNeedSendInfo(); } - CButtonContainer s_RandomizeColors; - if(*pUseCustomColor) + + // Default eyes { - RandomColorsButton.VSplitLeft(120.0f, &RandomColorsButton, 0); - if(DoButton_Menu(&s_RandomizeColors, "Randomize Colors", 0, &RandomColorsButton, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0, 0, 0, 0.5f))) + CTeeRenderInfo EyeSkinInfo = OwnSkinInfo; + EyeSkinInfo.m_Size = EyeButtonSize; + vec2 OffsetToMid; + CRenderTools::GetRenderTeeOffsetToRenderedTee(CAnimState::GetIdle(), &EyeSkinInfo, OffsetToMid); + + CUIRect EyesRow; + Eyes.HSplitTop(EyeButtonSize, &EyesRow, &Eyes); + static CButtonContainer s_aEyeButtons[NUM_EMOTES]; + for(int CurrentEyeEmote = 0; CurrentEyeEmote < NUM_EMOTES; CurrentEyeEmote++) { - if(m_Dummy) + EyesRow.VSplitLeft(EyeButtonSize, &Button, &EyesRow); + EyesRow.VSplitLeft(5.0f, nullptr, &EyesRow); + if(!RenderEyesBelow && (CurrentEyeEmote + 1) % 3 == 0) { - g_Config.m_ClDummyColorBody = ColorHSLA((std::rand() % 100) / 100.0f, (std::rand() % 100) / 100.0f, (std::rand() % 100) / 100.0f, 1).Pack(false); - g_Config.m_ClDummyColorFeet = ColorHSLA((std::rand() % 100) / 100.0f, (std::rand() % 100) / 100.0f, (std::rand() % 100) / 100.0f, 1).Pack(false); + Eyes.HSplitTop(5.0f, nullptr, &Eyes); + Eyes.HSplitTop(EyeButtonSize, &EyesRow, &Eyes); } - else + + const ColorRGBA EyeButtonColor = ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f + (*pEmote == CurrentEyeEmote ? 0.25f : 0.0f)); + if(DoButton_Menu(&s_aEyeButtons[CurrentEyeEmote], "", 0, &Button, nullptr, IGraphics::CORNER_ALL, 5.0f, 0.0f, EyeButtonColor)) { - g_Config.m_ClPlayerColorBody = ColorHSLA((std::rand() % 100) / 100.0f, (std::rand() % 100) / 100.0f, (std::rand() % 100) / 100.0f, 1).Pack(false); - g_Config.m_ClPlayerColorFeet = ColorHSLA((std::rand() % 100) / 100.0f, (std::rand() % 100) / 100.0f, (std::rand() % 100) / 100.0f, 1).Pack(false); + *pEmote = CurrentEyeEmote; + if((int)m_Dummy == g_Config.m_ClDummy) + GameClient()->m_Emoticon.EyeEmote(CurrentEyeEmote); } - SetNeedSendInfo(); + GameClient()->m_Tooltips.DoToolTip(&s_aEyeButtons[CurrentEyeEmote], &Button, Localize("Choose default eyes when joining a server")); + RenderTools()->RenderTee(CAnimState::GetIdle(), &EyeSkinInfo, CurrentEyeEmote, vec2(1.0f, 0.0f), vec2(Button.x + Button.w / 2.0f, Button.y + Button.h / 2.0f + OffsetToMid.y)); } } - MainView.HSplitTop(5.0f, 0, &MainView); - MainView.HSplitTop(82.5f, &Label, &MainView); + + // Custom color pickers + MainView.HSplitTop(5.0f, nullptr, &MainView); if(*pUseCustomColor) { + CUIRect CustomColors; + MainView.HSplitTop(95.0f, &CustomColors, &MainView); CUIRect aRects[2]; - Label.VSplitMid(&aRects[0], &aRects[1], 20.0f); + CustomColors.VSplitMid(&aRects[0], &aRects[1], 20.0f); - unsigned *apColors[2] = {pColorBody, pColorFeet}; + unsigned *apColors[] = {pColorBody, pColorFeet}; const char *apParts[] = {Localize("Body"), Localize("Feet")}; for(int i = 0; i < 2; i++) { aRects[i].HSplitTop(20.0f, &Label, &aRects[i]); - UI()->DoLabel(&Label, apParts[i], 14.0f, TEXTALIGN_ML); - aRects[i].VSplitLeft(10.0f, 0, &aRects[i]); - aRects[i].HSplitTop(2.5f, 0, &aRects[i]); - - unsigned PrevColor = *apColors[i]; - RenderHSLScrollbars(&aRects[i], apColors[i], false, true); - - if(PrevColor != *apColors[i]) + Ui()->DoLabel(&Label, apParts[i], 14.0f, TEXTALIGN_ML); + if(RenderHslaScrollbars(&aRects[i], apColors[i], false, ColorHSLA::DARKEST_LGT)) { SetNeedSendInfo(); } } } + MainView.HSplitTop(5.0f, nullptr, &MainView); + + // Layout bottom controls and use remainder for skin selector + CUIRect QuickSearch, DatabaseButton, DirectoryButton, RefreshButton; + MainView.HSplitBottom(20.0f, &MainView, &QuickSearch); + MainView.HSplitBottom(5.0f, &MainView, nullptr); + QuickSearch.VSplitLeft(220.0f, &QuickSearch, &DatabaseButton); + DatabaseButton.VSplitLeft(10.0f, nullptr, &DatabaseButton); + DatabaseButton.VSplitLeft(150.0f, &DatabaseButton, &DirectoryButton); + DirectoryButton.VSplitRight(175.0f, nullptr, &DirectoryButton); + DirectoryButton.VSplitRight(25.0f, &DirectoryButton, &RefreshButton); + DirectoryButton.VSplitRight(10.0f, &DirectoryButton, nullptr); - // skin selector - MainView.HSplitTop(20.0f, 0, &MainView); - MainView.HSplitTop(230.0f - RenderEyesBelow * 25.0f, &SkinList, &MainView); + // Skin selector static std::vector s_vSkinList; static std::vector s_vSkinListHelper; static std::vector s_vFavoriteSkinListHelper; @@ -791,7 +723,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView) // be nice to the CPU static auto s_SkinLastRebuildTime = time_get_nanoseconds(); const auto CurTime = time_get_nanoseconds(); - if(s_InitSkinlist || m_pClient->m_Skins.Num() != s_SkinCount || m_SkinFavoritesChanged || (m_pClient->m_Skins.IsDownloadingSkins() && (CurTime - s_SkinLastRebuildTime > 500ms))) + if(m_SkinListNeedsUpdate || m_pClient->m_Skins.Num() != s_SkinCount || m_SkinFavoritesChanged || (m_pClient->m_Skins.IsDownloadingSkins() && (CurTime - s_SkinLastRebuildTime > 500ms))) { s_SkinLastRebuildTime = CurTime; s_vSkinList.clear(); @@ -836,31 +768,14 @@ void CMenus::RenderSettingsTee(CUIRect MainView) std::sort(s_vFavoriteSkinListHelper.begin(), s_vFavoriteSkinListHelper.end()); s_vSkinList = s_vFavoriteSkinListHelper; s_vSkinList.insert(s_vSkinList.end(), s_vSkinListHelper.begin(), s_vSkinListHelper.end()); - s_InitSkinlist = false; + m_SkinListNeedsUpdate = false; } - auto &&RenderFavIcon = [&](const CUIRect &FavIcon, bool AsFav) { - TextRender()->SetFontPreset(EFontPreset::ICON_FONT); - TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - if(AsFav) - TextRender()->TextColor({1, 1, 0, 1}); - else - TextRender()->TextColor({0.5f, 0.5f, 0.5f, 1}); - TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor()); - SLabelProperties Props; - Props.m_MaxWidth = FavIcon.w; - UI()->DoLabel(&FavIcon, FONT_ICON_STAR, 12.0f, TEXTALIGN_MR, Props); - TextRender()->TextColor(TextRender()->DefaultTextColor()); - TextRender()->SetRenderFlags(0); - TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - }; - int OldSelected = -1; - s_ListBox.DoStart(50.0f, s_vSkinList.size(), 4, 1, OldSelected, &SkinList); + s_ListBox.DoStart(50.0f, s_vSkinList.size(), 4, 1, OldSelected, &MainView); for(size_t i = 0; i < s_vSkinList.size(); ++i) { const CSkin *pSkinToBeDraw = s_vSkinList[i].m_pSkin; - if(str_comp(pSkinToBeDraw->GetName(), pSkinName) == 0) OldSelected = i; @@ -868,28 +783,26 @@ void CMenus::RenderSettingsTee(CUIRect MainView) if(!Item.m_Visible) continue; - const CUIRect OriginalRect = Item.m_Rect; + Item.m_Rect.VSplitLeft(60.0f, &Button, &Label); CTeeRenderInfo Info = OwnSkinInfo; Info.m_CustomColoredSkin = *pUseCustomColor; - Info.m_OriginalRenderSkin = pSkinToBeDraw->m_OriginalSkin; Info.m_ColorableRenderSkin = pSkinToBeDraw->m_ColorableSkin; Info.m_SkinMetrics = pSkinToBeDraw->m_Metrics; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &Info, OffsetToMid); - TeeRenderPos = vec2(OriginalRect.x + 30, OriginalRect.y + OriginalRect.h / 2 + OffsetToMid.y); - RenderTools()->RenderTee(pIdleState, &Info, Emote, vec2(1.0f, 0.0f), TeeRenderPos); + vec2 OffsetToMid; + CRenderTools::GetRenderTeeOffsetToRenderedTee(CAnimState::GetIdle(), &Info, OffsetToMid); + const vec2 TeeRenderPos = vec2(Button.x + Button.w / 2.0f, Button.y + Button.h / 2 + OffsetToMid.y); + RenderTools()->RenderTee(CAnimState::GetIdle(), &Info, *pEmote, vec2(1.0f, 0.0f), TeeRenderPos); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w - 5.0f; + Ui()->DoLabel(&Label, pSkinToBeDraw->GetName(), 12.0f, TEXTALIGN_ML, Props); - OriginalRect.VSplitLeft(60.0f, 0, &Label); - { - SLabelProperties Props; - Props.m_MaxWidth = Label.w - 5.0f; - UI()->DoLabel(&Label, pSkinToBeDraw->GetName(), 12.0f, TEXTALIGN_ML, Props); - } if(g_Config.m_Debug) { - ColorRGBA BloodColor = *pUseCustomColor ? color_cast(ColorHSLA(*pColorBody).UnclampLighting()) : pSkinToBeDraw->m_BloodColor; + const ColorRGBA BloodColor = *pUseCustomColor ? color_cast(ColorHSLA(*pColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT)) : pSkinToBeDraw->m_BloodColor; Graphics()->TextureClear(); Graphics()->QuadsBegin(); Graphics()->SetColor(BloodColor.r, BloodColor.g, BloodColor.b, 1.0f); @@ -901,22 +814,11 @@ void CMenus::RenderSettingsTee(CUIRect MainView) // render skin favorite icon { const auto SkinItFav = m_SkinFavorites.find(pSkinToBeDraw->GetName()); - const auto IsFav = SkinItFav != m_SkinFavorites.end(); + const bool IsFav = SkinItFav != m_SkinFavorites.end(); CUIRect FavIcon; - OriginalRect.HSplitTop(20.0f, &FavIcon, nullptr); + Item.m_Rect.HSplitTop(20.0f, &FavIcon, nullptr); FavIcon.VSplitRight(20.0f, nullptr, &FavIcon); - if(IsFav) - { - RenderFavIcon(FavIcon, IsFav); - } - else - { - if(UI()->MouseInside(&FavIcon)) - { - RenderFavIcon(FavIcon, IsFav); - } - } - if(UI()->DoButtonLogic(&pSkinToBeDraw->m_Metrics.m_Body, 0, &FavIcon)) + if(DoButton_Favorite(&pSkinToBeDraw->m_Metrics.m_Body, pSkinToBeDraw, IsFav, &FavIcon)) { if(IsFav) { @@ -926,7 +828,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView) { m_SkinFavorites.emplace(pSkinToBeDraw->GetName()); } - s_InitSkinlist = true; + m_SkinListNeedsUpdate = true; } } } @@ -934,74 +836,40 @@ void CMenus::RenderSettingsTee(CUIRect MainView) const int NewSelected = s_ListBox.DoEnd(); if(OldSelected != NewSelected) { - mem_copy(pSkinName, s_vSkinList[NewSelected].m_pSkin->GetName(), sizeof(g_Config.m_ClPlayerSkin)); + str_copy(pSkinName, s_vSkinList[NewSelected].m_pSkin->GetName(), SkinNameSize); SetNeedSendInfo(); } - // render quick search + static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString)); + if(Ui()->DoEditBox_Search(&s_SkinFilterInput, &QuickSearch, 14.0f, !Ui()->IsPopupOpen() && m_pClient->m_GameConsole.IsClosed())) { - MainView.HSplitBottom(ms_ButtonHeight, &MainView, &QuickSearch); - QuickSearch.VSplitLeft(240.0f, &QuickSearch, &SkinDB); - QuickSearch.HSplitTop(5.0f, 0, &QuickSearch); - TextRender()->SetFontPreset(EFontPreset::ICON_FONT); - TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - - UI()->DoLabel(&QuickSearch, FONT_ICON_MAGNIFYING_GLASS, 14.0f, TEXTALIGN_ML); - float wSearch = TextRender()->TextWidth(14.0f, FONT_ICON_MAGNIFYING_GLASS, -1, -1.0f); - TextRender()->SetRenderFlags(0); - TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - QuickSearch.VSplitLeft(wSearch, 0, &QuickSearch); - QuickSearch.VSplitLeft(5.0f, 0, &QuickSearch); - QuickSearch.VSplitLeft(QuickSearch.w - 15.0f, &QuickSearch, &QuickSearchClearButton); - static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString)); - if(Input()->KeyPress(KEY_F) && Input()->ModifierIsPressed()) - { - UI()->SetActiveItem(&s_SkinFilterInput); - s_SkinFilterInput.SelectAll(); - } - s_SkinFilterInput.SetEmptyText(Localize("Search")); - if(UI()->DoClearableEditBox(&s_SkinFilterInput, &QuickSearch, 14.0f)) - s_InitSkinlist = true; + m_SkinListNeedsUpdate = true; } - SkinDB.VSplitLeft(150.0f, &SkinDB, &DirectoryButton); - SkinDB.HSplitTop(5.0f, 0, &SkinDB); - static CButtonContainer s_SkinDBDirID; - if(DoButton_Menu(&s_SkinDBDirID, Localize("Skin Database"), 0, &SkinDB)) + static CButtonContainer s_SkinDatabaseButton; + if(DoButton_Menu(&s_SkinDatabaseButton, Localize("Skin Database"), 0, &DatabaseButton)) { - const char *pLink = "https://ddnet.org/skins/"; - if(!open_link(pLink)) - { - dbg_msg("menus", "couldn't open link '%s'", pLink); - } + Client()->ViewLink("https://ddnet.org/skins/"); } - DirectoryButton.HSplitTop(5.0f, 0, &DirectoryButton); - DirectoryButton.VSplitRight(175.0f, 0, &DirectoryButton); - DirectoryButton.VSplitRight(25.0f, &DirectoryButton, &RefreshButton); - DirectoryButton.VSplitRight(10.0f, &DirectoryButton, 0); - static CButtonContainer s_DirectoryButtonID; - if(DoButton_Menu(&s_DirectoryButtonID, Localize("Skins directory"), 0, &DirectoryButton)) + static CButtonContainer s_DirectoryButton; + if(DoButton_Menu(&s_DirectoryButton, Localize("Skins directory"), 0, &DirectoryButton)) { Storage()->GetCompletePath(IStorage::TYPE_SAVE, "skins", aBuf, sizeof(aBuf)); Storage()->CreateFolder("skins", IStorage::TYPE_SAVE); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } - GameClient()->m_Tooltips.DoToolTip(&s_DirectoryButtonID, &DirectoryButton, Localize("Open the directory to add custom skins")); + GameClient()->m_Tooltips.DoToolTip(&s_DirectoryButton, &DirectoryButton, Localize("Open the directory to add custom skins")); TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - static CButtonContainer s_SkinRefreshButtonID; - if(DoButton_Menu(&s_SkinRefreshButtonID, FONT_ICON_ARROW_ROTATE_RIGHT, 0, &RefreshButton) || Input()->KeyPress(KEY_F5) || (Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed())) + static CButtonContainer s_SkinRefreshButton; + if(DoButton_Menu(&s_SkinRefreshButton, FONT_ICON_ARROW_ROTATE_RIGHT, 0, &RefreshButton) || Input()->KeyPress(KEY_F5) || (Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed())) { // reset render flags for possible loading screen TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - RefreshSkins(); - s_InitSkinlist = true; + m_pClient->RefreshSkins(); } TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); @@ -1066,15 +934,6 @@ static CKeyInfo gs_aKeys[] = {Localizable("Lock team"), "say /lock", 0, 0}, {Localizable("Show entities"), "toggle cl_overlay_entities 0 100", 0, 0}, {Localizable("Show HUD"), "toggle cl_showhud 0 1", 0, 0}, - {Localizable("BindWheel"), "+bind_wheel", 0, 0}, - { - Localizable("45° AIM"), - "+toggle cl_mouse_max_distance 2 400; +toggle inp_mousesens 1; +showhookcoll", - 0, - 0, - }, - //TODO: Make it work - //{Localizable("Toggle dummy deep fly"), "bind mouse1 \"+fire; +toggle cl_dummy_hammer 1 0\" ;cl_message_client_color green; echo Deep Fly ON; bind x \"bind mouse1 +fire; cl_dummy_hammer 0; cl_message_client_color red; echo Deep Fly OFF;""" , 0, 0}, }; void CMenus::DoSettingsControlsButtons(int Start, int Stop, CUIRect View) @@ -1090,7 +949,7 @@ void CMenus::DoSettingsControlsButtons(int Start, int Stop, CUIRect View) char aBuf[64]; str_format(aBuf, sizeof(aBuf), "%s:", Localize(Key.m_pName)); - UI()->DoLabel(&Label, aBuf, 13.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, aBuf, 13.0f, TEXTALIGN_ML); int OldId = Key.m_KeyId, OldModifierCombination = Key.m_ModifierCombination, NewModifierCombination; int NewId = DoKeyReader(&Key.m_KeyId, &Button, OldId, OldModifierCombination, &NewModifierCombination); if(NewId != OldId || NewModifierCombination != OldModifierCombination) @@ -1112,12 +971,9 @@ float CMenus::RenderSettingsControlsJoystick(CUIRect View) int NumOptions = 1; // expandable header if(JoystickEnabled) { - if(NumJoysticks == 0) - NumOptions++; // message - else + NumOptions++; // message or joystick name/selection + if(NumJoysticks > 0) { - if(NumJoysticks > 1) - NumOptions++; // joystick selection NumOptions += 3; // mode, ui sens, tolerance if(!g_Config.m_InpControllerAbsolute) NumOptions++; // ingame sens @@ -1163,11 +1019,11 @@ float CMenus::RenderSettingsControlsJoystick(CUIRect View) s_vpJoystickNames[i] = s_vJoystickNames[i].c_str(); } - static CUI::SDropDownState s_JoystickDropDownState; + static CUi::SDropDownState s_JoystickDropDownState; static CScrollRegion s_JoystickDropDownScrollRegion; s_JoystickDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_JoystickDropDownScrollRegion; const int CurrentJoystick = Input()->GetActiveJoystick()->GetIndex(); - const int NewJoystick = UI()->DoDropDown(&JoystickDropDown, CurrentJoystick, s_vpJoystickNames.data(), s_vpJoystickNames.size(), s_JoystickDropDownState); + const int NewJoystick = Ui()->DoDropDown(&JoystickDropDown, CurrentJoystick, s_vpJoystickNames.data(), s_vpJoystickNames.size(), s_JoystickDropDownState); if(NewJoystick != CurrentJoystick) { Input()->SetActiveJoystick(NewJoystick); @@ -1177,7 +1033,7 @@ float CMenus::RenderSettingsControlsJoystick(CUIRect View) { char aBuf[256]; str_format(aBuf, sizeof(aBuf), "%s 0: %s", Localize("Controller"), Input()->GetJoystick(0)->GetName()); - UI()->DoLabel(&JoystickDropDown, aBuf, 13.0f, TEXTALIGN_ML); + Ui()->DoLabel(&JoystickDropDown, aBuf, 13.0f, TEXTALIGN_ML); } } @@ -1188,7 +1044,7 @@ float CMenus::RenderSettingsControlsJoystick(CUIRect View) Button.VSplitMid(&Label, &Button, 10.0f); Button.HMargin(2.0f, &Button); Button.VSplitMid(&ButtonRelative, &ButtonAbsolute); - UI()->DoLabel(&Label, Localize("Ingame controller mode"), 13.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, Localize("Ingame controller mode"), 13.0f, TEXTALIGN_ML); CButtonContainer s_RelativeButton; if(DoButton_Menu(&s_RelativeButton, Localize("Relative", "Ingame controller mode"), g_Config.m_InpControllerAbsolute == 0, &ButtonRelative, nullptr, IGraphics::CORNER_L)) { @@ -1205,16 +1061,16 @@ float CMenus::RenderSettingsControlsJoystick(CUIRect View) { View.HSplitTop(Spacing, nullptr, &View); View.HSplitTop(ButtonHeight, &Button, &View); - UI()->DoScrollbarOption(&g_Config.m_InpControllerSens, &g_Config.m_InpControllerSens, &Button, Localize("Ingame controller sens."), 1, 500, &CUI::ms_LogarithmicScrollbarScale, CUI::SCROLLBAR_OPTION_NOCLAMPVALUE); + Ui()->DoScrollbarOption(&g_Config.m_InpControllerSens, &g_Config.m_InpControllerSens, &Button, Localize("Ingame controller sens."), 1, 500, &CUi::ms_LogarithmicScrollbarScale, CUi::SCROLLBAR_OPTION_NOCLAMPVALUE); } View.HSplitTop(Spacing, nullptr, &View); View.HSplitTop(ButtonHeight, &Button, &View); - UI()->DoScrollbarOption(&g_Config.m_UiControllerSens, &g_Config.m_UiControllerSens, &Button, Localize("UI controller sens."), 1, 500, &CUI::ms_LogarithmicScrollbarScale, CUI::SCROLLBAR_OPTION_NOCLAMPVALUE); + Ui()->DoScrollbarOption(&g_Config.m_UiControllerSens, &g_Config.m_UiControllerSens, &Button, Localize("UI controller sens."), 1, 500, &CUi::ms_LogarithmicScrollbarScale, CUi::SCROLLBAR_OPTION_NOCLAMPVALUE); View.HSplitTop(Spacing, nullptr, &View); View.HSplitTop(ButtonHeight, &Button, &View); - UI()->DoScrollbarOption(&g_Config.m_InpControllerTolerance, &g_Config.m_InpControllerTolerance, &Button, Localize("Controller jitter tolerance"), 0, 50); + Ui()->DoScrollbarOption(&g_Config.m_InpControllerTolerance, &g_Config.m_InpControllerTolerance, &Button, Localize("Controller jitter tolerance"), 0, 50); View.HSplitTop(Spacing, nullptr, &View); View.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.125f), IGraphics::CORNER_ALL, 5.0f); @@ -1224,7 +1080,7 @@ float CMenus::RenderSettingsControlsJoystick(CUIRect View) { View.HSplitTop(View.h - ButtonHeight, nullptr, &View); View.HSplitTop(ButtonHeight, &Button, &View); - UI()->DoLabel(&Button, Localize("No controller found. Plug in a controller."), 13.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("No controller found. Plug in a controller."), 13.0f, TEXTALIGN_ML); } } @@ -1250,9 +1106,9 @@ void CMenus::DoJoystickAxisPicker(CUIRect View) Row.VSplitLeft(SpacingV, nullptr, &Row); Row.VSplitLeft(AimBindWidth, &AimBind, &Row); - UI()->DoLabel(&Axis, Localize("Axis"), FontSize, TEXTALIGN_MC); - UI()->DoLabel(&Status, Localize("Status"), FontSize, TEXTALIGN_MC); - UI()->DoLabel(&AimBind, Localize("Aim bind"), FontSize, TEXTALIGN_MC); + Ui()->DoLabel(&Axis, Localize("Axis"), FontSize, TEXTALIGN_MC); + Ui()->DoLabel(&Status, Localize("Status"), FontSize, TEXTALIGN_MC); + Ui()->DoLabel(&AimBind, Localize("Aim bind"), FontSize, TEXTALIGN_MC); IInput::IJoystick *pJoystick = Input()->GetActiveJoystick(); static int s_aActive[NUM_JOYSTICK_AXES][2]; @@ -1271,12 +1127,12 @@ void CMenus::DoJoystickAxisPicker(CUIRect View) // Axis label char aBuf[16]; - str_from_int(i + 1, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", i + 1); if(Active) TextRender()->TextColor(TextRender()->DefaultTextColor()); else TextRender()->TextColor(0.7f, 0.7f, 0.7f, 1.0f); - UI()->DoLabel(&Axis, aBuf, FontSize, TEXTALIGN_MC); + Ui()->DoLabel(&Axis, aBuf, FontSize, TEXTALIGN_MC); // Axis status Status.HMargin(7.0f, &Status); @@ -1356,7 +1212,7 @@ void CMenus::RenderSettingsControls(CUIRect MainView) const float Margin = 10.0f; const float HeaderHeight = FontSize + 5.0f + Margin; - CUIRect MouseSettings, MovementSettings, WeaponSettings, VotingSettings, ChatSettings, DummySettings, MiscSettings, BindWheelSettings, JoystickSettings, ResetButton, Button; + CUIRect MouseSettings, MovementSettings, WeaponSettings, VotingSettings, ChatSettings, DummySettings, MiscSettings, JoystickSettings, ResetButton, Button; MainView.VSplitMid(&MouseSettings, &VotingSettings); // mouse settings @@ -1369,15 +1225,15 @@ void CMenus::RenderSettingsControls(CUIRect MainView) MouseSettings.VMargin(10.0f, &MouseSettings); MouseSettings.HSplitTop(HeaderHeight, &Button, &MouseSettings); - UI()->DoLabel(&Button, Localize("Mouse"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Mouse"), FontSize, TEXTALIGN_ML); MouseSettings.HSplitTop(20.0f, &Button, &MouseSettings); - UI()->DoScrollbarOption(&g_Config.m_InpMousesens, &g_Config.m_InpMousesens, &Button, Localize("Ingame mouse sens."), 1, 500, &CUI::ms_LogarithmicScrollbarScale, CUI::SCROLLBAR_OPTION_NOCLAMPVALUE); + Ui()->DoScrollbarOption(&g_Config.m_InpMousesens, &g_Config.m_InpMousesens, &Button, Localize("Ingame mouse sens."), 1, 500, &CUi::ms_LogarithmicScrollbarScale, CUi::SCROLLBAR_OPTION_NOCLAMPVALUE); MouseSettings.HSplitTop(2.0f, nullptr, &MouseSettings); MouseSettings.HSplitTop(20.0f, &Button, &MouseSettings); - UI()->DoScrollbarOption(&g_Config.m_UiMousesens, &g_Config.m_UiMousesens, &Button, Localize("UI mouse sens."), 1, 500, &CUI::ms_LogarithmicScrollbarScale, CUI::SCROLLBAR_OPTION_NOCLAMPVALUE); + Ui()->DoScrollbarOption(&g_Config.m_UiMousesens, &g_Config.m_UiMousesens, &Button, Localize("UI mouse sens."), 1, 500, &CUi::ms_LogarithmicScrollbarScale, CUi::SCROLLBAR_OPTION_NOCLAMPVALUE); } } @@ -1391,7 +1247,7 @@ void CMenus::RenderSettingsControls(CUIRect MainView) JoystickSettings.VMargin(Margin, &JoystickSettings); JoystickSettings.HSplitTop(HeaderHeight, &Button, &JoystickSettings); - UI()->DoLabel(&Button, Localize("Controller"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Controller"), FontSize, TEXTALIGN_ML); s_JoystickSettingsHeight = RenderSettingsControlsJoystick(JoystickSettings); } @@ -1407,7 +1263,7 @@ void CMenus::RenderSettingsControls(CUIRect MainView) MovementSettings.VMargin(Margin, &MovementSettings); MovementSettings.HSplitTop(HeaderHeight, &Button, &MovementSettings); - UI()->DoLabel(&Button, Localize("Movement"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Movement"), FontSize, TEXTALIGN_ML); DoSettingsControlsButtons(0, 15, MovementSettings); } @@ -1423,7 +1279,7 @@ void CMenus::RenderSettingsControls(CUIRect MainView) WeaponSettings.VMargin(Margin, &WeaponSettings); WeaponSettings.HSplitTop(HeaderHeight, &Button, &WeaponSettings); - UI()->DoLabel(&Button, Localize("Weapon"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Weapon"), FontSize, TEXTALIGN_ML); DoSettingsControlsButtons(15, 22, WeaponSettings); } @@ -1456,7 +1312,7 @@ void CMenus::RenderSettingsControls(CUIRect MainView) VotingSettings.VMargin(Margin, &VotingSettings); VotingSettings.HSplitTop(HeaderHeight, &Button, &VotingSettings); - UI()->DoLabel(&Button, Localize("Voting"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Voting"), FontSize, TEXTALIGN_ML); DoSettingsControlsButtons(22, 24, VotingSettings); } @@ -1472,7 +1328,7 @@ void CMenus::RenderSettingsControls(CUIRect MainView) ChatSettings.VMargin(Margin, &ChatSettings); ChatSettings.HSplitTop(HeaderHeight, &Button, &ChatSettings); - UI()->DoLabel(&Button, Localize("Chat"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Chat"), FontSize, TEXTALIGN_ML); DoSettingsControlsButtons(24, 29, ChatSettings); } @@ -1488,7 +1344,7 @@ void CMenus::RenderSettingsControls(CUIRect MainView) DummySettings.VMargin(Margin, &DummySettings); DummySettings.HSplitTop(HeaderHeight, &Button, &DummySettings); - UI()->DoLabel(&Button, Localize("Dummy"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Dummy"), FontSize, TEXTALIGN_ML); DoSettingsControlsButtons(29, 32, DummySettings); } @@ -1497,33 +1353,18 @@ void CMenus::RenderSettingsControls(CUIRect MainView) // misc settings { MiscSettings.HSplitTop(Margin, nullptr, &MiscSettings); - MiscSettings.HSplitTop(300.0f, &MiscSettings, &BindWheelSettings); + MiscSettings.HSplitTop(300.0f, &MiscSettings, 0); if(s_ScrollRegion.AddRect(MiscSettings)) { MiscSettings.Draw(ColorRGBA(1, 1, 1, 0.25f), IGraphics::CORNER_ALL, 10.0f); MiscSettings.VMargin(Margin, &MiscSettings); MiscSettings.HSplitTop(HeaderHeight, &Button, &MiscSettings); - UI()->DoLabel(&Button, Localize("Miscellaneous"), FontSize, TEXTALIGN_ML); + Ui()->DoLabel(&Button, Localize("Miscellaneous"), FontSize, TEXTALIGN_ML); DoSettingsControlsButtons(32, 44, MiscSettings); } } - // sta - { - BindWheelSettings.HSplitTop(Margin, nullptr, &BindWheelSettings); - BindWheelSettings.HSplitTop(80.0f, &BindWheelSettings, 0); - if(s_ScrollRegion.AddRect(BindWheelSettings)) - { - BindWheelSettings.Draw(vec4(1, 1, 1, 0.15f), IGraphics::CORNER_ALL, 10.0f); - BindWheelSettings.VMargin(Margin, &BindWheelSettings); - - BindWheelSettings.HSplitTop(HeaderHeight, &Button, &BindWheelSettings); - UI()->DoLabel(&Button, Localize("StA"), FontSize, TEXTALIGN_ML); - - DoSettingsControlsButtons(44, 46, BindWheelSettings); - } - } s_ScrollRegion.End(); } @@ -1556,7 +1397,7 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) static int s_NumNodes = Graphics()->GetVideoModes(s_aModes, MAX_RESOLUTIONS, g_Config.m_GfxScreen); static int s_GfxFsaaSamples = g_Config.m_GfxFsaaSamples; static bool s_GfxBackendChanged = false; - static bool s_GfxGPUChanged = false; + static bool s_GfxGpuChanged = false; static int s_GfxHighdpi = g_Config.m_GfxHighdpi; static int s_InitDisplayAllVideoModes = g_Config.m_GfxDisplayAllVideoModes; @@ -1593,11 +1434,11 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) { int G = std::gcd(g_Config.m_GfxScreenWidth, g_Config.m_GfxScreenHeight); str_format(aBuf, sizeof(aBuf), "%s: %dx%d @%dhz %d bit (%d:%d)", Localize("Current"), (int)(g_Config.m_GfxScreenWidth * Graphics()->ScreenHiDPIScale()), (int)(g_Config.m_GfxScreenHeight * Graphics()->ScreenHiDPIScale()), g_Config.m_GfxScreenRefreshRate, g_Config.m_GfxColorDepth, g_Config.m_GfxScreenWidth / G, g_Config.m_GfxScreenHeight / G); - UI()->DoLabel(&ModeLabel, aBuf, sc_FontSizeResListHeader, TEXTALIGN_MC); + Ui()->DoLabel(&ModeLabel, aBuf, sc_FontSizeResListHeader, TEXTALIGN_MC); } int OldSelected = -1; - s_ListBox.SetActive(!UI()->IsPopupOpen()); + s_ListBox.SetActive(!Ui()->IsPopupOpen()); s_ListBox.DoStart(sc_RowHeightResList, s_NumNodes, 1, 3, OldSelected, &ModeList); for(int i = 0; i < s_NumNodes; ++i) @@ -1617,7 +1458,7 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) int G = std::gcd(s_aModes[i].m_CanvasWidth, s_aModes[i].m_CanvasHeight); str_format(aBuf, sizeof(aBuf), " %dx%d @%dhz %d bit (%d:%d)", s_aModes[i].m_CanvasWidth, s_aModes[i].m_CanvasHeight, s_aModes[i].m_RefreshRate, Depth, s_aModes[i].m_CanvasWidth / G, s_aModes[i].m_CanvasHeight / G); - UI()->DoLabel(&Item.m_Rect, aBuf, sc_FontSizeResList, TEXTALIGN_ML); + Ui()->DoLabel(&Item.m_Rect, aBuf, sc_FontSizeResList, TEXTALIGN_ML); } const int NewSelected = s_ListBox.DoEnd(); @@ -1628,7 +1469,7 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) g_Config.m_GfxScreenWidth = s_aModes[NewSelected].m_WindowWidth; g_Config.m_GfxScreenHeight = s_aModes[NewSelected].m_WindowHeight; g_Config.m_GfxScreenRefreshRate = s_aModes[NewSelected].m_RefreshRate; - Graphics()->Resize(g_Config.m_GfxScreenWidth, g_Config.m_GfxScreenHeight, g_Config.m_GfxScreenRefreshRate); + Graphics()->ResizeToScreen(); } // switches @@ -1640,22 +1481,22 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) const int OldWindowMode = (g_Config.m_GfxFullscreen ? (g_Config.m_GfxFullscreen == 1 ? 4 : (g_Config.m_GfxFullscreen == 2 ? 3 : 2)) : (g_Config.m_GfxBorderless ? 1 : 0)); - static CUI::SDropDownState s_WindowModeDropDownState; + static CUi::SDropDownState s_WindowModeDropDownState; static CScrollRegion s_WindowModeDropDownScrollRegion; s_WindowModeDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_WindowModeDropDownScrollRegion; - const int NewWindowMode = UI()->DoDropDown(&WindowModeDropDown, OldWindowMode, apWindowModes, s_NumWindowMode, s_WindowModeDropDownState); + const int NewWindowMode = Ui()->DoDropDown(&WindowModeDropDown, OldWindowMode, apWindowModes, s_NumWindowMode, s_WindowModeDropDownState); if(OldWindowMode != NewWindowMode) { if(NewWindowMode == 0) - Client()->SetWindowParams(0, false, true); + Client()->SetWindowParams(0, false); else if(NewWindowMode == 1) - Client()->SetWindowParams(0, true, true); + Client()->SetWindowParams(0, true); else if(NewWindowMode == 2) - Client()->SetWindowParams(3, false, false); + Client()->SetWindowParams(3, false); else if(NewWindowMode == 3) - Client()->SetWindowParams(2, false, true); + Client()->SetWindowParams(2, false); else if(NewWindowMode == 4) - Client()->SetWindowParams(1, false, true); + Client()->SetWindowParams(1, false); } if(Graphics()->GetNumScreens() > 1) @@ -1677,15 +1518,12 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) s_vpScreenNames[i] = s_vScreenNames[i].c_str(); } - static CUI::SDropDownState s_ScreenDropDownState; + static CUi::SDropDownState s_ScreenDropDownState; static CScrollRegion s_ScreenDropDownScrollRegion; s_ScreenDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_ScreenDropDownScrollRegion; - const int NewScreen = UI()->DoDropDown(&ScreenDropDown, g_Config.m_GfxScreen, s_vpScreenNames.data(), s_vpScreenNames.size(), s_ScreenDropDownState); + const int NewScreen = Ui()->DoDropDown(&ScreenDropDown, g_Config.m_GfxScreen, s_vpScreenNames.data(), s_vpScreenNames.size(), s_ScreenDropDownState); if(NewScreen != g_Config.m_GfxScreen) - { Client()->SwitchWindowScreen(NewScreen); - s_NumNodes = Graphics()->GetVideoModes(s_aModes, MAX_RESOLUTIONS, g_Config.m_GfxScreen); - } } MainView.HSplitTop(2.0f, nullptr, &MainView); @@ -1699,16 +1537,16 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) bool MultiSamplingChanged = false; MainView.HSplitTop(20.0f, &Button, &MainView); str_format(aBuf, sizeof(aBuf), "%s (%s)", Localize("FSAA samples"), Localize("may cause delay")); - int GfxFsaaSamples_MouseButton = DoButton_CheckBox_Number(&g_Config.m_GfxFsaaSamples, aBuf, g_Config.m_GfxFsaaSamples, &Button); + int GfxFsaaSamplesMouseButton = DoButton_CheckBox_Number(&g_Config.m_GfxFsaaSamples, aBuf, g_Config.m_GfxFsaaSamples, &Button); int CurFSAA = g_Config.m_GfxFsaaSamples == 0 ? 1 : g_Config.m_GfxFsaaSamples; - if(GfxFsaaSamples_MouseButton == 1) // inc + if(GfxFsaaSamplesMouseButton == 1) // inc { g_Config.m_GfxFsaaSamples = std::pow(2, (int)std::log2(CurFSAA) + 1); if(g_Config.m_GfxFsaaSamples > 64) g_Config.m_GfxFsaaSamples = 0; MultiSamplingChanged = true; } - else if(GfxFsaaSamples_MouseButton == 2) // dec + else if(GfxFsaaSamplesMouseButton == 2) // dec { if(CurFSAA == 1) g_Config.m_GfxFsaaSamples = 64; @@ -1726,7 +1564,7 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) { // try again with 0 if mouse click was increasing multi sampling // else just accept the current value as is - if((uint32_t)g_Config.m_GfxFsaaSamples > MultiSamplingCountBackend && GfxFsaaSamples_MouseButton == 1) + if((uint32_t)g_Config.m_GfxFsaaSamples > MultiSamplingCountBackend && GfxFsaaSamplesMouseButton == 1) Graphics()->SetMultiSampling(0, MultiSamplingCountBackend); g_Config.m_GfxFsaaSamples = (int)MultiSamplingCountBackend; } @@ -1749,7 +1587,9 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) } MainView.HSplitTop(20.0f, &Button, &MainView); - UI()->DoScrollbarOption(&g_Config.m_GfxRefreshRate, &g_Config.m_GfxRefreshRate, &Button, Localize("Refresh Rate"), 10, 1000, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_INFINITE | CUI::SCROLLBAR_OPTION_NOCLAMPVALUE, " Hz"); + str_copy(aBuf, " "); + str_append(aBuf, Localize("Hz", "Hertz")); + Ui()->DoScrollbarOption(&g_Config.m_GfxRefreshRate, &g_Config.m_GfxRefreshRate, &Button, Localize("Refresh Rate"), 10, 1000, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_INFINITE | CUi::SCROLLBAR_OPTION_NOCLAMPVALUE, aBuf); MainView.HSplitTop(2.0f, nullptr, &MainView); static CButtonContainer s_UiColorResetId; @@ -1792,15 +1632,15 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) MainView.HSplitTop(20.0f, &Text, &MainView); MainView.HSplitTop(2.0f, nullptr, &MainView); MainView.HSplitTop(20.0f, &BackendDropDown, &MainView); - UI()->DoLabel(&Text, Localize("Renderer"), 16.0f, TEXTALIGN_MC); + Ui()->DoLabel(&Text, Localize("Renderer"), 16.0f, TEXTALIGN_MC); - static std::vector s_vBackendIDNames; - static std::vector s_vpBackendIDNamesCStr; + static std::vector s_vBackendIdNames; + static std::vector s_vpBackendIdNamesCStr; static std::vector s_vBackendInfos; size_t BackendCount = FoundBackendCount + 1; - s_vBackendIDNames.resize(BackendCount); - s_vpBackendIDNamesCStr.resize(BackendCount); + s_vBackendIdNames.resize(BackendCount); + s_vpBackendIdNamesCStr.resize(BackendCount); s_vBackendInfos.resize(BackendCount); char aTmpBackendName[256]; @@ -1820,8 +1660,8 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) { bool IsDefault = IsInfoDefault(Info); str_format(aTmpBackendName, sizeof(aTmpBackendName), "%s (%d.%d.%d)%s%s", Info.m_pBackendName, Info.m_Major, Info.m_Minor, Info.m_Patch, IsDefault ? " - " : "", IsDefault ? Localize("default") : ""); - s_vBackendIDNames[CurCounter] = aTmpBackendName; - s_vpBackendIDNamesCStr[CurCounter] = s_vBackendIDNames[CurCounter].c_str(); + s_vBackendIdNames[CurCounter] = aTmpBackendName; + s_vpBackendIdNamesCStr[CurCounter] = s_vBackendIdNames[CurCounter].c_str(); if(str_comp_nocase(Info.m_pBackendName, g_Config.m_GfxBackend) == 0 && g_Config.m_GfxGLMajor == Info.m_Major && g_Config.m_GfxGLMinor == Info.m_Minor && g_Config.m_GfxGLPatch == Info.m_Patch) { OldSelectedBackend = CurCounter; @@ -1842,8 +1682,8 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) { // custom selected one str_format(aTmpBackendName, sizeof(aTmpBackendName), "%s (%s %d.%d.%d)", Localize("custom"), g_Config.m_GfxBackend, g_Config.m_GfxGLMajor, g_Config.m_GfxGLMinor, g_Config.m_GfxGLPatch); - s_vBackendIDNames[CurCounter] = aTmpBackendName; - s_vpBackendIDNamesCStr[CurCounter] = s_vBackendIDNames[CurCounter].c_str(); + s_vBackendIdNames[CurCounter] = aTmpBackendName; + s_vpBackendIdNamesCStr[CurCounter] = s_vBackendIdNames[CurCounter].c_str(); OldSelectedBackend = CurCounter; s_vBackendInfos[CurCounter].m_pBackendName = "custom"; @@ -1856,10 +1696,10 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) if(s_OldSelectedBackend == -1) s_OldSelectedBackend = OldSelectedBackend; - static CUI::SDropDownState s_BackendDropDownState; + static CUi::SDropDownState s_BackendDropDownState; static CScrollRegion s_BackendDropDownScrollRegion; s_BackendDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_BackendDropDownScrollRegion; - const int NewBackend = UI()->DoDropDown(&BackendDropDown, OldSelectedBackend, s_vpBackendIDNamesCStr.data(), BackendCount, s_BackendDropDownState); + const int NewBackend = Ui()->DoDropDown(&BackendDropDown, OldSelectedBackend, s_vpBackendIdNamesCStr.data(), BackendCount, s_BackendDropDownState); if(OldSelectedBackend != NewBackend) { str_copy(g_Config.m_GfxBackend, s_vBackendInfos[NewBackend].m_pBackendName); @@ -1873,61 +1713,61 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) } // GPU list - const auto &GPUList = Graphics()->GetGPUs(); - if(GPUList.m_vGPUs.size() > 1) + const auto &GpuList = Graphics()->GetGpus(); + if(GpuList.m_vGpus.size() > 1) { CUIRect Text, GpuDropDown; MainView.HSplitTop(10.0f, nullptr, &MainView); MainView.HSplitTop(20.0f, &Text, &MainView); MainView.HSplitTop(2.0f, nullptr, &MainView); MainView.HSplitTop(20.0f, &GpuDropDown, &MainView); - UI()->DoLabel(&Text, Localize("Graphics card"), 16.0f, TEXTALIGN_MC); + Ui()->DoLabel(&Text, Localize("Graphics card"), 16.0f, TEXTALIGN_MC); - static std::vector s_vpGPUIDNames; + static std::vector s_vpGpuIdNames; - size_t GPUCount = GPUList.m_vGPUs.size() + 1; - s_vpGPUIDNames.resize(GPUCount); + size_t GpuCount = GpuList.m_vGpus.size() + 1; + s_vpGpuIdNames.resize(GpuCount); char aCurDeviceName[256 + 4]; - int OldSelectedGPU = -1; - for(size_t i = 0; i < GPUCount; ++i) + int OldSelectedGpu = -1; + for(size_t i = 0; i < GpuCount; ++i) { if(i == 0) { - str_format(aCurDeviceName, sizeof(aCurDeviceName), "%s (%s)", Localize("auto"), GPUList.m_AutoGPU.m_aName); - s_vpGPUIDNames[i] = aCurDeviceName; - if(str_comp("auto", g_Config.m_GfxGPUName) == 0) + str_format(aCurDeviceName, sizeof(aCurDeviceName), "%s (%s)", Localize("auto"), GpuList.m_AutoGpu.m_aName); + s_vpGpuIdNames[i] = aCurDeviceName; + if(str_comp("auto", g_Config.m_GfxGpuName) == 0) { - OldSelectedGPU = 0; + OldSelectedGpu = 0; } } else { - s_vpGPUIDNames[i] = GPUList.m_vGPUs[i - 1].m_aName; - if(str_comp(GPUList.m_vGPUs[i - 1].m_aName, g_Config.m_GfxGPUName) == 0) + s_vpGpuIdNames[i] = GpuList.m_vGpus[i - 1].m_aName; + if(str_comp(GpuList.m_vGpus[i - 1].m_aName, g_Config.m_GfxGpuName) == 0) { - OldSelectedGPU = i; + OldSelectedGpu = i; } } } - static int s_OldSelectedGPU = -1; - if(s_OldSelectedGPU == -1) - s_OldSelectedGPU = OldSelectedGPU; + static int s_OldSelectedGpu = -1; + if(s_OldSelectedGpu == -1) + s_OldSelectedGpu = OldSelectedGpu; - static CUI::SDropDownState s_GpuDropDownState; + static CUi::SDropDownState s_GpuDropDownState; static CScrollRegion s_GpuDropDownScrollRegion; s_GpuDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_GpuDropDownScrollRegion; - const int NewGPU = UI()->DoDropDown(&GpuDropDown, OldSelectedGPU, s_vpGPUIDNames.data(), GPUCount, s_GpuDropDownState); - if(OldSelectedGPU != NewGPU) + const int NewGpu = Ui()->DoDropDown(&GpuDropDown, OldSelectedGpu, s_vpGpuIdNames.data(), GpuCount, s_GpuDropDownState); + if(OldSelectedGpu != NewGpu) { - if(NewGPU == 0) - str_copy(g_Config.m_GfxGPUName, "auto"); + if(NewGpu == 0) + str_copy(g_Config.m_GfxGpuName, "auto"); else - str_copy(g_Config.m_GfxGPUName, GPUList.m_vGPUs[NewGPU - 1].m_aName); + str_copy(g_Config.m_GfxGpuName, GpuList.m_vGpus[NewGpu - 1].m_aName); CheckSettings = true; - s_GfxGPUChanged = NewGPU != s_OldSelectedGPU; + s_GfxGpuChanged = NewGpu != s_OldSelectedGpu; } } @@ -1936,7 +1776,7 @@ void CMenus::RenderSettingsGraphics(CUIRect MainView) { m_NeedRestartGraphics = !(s_GfxFsaaSamples == g_Config.m_GfxFsaaSamples && !s_GfxBackendChanged && - !s_GfxGPUChanged && + !s_GfxGpuChanged && s_GfxHighdpi == g_Config.m_GfxHighdpi); } } @@ -2000,35 +1840,35 @@ void CMenus::RenderSettingsSound(CUIRect MainView) { MainView.HSplitTop(5.0f, nullptr, &MainView); MainView.HSplitTop(20.0f, &Button, &MainView); - UI()->DoScrollbarOption(&g_Config.m_SndVolume, &g_Config.m_SndVolume, &Button, Localize("Sound volume"), 0, 100, &CUI::ms_LogarithmicScrollbarScale, 0u, "%"); + Ui()->DoScrollbarOption(&g_Config.m_SndVolume, &g_Config.m_SndVolume, &Button, Localize("Sound volume"), 0, 100, &CUi::ms_LogarithmicScrollbarScale, 0u, "%"); } // volume slider game sounds { MainView.HSplitTop(5.0f, nullptr, &MainView); MainView.HSplitTop(20.0f, &Button, &MainView); - UI()->DoScrollbarOption(&g_Config.m_SndGameSoundVolume, &g_Config.m_SndGameSoundVolume, &Button, Localize("Game sound volume"), 0, 100, &CUI::ms_LogarithmicScrollbarScale, 0u, "%"); + Ui()->DoScrollbarOption(&g_Config.m_SndGameSoundVolume, &g_Config.m_SndGameSoundVolume, &Button, Localize("Game sound volume"), 0, 100, &CUi::ms_LogarithmicScrollbarScale, 0u, "%"); } // volume slider gui sounds { MainView.HSplitTop(5.0f, nullptr, &MainView); MainView.HSplitTop(20.0f, &Button, &MainView); - UI()->DoScrollbarOption(&g_Config.m_SndChatSoundVolume, &g_Config.m_SndChatSoundVolume, &Button, Localize("Chat sound volume"), 0, 100, &CUI::ms_LogarithmicScrollbarScale, 0u, "%"); + Ui()->DoScrollbarOption(&g_Config.m_SndChatSoundVolume, &g_Config.m_SndChatSoundVolume, &Button, Localize("Chat sound volume"), 0, 100, &CUi::ms_LogarithmicScrollbarScale, 0u, "%"); } // volume slider map sounds { MainView.HSplitTop(5.0f, nullptr, &MainView); MainView.HSplitTop(20.0f, &Button, &MainView); - UI()->DoScrollbarOption(&g_Config.m_SndMapSoundVolume, &g_Config.m_SndMapSoundVolume, &Button, Localize("Map sound volume"), 0, 100, &CUI::ms_LogarithmicScrollbarScale, 0u, "%"); + Ui()->DoScrollbarOption(&g_Config.m_SndMapSoundVolume, &g_Config.m_SndMapSoundVolume, &Button, Localize("Map sound volume"), 0, 100, &CUi::ms_LogarithmicScrollbarScale, 0u, "%"); } // volume slider background music { MainView.HSplitTop(5.0f, nullptr, &MainView); MainView.HSplitTop(20.0f, &Button, &MainView); - UI()->DoScrollbarOption(&g_Config.m_SndBackgroundMusicVolume, &g_Config.m_SndBackgroundMusicVolume, &Button, Localize("Background music volume"), 0, 100, &CUI::ms_LogarithmicScrollbarScale, 0u, "%"); + Ui()->DoScrollbarOption(&g_Config.m_SndBackgroundMusicVolume, &g_Config.m_SndBackgroundMusicVolume, &Button, Localize("Background music volume"), 0, 100, &CUi::ms_LogarithmicScrollbarScale, 0u, "%"); } } @@ -2067,7 +1907,7 @@ bool CMenus::RenderLanguageSelection(CUIRect MainView) FlagRect.HMargin(3.0f, &FlagRect); m_pClient->m_CountryFlags.Render(Language.m_CountryCode, ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f), FlagRect.x, FlagRect.y, FlagRect.w, FlagRect.h); - UI()->DoLabel(&Label, Language.m_Name.c_str(), 16.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, Language.m_Name.c_str(), 16.0f, TEXTALIGN_ML); } s_SelectedLanguage = s_ListBox.DoEnd(); @@ -2084,30 +1924,32 @@ bool CMenus::RenderLanguageSelection(CUIRect MainView) void CMenus::RenderSettings(CUIRect MainView) { // render background - CUIRect Button, TabBar, RestartWarning; + CUIRect Button, TabBar, RestartBar; MainView.VSplitRight(120.0f, &MainView, &TabBar); MainView.Draw(ms_ColorTabbarActive, IGraphics::CORNER_B, 10.0f); - MainView.Margin(10.0f, &MainView); - MainView.HSplitBottom(15.0f, &MainView, &RestartWarning); + MainView.Margin(20.0f, &MainView); + + const bool NeedRestart = m_NeedRestartGraphics || m_NeedRestartSound || m_NeedRestartUpdate; + if(NeedRestart) + { + MainView.HSplitBottom(20.0f, &MainView, &RestartBar); + MainView.HSplitBottom(10.0f, &MainView, nullptr); + } + TabBar.HSplitTop(50.0f, &Button, &TabBar); Button.Draw(ms_ColorTabbarActive, IGraphics::CORNER_BR, 10.0f); - MainView.HSplitTop(10.0f, nullptr, &MainView); - const char *apTabs[SETTINGS_LENGTH] = { Localize("Language"), Localize("General"), Localize("Player"), - "Tee", + Client()->IsSixup() ? "Tee 0.7" : Localize("Tee"), Localize("Appearance"), Localize("Controls"), Localize("Graphics"), Localize("Sound"), Localize("DDNet"), - Localize("Assets"), - "StA", - "PlayerAssets"}; - + Localize("Assets")}; static CButtonContainer s_aTabButtons[SETTINGS_LENGTH]; for(int i = 0; i < SETTINGS_LENGTH; i++) @@ -2118,127 +1960,128 @@ void CMenus::RenderSettings(CUIRect MainView) g_Config.m_UiSettingsPage = i; } - MainView.Margin(10.0f, &MainView); - RestartWarning.VMargin(10.0f, &RestartWarning); - if(g_Config.m_UiSettingsPage == SETTINGS_LANGUAGE) { - m_pBackground->ChangePosition(CMenuBackground::POS_SETTINGS_LANGUAGE); + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_LANGUAGE); RenderLanguageSelection(MainView); } else if(g_Config.m_UiSettingsPage == SETTINGS_GENERAL) { - m_pBackground->ChangePosition(CMenuBackground::POS_SETTINGS_GENERAL); + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_GENERAL); RenderSettingsGeneral(MainView); } else if(g_Config.m_UiSettingsPage == SETTINGS_PLAYER) { - m_pBackground->ChangePosition(CMenuBackground::POS_SETTINGS_PLAYER); + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_PLAYER); RenderSettingsPlayer(MainView); } else if(g_Config.m_UiSettingsPage == SETTINGS_TEE) { - m_pBackground->ChangePosition(CMenuBackground::POS_SETTINGS_TEE); - RenderSettingsTee(MainView); + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_TEE); + if(Client()->IsSixup()) + RenderSettingsTee7(MainView); + else + RenderSettingsTee(MainView); } else if(g_Config.m_UiSettingsPage == SETTINGS_APPEARANCE) { - m_pBackground->ChangePosition(CMenuBackground::POS_SETTINGS_APPEARANCE); + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_APPEARANCE); RenderSettingsAppearance(MainView); } else if(g_Config.m_UiSettingsPage == SETTINGS_CONTROLS) { - m_pBackground->ChangePosition(CMenuBackground::POS_SETTINGS_CONTROLS); + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_CONTROLS); RenderSettingsControls(MainView); } else if(g_Config.m_UiSettingsPage == SETTINGS_GRAPHICS) { - m_pBackground->ChangePosition(CMenuBackground::POS_SETTINGS_GRAPHICS); + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_GRAPHICS); RenderSettingsGraphics(MainView); } else if(g_Config.m_UiSettingsPage == SETTINGS_SOUND) { - m_pBackground->ChangePosition(CMenuBackground::POS_SETTINGS_SOUND); + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_SOUND); RenderSettingsSound(MainView); } else if(g_Config.m_UiSettingsPage == SETTINGS_DDNET) { - m_pBackground->ChangePosition(CMenuBackground::POS_SETTINGS_DDNET); + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_DDNET); RenderSettingsDDNet(MainView); } else if(g_Config.m_UiSettingsPage == SETTINGS_ASSETS) { - m_pBackground->ChangePosition(CMenuBackground::POS_SETTINGS_ASSETS); + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_ASSETS); RenderSettingsCustom(MainView); } - - else if(g_Config.m_UiSettingsPage == SETTINGS_STA) - { - m_pBackground->ChangePosition(10); - RenderSettingsStA(MainView); - } - else if(g_Config.m_UiSettingsPage == SETTINGS_PROFILES) - { - m_pBackground->ChangePosition(11); - RenderSettingsProfiles(MainView); - } - if(m_NeedRestartUpdate) + else { - TextRender()->TextColor(1.0f, 0.4f, 0.4f, 1.0f); - UI()->DoLabel(&RestartWarning, Localize("DDNet Client needs to be restarted to complete update!"), 14.0f, TEXTALIGN_ML); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + dbg_assert(false, "ui_settings_page invalid"); } - else if(m_NeedRestartGeneral || m_NeedRestartSkins || m_NeedRestartGraphics || m_NeedRestartSound || m_NeedRestartDDNet) - UI()->DoLabel(&RestartWarning, Localize("You must restart the game for all settings to take effect."), 14.0f, TEXTALIGN_ML); -} - -ColorHSLA CMenus::RenderHSLScrollbars(CUIRect *pRect, unsigned int *pColor, bool Alpha, bool ClampedLight) -{ - ColorHSLA Color(*pColor, Alpha); - CUIRect Preview, Button, Label; - char aBuf[32]; - float *apComponent[] = {&Color.h, &Color.s, &Color.l, &Color.a}; - const char *apLabels[] = {Localize("Hue"), Localize("Sat."), Localize("Lht."), Localize("Alpha")}; - - float SizePerEntry = 20.0f; - float MarginPerEntry = 5.0f; - - float OffY = (SizePerEntry + MarginPerEntry) * (3 + (Alpha ? 1 : 0)) - 40.0f; - pRect->VSplitLeft(40.0f, &Preview, pRect); - Preview.HSplitTop(OffY / 2.0f, NULL, &Preview); - Preview.HSplitTop(40.0f, &Preview, NULL); - Graphics()->TextureClear(); + if(NeedRestart) { - const float SizeBorder = 5.0f; - Graphics()->SetColor(ColorRGBA(0.15f, 0.15f, 0.15f, 1)); - int TmpCont = Graphics()->CreateRectQuadContainer(Preview.x - SizeBorder / 2.0f, Preview.y - SizeBorder / 2.0f, Preview.w + SizeBorder, Preview.h + SizeBorder, 4.0f + SizeBorder / 2.0f, IGraphics::CORNER_ALL); - Graphics()->RenderQuadContainer(TmpCont, -1); - Graphics()->DeleteQuadContainer(TmpCont); - } - ColorHSLA RenderColorHSLA(Color.r, Color.g, Color.b, Color.a); - if(ClampedLight) - RenderColorHSLA = RenderColorHSLA.UnclampLighting(); - Graphics()->SetColor(color_cast(RenderColorHSLA)); - int TmpCont = Graphics()->CreateRectQuadContainer(Preview.x, Preview.y, Preview.w, Preview.h, 4.0f, IGraphics::CORNER_ALL); - Graphics()->RenderQuadContainer(TmpCont, -1); - Graphics()->DeleteQuadContainer(TmpCont); - - auto &&RenderHSLColorsRect = [&](CUIRect *pColorRect) { - Graphics()->TextureClear(); - Graphics()->TrianglesBegin(); - + CUIRect RestartWarning, RestartButton; + RestartBar.VSplitRight(125.0f, &RestartWarning, &RestartButton); + RestartWarning.VSplitRight(10.0f, &RestartWarning, nullptr); + if(m_NeedRestartUpdate) + { + TextRender()->TextColor(1.0f, 0.4f, 0.4f, 1.0f); + Ui()->DoLabel(&RestartWarning, Localize("DDNet Client needs to be restarted to complete update!"), 14.0f, TEXTALIGN_ML); + TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + } + else + { + Ui()->DoLabel(&RestartWarning, Localize("You must restart the game for all settings to take effect."), 14.0f, TEXTALIGN_ML); + } + + static CButtonContainer s_RestartButton; + if(DoButton_Menu(&s_RestartButton, Localize("Restart"), 0, &RestartButton)) + { + if(Client()->State() == IClient::STATE_ONLINE || m_pClient->Editor()->HasUnsavedData()) + { + m_Popup = POPUP_RESTART; + } + else + { + Client()->Restart(); + } + } + } +} + +bool CMenus::RenderHslaScrollbars(CUIRect *pRect, unsigned int *pColor, bool Alpha, float DarkestLight) +{ + const unsigned PrevPackedColor = *pColor; + ColorHSLA Color(*pColor, Alpha); + const ColorHSLA OriginalColor = Color; + const char *apLabels[] = {Localize("Hue"), Localize("Sat."), Localize("Lht."), Localize("Alpha")}; + const float SizePerEntry = 20.0f; + const float MarginPerEntry = 5.0f; + const float PreviewMargin = 2.5f; + const float PreviewHeight = 40.0f + 2 * PreviewMargin; + const float OffY = (SizePerEntry + MarginPerEntry) * (3 + (Alpha ? 1 : 0)) - PreviewHeight; + + CUIRect Preview; + pRect->VSplitLeft(PreviewHeight, &Preview, pRect); + Preview.HSplitTop(OffY / 2.0f, nullptr, &Preview); + Preview.HSplitTop(PreviewHeight, &Preview, nullptr); + + Preview.Draw(ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f), IGraphics::CORNER_ALL, 4.0f + PreviewMargin); + Preview.Margin(PreviewMargin, &Preview); + Preview.Draw(color_cast(Color.UnclampLighting(DarkestLight)), IGraphics::CORNER_ALL, 4.0f + PreviewMargin); + + auto &&RenderHueRect = [&](CUIRect *pColorRect) { float CurXOff = pColorRect->x; - float SizeColor = pColorRect->w / 6; + const float SizeColor = pColorRect->w / 6; // red to yellow { - IGraphics::CColorVertex Array[4] = { + IGraphics::CColorVertex aColorVertices[] = { IGraphics::CColorVertex(0, 1, 0, 0, 1), IGraphics::CColorVertex(1, 1, 1, 0, 1), IGraphics::CColorVertex(2, 1, 0, 0, 1), IGraphics::CColorVertex(3, 1, 1, 0, 1)}; - Graphics()->SetColorVertex(Array, 4); + Graphics()->SetColorVertex(aColorVertices, std::size(aColorVertices)); IGraphics::CFreeformItem Freeform( CurXOff, pColorRect->y, @@ -2251,12 +2094,12 @@ ColorHSLA CMenus::RenderHSLScrollbars(CUIRect *pRect, unsigned int *pColor, bool // yellow to green CurXOff += SizeColor; { - IGraphics::CColorVertex Array[4] = { + IGraphics::CColorVertex aColorVertices[] = { IGraphics::CColorVertex(0, 1, 1, 0, 1), IGraphics::CColorVertex(1, 0, 1, 0, 1), IGraphics::CColorVertex(2, 1, 1, 0, 1), IGraphics::CColorVertex(3, 0, 1, 0, 1)}; - Graphics()->SetColorVertex(Array, 4); + Graphics()->SetColorVertex(aColorVertices, std::size(aColorVertices)); IGraphics::CFreeformItem Freeform( CurXOff, pColorRect->y, @@ -2269,12 +2112,12 @@ ColorHSLA CMenus::RenderHSLScrollbars(CUIRect *pRect, unsigned int *pColor, bool CurXOff += SizeColor; // green to turquoise { - IGraphics::CColorVertex Array[4] = { + IGraphics::CColorVertex aColorVertices[] = { IGraphics::CColorVertex(0, 0, 1, 0, 1), IGraphics::CColorVertex(1, 0, 1, 1, 1), IGraphics::CColorVertex(2, 0, 1, 0, 1), IGraphics::CColorVertex(3, 0, 1, 1, 1)}; - Graphics()->SetColorVertex(Array, 4); + Graphics()->SetColorVertex(aColorVertices, std::size(aColorVertices)); IGraphics::CFreeformItem Freeform( CurXOff, pColorRect->y, @@ -2287,12 +2130,12 @@ ColorHSLA CMenus::RenderHSLScrollbars(CUIRect *pRect, unsigned int *pColor, bool CurXOff += SizeColor; // turquoise to blue { - IGraphics::CColorVertex Array[4] = { + IGraphics::CColorVertex aColorVertices[] = { IGraphics::CColorVertex(0, 0, 1, 1, 1), IGraphics::CColorVertex(1, 0, 0, 1, 1), IGraphics::CColorVertex(2, 0, 1, 1, 1), IGraphics::CColorVertex(3, 0, 0, 1, 1)}; - Graphics()->SetColorVertex(Array, 4); + Graphics()->SetColorVertex(aColorVertices, std::size(aColorVertices)); IGraphics::CFreeformItem Freeform( CurXOff, pColorRect->y, @@ -2305,12 +2148,12 @@ ColorHSLA CMenus::RenderHSLScrollbars(CUIRect *pRect, unsigned int *pColor, bool CurXOff += SizeColor; // blue to purple { - IGraphics::CColorVertex Array[4] = { + IGraphics::CColorVertex aColorVertices[] = { IGraphics::CColorVertex(0, 0, 0, 1, 1), IGraphics::CColorVertex(1, 1, 0, 1, 1), IGraphics::CColorVertex(2, 0, 0, 1, 1), IGraphics::CColorVertex(3, 1, 0, 1, 1)}; - Graphics()->SetColorVertex(Array, 4); + Graphics()->SetColorVertex(aColorVertices, std::size(aColorVertices)); IGraphics::CFreeformItem Freeform( CurXOff, pColorRect->y, @@ -2323,12 +2166,12 @@ ColorHSLA CMenus::RenderHSLScrollbars(CUIRect *pRect, unsigned int *pColor, bool CurXOff += SizeColor; // purple to red { - IGraphics::CColorVertex Array[4] = { + IGraphics::CColorVertex aColorVertices[] = { IGraphics::CColorVertex(0, 1, 0, 1, 1), IGraphics::CColorVertex(1, 1, 0, 0, 1), IGraphics::CColorVertex(2, 1, 0, 1, 1), IGraphics::CColorVertex(3, 1, 0, 0, 1)}; - Graphics()->SetColorVertex(Array, 4); + Graphics()->SetColorVertex(aColorVertices, std::size(aColorVertices)); IGraphics::CFreeformItem Freeform( CurXOff, pColorRect->y, @@ -2337,150 +2180,68 @@ ColorHSLA CMenus::RenderHSLScrollbars(CUIRect *pRect, unsigned int *pColor, bool CurXOff + SizeColor, pColorRect->y + pColorRect->h); Graphics()->QuadsDrawFreeform(&Freeform, 1); } - - Graphics()->TrianglesEnd(); }; - auto &&RenderHSLSatRect = [&](CUIRect *pColorRect, ColorRGBA &CurColor) { - Graphics()->TextureClear(); - Graphics()->TrianglesBegin(); - - float CurXOff = pColorRect->x; - float SizeColor = pColorRect->w; - - ColorHSLA RightColor = color_cast(CurColor); + auto &&RenderSaturationRect = [&](CUIRect *pColorRect, const ColorRGBA &CurColor) { ColorHSLA LeftColor = color_cast(CurColor); + ColorHSLA RightColor = color_cast(CurColor); - LeftColor.g = 0; - RightColor.g = 1; - - ColorRGBA RightColorRGBA = color_cast(RightColor); - ColorRGBA LeftColorRGBA = color_cast(LeftColor); + LeftColor.s = 0.0f; + RightColor.s = 1.0f; - // saturation - { - IGraphics::CColorVertex Array[4] = { - IGraphics::CColorVertex(0, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1), - IGraphics::CColorVertex(1, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1), - IGraphics::CColorVertex(2, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1), - IGraphics::CColorVertex(3, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1)}; - Graphics()->SetColorVertex(Array, 4); + const ColorRGBA LeftColorRGBA = color_cast(LeftColor); + const ColorRGBA RightColorRGBA = color_cast(RightColor); - IGraphics::CFreeformItem Freeform( - CurXOff, pColorRect->y, - CurXOff + SizeColor, pColorRect->y, - CurXOff, pColorRect->y + pColorRect->h, - CurXOff + SizeColor, pColorRect->y + pColorRect->h); - Graphics()->QuadsDrawFreeform(&Freeform, 1); - } + Graphics()->SetColor4(LeftColorRGBA, RightColorRGBA, RightColorRGBA, LeftColorRGBA); - Graphics()->TrianglesEnd(); + IGraphics::CFreeformItem Freeform( + pColorRect->x, pColorRect->y, + pColorRect->x + pColorRect->w, pColorRect->y, + pColorRect->x, pColorRect->y + pColorRect->h, + pColorRect->x + pColorRect->w, pColorRect->y + pColorRect->h); + Graphics()->QuadsDrawFreeform(&Freeform, 1); }; - auto &&RenderHSLLightRect = [&](CUIRect *pColorRect, ColorRGBA &CurColorSat) { - Graphics()->TextureClear(); - Graphics()->TrianglesBegin(); - - float CurXOff = pColorRect->x; - float SizeColor = pColorRect->w / (ClampedLight ? 1.0f : 2.0f); - - ColorHSLA RightColor = color_cast(CurColorSat); - ColorHSLA LeftColor = color_cast(CurColorSat); - - LeftColor.b = ColorHSLA::DARKEST_LGT; - RightColor.b = 1; - - ColorRGBA RightColorRGBA = color_cast(RightColor); - ColorRGBA LeftColorRGBA = color_cast(LeftColor); - - if(!ClampedLight) - CurXOff += SizeColor; - - // light - { - IGraphics::CColorVertex Array[4] = { - IGraphics::CColorVertex(0, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1), - IGraphics::CColorVertex(1, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1), - IGraphics::CColorVertex(2, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1), - IGraphics::CColorVertex(3, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1)}; - Graphics()->SetColorVertex(Array, 4); - - IGraphics::CFreeformItem Freeform( - CurXOff, pColorRect->y, - CurXOff + SizeColor, pColorRect->y, - CurXOff, pColorRect->y + pColorRect->h, - CurXOff + SizeColor, pColorRect->y + pColorRect->h); - Graphics()->QuadsDrawFreeform(&Freeform, 1); - } - - if(!ClampedLight) - { - CurXOff -= SizeColor; - LeftColor.b = 0; - RightColor.b = ColorHSLA::DARKEST_LGT; + auto &&RenderLightingRect = [&](CUIRect *pColorRect, const ColorRGBA &CurColor) { + ColorHSLA LeftColor = color_cast(CurColor); + ColorHSLA RightColor = color_cast(CurColor); - RightColorRGBA = color_cast(RightColor); - LeftColorRGBA = color_cast(LeftColor); + LeftColor.l = DarkestLight; + RightColor.l = 1.0f; - IGraphics::CColorVertex Array[4] = { - IGraphics::CColorVertex(0, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1), - IGraphics::CColorVertex(1, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1), - IGraphics::CColorVertex(2, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, 1), - IGraphics::CColorVertex(3, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, 1)}; - Graphics()->SetColorVertex(Array, 4); + const ColorRGBA LeftColorRGBA = color_cast(LeftColor); + const ColorRGBA RightColorRGBA = color_cast(RightColor); - IGraphics::CFreeformItem Freeform( - CurXOff, pColorRect->y, - CurXOff + SizeColor, pColorRect->y, - CurXOff, pColorRect->y + pColorRect->h, - CurXOff + SizeColor, pColorRect->y + pColorRect->h); - Graphics()->QuadsDrawFreeform(&Freeform, 1); - } + Graphics()->SetColor4(LeftColorRGBA, RightColorRGBA, RightColorRGBA, LeftColorRGBA); - Graphics()->TrianglesEnd(); + IGraphics::CFreeformItem Freeform( + pColorRect->x, pColorRect->y, + pColorRect->x + pColorRect->w, pColorRect->y, + pColorRect->x, pColorRect->y + pColorRect->h, + pColorRect->x + pColorRect->w, pColorRect->y + pColorRect->h); + Graphics()->QuadsDrawFreeform(&Freeform, 1); }; - auto &&RenderHSLAlphaRect = [&](CUIRect *pColorRect, ColorRGBA &CurColorFull) { - Graphics()->TextureClear(); - Graphics()->TrianglesBegin(); - - float CurXOff = pColorRect->x; - float SizeColor = pColorRect->w; - - ColorHSLA RightColor = color_cast(CurColorFull); - ColorHSLA LeftColor = color_cast(CurColorFull); + auto &&RenderAlphaRect = [&](CUIRect *pColorRect, const ColorRGBA &CurColorFull) { + const ColorRGBA LeftColorRGBA = color_cast(color_cast(CurColorFull).WithAlpha(0.0f)); + const ColorRGBA RightColorRGBA = color_cast(color_cast(CurColorFull).WithAlpha(1.0f)); - LeftColor.a = 0; - RightColor.a = 1; - - ColorRGBA RightColorRGBA = color_cast(RightColor); - ColorRGBA LeftColorRGBA = color_cast(LeftColor); - - // alpha - { - IGraphics::CColorVertex Array[4] = { - IGraphics::CColorVertex(0, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, LeftColorRGBA.a), - IGraphics::CColorVertex(1, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, RightColorRGBA.a), - IGraphics::CColorVertex(2, LeftColorRGBA.r, LeftColorRGBA.g, LeftColorRGBA.b, LeftColorRGBA.a), - IGraphics::CColorVertex(3, RightColorRGBA.r, RightColorRGBA.g, RightColorRGBA.b, RightColorRGBA.a)}; - Graphics()->SetColorVertex(Array, 4); + Graphics()->SetColor4(LeftColorRGBA, RightColorRGBA, RightColorRGBA, LeftColorRGBA); - IGraphics::CFreeformItem Freeform( - CurXOff, pColorRect->y, - CurXOff + SizeColor, pColorRect->y, - CurXOff, pColorRect->y + pColorRect->h, - CurXOff + SizeColor, pColorRect->y + pColorRect->h); - Graphics()->QuadsDrawFreeform(&Freeform, 1); - } - - Graphics()->TrianglesEnd(); + IGraphics::CFreeformItem Freeform( + pColorRect->x, pColorRect->y, + pColorRect->x + pColorRect->w, pColorRect->y, + pColorRect->x, pColorRect->y + pColorRect->h, + pColorRect->x + pColorRect->w, pColorRect->y + pColorRect->h); + Graphics()->QuadsDrawFreeform(&Freeform, 1); }; for(int i = 0; i < 3 + Alpha; i++) { + CUIRect Button, Label; pRect->HSplitTop(SizePerEntry, &Button, pRect); - pRect->HSplitTop(MarginPerEntry, NULL, pRect); - Button.VSplitLeft(10.0f, 0, &Button); + pRect->HSplitTop(MarginPerEntry, nullptr, pRect); + Button.VSplitLeft(10.0f, nullptr, &Button); Button.VSplitLeft(100.0f, &Label, &Button); Button.Draw(ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f), IGraphics::CORNER_ALL, 1.0f); @@ -2488,47 +2249,43 @@ ColorHSLA CMenus::RenderHSLScrollbars(CUIRect *pRect, unsigned int *pColor, bool CUIRect Rail; Button.Margin(2.0f, &Rail); - str_format(aBuf, sizeof(aBuf), "%s: %03d", apLabels[i], (int)(*apComponent[i] * 255)); - UI()->DoLabel(&Label, aBuf, 14.0f, TEXTALIGN_ML); - - ColorHSLA CurColorPureHSLA(RenderColorHSLA.r, 1, 0.5f, 1); - ColorRGBA CurColorPure = color_cast(CurColorPureHSLA); - ColorRGBA ColorInner(1, 1, 1, 0.25f); + char aBuf[32]; + str_format(aBuf, sizeof(aBuf), "%s: %03d", apLabels[i], round_to_int(Color[i] * 255.0f)); + Ui()->DoLabel(&Label, aBuf, 14.0f, TEXTALIGN_ML); + ColorRGBA HandleColor; + Graphics()->TextureClear(); + Graphics()->TrianglesBegin(); if(i == 0) { - ColorInner = CurColorPure; - RenderHSLColorsRect(&Rail); + RenderHueRect(&Rail); + HandleColor = color_cast(ColorHSLA(Color.h, 1.0f, 0.5f, 1.0f)); } else if(i == 1) { - RenderHSLSatRect(&Rail, CurColorPure); - ColorInner = color_cast(ColorHSLA(CurColorPureHSLA.r, *apComponent[1], CurColorPureHSLA.b, 1)); + RenderSaturationRect(&Rail, color_cast(ColorHSLA(Color.h, 1.0f, 0.5f, 1.0f))); + HandleColor = color_cast(ColorHSLA(Color.h, Color.s, 0.5f, 1.0f)); } else if(i == 2) { - ColorRGBA CurColorSat = color_cast(ColorHSLA(CurColorPureHSLA.r, *apComponent[1], 0.5f, 1)); - RenderHSLLightRect(&Rail, CurColorSat); - float LightVal = *apComponent[2]; - if(ClampedLight) - LightVal = ColorHSLA::DARKEST_LGT + LightVal * (1.0f - ColorHSLA::DARKEST_LGT); - ColorInner = color_cast(ColorHSLA(CurColorPureHSLA.r, *apComponent[1], LightVal, 1)); + RenderLightingRect(&Rail, color_cast(ColorHSLA(Color.h, Color.s, 0.5f, 1.0f))); + HandleColor = color_cast(ColorHSLA(Color.h, Color.s, Color.l, 1.0f).UnclampLighting(DarkestLight)); } else if(i == 3) { - ColorRGBA CurColorFull = color_cast(ColorHSLA(CurColorPureHSLA.r, *apComponent[1], *apComponent[2], 1)); - RenderHSLAlphaRect(&Rail, CurColorFull); - float LightVal = *apComponent[2]; - if(ClampedLight) - LightVal = ColorHSLA::DARKEST_LGT + LightVal * (1.0f - ColorHSLA::DARKEST_LGT); - ColorInner = color_cast(ColorHSLA(CurColorPureHSLA.r, *apComponent[1], LightVal, *apComponent[3])); + RenderAlphaRect(&Rail, color_cast(ColorHSLA(Color.h, Color.s, Color.l, 1.0f).UnclampLighting(DarkestLight))); + HandleColor = color_cast(Color.UnclampLighting(DarkestLight)); } + Graphics()->TrianglesEnd(); - *apComponent[i] = UI()->DoScrollbarH(&((char *)pColor)[i], &Button, *apComponent[i], &ColorInner); + Color[i] = Ui()->DoScrollbarH(&((char *)pColor)[i], &Button, Color[i], &HandleColor); } - *pColor = Color.Pack(Alpha); - return Color; + if(OriginalColor != Color) + { + *pColor = Color.Pack(Alpha); + } + return PrevPackedColor != *pColor; } enum @@ -2547,193 +2304,171 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) char aBuf[128]; static int s_CurTab = 0; - CUIRect TabBar, Page1Tab, Page2Tab, Page3Tab, Page4Tab, Page5Tab, Page6Tab, LeftView, RightView, Section, Button, Label; - - MainView.HSplitTop(20, &TabBar, &MainView); - float TabsW = TabBar.w; - TabBar.VSplitLeft(TabsW / NUMBER_OF_APPEARANCE_TABS, &Page1Tab, &Page2Tab); - Page2Tab.VSplitLeft(TabsW / NUMBER_OF_APPEARANCE_TABS, &Page2Tab, &Page3Tab); - Page3Tab.VSplitLeft(TabsW / NUMBER_OF_APPEARANCE_TABS, &Page3Tab, &Page4Tab); - Page4Tab.VSplitLeft(TabsW / NUMBER_OF_APPEARANCE_TABS, &Page4Tab, &Page5Tab); - Page5Tab.VSplitLeft(TabsW / NUMBER_OF_APPEARANCE_TABS, &Page5Tab, &Page6Tab); + CUIRect TabBar, LeftView, RightView, Button, Label; + MainView.HSplitTop(20.0f, &TabBar, &MainView); + const float TabWidth = TabBar.w / NUMBER_OF_APPEARANCE_TABS; static CButtonContainer s_aPageTabs[NUMBER_OF_APPEARANCE_TABS] = {}; + const char *apTabNames[NUMBER_OF_APPEARANCE_TABS] = { + Localize("HUD"), + Localize("Chat"), + Localize("Name Plate"), + Localize("Hook Collisions"), + Localize("Info Messages"), + Localize("Laser")}; - if(DoButton_MenuTab(&s_aPageTabs[APPEARANCE_TAB_HUD], Localize("HUD"), s_CurTab == APPEARANCE_TAB_HUD, &Page1Tab, IGraphics::CORNER_L, NULL, NULL, NULL, NULL, 4)) - s_CurTab = APPEARANCE_TAB_HUD; - if(DoButton_MenuTab(&s_aPageTabs[APPEARANCE_TAB_CHAT], Localize("Chat"), s_CurTab == APPEARANCE_TAB_CHAT, &Page2Tab, 0, NULL, NULL, NULL, NULL, 4)) - s_CurTab = APPEARANCE_TAB_CHAT; - if(DoButton_MenuTab(&s_aPageTabs[APPEARANCE_TAB_NAME_PLATE], Localize("Name Plate"), s_CurTab == APPEARANCE_TAB_NAME_PLATE, &Page3Tab, 0, NULL, NULL, NULL, NULL, 4)) - s_CurTab = APPEARANCE_TAB_NAME_PLATE; - if(DoButton_MenuTab(&s_aPageTabs[APPEARANCE_TAB_HOOK_COLLISION], Localize("Hook Collisions"), s_CurTab == APPEARANCE_TAB_HOOK_COLLISION, &Page4Tab, 0, NULL, NULL, NULL, NULL, 4)) - s_CurTab = APPEARANCE_TAB_HOOK_COLLISION; - if(DoButton_MenuTab(&s_aPageTabs[APPEARANCE_TAB_INFO_MESSAGES], Localize("Info Messages"), s_CurTab == APPEARANCE_TAB_INFO_MESSAGES, &Page5Tab, 0, NULL, NULL, NULL, NULL, 4)) - s_CurTab = APPEARANCE_TAB_INFO_MESSAGES; - if(DoButton_MenuTab(&s_aPageTabs[APPEARANCE_TAB_LASER], Localize("Laser"), s_CurTab == APPEARANCE_TAB_LASER, &Page6Tab, IGraphics::CORNER_R, NULL, NULL, NULL, NULL, 4)) - s_CurTab = APPEARANCE_TAB_LASER; + for(int Tab = APPEARANCE_TAB_HUD; Tab < NUMBER_OF_APPEARANCE_TABS; ++Tab) + { + TabBar.VSplitLeft(TabWidth, &Button, &TabBar); + const int Corners = Tab == APPEARANCE_TAB_HUD ? IGraphics::CORNER_L : Tab == NUMBER_OF_APPEARANCE_TABS - 1 ? IGraphics::CORNER_R : IGraphics::CORNER_NONE; + if(DoButton_MenuTab(&s_aPageTabs[Tab], apTabNames[Tab], s_CurTab == Tab, &Button, Corners, nullptr, nullptr, nullptr, nullptr, 4.0f)) + { + s_CurTab = Tab; + } + } MainView.HSplitTop(10.0f, nullptr, &MainView); const float LineSize = 20.0f; const float ColorPickerLineSize = 25.0f; - const float SectionMargin = 5.0f; - const float SectionTotalMargin = SectionMargin * 2; const float HeadlineFontSize = 20.0f; - const float HeadlineAndVMargin = HeadlineFontSize + SectionTotalMargin; - const float MarginToNextSection = 5.0f; + const float HeadlineHeight = 30.0f; + const float MarginSmall = 5.0f; + const float MarginBetweenViews = 20.0f; const float ColorPickerLabelSize = 13.0f; const float ColorPickerLineSpacing = 5.0f; if(s_CurTab == APPEARANCE_TAB_HUD) { - MainView.VSplitMid(&LeftView, &RightView); + MainView.VSplitMid(&LeftView, &RightView, MarginBetweenViews); // ***** HUD ***** // - LeftView.HSplitTop(HeadlineAndVMargin, &Label, &LeftView); - UI()->DoLabel(&Label, Localize("HUD"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(HeadlineHeight, &Label, &LeftView); + Ui()->DoLabel(&Label, Localize("HUD"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(MarginSmall, nullptr, &LeftView); // Switch of the entire HUD - LeftView.HSplitTop(SectionTotalMargin + LineSize, &Section, &LeftView); - Section.Margin(SectionMargin, &Section); - - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhud, Localize("Show ingame HUD"), &g_Config.m_ClShowhud, &Section, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhud, Localize("Show ingame HUD"), &g_Config.m_ClShowhud, &LeftView, LineSize); // Switches of the various normal HUD elements - LeftView.HSplitTop(SectionTotalMargin + 6 * LineSize, &Section, &LeftView); - Section.Margin(SectionMargin, &Section); - - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudHealthAmmo, Localize("Show health, shields and ammo"), &g_Config.m_ClShowhudHealthAmmo, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowChat, Localize("Show chat"), &g_Config.m_ClShowChat, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClNameplates, Localize("Show name plates"), &g_Config.m_ClNameplates, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowKillMessages, Localize("Show kill messages"), &g_Config.m_ClShowKillMessages, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudScore, Localize("Show score"), &g_Config.m_ClShowhudScore, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowLocalTimeAlways, Localize("Show local time always"), &g_Config.m_ClShowLocalTimeAlways, &Section, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudHealthAmmo, Localize("Show health, shields and ammo"), &g_Config.m_ClShowhudHealthAmmo, &LeftView, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudScore, Localize("Show score"), &g_Config.m_ClShowhudScore, &LeftView, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowLocalTimeAlways, Localize("Show local time always"), &g_Config.m_ClShowLocalTimeAlways, &LeftView, LineSize); // Settings of the HUD element for votes - LeftView.HSplitTop(SectionTotalMargin + LineSize, &Section, &LeftView); - Section.Margin(SectionMargin, &Section); - - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowVotesAfterVoting, Localize("Show votes window after voting"), &g_Config.m_ClShowVotesAfterVoting, &Section, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowVotesAfterVoting, Localize("Show votes window after voting"), &g_Config.m_ClShowVotesAfterVoting, &LeftView, LineSize); // ***** DDRace HUD ***** // - RightView.HSplitTop(HeadlineAndVMargin, &Label, &RightView); - UI()->DoLabel(&Label, Localize("DDRace HUD"), HeadlineFontSize, TEXTALIGN_ML); + RightView.HSplitTop(HeadlineHeight, &Label, &RightView); + Ui()->DoLabel(&Label, Localize("DDRace HUD"), HeadlineFontSize, TEXTALIGN_ML); + RightView.HSplitTop(MarginSmall, nullptr, &RightView); // Switches of various DDRace HUD elements - RightView.HSplitTop(SectionTotalMargin + 4 * LineSize, &Section, &RightView); - Section.Margin(SectionMargin, &Section); - - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClDDRaceScoreBoard, Localize("Use DDRace Scoreboard"), &g_Config.m_ClDDRaceScoreBoard, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowIDs, Localize("Show client IDs in scoreboard"), &g_Config.m_ClShowIDs, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudDDRace, Localize("Show DDRace HUD"), &g_Config.m_ClShowhudDDRace, &Section, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowIds, Localize("Show client IDs (scoreboard, chat, spectator)"), &g_Config.m_ClShowIds, &RightView, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudDDRace, Localize("Show DDRace HUD"), &g_Config.m_ClShowhudDDRace, &RightView, LineSize); if(g_Config.m_ClShowhudDDRace) { - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudJumpsIndicator, Localize("Show jumps indicator"), &g_Config.m_ClShowhudJumpsIndicator, &Section, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudJumpsIndicator, Localize("Show jumps indicator"), &g_Config.m_ClShowhudJumpsIndicator, &RightView, LineSize); } else { - Section.HSplitTop(LineSize, 0x0, &Section); // Create empty space for hidden option + RightView.HSplitTop(LineSize, nullptr, &RightView); // Create empty space for hidden option } // Switch for dummy actions display - RightView.HSplitTop(SectionTotalMargin + LineSize, &Section, &RightView); - Section.Margin(SectionMargin, &Section); - - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudDummyActions, Localize("Show dummy actions"), &g_Config.m_ClShowhudDummyActions, &Section, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudDummyActions, Localize("Show dummy actions"), &g_Config.m_ClShowhudDummyActions, &RightView, LineSize); // Player movement information display settings - RightView.HSplitTop(SectionTotalMargin + 3 * LineSize, &Section, &RightView); - Section.Margin(SectionMargin, &Section); - - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudPlayerPosition, Localize("Show player position"), &g_Config.m_ClShowhudPlayerPosition, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudPlayerSpeed, Localize("Show player speed"), &g_Config.m_ClShowhudPlayerSpeed, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudPlayerAngle, Localize("Show player target angle"), &g_Config.m_ClShowhudPlayerAngle, &Section, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudPlayerPosition, Localize("Show player position"), &g_Config.m_ClShowhudPlayerPosition, &RightView, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudPlayerSpeed, Localize("Show player speed"), &g_Config.m_ClShowhudPlayerSpeed, &RightView, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhudPlayerAngle, Localize("Show player target angle"), &g_Config.m_ClShowhudPlayerAngle, &RightView, LineSize); // Freeze bar settings - RightView.HSplitTop(SectionTotalMargin + 3 * LineSize, &Section, &RightView); - Section.Margin(SectionMargin, &Section); - - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowFreezeBars, Localize("Show freeze bars"), &g_Config.m_ClShowFreezeBars, &Section, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowFreezeBars, Localize("Show freeze bars"), &g_Config.m_ClShowFreezeBars, &RightView, LineSize); + RightView.HSplitTop(2 * LineSize, &Button, &RightView); + if(g_Config.m_ClShowFreezeBars) { - Section.HSplitTop(2 * LineSize, &Button, &Section); - if(g_Config.m_ClShowFreezeBars) - { - UI()->DoScrollbarOption(&g_Config.m_ClFreezeBarsAlphaInsideFreeze, &g_Config.m_ClFreezeBarsAlphaInsideFreeze, &Button, Localize("Opacity of freeze bars inside freeze"), 0, 100, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_MULTILINE, "%"); - } + Ui()->DoScrollbarOption(&g_Config.m_ClFreezeBarsAlphaInsideFreeze, &g_Config.m_ClFreezeBarsAlphaInsideFreeze, &Button, Localize("Opacity of freeze bars inside freeze"), 0, 100, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_MULTILINE, "%"); } } else if(s_CurTab == APPEARANCE_TAB_CHAT) { CChat &Chat = GameClient()->m_Chat; CUIRect TopView, PreviewView; - MainView.h += 20.f; // Increase height a little - MainView.HSplitTop(MainView.h - 260, &TopView, &PreviewView); - TopView.VSplitMid(&LeftView, &RightView); + MainView.HSplitBottom(220.0f, &TopView, &PreviewView); + TopView.HSplitBottom(MarginBetweenViews, &TopView, nullptr); + TopView.VSplitMid(&LeftView, &RightView, MarginBetweenViews); // ***** Chat ***** // - LeftView.HSplitTop(HeadlineAndVMargin, &Label, &LeftView); - UI()->DoLabel(&Label, Localize("Chat"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(HeadlineHeight, &Label, &LeftView); + Ui()->DoLabel(&Label, Localize("Chat"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(MarginSmall, nullptr, &LeftView); // General chat settings - LeftView.HSplitTop(SectionTotalMargin + 7 * LineSize, &Section, &LeftView); - Section.Margin(SectionMargin, &Section); + LeftView.HSplitTop(LineSize, &Button, &LeftView); + if(DoButton_CheckBox(&g_Config.m_ClShowChat, Localize("Show chat"), g_Config.m_ClShowChat, &Button)) + { + g_Config.m_ClShowChat = g_Config.m_ClShowChat ? 0 : 1; + } + LeftView.HSplitTop(LineSize, &Button, &LeftView); + if(g_Config.m_ClShowChat) + { + static int s_ShowChat = 0; + if(DoButton_CheckBox(&s_ShowChat, Localize("Always show chat"), g_Config.m_ClShowChat == 2, &Button)) + g_Config.m_ClShowChat = g_Config.m_ClShowChat != 2 ? 2 : 1; + } - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClChatTeamColors, Localize("Show names in chat in team colors"), &g_Config.m_ClChatTeamColors, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowChatFriends, Localize("Show only chat messages from friends"), &g_Config.m_ClShowChatFriends, &Section, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClChatTeamColors, Localize("Show names in chat in team colors"), &g_Config.m_ClChatTeamColors, &LeftView, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowChatFriends, Localize("Show only chat messages from friends"), &g_Config.m_ClShowChatFriends, &LeftView, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowChatTeamMembersOnly, Localize("Show only chat messages from team members"), &g_Config.m_ClShowChatTeamMembersOnly, &LeftView, LineSize); - if(DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClChatOld, Localize("Use old chat style"), &g_Config.m_ClChatOld, &Section, LineSize)) + if(DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClChatOld, Localize("Use old chat style"), &g_Config.m_ClChatOld, &LeftView, LineSize)) GameClient()->m_Chat.RebuildChat(); - Section.HSplitTop(2 * LineSize, &Button, &Section); - int PrevFontSize = g_Config.m_ClChatFontSize; - UI()->DoScrollbarOption(&g_Config.m_ClChatFontSize, &g_Config.m_ClChatFontSize, &Button, Localize("Chat font size"), 10, 100, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_MULTILINE); - if(PrevFontSize != g_Config.m_ClChatFontSize) + LeftView.HSplitTop(2 * LineSize, &Button, &LeftView); + if(Ui()->DoScrollbarOption(&g_Config.m_ClChatFontSize, &g_Config.m_ClChatFontSize, &Button, Localize("Chat font size"), 10, 100, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_MULTILINE)) { Chat.EnsureCoherentWidth(); Chat.RebuildChat(); } - Section.HSplitTop(2 * LineSize, &Button, &Section); - int PrevWidth = g_Config.m_ClChatWidth; - UI()->DoScrollbarOption(&g_Config.m_ClChatWidth, &g_Config.m_ClChatWidth, &Button, Localize("Chat width"), 120, 400, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_MULTILINE); - if(PrevWidth != g_Config.m_ClChatWidth) + LeftView.HSplitTop(2 * LineSize, &Button, &LeftView); + if(Ui()->DoScrollbarOption(&g_Config.m_ClChatWidth, &g_Config.m_ClChatWidth, &Button, Localize("Chat width"), 120, 400, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_MULTILINE)) { Chat.EnsureCoherentFontSize(); Chat.RebuildChat(); } // ***** Messages ***** // - RightView.HSplitTop(HeadlineAndVMargin, &Label, &RightView); - UI()->DoLabel(&Label, Localize("Messages"), HeadlineFontSize, TEXTALIGN_ML); + RightView.HSplitTop(HeadlineHeight, &Label, &RightView); + Ui()->DoLabel(&Label, Localize("Messages"), HeadlineFontSize, TEXTALIGN_ML); + RightView.HSplitTop(MarginSmall, nullptr, &RightView); // Message Colors and extra settings - RightView.HSplitTop(SectionTotalMargin + 6 * ColorPickerLineSize, &Section, &RightView); - Section.Margin(SectionMargin, &Section); - - int i = 0; - static CButtonContainer s_aResetIDs[24]; - - DoLine_ColorPicker(&s_aResetIDs[i++], ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("System message"), &g_Config.m_ClMessageSystemColor, ColorRGBA(1.0f, 1.0f, 0.5f), true, &g_Config.m_ClShowChatSystem); - DoLine_ColorPicker(&s_aResetIDs[i++], ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Highlighted message"), &g_Config.m_ClMessageHighlightColor, ColorRGBA(1.0f, 0.5f, 0.5f)); - DoLine_ColorPicker(&s_aResetIDs[i++], ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Team message"), &g_Config.m_ClMessageTeamColor, ColorRGBA(0.65f, 1.0f, 0.65f)); - DoLine_ColorPicker(&s_aResetIDs[i++], ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Friend message"), &g_Config.m_ClMessageFriendColor, ColorRGBA(1.0f, 0.137f, 0.137f), true, &g_Config.m_ClMessageFriend); - DoLine_ColorPicker(&s_aResetIDs[i++], ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Normal message"), &g_Config.m_ClMessageColor, ColorRGBA(1.0f, 1.0f, 1.0f)); + static CButtonContainer s_SystemMessageColor; + DoLine_ColorPicker(&s_SystemMessageColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &RightView, Localize("System message"), &g_Config.m_ClMessageSystemColor, ColorRGBA(1.0f, 1.0f, 0.5f), true, &g_Config.m_ClShowChatSystem); + static CButtonContainer s_HighlightedMessageColor; + DoLine_ColorPicker(&s_HighlightedMessageColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &RightView, Localize("Highlighted message"), &g_Config.m_ClMessageHighlightColor, ColorRGBA(1.0f, 0.5f, 0.5f)); + static CButtonContainer s_TeamMessageColor; + DoLine_ColorPicker(&s_TeamMessageColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &RightView, Localize("Team message"), &g_Config.m_ClMessageTeamColor, ColorRGBA(0.65f, 1.0f, 0.65f)); + static CButtonContainer s_FriendMessageColor; + DoLine_ColorPicker(&s_FriendMessageColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &RightView, Localize("Friend message"), &g_Config.m_ClMessageFriendColor, ColorRGBA(1.0f, 0.137f, 0.137f), true, &g_Config.m_ClMessageFriend); + static CButtonContainer s_NormalMessageColor; + DoLine_ColorPicker(&s_NormalMessageColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &RightView, Localize("Normal message"), &g_Config.m_ClMessageColor, ColorRGBA(1.0f, 1.0f, 1.0f)); str_format(aBuf, sizeof(aBuf), "%s (echo)", Localize("Client message")); - DoLine_ColorPicker(&s_aResetIDs[i++], ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, aBuf, &g_Config.m_ClMessageClientColor, ColorRGBA(0.5f, 0.78f, 1.0f)); + static CButtonContainer s_ClientMessageColor; + DoLine_ColorPicker(&s_ClientMessageColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &RightView, aBuf, &g_Config.m_ClMessageClientColor, ColorRGBA(0.5f, 0.78f, 1.0f)); // ***** Chat Preview ***** // - PreviewView.HSplitTop(HeadlineAndVMargin, &Label, &PreviewView); - UI()->DoLabel(&Label, Localize("Preview"), HeadlineFontSize, TEXTALIGN_ML); + PreviewView.HSplitTop(HeadlineHeight, &Label, &PreviewView); + Ui()->DoLabel(&Label, Localize("Preview"), HeadlineFontSize, TEXTALIGN_ML); + PreviewView.HSplitTop(MarginSmall, nullptr, &PreviewView); // Use the rest of the view for preview - Section = PreviewView; - Section.Margin(SectionMargin, &Section); - - Section.Draw(ColorRGBA(1, 1, 1, 0.1f), IGraphics::CORNER_ALL, 8.0f); - - Section.HSplitTop(10.0f, 0x0, &Section); // Margin + PreviewView.Draw(ColorRGBA(1, 1, 1, 0.1f), IGraphics::CORNER_ALL, 5.0f); + PreviewView.Margin(MarginSmall, &PreviewView); ColorRGBA SystemColor = color_cast(ColorHSLA(g_Config.m_ClMessageSystemColor)); ColorRGBA HighlightedColor = color_cast(ColorHSLA(g_Config.m_ClMessageHighlightColor)); @@ -2749,14 +2484,10 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) const float RealMsgPaddingTee = (!g_Config.m_ClChatOld ? Chat.MessageTeeSize() + CChat::MESSAGE_TEE_PADDING_RIGHT : 0) * 2; const float RealOffsetY = RealFontSize + RealMsgPaddingY; - const float X = 5.0f + RealMsgPaddingX / 2.0f + Section.x; - float Y = Section.y; + const float X = RealMsgPaddingX / 2.0f + PreviewView.x; + float Y = PreviewView.y; float LineWidth = g_Config.m_ClChatWidth * 2 - (RealMsgPaddingX * 1.5f) - RealMsgPaddingTee; - CTextCursor Cursor; - TextRender()->SetCursor(&Cursor, X, Y, RealFontSize, TEXTFLAG_RENDER); - Cursor.m_LineWidth = LineWidth; - str_copy(aBuf, Client()->PlayerName()); const CAnimState *pIdleState = CAnimState::GetIdle(); @@ -2768,7 +2499,7 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) struct SPreviewLine { - int m_ClientID; + int m_ClientId; bool m_Team; char m_aName[64]; char m_aText[256]; @@ -2800,13 +2531,21 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) PREVIEW_SPAMMER, PREVIEW_CLIENT }; - auto &&AddPreviewLine = [](int Index, int ClientID, const char *pName, const char *pText, int Flag, int Repeats) { - s_vLines.emplace_back(); - SPreviewLine *pLine = &s_vLines[s_vLines.size() - 1]; - pLine->m_ClientID = ClientID; + auto &&SetPreviewLine = [](int Index, int ClientId, const char *pName, const char *pText, int Flag, int Repeats) { + SPreviewLine *pLine; + if((int)s_vLines.size() <= Index) + { + s_vLines.emplace_back(); + pLine = &s_vLines.back(); + } + else + { + pLine = &s_vLines[Index]; + } + pLine->m_ClientId = ClientId; pLine->m_Team = Flag & FLAG_TEAM; pLine->m_Friend = Flag & FLAG_FRIEND; - pLine->m_Player = ClientID >= 0; + pLine->m_Player = ClientId >= 0; pLine->m_Highlighted = Flag & FLAG_HIGHLIGHT; pLine->m_Client = Flag & FLAG_CLIENT; pLine->m_TimesRepeated = Repeats; @@ -2832,20 +2571,14 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) LocalCursor.m_LineWidth = LineWidth; const auto &Line = s_vLines[LineIndex]; - char aName[64 + 12] = ""; - - if(g_Config.m_ClShowIDs && Line.m_ClientID >= 0 && Line.m_aName[0] != '\0') + char aClientId[16] = ""; + if(g_Config.m_ClShowIds && Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') { - if(Line.m_ClientID < 10) - str_format(aName, sizeof(aName), " %d: ", Line.m_ClientID); - else - str_format(aName, sizeof(aName), "%d: ", Line.m_ClientID); + GameClient()->FormatClientId(Line.m_ClientId, aClientId, EClientIdFormat::INDENT_FORCE); } - str_append(aName, Line.m_aName); - char aCount[12]; - if(Line.m_ClientID < 0) + if(Line.m_ClientId < 0) str_format(aCount, sizeof(aCount), "[%d] ", Line.m_TimesRepeated + 1); else str_format(aCount, sizeof(aCount), " [%d]", Line.m_TimesRepeated + 1); @@ -2875,7 +2608,8 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) if(Render) TextRender()->TextColor(NameColor); - TextRender()->TextEx(&LocalCursor, aName, -1); + TextRender()->TextEx(&LocalCursor, aClientId); + TextRender()->TextEx(&LocalCursor, Line.m_aName); if(Line.m_TimesRepeated > 0) { @@ -2884,7 +2618,7 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) TextRender()->TextEx(&LocalCursor, aCount, -1); } - if(Line.m_ClientID >= 0 && Line.m_aName[0] != '\0') + if(Line.m_ClientId >= 0 && Line.m_aName[0] != '\0') { if(Render) TextRender()->TextColor(NameColor); @@ -2916,21 +2650,20 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) return vec2{LocalCursor.m_LongestLineWidth + AppendCursor.m_LongestLineWidth, AppendCursor.Height() + RealMsgPaddingY}; }; - // Init lines - if(s_vLines.empty()) + // Set preview lines { char aLineBuilder[128]; str_format(aLineBuilder, sizeof(aLineBuilder), "'%s' entered and joined the game", aBuf); - AddPreviewLine(PREVIEW_SYS, -1, "*** ", aLineBuilder, 0, 0); + SetPreviewLine(PREVIEW_SYS, -1, "*** ", aLineBuilder, 0, 0); str_format(aLineBuilder, sizeof(aLineBuilder), "Hey, how are you %s?", aBuf); - AddPreviewLine(PREVIEW_HIGHLIGHT, 7, "Random Tee", aLineBuilder, FLAG_HIGHLIGHT, 0); + SetPreviewLine(PREVIEW_HIGHLIGHT, 7, "Random Tee", aLineBuilder, FLAG_HIGHLIGHT, 0); - AddPreviewLine(PREVIEW_TEAM, 11, "Your Teammate", "Let's speedrun this!", FLAG_TEAM, 0); - AddPreviewLine(PREVIEW_FRIEND, 8, "Friend", "Hello there", FLAG_FRIEND, 0); - AddPreviewLine(PREVIEW_SPAMMER, 9, "Spammer", "Hey fools, I'm spamming here!", 0, 5); - AddPreviewLine(PREVIEW_CLIENT, -1, "— ", "Echo command executed", FLAG_CLIENT, 0); + SetPreviewLine(PREVIEW_TEAM, 11, "Your Teammate", "Let's speedrun this!", FLAG_TEAM, 0); + SetPreviewLine(PREVIEW_FRIEND, 8, "Friend", "Hello there", FLAG_FRIEND, 0); + SetPreviewLine(PREVIEW_SPAMMER, 9, "Spammer", "Hey fools, I'm spamming here!", 0, 5); + SetPreviewLine(PREVIEW_CLIENT, -1, "— ", "Echo command executed", FLAG_CLIENT, 0); } SetLineSkin(1, GameClient()->m_Skins.FindOrNullptr("pinky")); @@ -2961,13 +2694,15 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) if(!g_Config.m_ClShowChatFriends) { - TempY += RenderMessageBackground(PREVIEW_HIGHLIGHT); + if(!g_Config.m_ClShowChatTeamMembersOnly) + TempY += RenderMessageBackground(PREVIEW_HIGHLIGHT); TempY += RenderMessageBackground(PREVIEW_TEAM); } - TempY += RenderMessageBackground(PREVIEW_FRIEND); + if(!g_Config.m_ClShowChatTeamMembersOnly) + TempY += RenderMessageBackground(PREVIEW_FRIEND); - if(!g_Config.m_ClShowChatFriends) + if(!g_Config.m_ClShowChatFriends && !g_Config.m_ClShowChatTeamMembersOnly) { TempY += RenderMessageBackground(PREVIEW_SPAMMER); } @@ -2986,9 +2721,10 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) if(!g_Config.m_ClShowChatFriends) { // Highlighted - if(!g_Config.m_ClChatOld) + if(!g_Config.m_ClChatOld && !g_Config.m_ClShowChatTeamMembersOnly) RenderTools()->RenderTee(pIdleState, &s_vLines[PREVIEW_HIGHLIGHT].m_RenderInfo, EMOTE_NORMAL, vec2(1, 0.1f), vec2(X + RealTeeSizeHalved, Y + OffsetTeeY + FullHeightMinusTee / 2.0f + TWSkinUnreliableOffset)); - Y += RenderPreview(PREVIEW_HIGHLIGHT, X, Y).y; + if(!g_Config.m_ClShowChatTeamMembersOnly) + Y += RenderPreview(PREVIEW_HIGHLIGHT, X, Y).y; // Team if(!g_Config.m_ClChatOld) @@ -2997,12 +2733,13 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) } // Friend - if(!g_Config.m_ClChatOld) + if(!g_Config.m_ClChatOld && !g_Config.m_ClShowChatTeamMembersOnly) RenderTools()->RenderTee(pIdleState, &s_vLines[PREVIEW_FRIEND].m_RenderInfo, EMOTE_NORMAL, vec2(1, 0.1f), vec2(X + RealTeeSizeHalved, Y + OffsetTeeY + FullHeightMinusTee / 2.0f + TWSkinUnreliableOffset)); - Y += RenderPreview(PREVIEW_FRIEND, X, Y).y; + if(!g_Config.m_ClShowChatTeamMembersOnly) + Y += RenderPreview(PREVIEW_FRIEND, X, Y).y; // Normal - if(!g_Config.m_ClShowChatFriends) + if(!g_Config.m_ClShowChatFriends && !g_Config.m_ClShowChatTeamMembersOnly) { if(!g_Config.m_ClChatOld) RenderTools()->RenderTee(pIdleState, &s_vLines[PREVIEW_SPAMMER].m_RenderInfo, EMOTE_NORMAL, vec2(1, 0.1f), vec2(X + RealTeeSizeHalved, Y + OffsetTeeY + FullHeightMinusTee / 2.0f + TWSkinUnreliableOffset)); @@ -3010,185 +2747,180 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) } // Client RenderPreview(PREVIEW_CLIENT, X, Y); - TextRender()->SetCursorPosition(&Cursor, X, Y); TextRender()->TextColor(TextRender()->DefaultTextColor()); } else if(s_CurTab == APPEARANCE_TAB_NAME_PLATE) { - MainView.VSplitMid(&LeftView, &RightView); + MainView.VSplitMid(&LeftView, &RightView, MarginBetweenViews); // ***** Name Plate ***** // - LeftView.HSplitTop(HeadlineAndVMargin, &Label, &LeftView); - UI()->DoLabel(&Label, Localize("Name Plate"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(HeadlineHeight, &Label, &LeftView); + Ui()->DoLabel(&Label, Localize("Name Plate"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(MarginSmall, nullptr, &LeftView); - // General chat settings - LeftView.HSplitTop(SectionTotalMargin + 9 * LineSize, &Section, &LeftView); - Section.Margin(SectionMargin, &Section); - Section.HSplitTop(2 * LineSize, &Button, &Section); - UI()->DoScrollbarOption(&g_Config.m_ClNameplatesSize, &g_Config.m_ClNameplatesSize, &Button, Localize("Name plates size"), 0, 100, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_MULTILINE); + // General name plate settings + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClNameplates, Localize("Show name plates"), &g_Config.m_ClNameplates, &LeftView, LineSize); + LeftView.HSplitTop(2 * LineSize, &Button, &LeftView); + Ui()->DoScrollbarOption(&g_Config.m_ClNameplatesSize, &g_Config.m_ClNameplatesSize, &Button, Localize("Name plates size"), 0, 100, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_MULTILINE); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClNameplatesClan, Localize("Show clan above name plates"), &g_Config.m_ClNameplatesClan, &Section, LineSize); - Section.HSplitTop(2 * LineSize, &Button, &Section); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClNameplatesClan, Localize("Show clan above name plates"), &g_Config.m_ClNameplatesClan, &LeftView, LineSize); + LeftView.HSplitTop(2 * LineSize, &Button, &LeftView); if(g_Config.m_ClNameplatesClan) { - UI()->DoScrollbarOption(&g_Config.m_ClNameplatesClanSize, &g_Config.m_ClNameplatesClanSize, &Button, Localize("Clan plates size"), 0, 100, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_MULTILINE); + Ui()->DoScrollbarOption(&g_Config.m_ClNameplatesClanSize, &g_Config.m_ClNameplatesClanSize, &Button, Localize("Clan plates size"), 0, 100, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_MULTILINE); } - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClNameplatesTeamcolors, Localize("Use team colors for name plates"), &g_Config.m_ClNameplatesTeamcolors, &Section, LineSize); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClNameplatesStrong, Localize("Show hook strength indicator"), &g_Config.m_ClNameplatesStrong, &Section, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClNameplatesTeamcolors, Localize("Use team colors for name plates"), &g_Config.m_ClNameplatesTeamcolors, &LeftView, LineSize); + DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClNameplatesFriendMark, Localize("Show friend mark (♥) in name plates"), &g_Config.m_ClNameplatesFriendMark, &LeftView, LineSize); + + LeftView.HSplitTop(LineSize, &Button, &LeftView); + if(DoButton_CheckBox(&g_Config.m_ClNameplatesStrong, Localize("Show hook strength icon indicator"), g_Config.m_ClNameplatesStrong, &Button)) + { + g_Config.m_ClNameplatesStrong = g_Config.m_ClNameplatesStrong ? 0 : 1; + } + LeftView.HSplitTop(LineSize, &Button, &LeftView); + if(g_Config.m_ClNameplatesStrong) + { + static int s_NameplatesStrong = 0; + if(DoButton_CheckBox(&s_NameplatesStrong, Localize("Show hook strength number indicator"), g_Config.m_ClNameplatesStrong == 2, &Button)) + g_Config.m_ClNameplatesStrong = g_Config.m_ClNameplatesStrong != 2 ? 2 : 1; + } - Section.HSplitTop(LineSize, &Button, &Section); + LeftView.HSplitTop(LineSize, &Button, &LeftView); if(DoButton_CheckBox(&g_Config.m_ClShowDirection, Localize("Show other players' key presses"), g_Config.m_ClShowDirection >= 1 && g_Config.m_ClShowDirection != 3, &Button)) { - switch(g_Config.m_ClShowDirection) - { - case 0: - g_Config.m_ClShowDirection = 1; - break; - case 1: - g_Config.m_ClShowDirection = 0; - break; - case 2: - g_Config.m_ClShowDirection = 3; - break; - case 3: - g_Config.m_ClShowDirection = 2; - break; - } + g_Config.m_ClShowDirection = g_Config.m_ClShowDirection ^ 1; } - Section.HSplitTop(LineSize, &Button, &Section); + LeftView.HSplitTop(LineSize, &Button, &LeftView); static int s_ShowLocalPlayer = 0; if(DoButton_CheckBox(&s_ShowLocalPlayer, Localize("Show local player's key presses"), g_Config.m_ClShowDirection >= 2, &Button)) { - switch(g_Config.m_ClShowDirection) - { - case 0: - g_Config.m_ClShowDirection = 3; - break; - case 1: - g_Config.m_ClShowDirection = 2; - break; - case 2: - g_Config.m_ClShowDirection = 1; - break; - case 3: - g_Config.m_ClShowDirection = 0; - break; - } + g_Config.m_ClShowDirection = g_Config.m_ClShowDirection ^ 3; } - Section.HSplitTop(LineSize, &Button, &Section); ColorRGBA GreenDefault(0.78f, 1.0f, 0.8f, 1.0f); - static CButtonContainer s_AuthedColor; - static CButtonContainer s_SameClanColor; - DoLine_ColorPicker(&s_AuthedColor, 25.0f, 13.0f, 5.0f, &Button, Localize("Authed name color in scoreboard"), &g_Config.m_ClAuthedPlayerColor, GreenDefault, false); - DoLine_ColorPicker(&s_SameClanColor, 25.0f, 13.0f, 5.0f, &Button, Localize("Same clan color in scoreboard"), &g_Config.m_ClSameClanColor, GreenDefault, false); + static CButtonContainer s_AuthedColor, s_SameClanColor; + DoLine_ColorPicker(&s_AuthedColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Authed name color in scoreboard"), &g_Config.m_ClAuthedPlayerColor, GreenDefault, false); + DoLine_ColorPicker(&s_SameClanColor, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Same clan color in scoreboard"), &g_Config.m_ClSameClanColor, GreenDefault, false); } else if(s_CurTab == APPEARANCE_TAB_HOOK_COLLISION) { - MainView.VSplitMid(&LeftView, &RightView); + MainView.VSplitMid(&LeftView, &RightView, MarginBetweenViews); // ***** Hookline ***** // - LeftView.HSplitTop(HeadlineAndVMargin, &Label, &LeftView); - UI()->DoLabel(&Label, Localize("Hook collision line"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(HeadlineHeight, &Label, &LeftView); + Ui()->DoLabel(&Label, Localize("Hook collision line"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(MarginSmall, nullptr, &LeftView); // General hookline settings - LeftView.HSplitTop(SectionTotalMargin + 6 * LineSize + 3 * ColorPickerLineSize, &Section, &LeftView); - Section.Margin(SectionMargin, &Section); + LeftView.HSplitTop(LineSize, &Button, &LeftView); + if(DoButton_CheckBox(&g_Config.m_ClShowHookCollOwn, Localize("Show own player's hook collision line"), g_Config.m_ClShowHookCollOwn, &Button)) + { + g_Config.m_ClShowHookCollOwn = g_Config.m_ClShowHookCollOwn ? 0 : 1; + } + LeftView.HSplitTop(LineSize, &Button, &LeftView); + if(g_Config.m_ClShowHookCollOwn) + { + static int s_ShowHookCollOwn = 0; + if(DoButton_CheckBox(&s_ShowHookCollOwn, Localize("Always show own player's hook collision line"), g_Config.m_ClShowHookCollOwn == 2, &Button)) + g_Config.m_ClShowHookCollOwn = g_Config.m_ClShowHookCollOwn != 2 ? 2 : 1; + } - Section.HSplitTop(LineSize, &Button, &Section); + LeftView.HSplitTop(LineSize, &Button, &LeftView); if(DoButton_CheckBox(&g_Config.m_ClShowHookCollOther, Localize("Show other players' hook collision lines"), g_Config.m_ClShowHookCollOther, &Button)) { g_Config.m_ClShowHookCollOther = g_Config.m_ClShowHookCollOther >= 1 ? 0 : 1; } + LeftView.HSplitTop(LineSize, &Button, &LeftView); + if(g_Config.m_ClShowHookCollOther) + { + static int s_ShowHookCollOther = 0; + if(DoButton_CheckBox(&s_ShowHookCollOther, Localize("Always show other players' hook collision lines"), g_Config.m_ClShowHookCollOther == 2, &Button)) + g_Config.m_ClShowHookCollOther = g_Config.m_ClShowHookCollOther != 2 ? 2 : 1; + } - Section.HSplitTop(2 * LineSize, &Button, &Section); - UI()->DoScrollbarOption(&g_Config.m_ClHookCollSize, &g_Config.m_ClHookCollSize, &Button, Localize("Hook collision line width"), 0, 20, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_MULTILINE); + LeftView.HSplitTop(2 * LineSize, &Button, &LeftView); + Ui()->DoScrollbarOption(&g_Config.m_ClHookCollSize, &g_Config.m_ClHookCollSize, &Button, Localize("Hook collision line width"), 0, 20, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_MULTILINE); - Section.HSplitTop(2 * LineSize, &Button, &Section); - UI()->DoScrollbarOption(&g_Config.m_ClHookCollAlpha, &g_Config.m_ClHookCollAlpha, &Button, Localize("Hook collision line opacity"), 0, 100, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_MULTILINE, "%"); + LeftView.HSplitTop(2 * LineSize, &Button, &LeftView); + Ui()->DoScrollbarOption(&g_Config.m_ClHookCollAlpha, &g_Config.m_ClHookCollAlpha, &Button, Localize("Hook collision line opacity"), 0, 100, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_MULTILINE, "%"); - static CButtonContainer s_HookCollNoCollResetID, s_HookCollHookableCollResetID, s_HookCollTeeCollResetID; + static CButtonContainer s_HookCollNoCollResetId, s_HookCollHookableCollResetId, s_HookCollTeeCollResetId; static int s_HookCollToolTip; - Section.HSplitTop(LineSize, &Label, &Section); - UI()->DoLabel(&Label, Localize("Colors of the hook collision line, in case of a possible collision with:"), 13.0f, TEXTALIGN_ML); - UI()->DoButtonLogic(&s_HookCollToolTip, 0, &Label); // Just for the tooltip, result ignored + LeftView.HSplitTop(LineSize, &Label, &LeftView); + LeftView.HSplitTop(MarginSmall, nullptr, &LeftView); + Ui()->DoLabel(&Label, Localize("Colors of the hook collision line, in case of a possible collision with:"), 13.0f, TEXTALIGN_ML); + Ui()->DoButtonLogic(&s_HookCollToolTip, 0, &Label); // Just for the tooltip, result ignored GameClient()->m_Tooltips.DoToolTip(&s_HookCollToolTip, &Label, Localize("Your movements are not taken into account when calculating the line colors")); - DoLine_ColorPicker(&s_HookCollNoCollResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Nothing hookable"), &g_Config.m_ClHookCollColorNoColl, ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), false); - DoLine_ColorPicker(&s_HookCollHookableCollResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Something hookable"), &g_Config.m_ClHookCollColorHookableColl, ColorRGBA(130.0f / 255.0f, 232.0f / 255.0f, 160.0f / 255.0f, 1.0f), false); - DoLine_ColorPicker(&s_HookCollTeeCollResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("A Tee"), &g_Config.m_ClHookCollColorTeeColl, ColorRGBA(1.0f, 1.0f, 0.0f, 1.0f), false); + DoLine_ColorPicker(&s_HookCollNoCollResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Nothing hookable"), &g_Config.m_ClHookCollColorNoColl, ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), false); + DoLine_ColorPicker(&s_HookCollHookableCollResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Something hookable"), &g_Config.m_ClHookCollColorHookableColl, ColorRGBA(130.0f / 255.0f, 232.0f / 255.0f, 160.0f / 255.0f, 1.0f), false); + DoLine_ColorPicker(&s_HookCollTeeCollResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("A Tee"), &g_Config.m_ClHookCollColorTeeColl, ColorRGBA(1.0f, 1.0f, 0.0f, 1.0f), false); } else if(s_CurTab == APPEARANCE_TAB_INFO_MESSAGES) { - MainView.VSplitMid(&LeftView, &RightView); + MainView.VSplitMid(&LeftView, &RightView, MarginBetweenViews); // ***** Info Messages ***** // - LeftView.HSplitTop(HeadlineAndVMargin, &Label, &LeftView); - UI()->DoLabel(&Label, Localize("Info Messages"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(HeadlineHeight, &Label, &LeftView); + Ui()->DoLabel(&Label, Localize("Info Messages"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(MarginSmall, nullptr, &LeftView); // General info messages settings - LeftView.HSplitTop(SectionTotalMargin + 2 * LineSize + 2 * ColorPickerLineSize, &Section, &LeftView); - Section.Margin(SectionMargin, &Section); - - Section.HSplitTop(LineSize, &Button, &Section); + LeftView.HSplitTop(LineSize, &Button, &LeftView); if(DoButton_CheckBox(&g_Config.m_ClShowKillMessages, Localize("Show kill messages"), g_Config.m_ClShowKillMessages, &Button)) { g_Config.m_ClShowKillMessages ^= 1; } - Section.HSplitTop(LineSize, &Button, &Section); + LeftView.HSplitTop(LineSize, &Button, &LeftView); if(DoButton_CheckBox(&g_Config.m_ClShowFinishMessages, Localize("Show finish messages"), g_Config.m_ClShowFinishMessages, &Button)) { g_Config.m_ClShowFinishMessages ^= 1; } - static CButtonContainer s_KillMessageNormalColorID, s_KillMessageHighlightColorID; - DoLine_ColorPicker(&s_KillMessageNormalColorID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Normal Color"), &g_Config.m_ClKillMessageNormalColor, ColorRGBA(1.0f, 1.0f, 1.0f), false); - DoLine_ColorPicker(&s_KillMessageHighlightColorID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Highlight Color"), &g_Config.m_ClKillMessageHighlightColor, ColorRGBA(1.0f, 1.0f, 1.0f), false); + static CButtonContainer s_KillMessageNormalColorId, s_KillMessageHighlightColorId; + DoLine_ColorPicker(&s_KillMessageNormalColorId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Normal Color"), &g_Config.m_ClKillMessageNormalColor, ColorRGBA(1.0f, 1.0f, 1.0f), false); + DoLine_ColorPicker(&s_KillMessageHighlightColorId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Highlight Color"), &g_Config.m_ClKillMessageHighlightColor, ColorRGBA(1.0f, 1.0f, 1.0f), false); } else if(s_CurTab == APPEARANCE_TAB_LASER) { - MainView.VSplitMid(&LeftView, &RightView); + MainView.VSplitMid(&LeftView, &RightView, MarginBetweenViews); // ***** Weapons ***** // - LeftView.HSplitTop(HeadlineAndVMargin, &Label, &LeftView); - UI()->DoLabel(&Label, Localize("Weapons"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(HeadlineHeight, &Label, &LeftView); + Ui()->DoLabel(&Label, Localize("Weapons"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(MarginSmall, nullptr, &LeftView); // General weapon laser settings - LeftView.HSplitTop(SectionTotalMargin + 4 * ColorPickerLineSize, &Section, &LeftView); - Section.Margin(SectionMargin, &Section); + static CButtonContainer s_LaserRifleOutResetId, s_LaserRifleInResetId, s_LaserShotgunOutResetId, s_LaserShotgunInResetId; - static CButtonContainer s_LaserRifleOutResetID, s_LaserRifleInResetID, s_LaserShotgunOutResetID, s_LaserShotgunInResetID; - - ColorHSLA LaserRifleOutlineColor = DoLine_ColorPicker(&s_LaserRifleOutResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Rifle Laser Outline Color"), &g_Config.m_ClLaserRifleOutlineColor, ColorRGBA(0.074402f, 0.074402f, 0.247166f, 1.0f), false); - ColorHSLA LaserRifleInnerColor = DoLine_ColorPicker(&s_LaserRifleInResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Rifle Laser Inner Color"), &g_Config.m_ClLaserRifleInnerColor, ColorRGBA(0.498039f, 0.498039f, 1.0f, 1.0f), false); - ColorHSLA LaserShotgunOutlineColor = DoLine_ColorPicker(&s_LaserShotgunOutResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Shotgun Laser Outline Color"), &g_Config.m_ClLaserShotgunOutlineColor, ColorRGBA(0.125490f, 0.098039f, 0.043137f, 1.0f), false); - ColorHSLA LaserShotgunInnerColor = DoLine_ColorPicker(&s_LaserShotgunInResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Shotgun Laser Inner Color"), &g_Config.m_ClLaserShotgunInnerColor, ColorRGBA(0.570588f, 0.417647f, 0.252941f, 1.0f), false); + ColorHSLA LaserRifleOutlineColor = DoLine_ColorPicker(&s_LaserRifleOutResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Rifle Laser Outline Color"), &g_Config.m_ClLaserRifleOutlineColor, ColorRGBA(0.074402f, 0.074402f, 0.247166f, 1.0f), false); + ColorHSLA LaserRifleInnerColor = DoLine_ColorPicker(&s_LaserRifleInResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Rifle Laser Inner Color"), &g_Config.m_ClLaserRifleInnerColor, ColorRGBA(0.498039f, 0.498039f, 1.0f, 1.0f), false); + ColorHSLA LaserShotgunOutlineColor = DoLine_ColorPicker(&s_LaserShotgunOutResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Shotgun Laser Outline Color"), &g_Config.m_ClLaserShotgunOutlineColor, ColorRGBA(0.125490f, 0.098039f, 0.043137f, 1.0f), false); + ColorHSLA LaserShotgunInnerColor = DoLine_ColorPicker(&s_LaserShotgunInResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Shotgun Laser Inner Color"), &g_Config.m_ClLaserShotgunInnerColor, ColorRGBA(0.570588f, 0.417647f, 0.252941f, 1.0f), false); // ***** Entities ***** // - LeftView.HSplitTop(MarginToNextSection * 2.0f, 0x0, &LeftView); - LeftView.HSplitTop(HeadlineAndVMargin, &Label, &LeftView); - UI()->DoLabel(&Label, Localize("Entities"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(10.0f, nullptr, &LeftView); + LeftView.HSplitTop(HeadlineHeight, &Label, &LeftView); + Ui()->DoLabel(&Label, Localize("Entities"), HeadlineFontSize, TEXTALIGN_ML); + LeftView.HSplitTop(MarginSmall, nullptr, &LeftView); // General entity laser settings - LeftView.HSplitTop(SectionTotalMargin + 4 * ColorPickerLineSize, &Section, &LeftView); - Section.Margin(SectionMargin, &Section); - - static CButtonContainer s_LaserDoorOutResetID, s_LaserDoorInResetID, s_LaserFreezeOutResetID, s_LaserFreezeInResetID; + static CButtonContainer s_LaserDoorOutResetId, s_LaserDoorInResetId, s_LaserFreezeOutResetId, s_LaserFreezeInResetId; - ColorHSLA LaserDoorOutlineColor = DoLine_ColorPicker(&s_LaserDoorOutResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Door Laser Outline Color"), &g_Config.m_ClLaserDoorOutlineColor, ColorRGBA(0.0f, 0.131372f, 0.096078f, 1.0f), false); - ColorHSLA LaserDoorInnerColor = DoLine_ColorPicker(&s_LaserDoorInResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Door Laser Inner Color"), &g_Config.m_ClLaserDoorInnerColor, ColorRGBA(0.262745f, 0.760784f, 0.639215f, 1.0f), false); - ColorHSLA LaserFreezeOutlineColor = DoLine_ColorPicker(&s_LaserFreezeOutResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Freeze Laser Outline Color"), &g_Config.m_ClLaserFreezeOutlineColor, ColorRGBA(0.131372f, 0.123529f, 0.182352f, 1.0f), false); - ColorHSLA LaserFreezeInnerColor = DoLine_ColorPicker(&s_LaserFreezeInResetID, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &Section, Localize("Freeze Laser Inner Color"), &g_Config.m_ClLaserFreezeInnerColor, ColorRGBA(0.482352f, 0.443137f, 0.564705f, 1.0f), false); + ColorHSLA LaserDoorOutlineColor = DoLine_ColorPicker(&s_LaserDoorOutResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Door Laser Outline Color"), &g_Config.m_ClLaserDoorOutlineColor, ColorRGBA(0.0f, 0.131372f, 0.096078f, 1.0f), false); + ColorHSLA LaserDoorInnerColor = DoLine_ColorPicker(&s_LaserDoorInResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Door Laser Inner Color"), &g_Config.m_ClLaserDoorInnerColor, ColorRGBA(0.262745f, 0.760784f, 0.639215f, 1.0f), false); + ColorHSLA LaserFreezeOutlineColor = DoLine_ColorPicker(&s_LaserFreezeOutResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Freeze Laser Outline Color"), &g_Config.m_ClLaserFreezeOutlineColor, ColorRGBA(0.131372f, 0.123529f, 0.182352f, 1.0f), false); + ColorHSLA LaserFreezeInnerColor = DoLine_ColorPicker(&s_LaserFreezeInResetId, ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, &LeftView, Localize("Freeze Laser Inner Color"), &g_Config.m_ClLaserFreezeInnerColor, ColorRGBA(0.482352f, 0.443137f, 0.564705f, 1.0f), false); - static CButtonContainer s_AllToRifleResetID, s_AllToDefaultResetID; + static CButtonContainer s_AllToRifleResetId, s_AllToDefaultResetId; - LeftView.HSplitTop(20.0f, 0x0, &LeftView); - LeftView.HSplitTop(20.0f, &Button, &LeftView); - if(DoButton_Menu(&s_AllToRifleResetID, Localize("Set all to Rifle"), 0, &Button)) + LeftView.HSplitTop(4 * MarginSmall, nullptr, &LeftView); + LeftView.HSplitTop(LineSize, &Button, &LeftView); + if(DoButton_Menu(&s_AllToRifleResetId, Localize("Set all to Rifle"), 0, &Button)) { g_Config.m_ClLaserShotgunOutlineColor = g_Config.m_ClLaserRifleOutlineColor; g_Config.m_ClLaserShotgunInnerColor = g_Config.m_ClLaserRifleInnerColor; @@ -3199,9 +2931,9 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) } // values taken from the CL commands - LeftView.HSplitTop(10.0f, 0x0, &LeftView); - LeftView.HSplitTop(20.0f, &Button, &LeftView); - if(DoButton_Menu(&s_AllToDefaultResetID, Localize("Reset to defaults"), 0, &Button)) + LeftView.HSplitTop(2 * MarginSmall, nullptr, &LeftView); + LeftView.HSplitTop(LineSize, &Button, &LeftView); + if(DoButton_Menu(&s_AllToDefaultResetId, Localize("Reset to defaults"), 0, &Button)) { g_Config.m_ClLaserRifleOutlineColor = 11176233; g_Config.m_ClLaserRifleInnerColor = 11206591; @@ -3214,820 +2946,30 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) } // ***** Laser Preview ***** // - RightView.HSplitTop(HeadlineAndVMargin, &Label, &RightView); - UI()->DoLabel(&Label, Localize("Preview"), HeadlineFontSize, TEXTALIGN_ML); + RightView.HSplitTop(HeadlineHeight, &Label, &RightView); + Ui()->DoLabel(&Label, Localize("Preview"), HeadlineFontSize, TEXTALIGN_ML); + RightView.HSplitTop(MarginSmall, nullptr, &RightView); - RightView.HSplitTop(SectionTotalMargin + 50.0f, &Section, &RightView); - Section.Margin(SectionMargin, &Section); - DoLaserPreview(&Section, LaserRifleOutlineColor, LaserRifleInnerColor, LASERTYPE_RIFLE); + const float LaserPreviewHeight = 50.0f; + CUIRect LaserPreview; + RightView.HSplitTop(LaserPreviewHeight, &LaserPreview, &RightView); + RightView.HSplitTop(2 * MarginSmall, nullptr, &RightView); + DoLaserPreview(&LaserPreview, LaserRifleOutlineColor, LaserRifleInnerColor, LASERTYPE_RIFLE); - RightView.HSplitTop(SectionTotalMargin + 50.0f, &Section, &RightView); - Section.Margin(SectionMargin, &Section); - DoLaserPreview(&Section, LaserShotgunOutlineColor, LaserShotgunInnerColor, LASERTYPE_SHOTGUN); + RightView.HSplitTop(LaserPreviewHeight, &LaserPreview, &RightView); + RightView.HSplitTop(2 * MarginSmall, nullptr, &RightView); + DoLaserPreview(&LaserPreview, LaserShotgunOutlineColor, LaserShotgunInnerColor, LASERTYPE_SHOTGUN); - RightView.HSplitTop(SectionTotalMargin + 50.0f, &Section, &RightView); - Section.Margin(SectionMargin, &Section); - DoLaserPreview(&Section, LaserDoorOutlineColor, LaserDoorInnerColor, LASERTYPE_DOOR); + RightView.HSplitTop(LaserPreviewHeight, &LaserPreview, &RightView); + RightView.HSplitTop(2 * MarginSmall, nullptr, &RightView); + DoLaserPreview(&LaserPreview, LaserDoorOutlineColor, LaserDoorInnerColor, LASERTYPE_DOOR); - RightView.HSplitTop(SectionTotalMargin + 50.0f, &Section, &RightView); - Section.Margin(SectionMargin, &Section); - DoLaserPreview(&Section, LaserFreezeOutlineColor, LaserFreezeInnerColor, LASERTYPE_DOOR); + RightView.HSplitTop(LaserPreviewHeight, &LaserPreview, &RightView); + RightView.HSplitTop(2 * MarginSmall, nullptr, &RightView); + DoLaserPreview(&LaserPreview, LaserFreezeOutlineColor, LaserFreezeInnerColor, LASERTYPE_DOOR); } } -enum -{ - STA_TAB_PAGE1 = 0, - STA_TAB_PAGE2 = 1, - STA_TAB_PAGE3 = 2, - NUMBER_OF_STA_TABS = 3, -}; - -void CMenus::RenderSettingsStA(CUIRect MainView) -{//TODO: buttons before upd - static int s_CurTab = 0; - - CUIRect TabBar, Page1Tab, Page2Tab, Page3Tab, Left, Right, Button, Label; - - MainView.HSplitTop(20, &TabBar, &MainView); - float TabsW = TabBar.w; - TabBar.VSplitLeft(TabsW / NUMBER_OF_STA_TABS, &Page1Tab, &Page2Tab); - Page2Tab.VSplitLeft(TabsW / NUMBER_OF_STA_TABS, &Page2Tab, &Page3Tab); - - static CButtonContainer s_aPageTabs[NUMBER_OF_STA_TABS] = {}; - - if(DoButton_MenuTab(&s_aPageTabs[STA_TAB_PAGE1], Localize("Options"), s_CurTab == STA_TAB_PAGE1, &Page1Tab, IGraphics::CORNER_L, NULL, NULL, NULL, NULL, 4)) - s_CurTab = STA_TAB_PAGE1; - if(DoButton_MenuTab(&s_aPageTabs[STA_TAB_PAGE2], Localize("Customization"), s_CurTab == STA_TAB_PAGE2, &Page2Tab, 0, NULL, NULL, NULL, NULL, 4)) - s_CurTab = STA_TAB_PAGE2; - if(DoButton_MenuTab(&s_aPageTabs[STA_TAB_PAGE3], Localize("3rd PAGE"), s_CurTab == STA_TAB_PAGE3, &Page3Tab, 0, NULL, NULL, NULL, NULL, 4)) - s_CurTab = STA_TAB_PAGE3; - - MainView.HSplitTop(10.0f, nullptr, &MainView); - MainView.VSplitMid(&Left, &Right, 10.f); - - const float LineMargin = 20.0f; - const float ColorPickerLineSize = 25.0f; - const float SectionMargin = 5.0f; - const float ColorPickerLabelSize = 13.0f; - const float ColorPickerLineSpacing = 5.0f; - - if(s_CurTab == STA_TAB_PAGE1) - { - ColorRGBA col = color_cast(ColorHSVA(round_to_int(2.0f) % 255 / 100.f, 0.4f, 1.f)); - CUIRect LeftLeft, Demo, Outline; - - Right.HSplitTop(25.f, &Label, &Outline); - TextRender()->TextColor(col); - UI()->DoLabel(&Label, Localize("Tile Outlines(TYSM TClient)"), 20.0f, TEXTALIGN_TC); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClOutline, ("Show any enabled outlines"), &g_Config.m_ClOutline, &Outline, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClOutlineEntities, ("Only show outlines in entities"), &g_Config.m_ClOutlineEntities, &Outline, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClOutlineFreeze, ("Outline freeze & deep"), &g_Config.m_ClOutlineFreeze, &Outline, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClOutlineSolid, ("Outline walls"), &g_Config.m_ClOutlineSolid, &Outline, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClOutlineTele, ("Outline teleporter"), &g_Config.m_ClOutlineTele, &Outline, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClOutlineUnFreeze, ("Outline unfreeze & undeep"), &g_Config.m_ClOutlineUnFreeze, &Outline, LineMargin); - - Outline.HSplitTop(20.f, &Button, &Outline); - UI()->DoScrollbarOption(&g_Config.m_ClOutlineWidth, &g_Config.m_ClOutlineWidth, &Button, Localize("Outline Width"), 0, 100, &CUI::ms_LinearScrollbarScale, 0u, "%"); - - Outline.HSplitTop(20.f, &Button, &Outline); - UI()->DoScrollbarOption(&g_Config.m_ClOutlineAlphaSolid, &g_Config.m_ClOutlineAlphaSolid, &Button, Localize("Outline Opacity (walls)"), 0, 100, &CUI::ms_LinearScrollbarScale, 0u, "%"); - - Outline.HSplitTop(20.f, &Button, &Outline); - UI()->DoScrollbarOption(&g_Config.m_ClOutlineAlpha, &g_Config.m_ClOutlineAlpha, &Button, Localize("Outline Opacity"), 0, 100, &CUI::ms_LinearScrollbarScale, 0u, "%"); - - static CButtonContainer s_aResetIDs[24]; - static CUI::SDropDownState s_BackendDropDownState; - static CScrollRegion s_BackendDropDownScrollRegion; - int i = 0; - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Outline, Localize("Freeze Outline Color"), - &g_Config.m_ClOutlineColorFreeze, - ColorRGBA(1), - false); - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Outline, Localize("Walls Outline Color"), - &g_Config.m_ClOutlineColorSolid, - ColorRGBA(1), - false); - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Outline, Localize("Teleporter Outline Color"), - &g_Config.m_ClOutlineColorTele, - ColorRGBA(1), - false); - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Outline, Localize("Unfreeze Outline Color"), - &g_Config.m_ClOutlineColorUnfreeze, - ColorRGBA(1), - false); - - Outline.HSplitTop(25.f, &Label, &Outline); - TextRender()->TextColor(col); - UI()->DoLabel(&Label, ("Frozen Tee Display"), 20.0f, TEXTALIGN_TC); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowFrozenHud, ("Enable Frozen Tee Display"), &g_Config.m_ClShowFrozenHud, &Outline, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowFrozenHudSkins, ("Use teeskin instead of ninja tee"), &g_Config.m_ClShowFrozenHudSkins, &Outline, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClFrozenHudTeamOnly, ("Enable only for Team"), &g_Config.m_ClFrozenHudTeamOnly, &Outline, LineMargin); - - Outline.HSplitTop(20.f, &Button, &Outline); - UI()->DoScrollbarOption(&g_Config.m_ClFrozenMaxRows, &g_Config.m_ClFrozenMaxRows, &Button, Localize("Max rows"), 1, 6, &CUI::ms_LinearScrollbarScale, 0u, ""); - - Outline.HSplitTop(20.f, &Button, &Outline); - UI()->DoScrollbarOption(&g_Config.m_ClFrozenHudTeeSize, &g_Config.m_ClFrozenHudTeeSize, &Button, Localize("Tee Size"), 8, 20, &CUI::ms_LinearScrollbarScale, 0u, ""); - - // Preferences - Left.HSplitTop(20.0f, &Demo, &MainView); - Demo.HSplitTop(0.0f, &Label, &Demo); - TextRender()->TextColor(col); - UI()->DoLabel(&Label, Localize("Preferences"), 20.0f, TEXTALIGN_TC); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - - Left.HSplitTop(20.0f, &Button, &Left); - Button.VSplitMid(&LeftLeft, &Button); - - Left.HSplitTop(20.0f, &Button, &Left); - if(DoButton_CheckBox(&g_Config.m_ClShowfps, Localize("Show FPS"), g_Config.m_ClShowfps, &Button)) - g_Config.m_ClShowfps ^= 1; - - Left.HSplitTop(20.0f, &Button, &Left); - if(DoButton_CheckBox(&g_Config.m_ClShowpred, Localize("Show ping(predict)"), g_Config.m_ClShowpred, &Button)) - g_Config.m_ClShowpred ^= 1; - - Left.HSplitTop(20.0f, &Button, &Left); - if(DoButton_CheckBox(&g_Config.m_ClAutoVerify, Localize("Server Auto Whitelist"), g_Config.m_ClAutoVerify, &Button)) - g_Config.m_ClAutoVerify ^= 1; - - Left.HSplitTop(20.0f, &Button, &Left); - if(DoButton_CheckBox(&g_Config.m_ClShowSkinName, Localize("Display skin name in nameplates"), g_Config.m_ClShowSkinName, &Button)) - g_Config.m_ClShowSkinName ^= 1; - - Left.HSplitTop(20.0f, &Button, &Left); - if(DoButton_CheckBox(&g_Config.m_ClOldFreezeMode, Localize("Old Freeze Mode"), g_Config.m_ClOldFreezeMode, &Button)) - g_Config.m_ClOldFreezeMode ^= 1; - - // Bind wheel config - Left.HSplitTop(30.f, &Label, &Right); - TextRender()->TextColor(col); - UI()->DoLabel(&Label, Localize("Bind wheel"), 20.0f, TEXTALIGN_TC); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - - CUIRect BindWheelZone, BindWheelList, BindWheelOptions; - Right.HSplitTop(60.f, &BindWheelZone, &Right); - BindWheelZone.VSplitMid(&BindWheelList, &BindWheelOptions, 10.f); - BindWheelList.h = 20.f; - - static int s_SelectedBind = -1; - - auto &vBindsList = GameClient()->m_BindWheel.m_vBinds; - std::vector vBindsName; - - for(auto &Bind : vBindsList) - vBindsName.push_back(Bind.m_aName); - - s_BackendDropDownState.m_SelectionPopupContext.m_pScrollRegion = &s_BackendDropDownScrollRegion; - s_SelectedBind = UI()->DoDropDown(&BindWheelList, s_SelectedBind, vBindsName.data(), vBindsName.size(), s_BackendDropDownState); - - // Input fields - static char s_aBindName[BIND_WHEEL_MAX_NAME]; - static char s_aBindBind[BIND_WHEEL_MAX_BIND]; - - BindWheelOptions.HSplitTop(20.f, &Button, &BindWheelOptions); - BindWheelOptions.HSplitTop(5.f, 0x0, &BindWheelOptions); // Offset - static CLineInput s_NameInput; - s_NameInput.SetBuffer(s_aBindName, sizeof(s_aBindName)); - s_NameInput.SetEmptyText("New Bind Description"); - UI()->DoEditBox(&s_NameInput, &Button, 14.0f); - - BindWheelOptions.HSplitTop(20.f, &Button, &BindWheelOptions); - BindWheelOptions.HSplitTop(5.f, 0x0, &BindWheelOptions); // Offset - static CLineInput s_BindInput; - s_BindInput.SetBuffer(s_aBindBind, sizeof(s_aBindBind)); - s_BindInput.SetEmptyText("New Bind Command"); - UI()->DoEditBox(&s_BindInput, &Button, 14.0f); - - BindWheelOptions.HSplitTop(20.f, &Button, &BindWheelOptions); - BindWheelOptions.HSplitTop(5.f, 0x0, &BindWheelOptions); // Offset - CUIRect LeftButton, RightButton; - Button.VSplitMid(&LeftButton, &RightButton, 5.f); - - // New bind button - static CButtonContainer s_NewBindButton, s_RemBindButton; - if(DoButton_Menu(&s_NewBindButton, Localize("New bind"), 0, &RightButton) && str_length(s_aBindName) > 0 && str_length(s_aBindBind)) - { - CBindWheel::SBind Bind; - str_copy(Bind.m_aName, s_aBindName); - str_copy(Bind.m_aBind, s_aBindBind); - - vBindsList.push_back(Bind); - } - - if(DoButton_Menu(&s_RemBindButton, Localize("Remove bind"), 0, &LeftButton) && s_SelectedBind >= 0) - { - vBindsList.erase(vBindsList.begin() + s_SelectedBind); - s_SelectedBind = -1; - } - } - - if(s_CurTab == STA_TAB_PAGE2) - { - int i = 0; - static CButtonContainer s_aResetIDs[24]; - ColorRGBA col = color_cast(ColorHSVA(round_to_int(2.0f) % 255 / 100.f, 0.4f, 1.f)); - - // Tab - Left.HSplitTop(25.f, &Label, &Left); - TextRender()->TextColor(col); - UI()->DoLabel(&Label, Localize("Tab"), 20.f, TEXTALIGN_TC); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Left, Localize("Blacklisted player color"), - &g_Config.m_ScBlacklistPColor, - ColorRGBA(1.0f, 0.39f, 0.39f), - false); - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Left, Localize("Friends color"), - &g_Config.m_ScFriendColor, - ColorRGBA(0.45f, 1.0f, 0.91f), - false); - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Left, Localize("Сlanmates color"), - &g_Config.m_ClSameClanColor, - ColorRGBA(0.2f, 0.7f, 1.0f), - false); - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Left, Localize("Staff player color(f2)"), - &g_Config.m_ClAuthedPlayerColor, - ColorRGBA(1.f, 0.25f, 0.6f), - false); - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Left, Localize("Own player color"), - &g_Config.m_ScPlayerOwnColor, - ColorRGBA(1.0f, 1.0f, 1.0f), - false); - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Left, Localize("Frozen Tee Color"), - &g_Config.m_ScFrozenTeeColor, - ColorRGBA(1.0f, 1.0f, 1.0f), - false); - - Left.HSplitTop(20.f, &Button, &Left); - if(DoButton_CheckBox(&g_Config.m_ScShowBlacklistNameplaterColor, Localize("Display Blacklisted Color in Nameplates"), g_Config.m_ScShowBlacklistNameplaterColor, &Button)) - g_Config.m_ScShowBlacklistNameplaterColor ^= 1; - - Left.HSplitTop(20.f, &Button, &Left); - if(DoButton_CheckBox(&g_Config.m_ScShowFriendsNameplatesColor, Localize("Display Friend Color in Nameplates"), g_Config.m_ScShowFriendsNameplatesColor, &Button)) - g_Config.m_ScShowFriendsNameplatesColor ^= 1; - - Left.HSplitTop(20.f, &Button, &Left); - if(DoButton_CheckBox(&g_Config.m_ScShowFrozenNameplaterColor, Localize("Display Frozen Tee's Color in Nameplates"), g_Config.m_ScShowFrozenNameplaterColor, &Button)) - g_Config.m_ScShowFrozenNameplaterColor ^= 1; - - Left.HSplitTop(25.f, &Label, &Left); - - TextRender()->TextColor(col); - UI()->DoLabel(&Label, Localize("Tee"), 20.f, TEXTALIGN_TC); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - - Left.HSplitTop(20.f, &Button, &Left); - if(DoButton_CheckBox(&g_Config.m_ClRainbow, Localize("Rainbow Tee"), g_Config.m_ClRainbow, &Button)) - g_Config.m_ClRainbow ^= 1; - Left.HSplitTop(20.f, &Button, &Left); - if(DoButton_CheckBox(&g_Config.m_ClRainbowHook, Localize("Rainbow Hook"), g_Config.m_ClRainbowHook, &Button)) - g_Config.m_ClRainbowHook ^= 1; - if(g_Config.m_ClRainbow == 1 || g_Config.m_ClRainbowHook == 1) - { - Left.HSplitTop(20.f, &Button, &Left); - UI()->DoScrollbarOption(&g_Config.m_ClRainbowSpeed, &g_Config.m_ClRainbowSpeed, &Button, Localize("Rainbow Speed"), 0, 100, &CUI::ms_LinearScrollbarScale, 0u, "%"); - } - - // Console color - Right.HSplitTop(25.f, &Label, &Right); - - TextRender()->TextColor(col); - UI()->DoLabel(&Label, Localize("Console Settings"), 20.0f, TEXTALIGN_TC); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - - Right.HSplitTop(20.0f, &Button, &Right); - if(DoButton_CheckBox(&g_Config.m_ClConsoleSimple, Localize("Use simpled console"), g_Config.m_ClConsoleSimple, &Button)) - g_Config.m_ClConsoleSimple ^= 1; - - Right.HSplitTop(20.0f, &Button, &Right); - if(DoButton_CheckBox(&g_Config.m_ClConsoleBarSimple, Localize("Use simpled console bar"), g_Config.m_ClConsoleBarSimple, &Button)) - g_Config.m_ClConsoleBarSimple ^= 1; - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Right, Localize("Local console color"), - &g_Config.m_ScLocalConsoleColor, - ColorRGBA(0.2f, 0.2f, 0.2f), - false); - - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Right, Localize("Remote console color"), - &g_Config.m_ScRemoteConsoleColor, - ColorRGBA(0.4f, 0.2f, 0.2f), - false); - - if(g_Config.m_ClConsoleBarSimple == 1) - { - DoLine_ColorPicker(&s_aResetIDs[i++], - ColorPickerLineSize, ColorPickerLabelSize, ColorPickerLineSpacing, - &Right, Localize("Simpled console bar color"), - &g_Config.m_ScConsoleBarColor, - ColorRGBA(8883654), - false); - } - - Right.HSplitTop(20.f, &Button, &Right); - UI()->DoScrollbarOption(&g_Config.m_ClLocalConsolaAlpha, &g_Config.m_ClLocalConsolaAlpha, &Button, Localize("Local Console Opacity"), 0, 100, &CUI::ms_LinearScrollbarScale, 0u, "%"); - Right.HSplitTop(20.f, &Button, &Right); - UI()->DoScrollbarOption(&g_Config.m_ClRemoteConsolaAlpha, &g_Config.m_ClRemoteConsolaAlpha, &Button, Localize("Remote Console Opacity"), 0, 100, &CUI::ms_LinearScrollbarScale, 0u, "%"); - - Right.HSplitTop(30.f, &Label, &Right); - - TextRender()->TextColor(col); - UI()->DoLabel(&Label, Localize("Better Animation"), 20.0f, TEXTALIGN_TC); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - - // feets - Right.HSplitTop(20.0f, &Button, &Right); - if(DoButton_CheckBox(&g_Config.m_ClAnimFeetSpeed, Localize("Enable Feet LERP"), g_Config.m_ClAnimFeetSpeed < 100, &Button)) - g_Config.m_ClAnimFeetSpeed = g_Config.m_ClAnimFeetSpeed < 100 ? 100 : 1; - - Right.HSplitTop(20.0f, &Button, &Right); - if(g_Config.m_ClAnimFeetSpeed > 0 && g_Config.m_ClAnimFeetSpeed < 100) - UI()->DoScrollbarOption(&g_Config.m_ClAnimFeetSpeed, &g_Config.m_ClAnimFeetSpeed, &Button, Localize("LERP speed"), 1, 99, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_NOCLAMPVALUE); - - // guns - Right.HSplitTop(20.0f, &Button, &Right); - if(DoButton_CheckBox(&g_Config.m_ClAnimGunsSpeed, Localize("Enable Guns LERP"), g_Config.m_ClAnimGunsSpeed < 100, &Button)) - g_Config.m_ClAnimGunsSpeed = g_Config.m_ClAnimGunsSpeed < 100 ? 100 : 1; - - Right.HSplitTop(20.0f, &Button, &Right); - if(g_Config.m_ClAnimGunsSpeed > 0 && g_Config.m_ClAnimGunsSpeed < 100) - UI()->DoScrollbarOption(&g_Config.m_ClAnimGunsSpeed, &g_Config.m_ClAnimGunsSpeed, &Button, Localize("LERP speed"), 1, 99, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_NOCLAMPVALUE); - - // hammer - Right.HSplitTop(20.0f, &Button, &Right); - if(DoButton_CheckBox(&g_Config.m_ClAnimHammerSpeed, Localize("Enable Hammer LERP"), g_Config.m_ClAnimHammerSpeed < 100, &Button)) - g_Config.m_ClAnimHammerSpeed = g_Config.m_ClAnimHammerSpeed < 100 ? 100 : 1; - - Right.HSplitTop(20.0f, &Button, &Right); - if(g_Config.m_ClAnimHammerSpeed > 0 && g_Config.m_ClAnimHammerSpeed < 100) - UI()->DoScrollbarOption(&g_Config.m_ClAnimHammerSpeed, &g_Config.m_ClAnimHammerSpeed, &Button, Localize("LERP speed"), 1, 99, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_NOCLAMPVALUE); - - // ninja - Right.HSplitTop(20.0f, &Button, &Right); - if(DoButton_CheckBox(&g_Config.m_ClAnimNinjaSpeed, Localize("Enable Ninja LERP"), g_Config.m_ClAnimNinjaSpeed < 100, &Button)) - g_Config.m_ClAnimNinjaSpeed = g_Config.m_ClAnimNinjaSpeed < 100 ? 100 : 1; - - Right.HSplitTop(20.0f, &Button, &Right); - if(g_Config.m_ClAnimNinjaSpeed > 0 && g_Config.m_ClAnimNinjaSpeed < 100) - UI()->DoScrollbarOption(&g_Config.m_ClAnimNinjaSpeed, &g_Config.m_ClAnimNinjaSpeed, &Button, Localize("LERP speed"), 1, 99, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_NOCLAMPVALUE); - } - if(s_CurTab == STA_TAB_PAGE3) - { - Left.HSplitTop(25.f, &Label, &Left); - UI()->DoLabel(&Label, Localize("Why are you looking right here?:o"), 20.f, TEXTALIGN_ML); - } -} -void CMenus::RenderSettingsProfiles(CUIRect MainView) -{ - CUIRect Label, LabelMid, Section, LabelRight; - static int SelectedProfile = -1; - - const float LineMargin = 22.0f; - char *pSkinName = g_Config.m_ClPlayerSkin; - int *pUseCustomColor = &g_Config.m_ClPlayerUseCustomColor; - unsigned *pColorBody = &g_Config.m_ClPlayerColorBody; - unsigned *pColorFeet = &g_Config.m_ClPlayerColorFeet; - int CurrentFlag = m_Dummy ? g_Config.m_ClDummyCountry : g_Config.m_PlayerCountry; - - if(m_Dummy) - { - pSkinName = g_Config.m_ClDummySkin; - pUseCustomColor = &g_Config.m_ClDummyUseCustomColor; - pColorBody = &g_Config.m_ClDummyColorBody; - pColorFeet = &g_Config.m_ClDummyColorFeet; - } - - // skin info - CTeeRenderInfo OwnSkinInfo; - const CSkin *pSkin = m_pClient->m_Skins.Find(pSkinName); - OwnSkinInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin; - OwnSkinInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin; - OwnSkinInfo.m_SkinMetrics = pSkin->m_Metrics; - OwnSkinInfo.m_CustomColoredSkin = *pUseCustomColor; - if(*pUseCustomColor) - { - OwnSkinInfo.m_ColorBody = color_cast(ColorHSLA(*pColorBody).UnclampLighting()); - OwnSkinInfo.m_ColorFeet = color_cast(ColorHSLA(*pColorFeet).UnclampLighting()); - } - else - { - OwnSkinInfo.m_ColorBody = ColorRGBA(1.0f, 1.0f, 1.0f); - OwnSkinInfo.m_ColorFeet = ColorRGBA(1.0f, 1.0f, 1.0f); - } - OwnSkinInfo.m_Size = 50.0f; - - //======YOUR PROFILE====== - MainView.HSplitTop(10.0f, &Label, &MainView); - char aTempBuf[256]; - str_format(aTempBuf, sizeof(aTempBuf), "%s:", Localize("PlayerAsset")); - UI()->DoLabel(&Label, aTempBuf, 14.0f, TEXTALIGN_LEFT); - - MainView.HSplitTop(50.0f, &Label, &MainView); - Label.VSplitLeft(250.0f, &Label, &LabelMid); - const CAnimState *pIdleState = CAnimState::GetIdle(); - vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &OwnSkinInfo, OffsetToMid); - vec2 TeeRenderPos(Label.x + 20.0f, Label.y + Label.h / 2.0f + OffsetToMid.y); - int Emote = m_Dummy ? g_Config.m_ClDummyDefaultEyes : g_Config.m_ClPlayerDefaultEyes; - RenderTools()->RenderTee(pIdleState, &OwnSkinInfo, Emote, vec2(1, 0), TeeRenderPos); - - char aName[64]; - char aClan[64]; - str_format(aName, sizeof(aName), ("%s"), m_Dummy ? g_Config.m_ClDummyName : g_Config.m_PlayerName); - str_format(aClan, sizeof(aClan), ("%s"), m_Dummy ? g_Config.m_ClDummyClan : g_Config.m_PlayerClan); - - CUIRect FlagRect; - Label.VSplitLeft(90.0, &FlagRect, &Label); - Label.HMargin(-5.0f, &Label); - Label.HSplitTop(25.0f, &Section, &Label); - - str_format(aTempBuf, sizeof(aTempBuf), ("Name: %s"), aName); - UI()->DoLabel(&Section, aTempBuf, 15.0f, TEXTALIGN_LEFT); - - Label.HSplitTop(20.0f, &Section, &Label); - str_format(aTempBuf, sizeof(aTempBuf), ("Clan: %s"), aClan); - UI()->DoLabel(&Section, aTempBuf, 15.0f, TEXTALIGN_LEFT); - - Label.HSplitTop(20.0f, &Section, &Label); - str_format(aTempBuf, sizeof(aTempBuf), ("Skin: %s"), pSkinName); - UI()->DoLabel(&Section, aTempBuf, 15.0f, TEXTALIGN_LEFT); - - FlagRect.VSplitRight(50, 0, &FlagRect); - FlagRect.HSplitBottom(25, 0, &FlagRect); - FlagRect.y -= 10.0f; - ColorRGBA Color(1.0f, 1.0f, 1.0f, 1.0f); - m_pClient->m_CountryFlags.Render(m_Dummy ? g_Config.m_ClDummyCountry : g_Config.m_PlayerCountry, Color, FlagRect.x, FlagRect.y, FlagRect.w, FlagRect.h); - - bool doSkin = g_Config.m_ClApplyProfileSkin; - bool doColors = g_Config.m_ClApplyProfileColors; - bool doEmote = g_Config.m_ClApplyProfileEmote; - bool doName = g_Config.m_ClApplyProfileName; - bool doClan = g_Config.m_ClApplyProfileClan; - bool doFlag = g_Config.m_ClApplyProfileFlag; - - //======AFTER LOAD====== - if(SelectedProfile != -1 && SelectedProfile < (int)GameClient()->m_SkinProfiles.m_Profiles.size()) - { - CProfile LoadProfile = GameClient()->m_SkinProfiles.m_Profiles[SelectedProfile]; - MainView.HSplitTop(20.0f, 0, &MainView); - MainView.HSplitTop(10.0f, &Label, &MainView); - str_format(aTempBuf, sizeof(aTempBuf), "%s:", ("After Load")); - UI()->DoLabel(&Label, aTempBuf, 14.0f, TEXTALIGN_LEFT); - - MainView.HSplitTop(50.0f, &Label, &MainView); - Label.VSplitLeft(250.0f, &Label, 0); - - if(doSkin && strlen(LoadProfile.SkinName) != 0) - { - const CSkin *pLoadSkin = m_pClient->m_Skins.Find(LoadProfile.SkinName); - OwnSkinInfo.m_OriginalRenderSkin = pLoadSkin->m_OriginalSkin; - OwnSkinInfo.m_ColorableRenderSkin = pLoadSkin->m_ColorableSkin; - OwnSkinInfo.m_SkinMetrics = pLoadSkin->m_Metrics; - } - if(*pUseCustomColor && doColors && LoadProfile.BodyColor != -1 && LoadProfile.FeetColor != -1) - { - OwnSkinInfo.m_ColorBody = color_cast(ColorHSLA(LoadProfile.BodyColor).UnclampLighting()); - OwnSkinInfo.m_ColorFeet = color_cast(ColorHSLA(LoadProfile.FeetColor).UnclampLighting()); - } - - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &OwnSkinInfo, OffsetToMid); - TeeRenderPos = vec2(Label.x + 20.0f, Label.y + Label.h / 2.0f + OffsetToMid.y); - int LoadEmote = Emote; - if(doEmote && LoadProfile.Emote != -1) - LoadEmote = LoadProfile.Emote; - RenderTools()->RenderTee(pIdleState, &OwnSkinInfo, LoadEmote, vec2(1, 0), TeeRenderPos); - - if(doName && strlen(LoadProfile.Name) != 0) - str_format(aName, sizeof(aName), ("%s"), LoadProfile.Name); - if(doClan && strlen(LoadProfile.Clan) != 0) - str_format(aClan, sizeof(aClan), ("%s"), LoadProfile.Clan); - - Label.VSplitLeft(90.0, &FlagRect, &Label); - Label.HMargin(-5.0f, &Label); - Label.HSplitTop(25.0f, &Section, &Label); - - str_format(aTempBuf, sizeof(aTempBuf), ("Name: %s"), aName); - UI()->DoLabel(&Section, aTempBuf, 15.0f, TEXTALIGN_LEFT); - - Label.HSplitTop(20.0f, &Section, &Label); - str_format(aTempBuf, sizeof(aTempBuf), ("Clan: %s"), aClan); - UI()->DoLabel(&Section, aTempBuf, 15.0f, TEXTALIGN_LEFT); - - Label.HSplitTop(20.0f, &Section, &Label); - str_format(aTempBuf, sizeof(aTempBuf), ("Skin: %s"), (doSkin && strlen(LoadProfile.SkinName) != 0) ? LoadProfile.SkinName : pSkinName); - UI()->DoLabel(&Section, aTempBuf, 15.0f, TEXTALIGN_LEFT); - - FlagRect.VSplitRight(50, 0, &FlagRect); - FlagRect.HSplitBottom(25, 0, &FlagRect); - FlagRect.y -= 10.0f; - int RenderFlag = m_Dummy ? g_Config.m_ClDummyCountry : g_Config.m_PlayerCountry; - if(doFlag && LoadProfile.CountryFlag != -2) - RenderFlag = LoadProfile.CountryFlag; - m_pClient->m_CountryFlags.Render(RenderFlag, Color, FlagRect.x, FlagRect.y, FlagRect.w, FlagRect.h); - - str_format(aName, sizeof(aName), ("%s"), m_Dummy ? g_Config.m_ClDummyName : g_Config.m_PlayerName); - str_format(aClan, sizeof(aClan), ("%s"), m_Dummy ? g_Config.m_ClDummyClan : g_Config.m_PlayerClan); - } - else - { - MainView.HSplitTop(80.0f, 0, &MainView); - } - - //===BUTTONS AND CHECK BOX=== - CUIRect DummyCheck, CustomCheck; - MainView.HSplitTop(30, &DummyCheck, 0); - DummyCheck.HSplitTop(13, 0, &DummyCheck); - - DummyCheck.VSplitLeft(100, &DummyCheck, &CustomCheck); - CustomCheck.VSplitLeft(150, &CustomCheck, 0); - - DoButton_CheckBoxAutoVMarginAndSet(&m_Dummy, Localize("Dummy"), (int *)&m_Dummy, &DummyCheck, LineMargin); - - static int s_CustomColorID = 0; - CustomCheck.HSplitTop(LineMargin, &CustomCheck, 0); - - if(DoButton_CheckBox(&s_CustomColorID, Localize("Custom colors"), *pUseCustomColor, &CustomCheck)) - { - *pUseCustomColor = *pUseCustomColor ? 0 : 1; - SetNeedSendInfo(); - } - - LabelMid.VSplitLeft(20.0f, 0, &LabelMid); - LabelMid.VSplitLeft(160.0f, &LabelMid, &LabelRight); - - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClApplyProfileSkin, ("Save/Load Skin"), &g_Config.m_ClApplyProfileSkin, &LabelMid, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClApplyProfileColors, ("Save/Load Colors"), &g_Config.m_ClApplyProfileColors, &LabelMid, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClApplyProfileEmote, ("Save/Load Emote"), &g_Config.m_ClApplyProfileEmote, &LabelMid, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClApplyProfileName, ("Save/Load Name"), &g_Config.m_ClApplyProfileName, &LabelMid, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClApplyProfileClan, ("Save/Load Clan"), &g_Config.m_ClApplyProfileClan, &LabelMid, LineMargin); - DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClApplyProfileFlag, ("Save/Load Flag"), &g_Config.m_ClApplyProfileFlag, &LabelMid, LineMargin); - - CUIRect Button; - LabelRight.VSplitLeft(150.0f, &LabelRight, 0); - - LabelRight.HSplitTop(30.0f, &Button, &LabelRight); - static CButtonContainer s_LoadButton; - - if(DoButton_Menu(&s_LoadButton, Localize("Load"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f)) - { - if(SelectedProfile != -1 && SelectedProfile < (int)GameClient()->m_SkinProfiles.m_Profiles.size()) - { - CProfile LoadProfile = GameClient()->m_SkinProfiles.m_Profiles[SelectedProfile]; - if(!m_Dummy) - { - if(doSkin && strlen(LoadProfile.SkinName) != 0) - str_copy(g_Config.m_ClPlayerSkin, LoadProfile.SkinName, sizeof(g_Config.m_ClPlayerSkin)); - if(doColors && LoadProfile.BodyColor != -1 && LoadProfile.FeetColor != -1) - { - g_Config.m_ClPlayerColorBody = LoadProfile.BodyColor; - g_Config.m_ClPlayerColorFeet = LoadProfile.FeetColor; - } - if(doEmote && LoadProfile.Emote != -1) - g_Config.m_ClPlayerDefaultEyes = LoadProfile.Emote; - if(doName && strlen(LoadProfile.Name) != 0) - str_copy(g_Config.m_PlayerName, LoadProfile.Name, sizeof(g_Config.m_PlayerName)); - if(doClan && strlen(LoadProfile.Clan) != 0) - str_copy(g_Config.m_PlayerClan, LoadProfile.Clan, sizeof(g_Config.m_PlayerClan)); - if(doFlag && LoadProfile.CountryFlag != -2) - g_Config.m_PlayerCountry = LoadProfile.CountryFlag; - } - else - { - if(doSkin && strlen(LoadProfile.SkinName) != 0) - str_copy(g_Config.m_ClDummySkin, LoadProfile.SkinName, sizeof(g_Config.m_ClDummySkin)); - if(doColors && LoadProfile.BodyColor != -1 && LoadProfile.FeetColor != -1) - { - g_Config.m_ClDummyColorBody = LoadProfile.BodyColor; - g_Config.m_ClDummyColorFeet = LoadProfile.FeetColor; - } - if(doEmote && LoadProfile.Emote != -1) - g_Config.m_ClDummyDefaultEyes = LoadProfile.Emote; - if(doName && strlen(LoadProfile.Name) != 0) - str_copy(g_Config.m_ClDummyName, LoadProfile.Name, sizeof(g_Config.m_ClDummyName)); - if(doClan && strlen(LoadProfile.Clan) != 0) - str_copy(g_Config.m_ClDummyClan, LoadProfile.Clan, sizeof(g_Config.m_ClDummyClan)); - if(doFlag && LoadProfile.CountryFlag != -2) - g_Config.m_ClDummyCountry = LoadProfile.CountryFlag; - } - } - SetNeedSendInfo(); - } - LabelRight.HSplitTop(5.0f, 0, &LabelRight); - - LabelRight.HSplitTop(30.0f, &Button, &LabelRight); - static CButtonContainer s_SaveButton; - if(DoButton_Menu(&s_SaveButton, Localize("Save"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f))) - { - GameClient()->m_SkinProfiles.AddProfile( - doColors ? *pColorBody : -1, - doColors ? *pColorFeet : -1, - doFlag ? CurrentFlag : -2, - doEmote ? Emote : -1, - doSkin ? pSkinName : "", - doName ? aName : "", - doClan ? aClan : ""); - GameClient()->m_SkinProfiles.SaveProfiles(); - } - LabelRight.HSplitTop(5.0f, 0, &LabelRight); - - static int s_AllowDelete; - DoButton_CheckBoxAutoVMarginAndSet(&s_AllowDelete, ("Enable Deleting"), &s_AllowDelete, &LabelRight, LineMargin); - LabelRight.HSplitTop(5.0f, 0, &LabelRight); - - if(s_AllowDelete) - { - LabelRight.HSplitTop(28.0f, &Button, &LabelRight); - static CButtonContainer s_DeleteButton; - if(DoButton_Menu(&s_DeleteButton, Localize("Delete"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f))) - { - if(SelectedProfile != -1 && SelectedProfile < (int)GameClient()->m_SkinProfiles.m_Profiles.size()) - { - GameClient()->m_SkinProfiles.m_Profiles.erase(GameClient()->m_SkinProfiles.m_Profiles.begin() + SelectedProfile); - GameClient()->m_SkinProfiles.SaveProfiles(); - } - } - LabelRight.HSplitTop(5.0f, 0, &LabelRight); - - LabelRight.HSplitTop(28.0f, &Button, &LabelRight); - static CButtonContainer s_OverrideButton; - if(DoButton_Menu(&s_OverrideButton, Localize("Override"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f))) - { - if(SelectedProfile != -1 && SelectedProfile < (int)GameClient()->m_SkinProfiles.m_Profiles.size()) - { - GameClient()->m_SkinProfiles.m_Profiles[SelectedProfile] = CProfile( - doColors ? *pColorBody : -1, - doColors ? *pColorFeet : -1, - doFlag ? CurrentFlag : -2, - doEmote ? Emote : -1, - doSkin ? pSkinName : "", - doName ? aName : "", - doClan ? aClan : ""); - GameClient()->m_SkinProfiles.SaveProfiles(); - } - } - } - - //---RENDER THE SELECTOR--- - CUIRect SelectorRect; - MainView.HSplitTop(50, 0, &SelectorRect); - SelectorRect.HSplitBottom(15.0, &SelectorRect, 0); - std::vector *pProfileList = &GameClient()->m_SkinProfiles.m_Profiles; - - static CListBox s_ListBox; - s_ListBox.DoStart(50.0f, pProfileList->size(), 4, 3, SelectedProfile, &SelectorRect, true); - - static bool s_Indexs[1024]; - - for(size_t i = 0; i < pProfileList->size(); ++i) - { - CProfile CurrentProfile = GameClient()->m_SkinProfiles.m_Profiles[i]; - - char RenderSkin[24]; - if(strlen(CurrentProfile.SkinName) == 0) - str_copy(RenderSkin, pSkinName, sizeof(RenderSkin)); - else - str_copy(RenderSkin, CurrentProfile.SkinName, sizeof(RenderSkin)); - - const CSkin *pSkinToBeDraw = m_pClient->m_Skins.Find(RenderSkin); - - CListboxItem Item = s_ListBox.DoNextItem(&s_Indexs[i], SelectedProfile >= 0 && (size_t)SelectedProfile == i); - - if(!Item.m_Visible) - continue; - - if(Item.m_Visible) - { - CTeeRenderInfo Info; - Info.m_ColorBody = color_cast(ColorHSLA(CurrentProfile.BodyColor).UnclampLighting()); - Info.m_ColorFeet = color_cast(ColorHSLA(CurrentProfile.FeetColor).UnclampLighting()); - Info.m_CustomColoredSkin = 1; - Info.m_OriginalRenderSkin = pSkinToBeDraw->m_OriginalSkin; - Info.m_ColorableRenderSkin = pSkinToBeDraw->m_ColorableSkin; - Info.m_SkinMetrics = pSkinToBeDraw->m_Metrics; - Info.m_Size = 50.0f; - if(CurrentProfile.BodyColor == -1 && CurrentProfile.FeetColor == -1) - { - Info.m_CustomColoredSkin = m_Dummy ? g_Config.m_ClDummyUseCustomColor : g_Config.m_ClPlayerUseCustomColor; - Info.m_ColorBody = ColorRGBA(1.0f, 1.0f, 1.0f); - Info.m_ColorFeet = ColorRGBA(1.0f, 1.0f, 1.0f); - } - - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &Info, OffsetToMid); - - int RenderEmote = CurrentProfile.Emote == -1 ? Emote : CurrentProfile.Emote; - TeeRenderPos = vec2(Item.m_Rect.x + 30, Item.m_Rect.y + Item.m_Rect.h / 2 + OffsetToMid.y); - - Item.m_Rect.VSplitLeft(60.0f, 0, &Item.m_Rect); - CUIRect PlayerRect, ClanRect, FeetColorSquare, BodyColorSquare; - - Item.m_Rect.VSplitLeft(60.0f, 0, &BodyColorSquare); //Delete this maybe - - Item.m_Rect.VSplitRight(60.0, &BodyColorSquare, &FlagRect); - BodyColorSquare.x -= 11.0; - BodyColorSquare.VSplitLeft(10, &BodyColorSquare, 0); - BodyColorSquare.HSplitMid(&BodyColorSquare, &FeetColorSquare); - BodyColorSquare.HSplitMid(0, &BodyColorSquare); - FeetColorSquare.HSplitMid(&FeetColorSquare, 0); - FlagRect.HSplitBottom(10.0, &FlagRect, 0); - FlagRect.HSplitTop(10.0, 0, &FlagRect); - - Item.m_Rect.HSplitMid(&PlayerRect, &ClanRect); - - SLabelProperties Props; - Props.m_MaxWidth = Item.m_Rect.w; - if(CurrentProfile.CountryFlag != -2) - m_pClient->m_CountryFlags.Render(CurrentProfile.CountryFlag, Color, FlagRect.x, FlagRect.y, FlagRect.w, FlagRect.h); - - if(CurrentProfile.BodyColor != -1 && CurrentProfile.FeetColor != -1) - { - ColorRGBA BodyColor = color_cast(ColorHSLA(CurrentProfile.BodyColor).UnclampLighting()); - ColorRGBA FeetColor = color_cast(ColorHSLA(CurrentProfile.FeetColor).UnclampLighting()); - - Graphics()->TextureClear(); - Graphics()->QuadsBegin(); - Graphics()->SetColor(BodyColor.r, BodyColor.g, BodyColor.b, 1.0f); - IGraphics::CQuadItem Quads[2]; - Quads[0] = IGraphics::CQuadItem(BodyColorSquare.x, BodyColorSquare.y, BodyColorSquare.w, BodyColorSquare.h); - Graphics()->QuadsDrawTL(&Quads[0], 1); - Graphics()->SetColor(FeetColor.r, FeetColor.g, FeetColor.b, 1.0f); - Quads[1] = IGraphics::CQuadItem(FeetColorSquare.x, FeetColorSquare.y, FeetColorSquare.w, FeetColorSquare.h); - Graphics()->QuadsDrawTL(&Quads[1], 1); - Graphics()->QuadsEnd(); - } - RenderTools()->RenderTee(pIdleState, &Info, RenderEmote, vec2(1.0f, 0.0f), TeeRenderPos); - - if(strlen(CurrentProfile.Name) == 0 && strlen(CurrentProfile.Clan) == 0) - { - PlayerRect = Item.m_Rect; - UI()->DoLabel(&PlayerRect, CurrentProfile.SkinName, 12.0f, TEXTALIGN_LEFT, Props); - } - else - { - UI()->DoLabel(&PlayerRect, CurrentProfile.Name, 12.0f, TEXTALIGN_LEFT, Props); - Item.m_Rect.HSplitTop(20.0f, 0, &Item.m_Rect); - Props.m_MaxWidth = Item.m_Rect.w; - UI()->DoLabel(&ClanRect, CurrentProfile.Clan, 12.0f, TEXTALIGN_LEFT, Props); - } - } - } - - const int NewSelected = s_ListBox.DoEnd(); - if(SelectedProfile != NewSelected) - { - SelectedProfile = NewSelected; - } - static CButtonContainer s_ProfilesFile; - CUIRect FileButton; - MainView.HSplitBottom(25.0, 0, &FileButton); - FileButton.y += 15.0; - FileButton.VSplitLeft(130.0, &FileButton, 0); - if(DoButton_Menu(&s_ProfilesFile, Localize("Profiles file"), 0, &FileButton)) - { - Storage()->GetCompletePath(IStorage::TYPE_SAVE, PROFILES_FILE, aTempBuf, sizeof(aTempBuf)); - if(!open_file(aTempBuf)) - { - dbg_msg("menus", "couldn't open file"); - } - } -} void CMenus::RenderSettingsDDNet(CUIRect MainView) { CUIRect Button, Left, Right, LeftLeft, Label; @@ -4042,9 +2984,9 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) CUIRect Demo; MainView.HSplitTop(110.0f, &Demo, &MainView); Demo.HSplitTop(30.0f, &Label, &Demo); - UI()->DoLabel(&Label, Localize("Demo"), 20.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, Localize("Demo"), 20.0f, TEXTALIGN_ML); Label.VSplitMid(nullptr, &Label, 20.0f); - UI()->DoLabel(&Label, Localize("Ghost"), 20.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, Localize("Ghost"), 20.0f, TEXTALIGN_ML); Demo.HSplitTop(5.0f, nullptr, &Demo); Demo.VSplitMid(&Left, &Right, 20.0f); @@ -4059,21 +3001,12 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) if(DoButton_CheckBox(&g_Config.m_ClReplays, Localize("Enable replays"), g_Config.m_ClReplays, &Button)) { g_Config.m_ClReplays ^= 1; - if(!g_Config.m_ClReplays) - { - // stop recording and remove the tmp demo file - Client()->DemoRecorder_Stop(RECORDER_REPLAYS, true); - } - else - { - // start recording - Client()->DemoRecorder_HandleAutoStart(); - } + Client()->DemoRecorder_UpdateReplayRecorder(); } Left.HSplitTop(20.0f, &Button, &Left); if(g_Config.m_ClReplays) - UI()->DoScrollbarOption(&g_Config.m_ClReplayLength, &g_Config.m_ClReplayLength, &Button, Localize("Default length"), 10, 600, &CUI::ms_LinearScrollbarScale, CUI::SCROLLBAR_OPTION_NOCLAMPVALUE); + Ui()->DoScrollbarOption(&g_Config.m_ClReplayLength, &g_Config.m_ClReplayLength, &Button, Localize("Default length"), 10, 600, &CUi::ms_LinearScrollbarScale, CUi::SCROLLBAR_OPTION_NOCLAMPVALUE); Right.HSplitTop(20.0f, &Button, &Right); if(DoButton_CheckBox(&g_Config.m_ClRaceGhost, Localize("Enable ghost"), g_Config.m_ClRaceGhost, &Button)) @@ -4090,7 +3023,7 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) { g_Config.m_ClRaceShowGhost ^= 1; } - UI()->DoScrollbarOption(&g_Config.m_ClRaceGhostAlpha, &g_Config.m_ClRaceGhostAlpha, &Button, Localize("Opacity"), 0, 100, &CUI::ms_LinearScrollbarScale, 0u, "%"); + Ui()->DoScrollbarOption(&g_Config.m_ClRaceGhostAlpha, &g_Config.m_ClRaceGhostAlpha, &Button, Localize("Opacity"), 0, 100, &CUi::ms_LinearScrollbarScale, 0u, "%"); Right.HSplitTop(20.0f, &Button, &Right); if(DoButton_CheckBox(&g_Config.m_ClRaceSaveGhost, Localize("Save ghost"), g_Config.m_ClRaceSaveGhost, &Button)) @@ -4112,12 +3045,12 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) CUIRect Gameplay; MainView.HSplitTop(150.0f, &Gameplay, &MainView); Gameplay.HSplitTop(30.0f, &Label, &Gameplay); - UI()->DoLabel(&Label, Localize("Gameplay"), 20.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, Localize("Gameplay"), 20.0f, TEXTALIGN_ML); Gameplay.HSplitTop(5.0f, nullptr, &Gameplay); Gameplay.VSplitMid(&Left, &Right, 20.0f); Left.HSplitTop(20.0f, &Button, &Left); - UI()->DoScrollbarOption(&g_Config.m_ClOverlayEntities, &g_Config.m_ClOverlayEntities, &Button, Localize("Overlay entities"), 0, 100); + Ui()->DoScrollbarOption(&g_Config.m_ClOverlayEntities, &g_Config.m_ClOverlayEntities, &Button, Localize("Overlay entities"), 0, 100); Left.HSplitTop(20.0f, &Button, &Left); Button.VSplitMid(&LeftLeft, &Button); @@ -4127,10 +3060,7 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) if(g_Config.m_ClTextEntities) { - int PreviousSize = g_Config.m_ClTextEntitiesSize; - UI()->DoScrollbarOption(&g_Config.m_ClTextEntitiesSize, &g_Config.m_ClTextEntitiesSize, &Button, Localize("Size"), 0, 100); - - if(PreviousSize != g_Config.m_ClTextEntitiesSize) + if(Ui()->DoScrollbarOption(&g_Config.m_ClTextEntitiesSize, &g_Config.m_ClTextEntitiesSize, &Button, Localize("Size"), 0, 100)) m_pClient->m_MapImages.SetTextureScale(g_Config.m_ClTextEntitiesSize); } @@ -4140,13 +3070,13 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) if(DoButton_CheckBox(&g_Config.m_ClShowOthers, Localize("Show others"), g_Config.m_ClShowOthers == SHOW_OTHERS_ON, &LeftLeft)) g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ON ? SHOW_OTHERS_ON : SHOW_OTHERS_OFF; - UI()->DoScrollbarOption(&g_Config.m_ClShowOthersAlpha, &g_Config.m_ClShowOthersAlpha, &Button, Localize("Opacity"), 0, 100, &CUI::ms_LinearScrollbarScale, 0u, "%"); + Ui()->DoScrollbarOption(&g_Config.m_ClShowOthersAlpha, &g_Config.m_ClShowOthersAlpha, &Button, Localize("Opacity"), 0, 100, &CUi::ms_LinearScrollbarScale, 0u, "%"); GameClient()->m_Tooltips.DoToolTip(&g_Config.m_ClShowOthersAlpha, &Button, Localize("Adjust the opacity of entities belonging to other teams, such as tees and nameplates")); Left.HSplitTop(20.0f, &Button, &Left); - static int s_ShowOwnTeamID = 0; - if(DoButton_CheckBox(&s_ShowOwnTeamID, Localize("Show others (own team only)"), g_Config.m_ClShowOthers == SHOW_OTHERS_ONLY_TEAM, &Button)) + static int s_ShowOwnTeamId = 0; + if(DoButton_CheckBox(&s_ShowOwnTeamId, Localize("Show others (own team only)"), g_Config.m_ClShowOthers == SHOW_OTHERS_ONLY_TEAM, &Button)) { g_Config.m_ClShowOthers = g_Config.m_ClShowOthers != SHOW_OTHERS_ONLY_TEAM ? SHOW_OTHERS_ONLY_TEAM : SHOW_OTHERS_OFF; } @@ -4159,9 +3089,7 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) GameClient()->m_Tooltips.DoToolTip(&g_Config.m_ClShowQuads, &Button, Localize("Quads are used for background decoration")); Right.HSplitTop(20.0f, &Button, &Right); - int PreviousZoom = g_Config.m_ClDefaultZoom; - UI()->DoScrollbarOption(&g_Config.m_ClDefaultZoom, &g_Config.m_ClDefaultZoom, &Button, Localize("Default zoom"), 0, 20); - if(PreviousZoom != g_Config.m_ClDefaultZoom) + if(Ui()->DoScrollbarOption(&g_Config.m_ClDefaultZoom, &g_Config.m_ClDefaultZoom, &Button, Localize("Default zoom"), 0, 20)) m_pClient->m_Camera.SetZoom(std::pow(CCamera::ZOOM_STEP, g_Config.m_ClDefaultZoom - 10), g_Config.m_ClSmoothZoomTime); Right.HSplitTop(20.0f, &Button, &Right); @@ -4198,39 +3126,50 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) // background Background.HSplitTop(30.0f, &Label, &Background); Background.HSplitTop(5.0f, nullptr, &Background); - UI()->DoLabel(&Label, Localize("Background"), 20.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, Localize("Background"), 20.0f, TEXTALIGN_ML); ColorRGBA GreyDefault(0.5f, 0.5f, 0.5f, 1); - static CButtonContainer s_ResetID1; - DoLine_ColorPicker(&s_ResetID1, 25.0f, 13.0f, 5.0f, &Background, Localize("Regular background color"), &g_Config.m_ClBackgroundColor, GreyDefault, false); + static CButtonContainer s_ResetId1; + DoLine_ColorPicker(&s_ResetId1, 25.0f, 13.0f, 5.0f, &Background, Localize("Regular background color"), &g_Config.m_ClBackgroundColor, GreyDefault, false); - static CButtonContainer s_ResetID2; - DoLine_ColorPicker(&s_ResetID2, 25.0f, 13.0f, 5.0f, &Background, Localize("Entities background color"), &g_Config.m_ClBackgroundEntitiesColor, GreyDefault, false); + static CButtonContainer s_ResetId2; + DoLine_ColorPicker(&s_ResetId2, 25.0f, 13.0f, 5.0f, &Background, Localize("Entities background color"), &g_Config.m_ClBackgroundEntitiesColor, GreyDefault, false); - CUIRect EditBox; + CUIRect EditBox, ReloadButton; Background.HSplitTop(20.0f, &Label, &Background); Background.HSplitTop(2.0f, nullptr, &Background); Label.VSplitLeft(100.0f, &Label, &EditBox); - EditBox.VSplitRight(100.0f, &EditBox, &Button); + EditBox.VSplitRight(60.0f, &EditBox, &Button); + Button.VSplitMid(&ReloadButton, &Button, 5.0f); EditBox.VSplitRight(5.0f, &EditBox, nullptr); - UI()->DoLabel(&Label, Localize("Map"), 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, Localize("Map"), 14.0f, TEXTALIGN_ML); static CLineInput s_BackgroundEntitiesInput(g_Config.m_ClBackgroundEntities, sizeof(g_Config.m_ClBackgroundEntities)); - UI()->DoEditBox(&s_BackgroundEntitiesInput, &EditBox, 14.0f); + Ui()->DoEditBox(&s_BackgroundEntitiesInput, &EditBox, 14.0f); + + static CButtonContainer s_BackgroundEntitiesMapPicker, s_BackgroundEntitiesReload; - static CButtonContainer s_BackgroundEntitiesReloadButton; - if(DoButton_Menu(&s_BackgroundEntitiesReloadButton, Localize("Reload"), 0, &Button)) + if(DoButton_FontIcon(&s_BackgroundEntitiesReload, FONT_ICON_ARROW_ROTATE_RIGHT, 0, &ReloadButton)) { - if(str_comp(g_Config.m_ClBackgroundEntities, m_pClient->m_Background.MapName()) != 0) - m_pClient->m_Background.LoadBackground(); + m_pClient->m_Background.LoadBackground(); + } + + if(DoButton_FontIcon(&s_BackgroundEntitiesMapPicker, FONT_ICON_FOLDER, 0, &Button)) + { + static SPopupMenuId s_PopupMapPickerId; + static CPopupMapPickerContext s_PopupMapPickerContext; + s_PopupMapPickerContext.m_pMenus = this; + + s_PopupMapPickerContext.MapListPopulate(); + Ui()->DoPopupMenu(&s_PopupMapPickerId, Ui()->MouseX(), Ui()->MouseY(), 300.0f, 250.0f, &s_PopupMapPickerContext, PopupMapPicker); } Background.HSplitTop(20.0f, &Button, &Background); const bool UseCurrentMap = str_comp(g_Config.m_ClBackgroundEntities, CURRENT_MAP) == 0; - static int s_UseCurrentMapID = 0; - if(DoButton_CheckBox(&s_UseCurrentMapID, Localize("Use current map as background"), UseCurrentMap, &Button)) + static int s_UseCurrentMapId = 0; + if(DoButton_CheckBox(&s_UseCurrentMapId, Localize("Use current map as background"), UseCurrentMap, &Button)) { if(UseCurrentMap) g_Config.m_ClBackgroundEntities[0] = '\0'; @@ -4247,7 +3186,7 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) Miscellaneous.HSplitTop(30.0f, &Label, &Miscellaneous); Miscellaneous.HSplitTop(5.0f, nullptr, &Miscellaneous); - UI()->DoLabel(&Label, Localize("Miscellaneous"), 20.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, Localize("Miscellaneous"), 20.0f, TEXTALIGN_ML); static CButtonContainer s_ButtonTimeout; Miscellaneous.HSplitTop(20.0f, &Button, &Miscellaneous); @@ -4259,11 +3198,11 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) Miscellaneous.HSplitTop(5.0f, nullptr, &Miscellaneous); Miscellaneous.HSplitTop(20.0f, &Label, &Miscellaneous); Miscellaneous.HSplitTop(2.0f, nullptr, &Miscellaneous); - UI()->DoLabel(&Label, Localize("Run on join"), 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, Localize("Run on join"), 14.0f, TEXTALIGN_ML); Miscellaneous.HSplitTop(20.0f, &Button, &Miscellaneous); static CLineInput s_RunOnJoinInput(g_Config.m_ClRunOnJoin, sizeof(g_Config.m_ClRunOnJoin)); s_RunOnJoinInput.SetEmptyText(Localize("Chat command (e.g. showall 1)")); - UI()->DoEditBox(&s_RunOnJoinInput, &Button, 14.0f); + Ui()->DoEditBox(&s_RunOnJoinInput, &Button, 14.0f); #if defined(CONF_FAMILY_WINDOWS) static CButtonContainer s_ButtonUnregisterShell; @@ -4279,7 +3218,7 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) #if defined(CONF_AUTOUPDATE) { bool NeedUpdate = str_comp(Client()->LatestVersion(), "0"); - int State = Updater()->GetCurrentState(); + IUpdater::EUpdaterState State = Updater()->GetCurrentState(); // Update Button char aBuf[256]; @@ -4295,15 +3234,15 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) } } else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART) - str_format(aBuf, sizeof(aBuf), Localize("Updating...")); + str_copy(aBuf, Localize("Updating…")); else if(State == IUpdater::NEED_RESTART) { - str_format(aBuf, sizeof(aBuf), Localize("DDNet Client updated!")); + str_copy(aBuf, Localize("DDNet Client updated!")); m_NeedRestartUpdate = true; } else { - str_format(aBuf, sizeof(aBuf), Localize("No updates available")); + str_copy(aBuf, Localize("No updates available")); UpdaterRect.VSplitLeft(TextRender()->TextWidth(14.0f, aBuf, -1, -1.0f) + 10.0f, &UpdaterRect, &Button); Button.VSplitLeft(100.0f, &Button, nullptr); static CButtonContainer s_ButtonUpdate; @@ -4312,8 +3251,109 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView) Client()->RequestDDNetInfo(); } } - UI()->DoLabel(&UpdaterRect, aBuf, 14.0f, TEXTALIGN_ML); + Ui()->DoLabel(&UpdaterRect, aBuf, 14.0f, TEXTALIGN_ML); TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); } #endif } + +CUi::EPopupMenuFunctionResult CMenus::PopupMapPicker(void *pContext, CUIRect View, bool Active) +{ + CPopupMapPickerContext *pPopupContext = static_cast(pContext); + CMenus *pMenus = pPopupContext->m_pMenus; + + static CListBox s_ListBox; + s_ListBox.SetActive(Active); + s_ListBox.DoStart(20.0f, pPopupContext->m_vMaps.size(), 1, 1, -1, &View, false); + + int MapIndex = 0; + for(auto &Map : pPopupContext->m_vMaps) + { + MapIndex++; + const CListboxItem Item = s_ListBox.DoNextItem(&Map, MapIndex == pPopupContext->m_Selection); + if(!Item.m_Visible) + continue; + + CUIRect Label, Icon; + Item.m_Rect.VSplitLeft(20.0f, &Icon, &Label); + + char aLabelText[IO_MAX_PATH_LENGTH]; + str_copy(aLabelText, Map.m_aFilename); + if(Map.m_IsDirectory) + str_append(aLabelText, "/", sizeof(aLabelText)); + + const char *pIconType; + if(!Map.m_IsDirectory) + { + pIconType = FONT_ICON_MAP; + } + else + { + if(!str_comp(Map.m_aFilename, "..")) + pIconType = FONT_ICON_FOLDER_TREE; + else + pIconType = FONT_ICON_FOLDER; + } + + pMenus->TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + pMenus->TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING); + pMenus->Ui()->DoLabel(&Icon, pIconType, 12.0f, TEXTALIGN_ML); + pMenus->TextRender()->SetRenderFlags(0); + pMenus->TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); + + pMenus->Ui()->DoLabel(&Label, aLabelText, 10.0f, TEXTALIGN_ML); + } + + const int NewSelected = s_ListBox.DoEnd(); + pPopupContext->m_Selection = NewSelected >= 0 ? NewSelected : -1; + if(s_ListBox.WasItemSelected() || s_ListBox.WasItemActivated()) + { + const CMapListItem SelectedItem = pPopupContext->m_vMaps[pPopupContext->m_Selection]; + + if(SelectedItem.m_IsDirectory) + { + if(!str_comp(SelectedItem.m_aFilename, "..")) + { + fs_parent_dir(pPopupContext->m_aCurrentMapFolder); + } + else + { + str_append(pPopupContext->m_aCurrentMapFolder, "/", sizeof(pPopupContext->m_aCurrentMapFolder)); + str_append(pPopupContext->m_aCurrentMapFolder, SelectedItem.m_aFilename, sizeof(pPopupContext->m_aCurrentMapFolder)); + } + pPopupContext->MapListPopulate(); + } + else + { + str_format(g_Config.m_ClBackgroundEntities, sizeof(g_Config.m_ClBackgroundEntities), "%s/%s", pPopupContext->m_aCurrentMapFolder, SelectedItem.m_aFilename); + pMenus->m_pClient->m_Background.LoadBackground(); + return CUi::POPUP_CLOSE_CURRENT; + } + } + + return CUi::POPUP_KEEP_OPEN; +} + +void CMenus::CPopupMapPickerContext::MapListPopulate() +{ + m_vMaps.clear(); + char aTemp[IO_MAX_PATH_LENGTH]; + str_format(aTemp, sizeof(aTemp), "maps/%s", m_aCurrentMapFolder); + m_pMenus->Storage()->ListDirectoryInfo(IStorage::TYPE_ALL, aTemp, MapListFetchCallback, this); + std::stable_sort(m_vMaps.begin(), m_vMaps.end(), CompareFilenameAscending); +} + +int CMenus::CPopupMapPickerContext::MapListFetchCallback(const CFsFileInfo *pInfo, int IsDir, int StorageType, void *pUser) +{ + CPopupMapPickerContext *pRealUser = (CPopupMapPickerContext *)pUser; + if((!IsDir && !str_endswith(pInfo->m_pName, ".map")) || !str_comp(pInfo->m_pName, ".") || (!str_comp(pInfo->m_pName, "..") && (!str_comp(pRealUser->m_aCurrentMapFolder, "")))) + return 0; + + CMapListItem Item; + str_copy(Item.m_aFilename, pInfo->m_pName); + Item.m_IsDirectory = IsDir; + + pRealUser->m_vMaps.emplace_back(Item); + + return 0; +} diff --git a/src/game/client/components/menus_settings7.cpp b/src/game/client/components/menus_settings7.cpp new file mode 100644 index 0000000000..cfde9d1875 --- /dev/null +++ b/src/game/client/components/menus_settings7.cpp @@ -0,0 +1,503 @@ +/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ +/* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "menus.h" +#include "skins7.h" + +#include + +using namespace FontIcons; + +void CMenus::RenderSettingsTee7(CUIRect MainView) +{ + CUIRect SkinPreview, NormalSkinPreview, RedTeamSkinPreview, BlueTeamSkinPreview, Buttons, QuickSearch, DirectoryButton, RefreshButton, SaveDeleteButton, TabBars, TabBar, LeftTab, RightTab; + MainView.HSplitBottom(20.0f, &MainView, &Buttons); + MainView.HSplitBottom(5.0f, &MainView, nullptr); + Buttons.VSplitRight(25.0f, &Buttons, &RefreshButton); + Buttons.VSplitRight(10.0f, &Buttons, nullptr); + Buttons.VSplitRight(140.0f, &Buttons, &DirectoryButton); + Buttons.VSplitLeft(220.0f, &QuickSearch, &Buttons); + Buttons.VSplitLeft(10.0f, nullptr, &Buttons); + Buttons.VSplitLeft(120.0f, &SaveDeleteButton, &Buttons); + MainView.HSplitTop(50.0f, &TabBars, &MainView); + MainView.HSplitTop(10.0f, nullptr, &MainView); + TabBars.VSplitMid(&TabBars, &SkinPreview, 20.0f); + + TabBars.HSplitTop(20.0f, &TabBar, &TabBars); + TabBar.VSplitMid(&LeftTab, &RightTab); + TabBars.HSplitTop(10.0f, nullptr, &TabBars); + + SkinPreview.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f), IGraphics::CORNER_ALL, 5.0f); + SkinPreview.VMargin(10.0f, &SkinPreview); + SkinPreview.VSplitRight(50.0f, &SkinPreview, &BlueTeamSkinPreview); + SkinPreview.VSplitRight(10.0f, &SkinPreview, nullptr); + SkinPreview.VSplitRight(50.0f, &SkinPreview, &RedTeamSkinPreview); + SkinPreview.VSplitRight(10.0f, &SkinPreview, nullptr); + SkinPreview.VSplitRight(50.0f, &SkinPreview, &NormalSkinPreview); + SkinPreview.VSplitRight(10.0f, &SkinPreview, nullptr); + + static CButtonContainer s_PlayerTabButton; + if(DoButton_MenuTab(&s_PlayerTabButton, Localize("Player"), !m_Dummy, &LeftTab, IGraphics::CORNER_L, nullptr, nullptr, nullptr, nullptr, 4.0f)) + { + m_Dummy = false; + } + + static CButtonContainer s_DummyTabButton; + if(DoButton_MenuTab(&s_DummyTabButton, Localize("Dummy"), m_Dummy, &RightTab, IGraphics::CORNER_R, nullptr, nullptr, nullptr, nullptr, 4.0f)) + { + m_Dummy = true; + } + + TabBars.HSplitTop(20.0f, &TabBar, &TabBars); + TabBar.VSplitMid(&LeftTab, &RightTab); + + static CButtonContainer s_BasicTabButton; + if(DoButton_MenuTab(&s_BasicTabButton, Localize("Basic"), !m_CustomSkinMenu, &LeftTab, IGraphics::CORNER_L, nullptr, nullptr, nullptr, nullptr, 4.0f)) + { + m_CustomSkinMenu = false; + } + + static CButtonContainer s_CustomTabButton; + if(DoButton_MenuTab(&s_CustomTabButton, Localize("Custom"), m_CustomSkinMenu, &RightTab, IGraphics::CORNER_R, nullptr, nullptr, nullptr, nullptr, 4.0f)) + { + m_CustomSkinMenu = true; + if(m_CustomSkinMenu && m_pSelectedSkin) + { + if(m_pSelectedSkin->m_Flags & CSkins7::SKINFLAG_STANDARD) + { + m_SkinNameInput.Set("copy_"); + m_SkinNameInput.Append(m_pSelectedSkin->m_aName); + } + else + m_SkinNameInput.Set(m_pSelectedSkin->m_aName); + } + } + + // validate skin parts for solo mode + char aSkinParts[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_ARRAY_SIZE]; + char *apSkinPartsPtr[protocol7::NUM_SKINPARTS]; + int aUCCVars[protocol7::NUM_SKINPARTS]; + int aColorVars[protocol7::NUM_SKINPARTS]; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + str_copy(aSkinParts[Part], CSkins7::ms_apSkinVariables[(int)m_Dummy][Part], protocol7::MAX_SKIN_ARRAY_SIZE); + apSkinPartsPtr[Part] = aSkinParts[Part]; + aUCCVars[Part] = *CSkins7::ms_apUCCVariables[(int)m_Dummy][Part]; + aColorVars[Part] = *CSkins7::ms_apColorVariables[(int)m_Dummy][Part]; + } + m_pClient->m_Skins7.ValidateSkinParts(apSkinPartsPtr, aUCCVars, aColorVars, 0); + + CTeeRenderInfo OwnSkinInfo; + OwnSkinInfo.m_Size = 50.0f; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + int SkinPart = m_pClient->m_Skins7.FindSkinPart(Part, apSkinPartsPtr[Part], false); + const CSkins7::CSkinPart *pSkinPart = m_pClient->m_Skins7.GetSkinPart(Part, SkinPart); + if(aUCCVars[Part]) + { + OwnSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aTextures[Part] = pSkinPart->m_ColorTexture; + OwnSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aColors[Part] = m_pClient->m_Skins7.GetColor(aColorVars[Part], Part == protocol7::SKINPART_MARKING); + } + else + { + OwnSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aTextures[Part] = pSkinPart->m_OrgTexture; + OwnSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aColors[Part] = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + } + + char aBuf[128 + IO_MAX_PATH_LENGTH]; + str_format(aBuf, sizeof(aBuf), "%s:", Localize("Your skin")); + Ui()->DoLabel(&SkinPreview, aBuf, 14.0f, TEXTALIGN_ML); + + { + // interactive tee: tee looking towards cursor, and it is happy when you touch it + const vec2 TeePosition = NormalSkinPreview.Center() + vec2(0.0f, 6.0f); + const vec2 DeltaPosition = Ui()->MousePos() - TeePosition; + const float Distance = length(DeltaPosition); + const float InteractionDistance = 20.0f; + const vec2 TeeDirection = Distance < InteractionDistance ? normalize(vec2(DeltaPosition.x, maximum(DeltaPosition.y, 0.5f))) : normalize(DeltaPosition); + const int TeeEmote = Distance < InteractionDistance ? EMOTE_HAPPY : EMOTE_NORMAL; + RenderTools()->RenderTee(CAnimState::GetIdle(), &OwnSkinInfo, TeeEmote, TeeDirection, TeePosition); + static char s_InteractiveTeeButtonId; + if(Distance < InteractionDistance && Ui()->DoButtonLogic(&s_InteractiveTeeButtonId, 0, &NormalSkinPreview)) + { + m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_PLAYER_SPAWN, 1.0f); + } + } + + // validate skin parts for team game mode + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + str_copy(aSkinParts[Part], CSkins7::ms_apSkinVariables[(int)m_Dummy][Part], protocol7::MAX_SKIN_ARRAY_SIZE); + apSkinPartsPtr[Part] = aSkinParts[Part]; + aUCCVars[Part] = *CSkins7::ms_apUCCVariables[(int)m_Dummy][Part]; + aColorVars[Part] = *CSkins7::ms_apColorVariables[(int)m_Dummy][Part]; + } + m_pClient->m_Skins7.ValidateSkinParts(apSkinPartsPtr, aUCCVars, aColorVars, GAMEFLAG_TEAMS); + + CTeeRenderInfo TeamSkinInfo = OwnSkinInfo; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + int SkinPart = m_pClient->m_Skins7.FindSkinPart(Part, apSkinPartsPtr[Part], false); + const CSkins7::CSkinPart *pSkinPart = m_pClient->m_Skins7.GetSkinPart(Part, SkinPart); + if(aUCCVars[Part]) + { + TeamSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aTextures[Part] = pSkinPart->m_ColorTexture; + TeamSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aColors[Part] = m_pClient->m_Skins7.GetColor(aColorVars[Part], Part == protocol7::SKINPART_MARKING); + } + else + { + TeamSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aTextures[Part] = pSkinPart->m_OrgTexture; + TeamSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aColors[Part] = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + } + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + TeamSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aColors[Part] = m_pClient->m_Skins7.GetTeamColor(aUCCVars[Part], aColorVars[Part], TEAM_RED, Part); + } + RenderTools()->RenderTee(CAnimState::GetIdle(), &TeamSkinInfo, 0, vec2(1, 0), RedTeamSkinPreview.Center() + vec2(0.0f, 6.0f)); + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + TeamSkinInfo.m_aSixup[g_Config.m_ClDummy].m_aColors[Part] = m_pClient->m_Skins7.GetTeamColor(aUCCVars[Part], aColorVars[Part], TEAM_BLUE, Part); + } + RenderTools()->RenderTee(CAnimState::GetIdle(), &TeamSkinInfo, 0, vec2(-1, 0), BlueTeamSkinPreview.Center() + vec2(0.0f, 6.0f)); + + if(m_CustomSkinMenu) + RenderSettingsTeeCustom7(MainView); + else + RenderSkinSelection7(MainView); + + if(m_CustomSkinMenu) + { + static CButtonContainer s_CustomSkinSaveButton; + if(DoButton_Menu(&s_CustomSkinSaveButton, Localize("Save"), 0, &SaveDeleteButton)) + { + m_Popup = POPUP_SAVE_SKIN; + m_SkinNameInput.SelectAll(); + Ui()->SetActiveItem(&m_SkinNameInput); + } + } + else if(m_pSelectedSkin && (m_pSelectedSkin->m_Flags & CSkins7::SKINFLAG_STANDARD) == 0) + { + static CButtonContainer s_CustomSkinDeleteButton; + if(DoButton_Menu(&s_CustomSkinDeleteButton, Localize("Delete"), 0, &SaveDeleteButton) || Ui()->ConsumeHotkey(CUi::HOTKEY_DELETE)) + { + str_format(aBuf, sizeof(aBuf), Localize("Are you sure that you want to delete '%s'?"), m_pSelectedSkin->m_aName); + PopupConfirm(Localize("Delete skin"), aBuf, Localize("Yes"), Localize("No"), &CMenus::PopupConfirmDeleteSkin7); + } + } + + static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString)); + if(Ui()->DoEditBox_Search(&s_SkinFilterInput, &QuickSearch, 14.0f, !Ui()->IsPopupOpen() && m_pClient->m_GameConsole.IsClosed())) + { + m_SkinListNeedsUpdate = true; + m_SkinPartListNeedsUpdate = true; + } + + static CButtonContainer s_DirectoryButton; + if(DoButton_Menu(&s_DirectoryButton, Localize("Skins directory"), 0, &DirectoryButton)) + { + Storage()->GetCompletePath(IStorage::TYPE_SAVE, "skins7", aBuf, sizeof(aBuf)); + Storage()->CreateFolder("skins7", IStorage::TYPE_SAVE); + Client()->ViewFile(aBuf); + } + GameClient()->m_Tooltips.DoToolTip(&s_DirectoryButton, &DirectoryButton, Localize("Open the directory to add custom skins")); + + TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); + static CButtonContainer s_SkinRefreshButton; + if(DoButton_Menu(&s_SkinRefreshButton, FONT_ICON_ARROW_ROTATE_RIGHT, 0, &RefreshButton) || + (!Ui()->IsPopupOpen() && m_pClient->m_GameConsole.IsClosed() && (Input()->KeyPress(KEY_F5) || (Input()->ModifierIsPressed() && Input()->KeyPress(KEY_R))))) + { + // reset render flags for possible loading screen + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); + // TODO: m_pClient->RefreshSkins(); + } + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); +} + +void CMenus::PopupConfirmDeleteSkin7() +{ + dbg_assert(m_pSelectedSkin, "no skin selected for deletion"); + + if(!m_pClient->m_Skins7.RemoveSkin(m_pSelectedSkin)) + { + PopupMessage(Localize("Error"), Localize("Unable to delete skin"), Localize("Ok")); + return; + } + m_pSelectedSkin = nullptr; +} + +void CMenus::RenderSettingsTeeCustom7(CUIRect MainView) +{ + CUIRect ButtonBar, SkinPartSelection, CustomColors; + + MainView.HSplitTop(20.0f, &ButtonBar, &MainView); + MainView.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f), IGraphics::CORNER_B, 5.0f); + MainView.VSplitMid(&SkinPartSelection, &CustomColors, 10.0f); + CustomColors.Margin(5.0f, &CustomColors); + CUIRect CustomColorsButton, RandomSkinButton; + CustomColors.HSplitTop(20.0f, &CustomColorsButton, &CustomColors); + CustomColorsButton.VSplitRight(30.0f, &CustomColorsButton, &RandomSkinButton); + CustomColorsButton.VSplitRight(20.0f, &CustomColorsButton, nullptr); + + const float ButtonWidth = ButtonBar.w / protocol7::NUM_SKINPARTS; + + static CButtonContainer s_aSkinPartButtons[protocol7::NUM_SKINPARTS]; + for(int i = 0; i < protocol7::NUM_SKINPARTS; i++) + { + CUIRect Button; + ButtonBar.VSplitLeft(ButtonWidth, &Button, &ButtonBar); + const int Corners = i == 0 ? IGraphics::CORNER_TL : (i == (protocol7::NUM_SKINPARTS - 1) ? IGraphics::CORNER_TR : IGraphics::CORNER_NONE); + if(DoButton_MenuTab(&s_aSkinPartButtons[i], Localize(CSkins7::ms_apSkinPartNamesLocalized[i], "skins"), m_TeePartSelected == i, &Button, Corners, nullptr, nullptr, nullptr, nullptr, 4.0f)) + { + m_TeePartSelected = i; + } + } + + RenderSkinPartSelection7(SkinPartSelection); + + int *pUseCustomColor = CSkins7::ms_apUCCVariables[(int)m_Dummy][m_TeePartSelected]; + if(DoButton_CheckBox(pUseCustomColor, Localize("Custom colors"), *pUseCustomColor, &CustomColorsButton)) + { + *pUseCustomColor = !*pUseCustomColor; + SetNeedSendInfo(); + } + + if(*pUseCustomColor) + { + CUIRect CustomColorScrollbars; + CustomColors.HSplitTop(5.0f, nullptr, &CustomColors); + CustomColors.HSplitTop(95.0f, &CustomColorScrollbars, &CustomColors); + + if(RenderHslaScrollbars(&CustomColorScrollbars, CSkins7::ms_apColorVariables[(int)m_Dummy][m_TeePartSelected], m_TeePartSelected == protocol7::SKINPART_MARKING, ColorHSLA::DARKEST_LGT7)) + { + SetNeedSendInfo(); + } + } + + // Random skin button + static CButtonContainer s_RandomSkinButton; + static const char *s_apDice[] = {FONT_ICON_DICE_ONE, FONT_ICON_DICE_TWO, FONT_ICON_DICE_THREE, FONT_ICON_DICE_FOUR, FONT_ICON_DICE_FIVE, FONT_ICON_DICE_SIX}; + static int s_CurrentDie = rand() % std::size(s_apDice); + TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); + if(DoButton_Menu(&s_RandomSkinButton, s_apDice[s_CurrentDie], 0, &RandomSkinButton, nullptr, IGraphics::CORNER_ALL, 5.0f, -0.2f)) + { + m_pClient->m_Skins7.RandomizeSkin(m_Dummy); + SetNeedSendInfo(); + s_CurrentDie = rand() % std::size(s_apDice); + } + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); + GameClient()->m_Tooltips.DoToolTip(&s_RandomSkinButton, &RandomSkinButton, Localize("Create a random skin")); +} + +void CMenus::RenderSkinSelection7(CUIRect MainView) +{ + static float s_LastSelectionTime = -10.0f; + static std::vector s_vpSkinList; + static CListBox s_ListBox; + static int s_SkinCount = 0; + if(m_SkinListNeedsUpdate || m_pClient->m_Skins7.Num() != s_SkinCount) + { + s_vpSkinList.clear(); + s_SkinCount = m_pClient->m_Skins7.Num(); + for(int i = 0; i < s_SkinCount; ++i) + { + const CSkins7::CSkin *pSkin = m_pClient->m_Skins7.Get(i); + if(g_Config.m_ClSkinFilterString[0] != '\0' && !str_utf8_find_nocase(pSkin->m_aName, g_Config.m_ClSkinFilterString)) + continue; + + // no special skins + if((pSkin->m_Flags & CSkins7::SKINFLAG_SPECIAL) == 0) + { + s_vpSkinList.emplace_back(pSkin); + } + } + m_SkinListNeedsUpdate = false; + } + + m_pSelectedSkin = nullptr; + int s_OldSelected = -1; + s_ListBox.DoStart(50.0f, s_vpSkinList.size(), 4, 1, s_OldSelected, &MainView); + + for(int i = 0; i < (int)s_vpSkinList.size(); ++i) + { + const CSkins7::CSkin *pSkin = s_vpSkinList[i]; + if(pSkin == nullptr) + continue; + if(!str_comp(pSkin->m_aName, CSkins7::ms_apSkinNameVariables[m_Dummy])) + { + m_pSelectedSkin = pSkin; + s_OldSelected = i; + } + + const CListboxItem Item = s_ListBox.DoNextItem(&s_vpSkinList[i], s_OldSelected == i); + if(!Item.m_Visible) + continue; + + CUIRect TeePreview, Label; + Item.m_Rect.VSplitLeft(60.0f, &TeePreview, &Label); + + CTeeRenderInfo Info; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + if(pSkin->m_aUseCustomColors[Part]) + { + Info.m_aSixup[g_Config.m_ClDummy].m_aTextures[Part] = pSkin->m_apParts[Part]->m_ColorTexture; + Info.m_aSixup[g_Config.m_ClDummy].m_aColors[Part] = m_pClient->m_Skins7.GetColor(pSkin->m_aPartColors[Part], Part == protocol7::SKINPART_MARKING); + } + else + { + Info.m_aSixup[g_Config.m_ClDummy].m_aTextures[Part] = pSkin->m_apParts[Part]->m_OrgTexture; + Info.m_aSixup[g_Config.m_ClDummy].m_aColors[Part] = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + } + Info.m_Size = 50.0f; + + { + // interactive tee: tee is happy to be selected + int TeeEmote = (Item.m_Selected && s_LastSelectionTime + 0.75f > Client()->GlobalTime()) ? EMOTE_HAPPY : EMOTE_NORMAL; + RenderTools()->RenderTee(CAnimState::GetIdle(), &Info, TeeEmote, vec2(1.0f, 0.0f), TeePreview.Center() + vec2(0.0f, 6.0f)); + } + + SLabelProperties Props; + Props.m_MaxWidth = Label.w - 5.0f; + Ui()->DoLabel(&Label, pSkin->m_aName, 12.0f, TEXTALIGN_ML, Props); + } + + const int NewSelected = s_ListBox.DoEnd(); + if(NewSelected != -1 && NewSelected != s_OldSelected) + { + s_LastSelectionTime = Client()->GlobalTime(); + m_pSelectedSkin = s_vpSkinList[NewSelected]; + str_copy(CSkins7::ms_apSkinNameVariables[m_Dummy], m_pSelectedSkin->m_aName, protocol7::MAX_SKIN_ARRAY_SIZE); + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + str_copy(CSkins7::ms_apSkinVariables[(int)m_Dummy][Part], m_pSelectedSkin->m_apParts[Part]->m_aName, protocol7::MAX_SKIN_ARRAY_SIZE); + *CSkins7::ms_apUCCVariables[(int)m_Dummy][Part] = m_pSelectedSkin->m_aUseCustomColors[Part]; + *CSkins7::ms_apColorVariables[(int)m_Dummy][Part] = m_pSelectedSkin->m_aPartColors[Part]; + } + SetNeedSendInfo(); + } +} + +void CMenus::RenderSkinPartSelection7(CUIRect MainView) +{ + static std::vector s_paList[protocol7::NUM_SKINPARTS]; + static CListBox s_ListBox; + static int s_aSkinPartCount[protocol7::NUM_SKINPARTS] = {0}; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + if(m_SkinPartListNeedsUpdate || m_pClient->m_Skins7.NumSkinPart(Part) != s_aSkinPartCount[Part]) + { + s_paList[Part].clear(); + s_aSkinPartCount[Part] = m_pClient->m_Skins7.NumSkinPart(Part); + for(int i = 0; i < s_aSkinPartCount[Part]; ++i) + { + const CSkins7::CSkinPart *pPart = m_pClient->m_Skins7.GetSkinPart(Part, i); + if(g_Config.m_ClSkinFilterString[0] != '\0' && !str_utf8_find_nocase(pPart->m_aName, g_Config.m_ClSkinFilterString)) + continue; + + // no special skins + if((pPart->m_Flags & CSkins7::SKINFLAG_SPECIAL) == 0) + { + s_paList[Part].emplace_back(pPart); + } + } + } + } + m_SkinPartListNeedsUpdate = false; + + static int s_OldSelected = -1; + s_ListBox.DoBegin(&MainView); + s_ListBox.DoStart(72.0f, s_paList[m_TeePartSelected].size(), 4, 1, s_OldSelected, nullptr, false, IGraphics::CORNER_NONE, true); + + for(int i = 0; i < (int)s_paList[m_TeePartSelected].size(); ++i) + { + const CSkins7::CSkinPart *pPart = s_paList[m_TeePartSelected][i]; + if(pPart == nullptr) + continue; + if(!str_comp(pPart->m_aName, CSkins7::ms_apSkinVariables[(int)m_Dummy][m_TeePartSelected])) + s_OldSelected = i; + + CListboxItem Item = s_ListBox.DoNextItem(&s_paList[m_TeePartSelected][i], s_OldSelected == i); + if(!Item.m_Visible) + continue; + + CUIRect Label; + Item.m_Rect.Margin(5.0f, &Item.m_Rect); + Item.m_Rect.HSplitBottom(12.0f, &Item.m_Rect, &Label); + + CTeeRenderInfo Info; + for(int j = 0; j < protocol7::NUM_SKINPARTS; j++) + { + int SkinPart = m_pClient->m_Skins7.FindSkinPart(j, CSkins7::ms_apSkinVariables[(int)m_Dummy][j], false); + const CSkins7::CSkinPart *pSkinPart = m_pClient->m_Skins7.GetSkinPart(j, SkinPart); + if(*CSkins7::ms_apUCCVariables[(int)m_Dummy][j]) + { + Info.m_aSixup[g_Config.m_ClDummy].m_aTextures[j] = m_TeePartSelected == j ? pPart->m_ColorTexture : pSkinPart->m_ColorTexture; + Info.m_aSixup[g_Config.m_ClDummy].m_aColors[j] = m_pClient->m_Skins7.GetColor(*CSkins7::ms_apColorVariables[(int)m_Dummy][j], j == protocol7::SKINPART_MARKING); + } + else + { + Info.m_aSixup[g_Config.m_ClDummy].m_aTextures[j] = m_TeePartSelected == j ? pPart->m_OrgTexture : pSkinPart->m_OrgTexture; + Info.m_aSixup[0].m_aColors[j] = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + } + Info.m_Size = 50.0f; + + const vec2 TeePos = Item.m_Rect.Center() + vec2(0.0f, 6.0f); + if(m_TeePartSelected == protocol7::SKINPART_HANDS) + { + // RenderTools()->RenderTeeHand(&Info, TeePos, vec2(1.0f, 0.0f), -pi*0.5f, vec2(18, 0)); + } + int TeePartEmote = EMOTE_NORMAL; + if(m_TeePartSelected == protocol7::SKINPART_EYES) + { + TeePartEmote = (int)(Client()->GlobalTime() * 0.5f) % NUM_EMOTES; + } + RenderTools()->RenderTee(CAnimState::GetIdle(), &Info, TeePartEmote, vec2(1.0f, 0.0f), TeePos); + + Ui()->DoLabel(&Label, pPart->m_aName, 12.0f, TEXTALIGN_MC); + } + + const int NewSelected = s_ListBox.DoEnd(); + if(NewSelected != -1 && NewSelected != s_OldSelected) + { + str_copy(CSkins7::ms_apSkinVariables[(int)m_Dummy][m_TeePartSelected], s_paList[m_TeePartSelected][NewSelected]->m_aName, protocol7::MAX_SKIN_ARRAY_SIZE); + CSkins7::ms_apSkinNameVariables[m_Dummy][0] = '\0'; + SetNeedSendInfo(); + } + s_OldSelected = NewSelected; +} diff --git a/src/game/client/components/menus_settings_assets.cpp b/src/game/client/components/menus_settings_assets.cpp index e0ff5a4081..9d022bb841 100644 --- a/src/game/client/components/menus_settings_assets.cpp +++ b/src/game/client/components/menus_settings_assets.cpp @@ -352,30 +352,29 @@ int InitSearchList(std::vector &vpSearchList, std::vector void CMenus::RenderSettingsCustom(CUIRect MainView) { - CUIRect TabBar, CustomList, QuickSearch, QuickSearchClearButton, DirectoryButton, Page1Tab, Page2Tab, Page3Tab, Page4Tab, Page5Tab, Page6Tab, ReloadButton; - - MainView.HSplitTop(20, &TabBar, &MainView); - float TabsW = TabBar.w; - TabBar.VSplitLeft(TabsW / NUMBER_OF_ASSETS_TABS, &Page1Tab, &Page2Tab); - Page2Tab.VSplitLeft(TabsW / NUMBER_OF_ASSETS_TABS, &Page2Tab, &Page3Tab); - Page3Tab.VSplitLeft(TabsW / NUMBER_OF_ASSETS_TABS, &Page3Tab, &Page4Tab); - Page4Tab.VSplitLeft(TabsW / NUMBER_OF_ASSETS_TABS, &Page4Tab, &Page5Tab); - Page5Tab.VSplitLeft(TabsW / NUMBER_OF_ASSETS_TABS, &Page5Tab, &Page6Tab); + CUIRect TabBar, CustomList, QuickSearch, DirectoryButton, ReloadButton; + MainView.HSplitTop(20.0f, &TabBar, &MainView); + const float TabWidth = TabBar.w / NUMBER_OF_ASSETS_TABS; static CButtonContainer s_aPageTabs[NUMBER_OF_ASSETS_TABS] = {}; - - if(DoButton_MenuTab((CButtonContainer *)&s_aPageTabs[ASSETS_TAB_ENTITIES], Localize("Entities"), s_CurCustomTab == ASSETS_TAB_ENTITIES, &Page1Tab, IGraphics::CORNER_L, NULL, NULL, NULL, NULL, 4)) - s_CurCustomTab = ASSETS_TAB_ENTITIES; - if(DoButton_MenuTab((CButtonContainer *)&s_aPageTabs[ASSETS_TAB_GAME], Localize("Game"), s_CurCustomTab == ASSETS_TAB_GAME, &Page2Tab, 0, NULL, NULL, NULL, NULL, 4)) - s_CurCustomTab = ASSETS_TAB_GAME; - if(DoButton_MenuTab((CButtonContainer *)&s_aPageTabs[ASSETS_TAB_EMOTICONS], Localize("Emoticons"), s_CurCustomTab == ASSETS_TAB_EMOTICONS, &Page3Tab, 0, NULL, NULL, NULL, NULL, 4)) - s_CurCustomTab = ASSETS_TAB_EMOTICONS; - if(DoButton_MenuTab((CButtonContainer *)&s_aPageTabs[ASSETS_TAB_PARTICLES], Localize("Particles"), s_CurCustomTab == ASSETS_TAB_PARTICLES, &Page4Tab, 0, NULL, NULL, NULL, NULL, 4)) - s_CurCustomTab = ASSETS_TAB_PARTICLES; - if(DoButton_MenuTab((CButtonContainer *)&s_aPageTabs[ASSETS_TAB_HUD], Localize("HUD"), s_CurCustomTab == ASSETS_TAB_HUD, &Page5Tab, 0, NULL, NULL, NULL, NULL, 4)) - s_CurCustomTab = ASSETS_TAB_HUD; - if(DoButton_MenuTab((CButtonContainer *)&s_aPageTabs[ASSETS_TAB_EXTRAS], Localize("Extras"), s_CurCustomTab == ASSETS_TAB_EXTRAS, &Page6Tab, IGraphics::CORNER_R, NULL, NULL, NULL, NULL, 4)) - s_CurCustomTab = ASSETS_TAB_EXTRAS; + const char *apTabNames[NUMBER_OF_ASSETS_TABS] = { + Localize("Entities"), + Localize("Game"), + Localize("Emoticons"), + Localize("Particles"), + Localize("HUD"), + Localize("Extras")}; + + for(int Tab = ASSETS_TAB_ENTITIES; Tab < NUMBER_OF_ASSETS_TABS; ++Tab) + { + CUIRect Button; + TabBar.VSplitLeft(TabWidth, &Button, &TabBar); + const int Corners = Tab == ASSETS_TAB_ENTITIES ? IGraphics::CORNER_L : Tab == NUMBER_OF_ASSETS_TABS - 1 ? IGraphics::CORNER_R : IGraphics::CORNER_NONE; + if(DoButton_MenuTab(&s_aPageTabs[Tab], apTabNames[Tab], s_CurCustomTab == Tab, &Button, Corners, nullptr, nullptr, nullptr, nullptr, 4.0f)) + { + s_CurCustomTab = Tab; + } + } auto LoadStartTime = time_get_nanoseconds(); SMenuAssetScanUser User; @@ -548,7 +547,7 @@ void CMenus::RenderSettingsCustom(CUIRect MainView) CUIRect TextureRect; ItemRect.HSplitTop(15, &ItemRect, &TextureRect); TextureRect.HSplitTop(10, NULL, &TextureRect); - UI()->DoLabel(&ItemRect, pItem->m_aName, ItemRect.h - 2, TEXTALIGN_MC); + Ui()->DoLabel(&ItemRect, pItem->m_aName, ItemRect.h - 2, TEXTALIGN_MC); if(pItem->m_RenderTexture.IsValid()) { Graphics()->WrapClamp(); @@ -600,37 +599,21 @@ void CMenus::RenderSettingsCustom(CUIRect MainView) } } - // render quick search + // Quick search + MainView.HSplitBottom(ms_ButtonHeight, &MainView, &QuickSearch); + QuickSearch.VSplitLeft(220.0f, &QuickSearch, &DirectoryButton); + QuickSearch.HSplitTop(5.0f, nullptr, &QuickSearch); + if(Ui()->DoEditBox_Search(&s_aFilterInputs[s_CurCustomTab], &QuickSearch, 14.0f, !Ui()->IsPopupOpen() && m_pClient->m_GameConsole.IsClosed())) { - MainView.HSplitBottom(ms_ButtonHeight, &MainView, &QuickSearch); - QuickSearch.VSplitLeft(240.0f, &QuickSearch, &DirectoryButton); - QuickSearch.HSplitTop(5.0f, 0, &QuickSearch); - TextRender()->SetFontPreset(EFontPreset::ICON_FONT); - TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - - UI()->DoLabel(&QuickSearch, FONT_ICON_MAGNIFYING_GLASS, 14.0f, TEXTALIGN_ML); - float wSearch = TextRender()->TextWidth(14.0f, FONT_ICON_MAGNIFYING_GLASS, -1, -1.0f); - TextRender()->SetRenderFlags(0); - TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - QuickSearch.VSplitLeft(wSearch, 0, &QuickSearch); - QuickSearch.VSplitLeft(5.0f, 0, &QuickSearch); - QuickSearch.VSplitLeft(QuickSearch.w - 15.0f, &QuickSearch, &QuickSearchClearButton); - if(Input()->KeyPress(KEY_F) && Input()->ModifierIsPressed()) - { - UI()->SetActiveItem(&s_aFilterInputs[s_CurCustomTab]); - s_aFilterInputs[s_CurCustomTab].SelectAll(); - } - s_aFilterInputs[s_CurCustomTab].SetEmptyText(Localize("Search")); - if(UI()->DoClearableEditBox(&s_aFilterInputs[s_CurCustomTab], &QuickSearch, 14.0f)) - gs_aInitCustomList[s_CurCustomTab] = true; + gs_aInitCustomList[s_CurCustomTab] = true; } DirectoryButton.HSplitTop(5.0f, 0, &DirectoryButton); DirectoryButton.VSplitRight(175.0f, 0, &DirectoryButton); DirectoryButton.VSplitRight(25.0f, &DirectoryButton, &ReloadButton); DirectoryButton.VSplitRight(10.0f, &DirectoryButton, 0); - static CButtonContainer s_AssetsDirID; - if(DoButton_Menu(&s_AssetsDirID, Localize("Assets directory"), 0, &DirectoryButton)) + static CButtonContainer s_AssetsDirId; + if(DoButton_Menu(&s_AssetsDirId, Localize("Assets directory"), 0, &DirectoryButton)) { char aBuf[IO_MAX_PATH_LENGTH]; char aBufFull[IO_MAX_PATH_LENGTH + 7]; @@ -649,17 +632,14 @@ void CMenus::RenderSettingsCustom(CUIRect MainView) Storage()->GetCompletePath(IStorage::TYPE_SAVE, aBufFull, aBuf, sizeof(aBuf)); Storage()->CreateFolder("assets", IStorage::TYPE_SAVE); Storage()->CreateFolder(aBufFull, IStorage::TYPE_SAVE); - if(!open_file(aBuf)) - { - dbg_msg("menus", "couldn't open file '%s'", aBuf); - } + Client()->ViewFile(aBuf); } - GameClient()->m_Tooltips.DoToolTip(&s_AssetsDirID, &DirectoryButton, Localize("Open the directory to add custom assets")); + GameClient()->m_Tooltips.DoToolTip(&s_AssetsDirId, &DirectoryButton, Localize("Open the directory to add custom assets")); TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - static CButtonContainer s_AssetsReloadBtnID; - if(DoButton_Menu(&s_AssetsReloadBtnID, FONT_ICON_ARROW_ROTATE_RIGHT, 0, &ReloadButton) || Input()->KeyPress(KEY_F5) || (Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed())) + static CButtonContainer s_AssetsReloadBtnId; + if(DoButton_Menu(&s_AssetsReloadBtnId, FONT_ICON_ARROW_ROTATE_RIGHT, 0, &ReloadButton) || Input()->KeyPress(KEY_F5) || (Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed())) { ClearCustomItems(s_CurCustomTab); } diff --git a/src/game/client/components/menus_start.cpp b/src/game/client/components/menus_start.cpp index c871c4229c..de7520404b 100644 --- a/src/game/client/components/menus_start.cpp +++ b/src/game/client/components/menus_start.cpp @@ -16,157 +16,21 @@ #include #include "menus.h" -#include -#include -#include // For rand() and srand() -#include // For time() - -std::vector quotes = { - "Client was made by -StormAx", - "don't use S-Client lol xD", - "Aslo try S-Client", - "Cook some maps today >:D", - "Hi!", - "Bye;c", - "You go gaming?", - "-StormAxD", - "Inner peace clan on top, isn't it?", - "The cake is lie :c", - "AH GG!!!!", - "YEPYEPYEPYEPYEP", - "My discord is 'stormaxd' go on!", - "Cheeru?", - "Cheeru!", - "Cheeru!?", - "Cheeru?!", - "The gamer is here!:D", - "OwO", - "UWU", - "QWQ", - "!ban 0.0.0.0-255.255.255.255", - "Me gamer :0, Me go hook random tees", - "PDGADHHAHDHAHAHADADAAAHAHAHAHAH", - "What is your best Grandma time? mine is 2:21:43", - "Со мной воюет сатана!!!!!!!", - "Never gonna give you.... forget it", - "AWwwwwww, you so cutie >.<", - "WhAt ThE...?", - "Kurw........Amach", - "Suka blyad'", - "Paxtell...", - "Number one roblox player", - "AYAYAAAAAA, ayakaaaaa", - "liga a camera ae!", - "Я рот ебал ваших ДДнетов", - "Я щас на атомы разорвусь", - "caos e regresso", - "Do HH right broh!", - "ПК это печь керамическая", - "melo melo melooon", - ":gigachad:", - "-Dummygod was here", - "PL is not poland, is PlantKnight", - "Im so in love with ddnet code c:", - "Loving Ayako", - "deen>all!!!", - "Blocker:C", - "Bro i love this game so much", - "testtext", - "220km/h :wheelchair:", - "YEP CLAN on top;p", - "no sex ;c", - "still no sex ;c", - "and still no sex ;c", - "pepega xD", - "Go play Luxis", - "Go play Stronghold 4", - "AOE is not a YT guy, its splash DMG", - "VAMO VER QM DA MAIS O RABO, PRA VER SE EU NUM GANHO!", - "muhehehehe", - "10/10", - "10010101010ERROR11010101010101010101011011010110ERROR101010011", - "voulez-vous couchez avec moi?!?", - "BlueMeww, Bluee... BlueMewing!... F*CK", - "ronyan'", - "W's in the shhhhhaaata :screamin':", - "W's in the shhhhhaaat :screamin':", - "SpinbrosTV... you'r onto something, you'r onto something man", - "Also try M-Client v3 :D", - "Choo-Choo", - "Hi Atsuko!", - "pinuuuuuuuu!", - "n9", - "gamer", - "Inner Sillyness", - "Im am the storm that is approaching!", - "Dev love Lukov so much<3", - "DDnet moderator with most bans - IZA 12k bans", - "Mewing insead of Meowing", - "Prepare to suffer", - "Did you saw ddnet describtion in Steam?", - "allleeeeeee, hopa!", - "std::string GetRandomQuote()", - "there's 0.0001% to get special rainbow text, gl^^", - "https://open.spotify.com/track/1SQDvOrbSykg0lP5y7EQ8o?si=a002ae00a4eb4935", - "Don't use M-Client V3", - -}; - -std::vector rares = { - "no fucking way you read this..." -}; - - -// Function to get a random quote -std::string GetRandomQuote() { - // Seed the random number generator - std::srand(static_cast(std::time(nullptr))); - - // Generate a random number between 0 and 9999 - int randomNum = std::rand() % 10000; - - // Check if the random number falls in the 0.01% range - if (randomNum < 1) { - return rares[0]; // Return the super rare quote - } else { - // Otherwise, return a regular quote - int index = std::rand() % quotes.size(); - return quotes[index]; - } -} + +using namespace FontIcons; void CMenus::RenderStartMenu(CUIRect MainView) { - // Render logo + GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_START); + + // render logo Graphics()->TextureSet(g_pData->m_aImages[IMAGE_BANNER].m_Id); Graphics()->QuadsBegin(); Graphics()->SetColor(1, 1, 1, 1); - IGraphics::CQuadItem QuadItem(MainView.w / 2 - 210, 50, 490, 103); + IGraphics::CQuadItem QuadItem(MainView.w / 2 - 170, 60, 360, 103); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); - // Get a random quote when the game starts - static bool firstTime = true; - static std::string randomQuote; - - if (firstTime) { - srand(::time(nullptr)); // Seed the random number generator only once - randomQuote = GetRandomQuote(); - firstTime = false; - } - - - - const float Rounding = 10.0f; const float VMargin = MainView.w / 2 - 190.0f; @@ -181,11 +45,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) static CButtonContainer s_DiscordButton; if(DoButton_Menu(&s_DiscordButton, Localize("Discord"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f))) { - const char *pLink = Localize("https://ddnet.org/discord"); - if(!open_link(pLink)) - { - dbg_msg("menus", "couldn't open link '%s'", pLink); - } + Client()->ViewLink(Localize("https://ddnet.org/discord")); } ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space @@ -193,11 +53,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) static CButtonContainer s_LearnButton; if(DoButton_Menu(&s_LearnButton, Localize("Learn"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f))) { - const char *pLink = Localize("https://wiki.ddnet.org/"); - if(!open_link(pLink)) - { - dbg_msg("menus", "couldn't open link '%s'", pLink); - } + Client()->ViewLink(Localize("https://wiki.ddnet.org/")); } ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space @@ -207,6 +63,10 @@ void CMenus::RenderStartMenu(CUIRect MainView) if(DoButton_Menu(&s_TutorialButton, Localize("Tutorial"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (s_JoinTutorialTime != 0.0f && Client()->LocalTime() >= s_JoinTutorialTime)) { + // Activate internet tab before joining tutorial to make sure the server info + // for the tutorial servers is available. + SetMenuPage(PAGE_INTERNET); + RefreshBrowserTab(true); const char *pAddr = ServerBrowser()->GetTutorialServer(); if(pAddr) { @@ -230,11 +90,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) static CButtonContainer s_WebsiteButton; if(DoButton_Menu(&s_WebsiteButton, Localize("Website"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f))) { - const char *pLink = "https://ddnet.org/"; - if(!open_link(pLink)) - { - dbg_msg("menus", "couldn't open link '%s'", pLink); - } + Client()->ViewLink("https://ddnet.org/"); } ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space @@ -244,16 +100,13 @@ void CMenus::RenderStartMenu(CUIRect MainView) NewPage = PAGE_NEWS; CUIRect Menu; - MainView.VMargin(VMargin, &Menu); - Menu.HSplitBottom(15.0f, &Menu, 0); + Menu.HSplitBottom(25.0f, &Menu, 0); - MainView.VSplitLeft(MainView.w / 2 + 115, &Menu, nullptr); - Menu.VSplitLeft(Menu.w / 1.45, nullptr, &Menu); Menu.HSplitBottom(40.0f, &Menu, &Button); static CButtonContainer s_QuitButton; bool UsedEscape = false; - if(DoButton_Menu(&s_QuitButton, Localize("Quit"), 0, &Button, 0, IGraphics::CORNER_ALL, Rounding, 0.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (UsedEscape = UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) || CheckHotKey(KEY_Q)) + if(DoButton_Menu(&s_QuitButton, Localize("Quit"), 0, &Button, 0, IGraphics::CORNER_ALL, Rounding, 0.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (UsedEscape = Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) || CheckHotKey(KEY_Q)) { if(UsedEscape || m_pClient->Editor()->HasUnsavedData() || (Client()->GetCurrentRaceTime() / 60 >= g_Config.m_ClConfirmQuitTime && g_Config.m_ClConfirmQuitTime >= 0)) { @@ -264,23 +117,22 @@ void CMenus::RenderStartMenu(CUIRect MainView) Client()->Quit(); } } - //Main buttons Menu.HSplitBottom(100.0f, &Menu, 0); - Menu.HSplitBottom(55.0f, &Menu, &Button); + Menu.HSplitBottom(40.0f, &Menu, &Button); static CButtonContainer s_SettingsButton; - if(DoButton_Menu(&s_SettingsButton, Localize(""), 0, &Button, g_Config.m_ClShowStartMenuImages ? "settings" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(KEY_S)) + if(DoButton_Menu(&s_SettingsButton, Localize("Settings"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "settings" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(KEY_S)) NewPage = PAGE_SETTINGS; +#if !defined(CONF_PLATFORM_ANDROID) Menu.HSplitBottom(5.0f, &Menu, 0); // little space - Menu.HSplitBottom(55.0f, &Menu, &Button); + Menu.HSplitBottom(40.0f, &Menu, &Button); static CButtonContainer s_LocalServerButton; - if(!is_process_alive(m_ServerProcess.m_Process)) KillServer(); -if(DoButton_Menu(&s_LocalServerButton, Localize(""), 0, &Button, g_Config.m_ClShowStartMenuImages? "local_server" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, m_ServerProcess.m_Process? ColorRGBA(0.0f, 1.0f, 0.0f, 0.25f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (CheckHotKey(KEY_R) && Input()->KeyPress(KEY_R))) + if(DoButton_Menu(&s_LocalServerButton, m_ServerProcess.m_Process ? Localize("Stop server") : Localize("Run server"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "local_server" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, m_ServerProcess.m_Process ? ColorRGBA(0.0f, 1.0f, 0.0f, 0.25f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (CheckHotKey(KEY_R) && Input()->KeyPress(KEY_R))) { if(m_ServerProcess.m_Process) { @@ -291,13 +143,10 @@ if(DoButton_Menu(&s_LocalServerButton, Localize(""), 0, &Button, g_Config.m_ClSh char aBuf[IO_MAX_PATH_LENGTH]; Storage()->GetBinaryPath(PLAT_SERVER_EXEC, aBuf, sizeof(aBuf)); // No / in binary path means to search in $PATH, so it is expected that the file can't be opened. Just try executing anyway. - if(str_find(aBuf, "/") == 0) + if(str_find(aBuf, "/") == 0 || fs_is_file(aBuf)) { - m_ServerProcess.m_Process = shell_execute(aBuf); - } - else if(fs_is_file(aBuf)) - { - m_ServerProcess.m_Process = shell_execute(aBuf); + m_ServerProcess.m_Process = shell_execute(aBuf, EShellExecuteWindowState::BACKGROUND); + m_ForceRefreshLanPage = true; } else { @@ -305,98 +154,91 @@ if(DoButton_Menu(&s_LocalServerButton, Localize(""), 0, &Button, g_Config.m_ClSh } } } +#endif - static bool EditorHotkeyWasPressed = true; - static float EditorHotKeyChecktime = 0.0f; Menu.HSplitBottom(5.0f, &Menu, 0); // little space - Menu.HSplitBottom(55.0f, &Menu, &Button); + Menu.HSplitBottom(40.0f, &Menu, &Button); static CButtonContainer s_MapEditorButton; - if(DoButton_Menu(&s_MapEditorButton, Localize(""), 0, &Button, g_Config.m_ClShowStartMenuImages ? "editor" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, m_pClient->Editor()->HasUnsavedData() ? ColorRGBA(0.0f, 1.0f, 0.0f, 0.25f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (!EditorHotkeyWasPressed && Client()->LocalTime() - EditorHotKeyChecktime < 0.1f && CheckHotKey(KEY_E))) + if(DoButton_Menu(&s_MapEditorButton, Localize("Editor"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "editor" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, m_pClient->Editor()->HasUnsavedData() ? ColorRGBA(0.0f, 1.0f, 0.0f, 0.25f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || (!m_EditorHotkeyWasPressed && Client()->LocalTime() - m_EditorHotKeyChecktime < 0.1f && CheckHotKey(KEY_E))) { g_Config.m_ClEditor = 1; Input()->MouseModeRelative(); - EditorHotkeyWasPressed = true; + m_EditorHotkeyWasPressed = true; } if(!Input()->KeyIsPressed(KEY_E)) { - EditorHotkeyWasPressed = false; - EditorHotKeyChecktime = Client()->LocalTime(); + m_EditorHotkeyWasPressed = false; + m_EditorHotKeyChecktime = Client()->LocalTime(); } Menu.HSplitBottom(5.0f, &Menu, 0); // little space - Menu.HSplitBottom(55.0f, &Menu, &Button); + Menu.HSplitBottom(40.0f, &Menu, &Button); static CButtonContainer s_DemoButton; - if(DoButton_Menu(&s_DemoButton, Localize(""), 0, &Button, g_Config.m_ClShowStartMenuImages ? "demos" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(KEY_D)) + if(DoButton_Menu(&s_DemoButton, Localize("Demos"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "demos" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(KEY_D)) { NewPage = PAGE_DEMOS; } Menu.HSplitBottom(5.0f, &Menu, 0); // little space - Menu.HSplitBottom(55.0f, &Menu, &Button); + Menu.HSplitBottom(40.0f, &Menu, &Button); static CButtonContainer s_PlayButton; - if(DoButton_Menu(&s_PlayButton, Localize("", "Start menu"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "play_game" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || CheckHotKey(KEY_P)) + if(DoButton_Menu(&s_PlayButton, Localize("Play", "Start menu"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "play_game" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER) || CheckHotKey(KEY_P)) { - NewPage = g_Config.m_UiPage >= PAGE_INTERNET && g_Config.m_UiPage <= PAGE_FAVORITES ? g_Config.m_UiPage : PAGE_INTERNET; + NewPage = g_Config.m_UiPage >= PAGE_INTERNET && g_Config.m_UiPage <= PAGE_FAVORITE_COMMUNITY_5 ? g_Config.m_UiPage : PAGE_INTERNET; } // render version - CUIRect VersionUpdate, CurVersion; - MainView.HSplitBottom(30.0f, 0, 0); - MainView.HSplitBottom(55.0f, 0, &VersionUpdate); + CUIRect CurVersion, ConsoleButton; + MainView.HSplitBottom(45.0f, nullptr, &CurVersion); + CurVersion.VSplitRight(40.0f, &CurVersion, nullptr); + CurVersion.HSplitTop(20.0f, &ConsoleButton, &CurVersion); + CurVersion.HSplitTop(5.0f, nullptr, &CurVersion); + ConsoleButton.VSplitRight(40.0f, nullptr, &ConsoleButton); + Ui()->DoLabel(&CurVersion, GAME_RELEASE_VERSION, 14.0f, TEXTALIGN_MR); + + static CButtonContainer s_ConsoleButton; + TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); + if(DoButton_Menu(&s_ConsoleButton, FONT_ICON_TERMINAL, 0, &ConsoleButton, nullptr, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.1f))) + { + GameClient()->m_GameConsole.Toggle(CGameConsole::CONSOLETYPE_LOCAL); + } + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - VersionUpdate.VSplitRight(50.0f, &CurVersion, 0); + CUIRect VersionUpdate; + MainView.HSplitBottom(20.0f, nullptr, &VersionUpdate); VersionUpdate.VMargin(VMargin, &VersionUpdate); +#if defined(CONF_AUTOUPDATE) + CUIRect UpdateButton; + VersionUpdate.VSplitRight(100.0f, &VersionUpdate, &UpdateButton); + VersionUpdate.VSplitRight(10.0f, &VersionUpdate, nullptr); - CUIRect RandomText; - RandomText.x = 605.0f; - RandomText.y = 130.0f; - RandomText.w = 330.0f; - RandomText.h = 60.0f; - static bool Quote = false; + char aBuf[128]; + const IUpdater::EUpdaterState State = Updater()->GetCurrentState(); + const bool NeedUpdate = str_comp(Client()->LatestVersion(), "0"); - if (!Quote) + if(State == IUpdater::CLEAN && NeedUpdate) { - Quote = true; - dbg_msg("Quotes", "%s", randomQuote.c_str()); + static CButtonContainer s_VersionUpdate; + if(DoButton_Menu(&s_VersionUpdate, Localize("Update now"), 0, &UpdateButton, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f))) + { + Updater()->InitiateUpdate(); + } } - - if(!str_comp(randomQuote.c_str(), "no fucking way you read this...")) + else if(State == IUpdater::NEED_RESTART) { - ColorRGBA color = color_cast(ColorHSVA(round_to_int(LocalTime() * 25) % 255 / 255.f, 1.f, 1.f)); - color.a = 0.9f; - TextRender()->TextColor(color); - - UI()->DoLabel(&RandomText, "no fucking way you read this...", 20.0f, TEXTALIGN_ML); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + static CButtonContainer s_VersionUpdate; + if(DoButton_Menu(&s_VersionUpdate, Localize("Restart"), 0, &UpdateButton, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f))) + { + Client()->Restart(); + } } - else + else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART) { - TextRender()->TextColor(1.0f, 1.0f, 0.0f, 1.0f); - - - float animationFactor = sinf(LocalTime() * 10); - - float animationFactorSmooth = animationFactor * animationFactor; - - float baseSizeMin = 18.0f; - float baseSizeMax = 20.0f; - - float scale = 0.5f; - - float newSize = baseSizeMin + (baseSizeMax - baseSizeMin) * (0.5f + scale * animationFactorSmooth); - - TextRender()->Text(605.0f, 130.0f, newSize, randomQuote.c_str()); - - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - + Ui()->RenderProgressBar(UpdateButton, Updater()->GetCurrentPercent() / 100.0f); } -#if defined(CONF_AUTOUPDATE) - - char aBuf[64]; - CUIRect Part; - int State = Updater()->GetCurrentState(); - bool NeedUpdate = str_comp(Client()->LatestVersion(), "0"); if(State == IUpdater::CLEAN && NeedUpdate) { str_format(aBuf, sizeof(aBuf), Localize("DDNet %s is out!"), Client()->LatestVersion()); @@ -414,81 +256,43 @@ if(DoButton_Menu(&s_LocalServerButton, Localize(""), 0, &Button, g_Config.m_ClSh } else if(State == IUpdater::FAIL) { - str_format(aBuf, sizeof(aBuf), Localize("Update failed! Check log...")); + str_copy(aBuf, Localize("Update failed! Check log…")); TextRender()->TextColor(1.0f, 0.4f, 0.4f, 1.0f); } else if(State == IUpdater::NEED_RESTART) { - str_format(aBuf, sizeof(aBuf), Localize("DDNet Client updated!")); + str_copy(aBuf, Localize("DDNet Client updated!")); TextRender()->TextColor(1.0f, 0.4f, 0.4f, 1.0f); } - UI()->DoLabel(&VersionUpdate, aBuf, 14.0f, TEXTALIGN_ML); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - - VersionUpdate.VSplitLeft(TextRender()->TextWidth(14.0f, aBuf, -1, -1.0f) + 10.0f, 0, &Part); - - if(State == IUpdater::CLEAN && NeedUpdate) - { - CUIRect Update; - Part.VSplitLeft(100.0f, &Update, NULL); - - static CButtonContainer s_VersionUpdate; - if(DoButton_Menu(&s_VersionUpdate, Localize("Update now"), 0, &Update, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f))) - { - Updater()->InitiateUpdate(); - } - } - else if(State == IUpdater::NEED_RESTART) - { - CUIRect Restart; - Part.VSplitLeft(50.0f, &Restart, &Part); - - static CButtonContainer s_VersionUpdate; - if(DoButton_Menu(&s_VersionUpdate, Localize("Restart"), 0, &Restart, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f))) - { - Client()->Restart(); - } - } - else if(State >= IUpdater::GETTING_MANIFEST && State < IUpdater::NEED_RESTART) - { - CUIRect ProgressBar, Percent; - Part.VSplitLeft(100.0f, &ProgressBar, &Percent); - ProgressBar.y += 2.0f; - ProgressBar.HMargin(1.0f, &ProgressBar); - ProgressBar.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 5.0f); - ProgressBar.w = clamp((float)Updater()->GetCurrentPercent(), 10.0f, 100.0f); - ProgressBar.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), IGraphics::CORNER_ALL, 5.0f); - } - /* + Ui()->DoLabel(&VersionUpdate, aBuf, 14.0f, TEXTALIGN_ML); + TextRender()->TextColor(TextRender()->DefaultTextColor()); #elif defined(CONF_INFORM_UPDATE) if(str_comp(Client()->LatestVersion(), "0") != 0) { char aBuf[64]; str_format(aBuf, sizeof(aBuf), Localize("DDNet %s is out!"), Client()->LatestVersion()); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - UI()->DoLabel(&VersionUpdate, aBuf, 14.0f, TEXTALIGN_MC); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + Ui()->DoLabel(&VersionUpdate, aBuf, 14.0f, TEXTALIGN_MC); } -*/ #endif - UI()->DoLabel(&CurVersion, GAME_RELEASE_VERSION, 14.0f, TEXTALIGN_MR); - if(NewPage != -1) { - m_MenuPage = NewPage; m_ShowStart = false; + SetMenuPage(NewPage); } - } void CMenus::KillServer() { +#if !defined(CONF_PLATFORM_ANDROID) if(m_ServerProcess.m_Process) { if(kill_process(m_ServerProcess.m_Process)) { m_ServerProcess.m_Process = INVALID_PROCESS; + m_ForceRefreshLanPage = true; } } +#endif } diff --git a/src/game/client/components/motd.cpp b/src/game/client/components/motd.cpp index 3e7f9a9fe5..2d90e33f7b 100644 --- a/src/game/client/components/motd.cpp +++ b/src/game/client/components/motd.cpp @@ -43,6 +43,9 @@ void CMotd::OnWindowResize() void CMotd::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if(!IsActive()) return; diff --git a/src/game/client/components/motd.h b/src/game/client/components/motd.h index 3971097c7c..cd110b2dcf 100644 --- a/src/game/client/components/motd.h +++ b/src/game/client/components/motd.h @@ -4,6 +4,7 @@ #define GAME_CLIENT_COMPONENTS_MOTD_H #include +#include #include diff --git a/src/game/client/components/nameplates.cpp b/src/game/client/components/nameplates.cpp index d6012e36bf..59366afe51 100644 --- a/src/game/client/components/nameplates.cpp +++ b/src/game/client/components/nameplates.cpp @@ -14,30 +14,14 @@ #include "controls.h" #include "nameplates.h" -void CNamePlates::RenderNameplate( - const CNetObj_Character *pPrevChar, - const CNetObj_Character *pPlayerChar, - const CNetObj_PlayerInfo *pPlayerInfo) +void CNamePlates::RenderNameplate(vec2 Position, const CNetObj_PlayerInfo *pPlayerInfo, float Alpha, bool ForceAlpha) { - int ClientID = pPlayerInfo->m_ClientID; + SPlayerNamePlate &NamePlate = m_aNamePlates[pPlayerInfo->m_ClientId]; + const auto &ClientData = m_pClient->m_aClients[pPlayerInfo->m_ClientId]; + const bool OtherTeam = m_pClient->IsOtherTeam(pPlayerInfo->m_ClientId); - vec2 Position; - if(ClientID >= 0 && ClientID < MAX_CLIENTS) - Position = m_pClient->m_aClients[ClientID].m_RenderPos; - else - Position = mix(vec2(pPrevChar->m_X, pPrevChar->m_Y), vec2(pPlayerChar->m_X, pPlayerChar->m_Y), Client()->IntraGameTick(g_Config.m_ClDummy)); - - RenderNameplatePos(Position, pPlayerInfo, 1.0f); -} - -void CNamePlates::RenderNameplatePos(vec2 Position, const CNetObj_PlayerInfo *pPlayerInfo, float Alpha, bool ForceAlpha) -{ - int ClientID = pPlayerInfo->m_ClientID; - - bool OtherTeam = m_pClient->IsOtherTeam(ClientID); - - float FontSize = 18.0f + 20.0f * g_Config.m_ClNameplatesSize / 100.0f; - float FontSizeClan = 18.0f + 20.0f * g_Config.m_ClNameplatesClanSize / 100.0f; + const float FontSize = 18.0f + 20.0f * g_Config.m_ClNameplatesSize / 100.0f; + const float FontSizeClan = 18.0f + 20.0f * g_Config.m_ClNameplatesClanSize / 100.0f; TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_NO_FIRST_CHARACTER_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_LAST_CHARACTER_ADVANCE); float YOffset = Position.y - 38; @@ -49,31 +33,41 @@ void CNamePlates::RenderNameplatePos(vec2 Position, const CNetObj_PlayerInfo *pP if(IVideo::Current()) ShowDirection = g_Config.m_ClVideoShowDirection; #endif - if((ShowDirection && ShowDirection != 3 && !pPlayerInfo->m_Local) || (ShowDirection >= 2 && pPlayerInfo->m_Local) || (ShowDirection == 3 && Client()->DummyConnected() && Client()->State() != IClient::STATE_DEMOPLAYBACK && ClientID == m_pClient->m_aLocalIDs[!g_Config.m_ClDummy])) + if((ShowDirection && ShowDirection != 3 && !pPlayerInfo->m_Local) || (ShowDirection >= 2 && pPlayerInfo->m_Local) || (ShowDirection == 3 && Client()->DummyConnected() && Client()->State() != IClient::STATE_DEMOPLAYBACK && pPlayerInfo->m_ClientId == m_pClient->m_aLocalIds[!g_Config.m_ClDummy])) { - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); - Graphics()->QuadsSetRotation(0); - - const float ShowDirectionImgSize = 22.0f; - YOffset -= ShowDirectionImgSize; - vec2 ShowDirectionPos = vec2(Position.x - 11.0f, YOffset); - - bool DirLeft = m_pClient->m_Snap.m_aCharacters[pPlayerInfo->m_ClientID].m_Cur.m_Direction == -1; - bool DirRight = m_pClient->m_Snap.m_aCharacters[pPlayerInfo->m_ClientID].m_Cur.m_Direction == 1; - bool Jump = m_pClient->m_Snap.m_aCharacters[pPlayerInfo->m_ClientID].m_Cur.m_Jumped & 1; - - if(pPlayerInfo->m_Local && Client()->State() != IClient::STATE_DEMOPLAYBACK) + bool DirLeft; + bool DirRight; + bool Jump; + if(Client()->DummyConnected() && Client()->State() != IClient::STATE_DEMOPLAYBACK && pPlayerInfo->m_ClientId == m_pClient->m_aLocalIds[!g_Config.m_ClDummy]) { - DirLeft = m_pClient->m_Controls.m_aInputData[g_Config.m_ClDummy].m_Direction == -1; - DirRight = m_pClient->m_Controls.m_aInputData[g_Config.m_ClDummy].m_Direction == 1; - Jump = m_pClient->m_Controls.m_aInputData[g_Config.m_ClDummy].m_Jump == 1; + const auto &InputData = m_pClient->m_Controls.m_aInputData[!g_Config.m_ClDummy]; + DirLeft = InputData.m_Direction == -1; + DirRight = InputData.m_Direction == 1; + Jump = InputData.m_Jump == 1; } - if(Client()->DummyConnected() && Client()->State() != IClient::STATE_DEMOPLAYBACK && pPlayerInfo->m_ClientID == m_pClient->m_aLocalIDs[!g_Config.m_ClDummy]) + else if(pPlayerInfo->m_Local && Client()->State() != IClient::STATE_DEMOPLAYBACK) { - DirLeft = m_pClient->m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Direction == -1; - DirRight = m_pClient->m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Direction == 1; - Jump = m_pClient->m_Controls.m_aInputData[!g_Config.m_ClDummy].m_Jump == 1; + const auto &InputData = m_pClient->m_Controls.m_aInputData[g_Config.m_ClDummy]; + DirLeft = InputData.m_Direction == -1; + DirRight = InputData.m_Direction == 1; + Jump = InputData.m_Jump == 1; } + else + { + const auto &Character = m_pClient->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId]; + DirLeft = Character.m_Cur.m_Direction == -1; + DirRight = Character.m_Cur.m_Direction == 1; + Jump = Character.m_Cur.m_Jumped & 1; + } + + if(OtherTeam && !ForceAlpha) + Graphics()->SetColor(1.0f, 1.0f, 1.0f, g_Config.m_ClShowOthersAlpha / 100.0f); + else + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + + const float ShowDirectionImgSize = 22.0f; + YOffset -= ShowDirectionImgSize; + const vec2 ShowDirectionPos = vec2(Position.x - 11.0f, YOffset); if(DirLeft) { Graphics()->TextureSet(g_pData->m_aImages[IMAGE_ARROW].m_Id); @@ -83,6 +77,7 @@ void CNamePlates::RenderNameplatePos(vec2 Position, const CNetObj_PlayerInfo *pP else if(DirRight) { Graphics()->TextureSet(g_pData->m_aImages[IMAGE_ARROW].m_Id); + Graphics()->QuadsSetRotation(0); Graphics()->RenderQuadContainerAsSprite(m_DirectionQuadContainerIndex, 0, ShowDirectionPos.x + 30.f, ShowDirectionPos.y); } if(Jump) @@ -98,209 +93,163 @@ void CNamePlates::RenderNameplatePos(vec2 Position, const CNetObj_PlayerInfo *pP // render name plate if((!pPlayerInfo->m_Local || g_Config.m_ClNameplatesOwn) && g_Config.m_ClNameplates) { - float a = 1; + float a; if(g_Config.m_ClNameplatesAlways == 0) a = clamp(1 - std::pow(distance(m_pClient->m_Controls.m_aTargetPos[g_Config.m_ClDummy], Position) / 200.0f, 16.0f), 0.0f, 1.0f); + else + a = 1.0f; - const char *pName = m_pClient->m_aClients[pPlayerInfo->m_ClientID].m_aName; - if(str_comp(pName, m_aNamePlates[ClientID].m_aName) != 0 || FontSize != m_aNamePlates[ClientID].m_NameTextFontSize) + if(str_comp(ClientData.m_aName, NamePlate.m_aName) != 0 || FontSize != NamePlate.m_NameTextFontSize) { - mem_copy(m_aNamePlates[ClientID].m_aName, pName, sizeof(m_aNamePlates[ClientID].m_aName)); - m_aNamePlates[ClientID].m_NameTextFontSize = FontSize; + str_copy(NamePlate.m_aName, ClientData.m_aName); + NamePlate.m_NameTextFontSize = FontSize; CTextCursor Cursor; TextRender()->SetCursor(&Cursor, 0, 0, FontSize, TEXTFLAG_RENDER); - Cursor.m_LineWidth = -1; // create nameplates at standard zoom float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); RenderTools()->MapScreenToInterface(m_pClient->m_Camera.m_Center.x, m_pClient->m_Camera.m_Center.y); - - m_aNamePlates[ClientID].m_NameTextWidth = TextRender()->TextWidth(FontSize, pName, -1, -1.0f); - - TextRender()->RecreateTextContainer(m_aNamePlates[ClientID].m_NameTextContainerIndex, &Cursor, pName); + TextRender()->RecreateTextContainer(NamePlate.m_NameTextContainerIndex, &Cursor, ClientData.m_aName); Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } if(g_Config.m_ClNameplatesClan) { - const char *pClan = m_pClient->m_aClients[ClientID].m_aClan; - if(str_comp(pClan, m_aNamePlates[ClientID].m_aClanName) != 0 || FontSizeClan != m_aNamePlates[ClientID].m_ClanNameTextFontSize) + if(str_comp(ClientData.m_aClan, NamePlate.m_aClan) != 0 || FontSizeClan != NamePlate.m_ClanTextFontSize) { - mem_copy(m_aNamePlates[ClientID].m_aClanName, pClan, sizeof(m_aNamePlates[ClientID].m_aClanName)); - m_aNamePlates[ClientID].m_ClanNameTextFontSize = FontSizeClan; + str_copy(NamePlate.m_aClan, ClientData.m_aClan); + NamePlate.m_ClanTextFontSize = FontSizeClan; CTextCursor Cursor; TextRender()->SetCursor(&Cursor, 0, 0, FontSizeClan, TEXTFLAG_RENDER); - Cursor.m_LineWidth = -1; // create nameplates at standard zoom float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); RenderTools()->MapScreenToInterface(m_pClient->m_Camera.m_Center.x, m_pClient->m_Camera.m_Center.y); - - m_aNamePlates[ClientID].m_ClanNameTextWidth = TextRender()->TextWidth(FontSizeClan, pClan, -1, -1.0f); - - TextRender()->RecreateTextContainer(m_aNamePlates[ClientID].m_ClanNameTextContainerIndex, &Cursor, pClan); + TextRender()->RecreateTextContainer(NamePlate.m_ClanTextContainerIndex, &Cursor, ClientData.m_aClan); Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } } - ColorRGBA TColor; - if(g_Config.m_ScShowFrozenNameplaterColor == 1) - if(GameClient()->m_aClients[ClientID].m_FreezeEnd) + + if(g_Config.m_ClNameplatesTeamcolors) + { + const int Team = m_pClient->m_Teams.Team(pPlayerInfo->m_ClientId); + if(Team) { - TColor = color_cast(ColorHSVA(g_Config.m_ScFrozenTeeColor)); + rgb = m_pClient->GetDDTeamColor(Team, 0.75f); } + } - float tw = m_aNamePlates[ClientID].m_NameTextWidth; - if(g_Config.m_ClNameplatesTeamcolors && m_pClient->m_Teams.Team(ClientID)) - rgb = m_pClient->GetDDTeamColor(m_pClient->m_Teams.Team(ClientID), 0.75f); + ColorRGBA TColor; ColorRGBA TOutlineColor; if(OtherTeam && !ForceAlpha) { TOutlineColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.2f * g_Config.m_ClShowOthersAlpha / 100.0f); - TColor = ColorRGBA(rgb.r, rgb.g, rgb.b, g_Config.m_ClShowOthersAlpha / 100.0f); + TColor = rgb.WithAlpha(g_Config.m_ClShowOthersAlpha / 100.0f); } else { TOutlineColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f * a); - TColor = ColorRGBA(rgb.r, rgb.g, rgb.b, a); + TColor = rgb.WithAlpha(a); } - - if(g_Config.m_ScShowFrozenNameplaterColor == 1) - if(GameClient()->m_aClients[ClientID].m_FreezeEnd) - { - TColor = color_cast(ColorHSVA(g_Config.m_ScFrozenTeeColor)); - } - if(g_Config.m_ClNameplatesTeamcolors && m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS) { - if(m_pClient->m_aClients[ClientID].m_Team == TEAM_RED) + if(ClientData.m_Team == TEAM_RED) TColor = ColorRGBA(1.0f, 0.5f, 0.5f, a); - else if(m_pClient->m_aClients[ClientID].m_Team == TEAM_BLUE) + else if(ClientData.m_Team == TEAM_BLUE) TColor = ColorRGBA(0.7f, 0.7f, 1.0f, a); } - if(g_Config.m_ScShowBlacklistNameplaterColor == 1) - { - if(GameClient()->m_aClients[ClientID].m_Foe) - { - TColor = color_cast(ColorHSVA(g_Config.m_ScBlacklistPColor)); - } - } - - if(g_Config.m_ScShowFriendsNameplatesColor == 1) - { - if(GameClient()->m_aClients[ClientID].m_Friend) - { - TColor = color_cast(ColorHSVA(g_Config.m_ScFriendColor)); - } - } - TOutlineColor.a *= Alpha; TColor.a *= Alpha; - if(m_aNamePlates[ClientID].m_NameTextContainerIndex.Valid()) + if(NamePlate.m_NameTextContainerIndex.Valid()) { YOffset -= FontSize; - TextRender()->RenderTextContainer(m_aNamePlates[ClientID].m_NameTextContainerIndex, TColor, TOutlineColor, Position.x - tw / 2.0f, YOffset); + TextRender()->RenderTextContainer(NamePlate.m_NameTextContainerIndex, TColor, TOutlineColor, Position.x - TextRender()->GetBoundingBoxTextContainer(NamePlate.m_NameTextContainerIndex).m_W / 2.0f, YOffset); } if(g_Config.m_ClNameplatesClan) { YOffset -= FontSizeClan; - if(m_aNamePlates[ClientID].m_ClanNameTextContainerIndex.Valid()) - TextRender()->RenderTextContainer(m_aNamePlates[ClientID].m_ClanNameTextContainerIndex, TColor, TOutlineColor, Position.x - m_aNamePlates[ClientID].m_ClanNameTextWidth / 2.0f, YOffset); + if(NamePlate.m_ClanTextContainerIndex.Valid()) + TextRender()->RenderTextContainer(NamePlate.m_ClanTextContainerIndex, TColor, TOutlineColor, Position.x - TextRender()->GetBoundingBoxTextContainer(NamePlate.m_ClanTextContainerIndex).m_W / 2.0f, YOffset); } - //tee skin name - if(g_Config.m_ClShowSkinName) - { - const CGameClient::CClientData &ClientData = m_pClient->m_aClients[pPlayerInfo->m_ClientID]; - const char *pSkinName = ClientData.m_aSkinName; - YOffset -= FontSizeClan; - float XOffset = TextRender()->TextWidth(FontSize, pSkinName, -1, -1.0f) / 2.0f; - TextRender()->TextColor(rgb); - TextRender()->Text(Position.x - XOffset, YOffset, FontSize, pSkinName, -1.0f); - } - - if(g_Config.m_ClNameplatesFriendMark && m_pClient->m_aClients[ClientID].m_Friend) + if(g_Config.m_ClNameplatesFriendMark && ClientData.m_Friend) { YOffset -= FontSize; - char aFriendMark[] = "♥"; - - ColorRGBA Color; - + ColorRGBA Color = ColorRGBA(1.0f, 0.0f, 0.0f, Alpha); if(OtherTeam && !ForceAlpha) - Color = ColorRGBA(1.0f, 0.0f, 0.0f, g_Config.m_ClShowOthersAlpha / 100.0f); + Color.a *= g_Config.m_ClShowOthersAlpha / 100.0f; else - Color = ColorRGBA(1.0f, 0.0f, 0.0f, a); - - Color.a *= Alpha; + Color.a *= a; + const char *pFriendMark = "♥"; TextRender()->TextColor(Color); - float XOffSet = TextRender()->TextWidth(FontSize, aFriendMark, -1, -1.0f) / 2.0f; - TextRender()->Text(Position.x - XOffSet, YOffset, FontSize, aFriendMark, -1.0f); + TextRender()->Text(Position.x - TextRender()->TextWidth(FontSize, pFriendMark) / 2.0f, YOffset, FontSize, pFriendMark); } - if(g_Config.m_Debug || g_Config.m_ClNameplatesIDs) // render client id when in debug as well + if(g_Config.m_Debug || g_Config.m_ClNameplatesIds) // render client id when in debug as well { YOffset -= FontSize; - char aBuf[128]; - str_from_int(pPlayerInfo->m_ClientID, aBuf); - float XOffset = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f) / 2.0f; + char aBuf[12]; + str_format(aBuf, sizeof(aBuf), "%d", pPlayerInfo->m_ClientId); TextRender()->TextColor(rgb); - TextRender()->Text(Position.x - XOffset, YOffset, FontSize, aBuf, -1.0f); + TextRender()->Text(Position.x - TextRender()->TextWidth(FontSize, aBuf) / 2.0f, YOffset, FontSize, aBuf); } } if((g_Config.m_Debug || g_Config.m_ClNameplatesStrong) && g_Config.m_ClNameplates) { - if(m_pClient->m_Snap.m_LocalClientID != -1 && m_pClient->m_Snap.m_aCharacters[pPlayerInfo->m_ClientID].m_HasExtendedData && m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_LocalClientID].m_HasExtendedData) + const bool Following = (m_pClient->m_Snap.m_SpecInfo.m_Active && !GameClient()->m_MultiViewActivated && m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW); + if(m_pClient->m_Snap.m_LocalClientId != -1 || Following) { - CCharacter *pLocalChar = m_pClient->m_GameWorld.GetCharacterByID(m_pClient->m_Snap.m_LocalClientID); - CCharacter *pCharacter = m_pClient->m_GameWorld.GetCharacterByID(pPlayerInfo->m_ClientID); - if(pCharacter && pLocalChar) + const int SelectedId = Following ? m_pClient->m_Snap.m_SpecInfo.m_SpectatorId : m_pClient->m_Snap.m_LocalClientId; + const CGameClient::CSnapState::CCharacterInfo &Selected = m_pClient->m_Snap.m_aCharacters[SelectedId]; + const CGameClient::CSnapState::CCharacterInfo &Other = m_pClient->m_Snap.m_aCharacters[pPlayerInfo->m_ClientId]; + if(Selected.m_HasExtendedData && Other.m_HasExtendedData) { - if(pPlayerInfo->m_Local) + if(SelectedId == pPlayerInfo->m_ClientId) + { TextRender()->TextColor(rgb); + } else { - float ScaleX, ScaleY; - const float StrongWeakImgSize = 40.0f; - Graphics()->TextureClear(); Graphics()->TextureSet(g_pData->m_aImages[IMAGE_STRONGWEAK].m_Id); Graphics()->QuadsBegin(); ColorRGBA StrongWeakStatusColor; - int StrongWeakSpriteID; - if(pLocalChar->GetStrongWeakID() > pCharacter->GetStrongWeakID()) + int StrongWeakSpriteId; + if(Selected.m_ExtendedData.m_StrongWeakId > Other.m_ExtendedData.m_StrongWeakId) { StrongWeakStatusColor = color_cast(ColorHSLA(6401973)); - StrongWeakSpriteID = SPRITE_HOOK_STRONG; + StrongWeakSpriteId = SPRITE_HOOK_STRONG; } else { StrongWeakStatusColor = color_cast(ColorHSLA(41131)); - StrongWeakSpriteID = SPRITE_HOOK_WEAK; + StrongWeakSpriteId = SPRITE_HOOK_WEAK; } - float ClampedAlpha = 1; - if(g_Config.m_ClNameplatesAlways == 0) - ClampedAlpha = clamp(1 - std::pow(distance(m_pClient->m_Controls.m_aTargetPos[g_Config.m_ClDummy], Position) / 200.0f, 16.0f), 0.0f, 1.0f); - if(OtherTeam && !ForceAlpha) StrongWeakStatusColor.a = g_Config.m_ClShowOthersAlpha / 100.0f; + else if(g_Config.m_ClNameplatesAlways == 0) + StrongWeakStatusColor.a = clamp(1 - std::pow(distance(m_pClient->m_Controls.m_aTargetPos[g_Config.m_ClDummy], Position) / 200.0f, 16.0f), 0.0f, 1.0f); else - StrongWeakStatusColor.a = ClampedAlpha; + StrongWeakStatusColor.a = 1.0f; StrongWeakStatusColor.a *= Alpha; Graphics()->SetColor(StrongWeakStatusColor); - RenderTools()->SelectSprite(StrongWeakSpriteID); - RenderTools()->GetSpriteScale(StrongWeakSpriteID, ScaleX, ScaleY); + float ScaleX, ScaleY; + RenderTools()->SelectSprite(StrongWeakSpriteId); + RenderTools()->GetSpriteScale(StrongWeakSpriteId, ScaleX, ScaleY); TextRender()->TextColor(StrongWeakStatusColor); + const float StrongWeakImgSize = 40.0f; YOffset -= StrongWeakImgSize * ScaleY; RenderTools()->DrawSprite(Position.x, YOffset + (StrongWeakImgSize / 2.0f) * ScaleY, StrongWeakImgSize); Graphics()->QuadsEnd(); @@ -309,9 +258,8 @@ void CNamePlates::RenderNameplatePos(vec2 Position, const CNetObj_PlayerInfo *pP { YOffset -= FontSize; char aBuf[12]; - str_from_int(pCharacter->GetStrongWeakID(), aBuf); - float XOffset = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f) / 2.0f; - TextRender()->Text(Position.x - XOffset, YOffset, FontSize, aBuf, -1.0f); + str_format(aBuf, sizeof(aBuf), "%d", Other.m_ExtendedData.m_StrongWeakId); + TextRender()->Text(Position.x - TextRender()->TextWidth(FontSize, aBuf) / 2.0f, YOffset, FontSize, aBuf); } } } @@ -325,6 +273,9 @@ void CNamePlates::RenderNameplatePos(vec2 Position, const CNetObj_PlayerInfo *pP void CNamePlates::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + int ShowDirection = g_Config.m_ClShowDirection; #if defined(CONF_VIDEORECORDER) if(IVideo::Current()) @@ -353,44 +304,35 @@ void CNamePlates::OnRender() continue; } - vec2 *pRenderPos; if(m_pClient->m_aClients[i].m_SpecCharPresent) { // Each player can also have a spec char whose nameplate is displayed independently - pRenderPos = &m_pClient->m_aClients[i].m_SpecChar; + const vec2 RenderPos = m_pClient->m_aClients[i].m_SpecChar; // don't render offscreen - if(!(pRenderPos->x < ScreenX0) && !(pRenderPos->x > ScreenX1) && !(pRenderPos->y < ScreenY0) && !(pRenderPos->y > ScreenY1)) + if(in_range(RenderPos.x, ScreenX0, ScreenX1) && in_range(RenderPos.y, ScreenY0, ScreenY1)) { - RenderNameplatePos(m_pClient->m_aClients[i].m_SpecChar, pInfo, 0.4f, true); + RenderNameplate(RenderPos, pInfo, 0.4f, true); } } if(m_pClient->m_Snap.m_aCharacters[i].m_Active) { // Only render nameplates for active characters - pRenderPos = &m_pClient->m_aClients[i].m_RenderPos; + const vec2 RenderPos = m_pClient->m_aClients[i].m_RenderPos; // don't render offscreen - if(!(pRenderPos->x < ScreenX0) && !(pRenderPos->x > ScreenX1) && !(pRenderPos->y < ScreenY0) && !(pRenderPos->y > ScreenY1)) + if(in_range(RenderPos.x, ScreenX0, ScreenX1) && in_range(RenderPos.y, ScreenY0, ScreenY1)) { - RenderNameplate( - &m_pClient->m_Snap.m_aCharacters[i].m_Prev, - &m_pClient->m_Snap.m_aCharacters[i].m_Cur, - pInfo); + RenderNameplate(RenderPos, pInfo, 1.0f, false); } } } } -void CNamePlates::SetPlayers(CPlayers *pPlayers) -{ - m_pPlayers = pPlayers; -} - void CNamePlates::ResetNamePlates() { for(auto &NamePlate : m_aNamePlates) { TextRender()->DeleteTextContainer(NamePlate.m_NameTextContainerIndex); - TextRender()->DeleteTextContainer(NamePlate.m_ClanNameTextContainerIndex); + TextRender()->DeleteTextContainer(NamePlate.m_ClanTextContainerIndex); NamePlate.Reset(); } diff --git a/src/game/client/components/nameplates.h b/src/game/client/components/nameplates.h index 24042f8fde..7148dd7d56 100644 --- a/src/game/client/components/nameplates.h +++ b/src/game/client/components/nameplates.h @@ -5,6 +5,7 @@ #include #include +#include #include @@ -21,34 +22,26 @@ struct SPlayerNamePlate void Reset() { m_NameTextContainerIndex.Reset(); - m_ClanNameTextContainerIndex.Reset(); - m_aName[0] = 0; - m_aClanName[0] = 0; - m_NameTextWidth = m_ClanNameTextWidth = 0.f; - m_NameTextFontSize = m_ClanNameTextFontSize = 0; + m_ClanTextContainerIndex.Reset(); + m_aName[0] = '\0'; + m_aClan[0] = '\0'; + m_NameTextFontSize = m_ClanTextFontSize = 0.0f; } char m_aName[MAX_NAME_LENGTH]; - float m_NameTextWidth; STextContainerIndex m_NameTextContainerIndex; float m_NameTextFontSize; - char m_aClanName[MAX_CLAN_LENGTH]; - float m_ClanNameTextWidth; - STextContainerIndex m_ClanNameTextContainerIndex; - float m_ClanNameTextFontSize; + char m_aClan[MAX_CLAN_LENGTH]; + STextContainerIndex m_ClanTextContainerIndex; + float m_ClanTextFontSize; }; class CNamePlates : public CComponent { - void RenderNameplate( - const CNetObj_Character *pPrevChar, - const CNetObj_Character *pPlayerChar, - const CNetObj_PlayerInfo *pPlayerInfo); - void RenderNameplatePos(vec2 Position, const CNetObj_PlayerInfo *pPlayerInfo, float Alpha, bool ForceAlpha = false); + void RenderNameplate(vec2 Position, const CNetObj_PlayerInfo *pPlayerInfo, float Alpha, bool ForceAlpha); SPlayerNamePlate m_aNamePlates[MAX_CLIENTS]; - class CPlayers *m_pPlayers; void ResetNamePlates(); @@ -59,8 +52,6 @@ class CNamePlates : public CComponent virtual void OnWindowResize() override; virtual void OnInit() override; virtual void OnRender() override; - - void SetPlayers(class CPlayers *pPlayers); }; #endif diff --git a/src/game/client/components/particles.cpp b/src/game/client/components/particles.cpp index d076939de6..fef6e886d6 100644 --- a/src/game/client/components/particles.cpp +++ b/src/game/client/components/particles.cpp @@ -78,17 +78,16 @@ void CParticles::Update(float TimePassed) if(TimePassed <= 0.0f) return; - static float FrictionFraction = 0; - FrictionFraction += TimePassed; + m_FrictionFraction += TimePassed; - if(FrictionFraction > 2.0f) // safety measure - FrictionFraction = 0; + if(m_FrictionFraction > 2.0f) // safety measure + m_FrictionFraction = 0; int FrictionCount = 0; - while(FrictionFraction > 0.05f) + while(m_FrictionFraction > 0.05f) { FrictionCount++; - FrictionFraction -= 0.05f; + m_FrictionFraction -= 0.05f; } for(int &FirstPart : m_aFirstPart) @@ -149,22 +148,21 @@ void CParticles::OnRender() return; set_new_tick(); - static int64_t LastTime = 0; int64_t t = time(); if(Client()->State() == IClient::STATE_DEMOPLAYBACK) { const IDemoPlayer::CInfo *pInfo = DemoPlayer()->BaseInfo(); if(!pInfo->m_Paused) - Update((float)((t - LastTime) / (double)time_freq()) * pInfo->m_Speed); + Update((float)((t - m_LastRenderTime) / (double)time_freq()) * pInfo->m_Speed); } else { if(m_pClient->m_Snap.m_pGameInfoObj && !(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)) - Update((float)((t - LastTime) / (double)time_freq())); + Update((float)((t - m_LastRenderTime) / (double)time_freq())); } - LastTime = t; + m_LastRenderTime = t; } void CParticles::OnInit() diff --git a/src/game/client/components/particles.h b/src/game/client/components/particles.h index eb32993ceb..0d2f4937db 100644 --- a/src/game/client/components/particles.h +++ b/src/game/client/components/particles.h @@ -2,6 +2,7 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #ifndef GAME_CLIENT_COMPONENTS_PARTICLES_H #define GAME_CLIENT_COMPONENTS_PARTICLES_H +#include #include #include @@ -95,6 +96,9 @@ class CParticles : public CComponent int m_FirstFree; int m_aFirstPart[NUM_GROUPS]; + float m_FrictionFraction = 0.0f; + int64_t m_LastRenderTime = 0; + void RenderGroup(int Group); void Update(float TimePassed); diff --git a/src/game/client/components/players.cpp b/src/game/client/components/players.cpp index cc425e206e..3876c5331d 100644 --- a/src/game/client/components/players.cpp +++ b/src/game/client/components/players.cpp @@ -1,11 +1,13 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include #include #include #include #include #include +#include #include #include @@ -21,10 +23,59 @@ #include #include "players.h" + #include #include void CPlayers::RenderHand(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha) +{ + if(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_BODY].IsValid()) + RenderHand7(pInfo, CenterPos, Dir, AngleOffset, PostRotOffset, Alpha); + else + RenderHand6(pInfo, CenterPos, Dir, AngleOffset, PostRotOffset, Alpha); +} + +void CPlayers::RenderHand7(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha) +{ + // in-game hand size is 15 when tee size is 64 + float BaseSize = 15.0f * (pInfo->m_Size / 64.0f); + + vec2 HandPos = CenterPos + Dir; + float Angle = angle(Dir); + if(Dir.x < 0) + Angle -= AngleOffset; + else + Angle += AngleOffset; + + vec2 DirX = Dir; + vec2 DirY(-Dir.y, Dir.x); + + if(Dir.x < 0) + DirY = -DirY; + + HandPos += DirX * PostRotOffset.x; + HandPos += DirY * PostRotOffset.y; + + ColorRGBA Color = pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_HANDS]; + Color.a = Alpha; + IGraphics::CQuadItem QuadOutline(HandPos.x, HandPos.y, 2 * BaseSize, 2 * BaseSize); + IGraphics::CQuadItem QuadHand = QuadOutline; + + Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_HANDS]); + Graphics()->QuadsBegin(); + Graphics()->SetColor(Color); + Graphics()->QuadsSetRotation(Angle); + + RenderTools()->SelectSprite7(client_data7::SPRITE_TEE_HAND_OUTLINE); + Graphics()->QuadsDraw(&QuadOutline, 1); + RenderTools()->SelectSprite7(client_data7::SPRITE_TEE_HAND); + Graphics()->QuadsDraw(&QuadHand, 1); + + Graphics()->QuadsSetRotation(0); + Graphics()->QuadsEnd(); +} + +void CPlayers::RenderHand6(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha) { vec2 HandPos = CenterPos + Dir; float Angle = angle(Dir); @@ -70,19 +121,19 @@ inline float AngularApproach(float Src, float Dst, float Amount) float CPlayers::GetPlayerTargetAngle( const CNetObj_Character *pPrevChar, const CNetObj_Character *pPlayerChar, - int ClientID, + int ClientId, float Intra) { float AngleIntraTick = Intra; // using unpredicted angle when rendering other players in-game - if(ClientID >= 0) + if(ClientId >= 0) AngleIntraTick = Client()->IntraGameTick(g_Config.m_ClDummy); - if(ClientID >= 0 && m_pClient->m_Snap.m_aCharacters[ClientID].m_HasExtendedDisplayInfo) + if(ClientId >= 0 && m_pClient->m_Snap.m_aCharacters[ClientId].m_HasExtendedDisplayInfo) { - CNetObj_DDNetCharacter *pExtendedData = &m_pClient->m_Snap.m_aCharacters[ClientID].m_ExtendedData; - if(m_pClient->m_Snap.m_aCharacters[ClientID].m_PrevExtendedData) + CNetObj_DDNetCharacter *pExtendedData = &m_pClient->m_Snap.m_aCharacters[ClientId].m_ExtendedData; + if(m_pClient->m_Snap.m_aCharacters[ClientId].m_PrevExtendedData) { - const CNetObj_DDNetCharacter *PrevExtendedData = m_pClient->m_Snap.m_aCharacters[ClientID].m_PrevExtendedData; + const CNetObj_DDNetCharacter *PrevExtendedData = m_pClient->m_Snap.m_aCharacters[ClientId].m_PrevExtendedData; float MixX = mix((float)PrevExtendedData->m_TargetX, (float)pExtendedData->m_TargetX, AngleIntraTick); float MixY = mix((float)PrevExtendedData->m_TargetY, (float)pExtendedData->m_TargetY, AngleIntraTick); @@ -117,7 +168,7 @@ float CPlayers::GetPlayerTargetAngle( void CPlayers::RenderHookCollLine( const CNetObj_Character *pPrevChar, const CNetObj_Character *pPlayerChar, - int ClientID, + int ClientId, float Intra) { CNetObj_Character Prev; @@ -125,36 +176,49 @@ void CPlayers::RenderHookCollLine( Prev = *pPrevChar; Player = *pPlayerChar; - bool Local = m_pClient->m_Snap.m_LocalClientID == ClientID; - bool OtherTeam = m_pClient->IsOtherTeam(ClientID); - float Alpha = (OtherTeam || ClientID < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f; + bool Local = m_pClient->m_Snap.m_LocalClientId == ClientId; + bool OtherTeam = m_pClient->IsOtherTeam(ClientId); + float Alpha = (OtherTeam || ClientId < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f; Alpha *= (float)g_Config.m_ClHookCollAlpha / 100; float IntraTick = Intra; - if(ClientID >= 0) - IntraTick = m_pClient->m_aClients[ClientID].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy); + if(ClientId >= 0) + IntraTick = m_pClient->m_aClients[ClientId].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy); float Angle; - if(Local && Client()->State() != IClient::STATE_DEMOPLAYBACK) + if(Local && (!m_pClient->m_Snap.m_SpecInfo.m_Active || m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) && Client()->State() != IClient::STATE_DEMOPLAYBACK) { // just use the direct input if it's the local player we are rendering - Angle = angle(m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy]); + Angle = angle(m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy] * m_pClient->m_Camera.m_Zoom); } else { - Angle = GetPlayerTargetAngle(&Prev, &Player, ClientID, IntraTick); + Angle = GetPlayerTargetAngle(&Prev, &Player, ClientId, IntraTick); } vec2 Direction = direction(Angle); vec2 Position; - if(in_range(ClientID, MAX_CLIENTS - 1)) - Position = m_pClient->m_aClients[ClientID].m_RenderPos; + if(in_range(ClientId, MAX_CLIENTS - 1)) + Position = m_pClient->m_aClients[ClientId].m_RenderPos; else Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), IntraTick); // draw hook collision line { + bool Aim = (Player.m_PlayerFlags & PLAYERFLAG_AIM); + if(!Client()->ServerCapAnyPlayerFlag()) + { + for(int i = 0; i < NUM_DUMMIES; i++) + { + if(ClientId == m_pClient->m_aLocalIds[i]) + { + Aim = GameClient()->m_Controls.m_aShowHookColl[i]; + break; + } + } + } + bool AlwaysRenderHookColl = GameClient()->m_GameInfo.m_AllowHookColl && (Local ? g_Config.m_ClShowHookCollOwn : g_Config.m_ClShowHookCollOther) == 2; - bool RenderHookCollPlayer = ClientID >= 0 && Player.m_PlayerFlags & PLAYERFLAG_AIM && (Local ? g_Config.m_ClShowHookCollOwn : g_Config.m_ClShowHookCollOther) > 0; + bool RenderHookCollPlayer = ClientId >= 0 && Aim && (Local ? g_Config.m_ClShowHookCollOwn : g_Config.m_ClShowHookCollOther) > 0; if(Local && GameClient()->m_GameInfo.m_AllowHookColl && Client()->State() != IClient::STATE_DEMOPLAYBACK) RenderHookCollPlayer = GameClient()->m_Controls.m_aShowHookColl[g_Config.m_ClDummy] && g_Config.m_ClShowHookCollOwn > 0; @@ -166,9 +230,11 @@ void CPlayers::RenderHookCollLine( { vec2 ExDirection = Direction; - if(Local && Client()->State() != IClient::STATE_DEMOPLAYBACK) + if(Local && (!m_pClient->m_Snap.m_SpecInfo.m_Active || m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) && Client()->State() != IClient::STATE_DEMOPLAYBACK) { - ExDirection = normalize(vec2((int)m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy].x, (int)m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy].y)); + ExDirection = normalize( + vec2((int)((int)m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy].x * m_pClient->m_Camera.m_Zoom), + (int)((int)m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy].y * m_pClient->m_Camera.m_Zoom))); // fix direction if mouse is exactly in the center if(!(int)m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy].x && !(int)m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy].y) @@ -211,7 +277,7 @@ void CPlayers::RenderHookCollLine( } } - if(m_pClient->IntersectCharacter(OldPos, FinishPos, FinishPos, ClientID) != -1) + if(ClientId >= 0 && m_pClient->IntersectCharacter(OldPos, FinishPos, FinishPos, ClientId) != -1) { HookCollColor = color_cast(ColorHSLA(g_Config.m_ClHookCollColorTeeColl)); break; @@ -261,7 +327,7 @@ void CPlayers::RenderHook( const CNetObj_Character *pPrevChar, const CNetObj_Character *pPlayerChar, const CTeeRenderInfo *pRenderInfo, - int ClientID, + int ClientId, float Intra) { CNetObj_Character Prev; @@ -276,19 +342,19 @@ void CPlayers::RenderHook( return; float IntraTick = Intra; - if(ClientID >= 0) - IntraTick = (m_pClient->m_aClients[ClientID].m_IsPredicted) ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy); + if(ClientId >= 0) + IntraTick = (m_pClient->m_aClients[ClientId].m_IsPredicted) ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy); - bool OtherTeam = m_pClient->IsOtherTeam(ClientID); - float Alpha = (OtherTeam || ClientID < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f; - if(ClientID == -2) // ghost + bool OtherTeam = m_pClient->IsOtherTeam(ClientId); + float Alpha = (OtherTeam || ClientId < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f; + if(ClientId == -2) // ghost Alpha = g_Config.m_ClRaceGhostAlpha / 100.0f; RenderInfo.m_Size = 64.0f; vec2 Position; - if(in_range(ClientID, MAX_CLIENTS - 1)) - Position = m_pClient->m_aClients[ClientID].m_RenderPos; + if(in_range(ClientId, MAX_CLIENTS - 1)) + Position = m_pClient->m_aClients[ClientId].m_RenderPos; else Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), IntraTick); @@ -296,7 +362,7 @@ void CPlayers::RenderHook( if(Prev.m_HookState > 0 && Player.m_HookState > 0) { Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); - if(ClientID < 0) + if(ClientId < 0) Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f); vec2 Pos = Position; @@ -314,19 +380,7 @@ void CPlayers::RenderHook( Graphics()->QuadsSetRotation(angle(Dir) + pi); // render head int QuadOffset = NUM_WEAPONS * 2 + 2; - if(g_Config.m_ClRainbowHook == 0){ - - Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); - } - else - { - ColorHSVA ColorHSV(round_to_int(LocalTime() * g_Config.m_ClRainbowSpeed) % 255 / 255.f, 1.f, 1.f); - ColorRGBA ColorWithAlpha = color_cast(ColorHSV); - ColorWithAlpha.a = Alpha; - - Graphics()->SetColor(ColorWithAlpha); - - } + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, HookPos.x, HookPos.y); // render chain @@ -355,7 +409,7 @@ void CPlayers::RenderPlayer( const CNetObj_Character *pPrevChar, const CNetObj_Character *pPlayerChar, const CTeeRenderInfo *pRenderInfo, - int ClientID, + int ClientId, float Intra) { CNetObj_Character Prev; @@ -365,18 +419,18 @@ void CPlayers::RenderPlayer( CTeeRenderInfo RenderInfo = *pRenderInfo; - bool Local = m_pClient->m_Snap.m_LocalClientID == ClientID; - bool OtherTeam = m_pClient->IsOtherTeam(ClientID); - float Alpha = (OtherTeam || ClientID < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f; - if(ClientID == -2) // ghost + bool Local = m_pClient->m_Snap.m_LocalClientId == ClientId; + bool OtherTeam = m_pClient->IsOtherTeam(ClientId); + float Alpha = (OtherTeam || ClientId < 0) ? g_Config.m_ClShowOthersAlpha / 100.0f : 1.0f; + if(ClientId == -2) // ghost Alpha = g_Config.m_ClRaceGhostAlpha / 100.0f; // set size RenderInfo.m_Size = 64.0f; float IntraTick = Intra; - if(ClientID >= 0) - IntraTick = m_pClient->m_aClients[ClientID].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy); + if(ClientId >= 0) + IntraTick = m_pClient->m_aClients[ClientId].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy); static float s_LastGameTickTime = Client()->GameTickTime(g_Config.m_ClDummy); static float s_LastPredIntraTick = Client()->PredIntraGameTick(g_Config.m_ClDummy); @@ -389,7 +443,7 @@ void CPlayers::RenderPlayer( bool PredictLocalWeapons = false; float AttackTime = (Client()->PrevGameTick(g_Config.m_ClDummy) - Player.m_AttackTick) / (float)Client()->GameTickSpeed() + Client()->GameTickTime(g_Config.m_ClDummy); float LastAttackTime = (Client()->PrevGameTick(g_Config.m_ClDummy) - Player.m_AttackTick) / (float)Client()->GameTickSpeed() + s_LastGameTickTime; - if(ClientID >= 0 && m_pClient->m_aClients[ClientID].m_IsPredictedLocal && m_pClient->AntiPingGunfire()) + if(ClientId >= 0 && m_pClient->m_aClients[ClientId].m_IsPredictedLocal && m_pClient->AntiPingGunfire()) { PredictLocalWeapons = true; AttackTime = (Client()->PredIntraGameTick(g_Config.m_ClDummy) + (Client()->PredGameTick(g_Config.m_ClDummy) - 1 - Player.m_AttackTick)) / (float)Client()->GameTickSpeed(); @@ -398,27 +452,27 @@ void CPlayers::RenderPlayer( float AttackTicksPassed = AttackTime * (float)Client()->GameTickSpeed(); float Angle; - if(Local && (!m_pClient->m_Snap.m_SpecInfo.m_Active || m_pClient->m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW) && Client()->State() != IClient::STATE_DEMOPLAYBACK) + if(Local && (!m_pClient->m_Snap.m_SpecInfo.m_Active || m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) && Client()->State() != IClient::STATE_DEMOPLAYBACK) { // just use the direct input if it's the local player we are rendering - Angle = angle(m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy]); + Angle = angle(m_pClient->m_Controls.m_aMousePos[g_Config.m_ClDummy] * m_pClient->m_Camera.m_Zoom); } else { - Angle = GetPlayerTargetAngle(&Prev, &Player, ClientID, IntraTick); + Angle = GetPlayerTargetAngle(&Prev, &Player, ClientId, IntraTick); } vec2 Direction = direction(Angle); vec2 Position; - if(in_range(ClientID, MAX_CLIENTS - 1)) - Position = m_pClient->m_aClients[ClientID].m_RenderPos; + if(in_range(ClientId, MAX_CLIENTS - 1)) + Position = m_pClient->m_aClients[ClientId].m_RenderPos; else Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), IntraTick); vec2 Vel = mix(vec2(Prev.m_VelX / 256.0f, Prev.m_VelY / 256.0f), vec2(Player.m_VelX / 256.0f, Player.m_VelY / 256.0f), IntraTick); m_pClient->m_Flow.Add(Position, Vel * 100.0f, 10.0f); - RenderInfo.m_GotAirJump = (Player.m_Jumped & 2) == 0; + RenderInfo.m_GotAirJump = Player.m_Jumped & 2 ? false : true; RenderInfo.m_FeetFlipped = false; @@ -426,7 +480,7 @@ void CPlayers::RenderPlayer( bool InAir = !Collision()->CheckPoint(Player.m_X, Player.m_Y + 16); bool Running = Player.m_VelX >= 5000 || Player.m_VelX <= -5000; bool WantOtherDir = (Player.m_Direction == -1 && Vel.x > 0) || (Player.m_Direction == 1 && Vel.x < 0); - bool Inactive = m_pClient->m_aClients[ClientID].m_Afk || m_pClient->m_aClients[ClientID].m_Paused; + bool Inactive = ClientId >= 0 && (m_pClient->m_aClients[ClientId].m_Afk || m_pClient->m_aClients[ClientId].m_Paused); // evaluate animation float WalkTime = std::fmod(Position.x, 100.0f) / 100.0f; @@ -461,26 +515,19 @@ void CPlayers::RenderPlayer( State.Add(&g_pData->m_aAnimations[ANIM_WALK], WalkTime, 1.0f); } - int CurrentWeapon = clamp(Player.m_Weapon, 0, NUM_WEAPONS - 1); - if(m_pClient->m_aClients[ClientID].m_FreezeEnd) - { - CurrentWeapon = WEAPON_NINJA; - } - - if(CurrentWeapon == WEAPON_HAMMER) + if(Player.m_Weapon == WEAPON_HAMMER) State.Add(&g_pData->m_aAnimations[ANIM_HAMMER_SWING], clamp(LastAttackTime * 5.0f, 0.0f, 1.0f), 1.0f); - if(CurrentWeapon == WEAPON_NINJA) + if(Player.m_Weapon == WEAPON_NINJA) State.Add(&g_pData->m_aAnimations[ANIM_NINJA_SWING], clamp(LastAttackTime * 2.0f, 0.0f, 1.0f), 1.0f); // do skidding if(!InAir && WantOtherDir && length(Vel * 50) > 500.0f) { - static int64_t SkidSoundTime = 0; - if(time() - SkidSoundTime > time_freq() / 10) + if(time() - m_SkidSoundTime > time_freq() / 10) { if(g_Config.m_SndGame) - m_pClient->m_Sounds.PlayAt(CSounds::CHN_WORLD, SOUND_PLAYER_SKID, 0.25f, Position); - SkidSoundTime = time(); + m_pClient->m_Sounds.PlayAt(CSounds::CHN_WORLD, SOUND_PLAYER_SKID, 1.0f, Position); + m_SkidSoundTime = time(); } m_pClient->m_Effects.SkidTrail( @@ -489,36 +536,18 @@ void CPlayers::RenderPlayer( Alpha); } - // rainbow - - bool IsRainbowBody = g_Config.m_ClRainbow, - IsRainbowFeet = g_Config.m_ClRainbow; - - if(g_Config.m_ClRainbow == 1 && (m_pClient->m_Snap.m_LocalClientID == ClientID) == 1) - { - IsRainbowBody = true; - IsRainbowFeet = true; - } - else - { - IsRainbowBody = false; - IsRainbowFeet = false; - } - // draw gun { - static vec2 s_aGunPositions[MAX_CLIENTS]; - if(!(RenderInfo.m_TeeRenderFlags & TEE_NO_WEAPON)) { Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); Graphics()->QuadsSetRotation(State.GetAttach()->m_Angle * pi * 2 + Angle); - if(ClientID < 0) + if(ClientId < 0) Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.5f); // normal weapons - + int CurrentWeapon = clamp(Player.m_Weapon, 0, NUM_WEAPONS - 1); Graphics()->TextureSet(GameClient()->m_GameSkin.m_aSpriteWeapons[CurrentWeapon]); int QuadOffset = CurrentWeapon * 2 + (Direction.x < 0 ? 1 : 0); @@ -529,7 +558,7 @@ void CPlayers::RenderPlayer( vec2 WeaponPosition; bool IsSit = Inactive && !InAir && Stationary; - if(CurrentWeapon == WEAPON_HAMMER) + if(Player.m_Weapon == WEAPON_HAMMER) { // static position for hammer WeaponPosition = Position + vec2(State.GetAttach()->m_X, State.GetAttach()->m_Y); @@ -539,10 +568,6 @@ void CPlayers::RenderPlayer( if(IsSit) WeaponPosition.y += 3.0f; - if(s_aGunPositions[ClientID] != vec2()) - WeaponPosition = mix(s_aGunPositions[ClientID], WeaponPosition, 1.f / 100.f * g_Config.m_ClAnimHammerSpeed); - s_aGunPositions[ClientID] = WeaponPosition; - // if active and attack is under way, bash stuffs if(!Inactive || LastAttackTime < m_pClient->m_aTuning[g_Config.m_ClDummy].GetWeaponFireDelay(Player.m_Weapon)) { @@ -556,7 +581,7 @@ void CPlayers::RenderPlayer( Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y); } - else if(CurrentWeapon == WEAPON_NINJA) + else if(Player.m_Weapon == WEAPON_NINJA) { WeaponPosition = Position; WeaponPosition.y += g_pData->m_Weapons.m_aId[CurrentWeapon].m_Offsety; @@ -574,14 +599,9 @@ void CPlayers::RenderPlayer( Graphics()->QuadsSetRotation(-pi / 2 + State.GetAttach()->m_Angle * pi * 2); m_pClient->m_Effects.PowerupShine(WeaponPosition - vec2(32, 0), vec2(32, 12), Alpha); } - - if(s_aGunPositions[ClientID] != vec2()) - WeaponPosition = mix(s_aGunPositions[ClientID], WeaponPosition, 1.f / 100.f * g_Config.m_ClAnimNinjaSpeed); - s_aGunPositions[ClientID] = WeaponPosition; - Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y); - // NO HADOKEN ;( + // HADOKEN if(AttackTime <= 1 / 6.f && g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles) { int IteX = rand() % g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles; @@ -603,10 +623,10 @@ void CPlayers::RenderPlayer( } if(g_pData->m_Weapons.m_aId[CurrentWeapon].m_aSpriteMuzzles[IteX]) { - if(PredictLocalWeapons) + if(PredictLocalWeapons || ClientId < 0) Dir = vec2(pPlayerChar->m_X, pPlayerChar->m_Y) - vec2(pPrevChar->m_X, pPrevChar->m_Y); else - Dir = vec2(m_pClient->m_Snap.m_aCharacters[ClientID].m_Cur.m_X, m_pClient->m_Snap.m_aCharacters[ClientID].m_Cur.m_Y) - vec2(m_pClient->m_Snap.m_aCharacters[ClientID].m_Prev.m_X, m_pClient->m_Snap.m_aCharacters[ClientID].m_Prev.m_Y); + Dir = vec2(m_pClient->m_Snap.m_aCharacters[ClientId].m_Cur.m_X, m_pClient->m_Snap.m_aCharacters[ClientId].m_Cur.m_Y) - vec2(m_pClient->m_Snap.m_aCharacters[ClientId].m_Prev.m_X, m_pClient->m_Snap.m_aCharacters[ClientId].m_Prev.m_Y); float HadOkenAngle = 0; if(absolute(Dir.x) > 0.0001f || absolute(Dir.y) > 0.0001f) { @@ -641,15 +661,10 @@ void CPlayers::RenderPlayer( WeaponPosition.y += 3.0f; if(Player.m_Weapon == WEAPON_GUN && g_Config.m_ClOldGunPosition) WeaponPosition.y -= 8; - - if(s_aGunPositions[ClientID] != vec2()) - WeaponPosition = mix(s_aGunPositions[ClientID], WeaponPosition, 1.f / 100.f * g_Config.m_ClAnimGunsSpeed); - s_aGunPositions[ClientID] = WeaponPosition; - Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, WeaponPosition.x, WeaponPosition.y); } - if(CurrentWeapon == WEAPON_GUN || Player.m_Weapon == WEAPON_SHOTGUN) + if(Player.m_Weapon == WEAPON_GUN || Player.m_Weapon == WEAPON_SHOTGUN) { // check if we're firing stuff if(g_pData->m_Weapons.m_aId[CurrentWeapon].m_NumSpriteMuzzles) // prev.attackticks) @@ -692,13 +707,9 @@ void CPlayers::RenderPlayer( } } } - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); Graphics()->QuadsSetRotation(0); - if(IsRainbowBody) - RenderInfo.m_ColorBody = color_cast(ColorHSVA(round_to_int(LocalTime() * 20.f) % 255 / 255.f, 1.f, 1.f)); - switch(Player.m_Weapon) { case WEAPON_GUN: RenderHand(&RenderInfo, WeaponPosition, Direction, -3 * pi / 4, vec2(-15, 4), Alpha); break; @@ -712,33 +723,31 @@ void CPlayers::RenderPlayer( if(Local && ((g_Config.m_Debug && g_Config.m_ClUnpredictedShadow >= 0) || g_Config.m_ClUnpredictedShadow == 1)) { vec2 ShadowPosition = Position; - if(ClientID >= 0) + if(ClientId >= 0) ShadowPosition = mix( - vec2(m_pClient->m_Snap.m_aCharacters[ClientID].m_Prev.m_X, m_pClient->m_Snap.m_aCharacters[ClientID].m_Prev.m_Y), - vec2(m_pClient->m_Snap.m_aCharacters[ClientID].m_Cur.m_X, m_pClient->m_Snap.m_aCharacters[ClientID].m_Cur.m_Y), + vec2(m_pClient->m_Snap.m_aCharacters[ClientId].m_Prev.m_X, m_pClient->m_Snap.m_aCharacters[ClientId].m_Prev.m_Y), + vec2(m_pClient->m_Snap.m_aCharacters[ClientId].m_Cur.m_X, m_pClient->m_Snap.m_aCharacters[ClientId].m_Cur.m_Y), Client()->IntraGameTick(g_Config.m_ClDummy)); CTeeRenderInfo Shadow = RenderInfo; RenderTools()->RenderTee(&State, &Shadow, Player.m_Emote, Direction, ShadowPosition, 0.5f); // render ghost } - if(IsRainbowBody) - RenderInfo.m_ColorBody = color_cast(ColorHSVA(round_to_int(LocalTime() * g_Config.m_ClRainbowSpeed) % 255 / 255.f, 1.f, 1.f)); - if(IsRainbowFeet) - RenderInfo.m_ColorFeet = color_cast(ColorHSVA(round_to_int(LocalTime() * g_Config.m_ClRainbowSpeed) % 255 / 255.f, 1.f, 1.f)); - - RenderTools()->RenderTee(&State, &RenderInfo, Player.m_Emote, Direction, Position, Alpha, true, ClientID, InAir); + RenderTools()->RenderTee(&State, &RenderInfo, Player.m_Emote, Direction, Position, Alpha); float TeeAnimScale, TeeBaseSize; - RenderTools()->GetRenderTeeAnimScaleAndBaseSize(&RenderInfo, TeeAnimScale, TeeBaseSize); + CRenderTools::GetRenderTeeAnimScaleAndBaseSize(&RenderInfo, TeeAnimScale, TeeBaseSize); vec2 BodyPos = Position + vec2(State.GetBody()->m_X, State.GetBody()->m_Y) * TeeAnimScale; if(RenderInfo.m_TeeRenderFlags & TEE_EFFECT_FROZEN) { GameClient()->m_Effects.FreezingFlakes(BodyPos, vec2(32, 32), Alpha); } + if(ClientId < 0) + return; + int QuadOffsetToEmoticon = NUM_WEAPONS * 2 + 2 + 2; - if((Player.m_PlayerFlags & PLAYERFLAG_CHATTING) && !m_pClient->m_aClients[ClientID].m_Afk) + if((Player.m_PlayerFlags & PLAYERFLAG_CHATTING) && !m_pClient->m_aClients[ClientId].m_Afk) { int CurEmoticon = (SPRITE_DOTDOT - SPRITE_OOP); Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[CurEmoticon]); @@ -750,10 +759,7 @@ void CPlayers::RenderPlayer( Graphics()->QuadsSetRotation(0); } - if(ClientID < 0) - return; - - if(g_Config.m_ClAfkEmote && m_pClient->m_aClients[ClientID].m_Afk && !(Client()->DummyConnected() && ClientID == m_pClient->m_aLocalIDs[!g_Config.m_ClDummy])) + if(g_Config.m_ClAfkEmote && m_pClient->m_aClients[ClientId].m_Afk && !(Client()->DummyConnected() && ClientId == m_pClient->m_aLocalIds[!g_Config.m_ClDummy])) { int CurEmoticon = (SPRITE_ZZZ - SPRITE_OOP); Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[CurEmoticon]); @@ -765,9 +771,9 @@ void CPlayers::RenderPlayer( Graphics()->QuadsSetRotation(0); } - if(g_Config.m_ClShowEmotes && !m_pClient->m_aClients[ClientID].m_EmoticonIgnore && m_pClient->m_aClients[ClientID].m_EmoticonStartTick != -1) + if(g_Config.m_ClShowEmotes && !m_pClient->m_aClients[ClientId].m_EmoticonIgnore && m_pClient->m_aClients[ClientId].m_EmoticonStartTick != -1) { - float SinceStart = (Client()->GameTick(g_Config.m_ClDummy) - m_pClient->m_aClients[ClientID].m_EmoticonStartTick) + (Client()->IntraGameTickSincePrev(g_Config.m_ClDummy) - m_pClient->m_aClients[ClientID].m_EmoticonStartFraction); + float SinceStart = (Client()->GameTick(g_Config.m_ClDummy) - m_pClient->m_aClients[ClientId].m_EmoticonStartTick) + (Client()->IntraGameTickSincePrev(g_Config.m_ClDummy) - m_pClient->m_aClients[ClientId].m_EmoticonStartFraction); float FromEnd = (2 * Client()->GameTickSpeed()) - SinceStart; if(0 <= SinceStart && FromEnd > 0) @@ -791,8 +797,8 @@ void CPlayers::RenderPlayer( Graphics()->SetColor(1.0f, 1.0f, 1.0f, a * Alpha); // client_datas::emoticon is an offset from the first emoticon - int QuadOffset = QuadOffsetToEmoticon + m_pClient->m_aClients[ClientID].m_Emoticon; - Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[m_pClient->m_aClients[ClientID].m_Emoticon]); + int QuadOffset = QuadOffsetToEmoticon + m_pClient->m_aClients[ClientId].m_Emoticon; + Graphics()->TextureSet(GameClient()->m_EmoticonsSkin.m_aSpriteEmoticons[m_pClient->m_aClients[ClientId].m_Emoticon]); Graphics()->RenderQuadContainerAsSprite(m_WeaponEmoteQuadContainerIndex, QuadOffset, Position.x, Position.y - 23.f - 32.f * h, 1.f, (64.f * h) / 64.f); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); @@ -801,15 +807,18 @@ void CPlayers::RenderPlayer( } } -inline bool CPlayers::IsPlayerInfoAvailable(int ClientID) const +inline bool CPlayers::IsPlayerInfoAvailable(int ClientId) const { - const void *pPrevInfo = Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_PLAYERINFO, ClientID); - const void *pInfo = Client()->SnapFindItem(IClient::SNAP_CURRENT, NETOBJTYPE_PLAYERINFO, ClientID); + const void *pPrevInfo = Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_PLAYERINFO, ClientId); + const void *pInfo = Client()->SnapFindItem(IClient::SNAP_CURRENT, NETOBJTYPE_PLAYERINFO, ClientId); return pPrevInfo && pInfo; } void CPlayers::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + CTeeRenderInfo aRenderInfo[MAX_CLIENTS]; // update RenderInfo for ninja @@ -819,41 +828,23 @@ void CPlayers::OnRender() for(int i = 0; i < MAX_CLIENTS; ++i) { aRenderInfo[i] = m_pClient->m_aClients[i].m_RenderInfo; - //aRenderInfo[i].m_ShineDecoration = m_pClient->m_aClients[i].m_LiveFrozen; - aRenderInfo[i].m_TeeRenderFlags = 0; - - CGameClient::CSnapState::CCharacterInfo &CharacterInfo = m_pClient->m_Snap.m_aCharacters[i]; - const bool Frozen = CharacterInfo.m_HasExtendedData && CharacterInfo.m_ExtendedData.m_FreezeEnd != 0; - if(g_Config.m_ClOldFreezeMode == 1) - { - if(m_pClient->m_aClients[i].m_FreezeEnd != 0) - { - aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN; - } - } - else - { - if(m_pClient->m_aClients[i].m_FreezeEnd != 0) - { - aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN | TEE_NO_WEAPON; - } - } + if(m_pClient->m_aClients[i].m_FreezeEnd != 0) + aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN | TEE_NO_WEAPON; if(m_pClient->m_aClients[i].m_LiveFrozen) - { aRenderInfo[i].m_TeeRenderFlags |= TEE_EFFECT_FROZEN; - } + const CGameClient::CSnapState::CCharacterInfo &CharacterInfo = m_pClient->m_Snap.m_aCharacters[i]; + const bool Frozen = CharacterInfo.m_HasExtendedData && CharacterInfo.m_ExtendedData.m_FreezeEnd != 0; if((CharacterInfo.m_Cur.m_Weapon == WEAPON_NINJA || (Frozen && !m_pClient->m_GameInfo.m_NoSkinChangeForFrozen)) && g_Config.m_ClShowNinja) { // change the skin for the player to the ninja const auto *pSkin = m_pClient->m_Skins.FindOrNullptr("x_ninja"); if(pSkin != nullptr) { - aRenderInfo[i].m_OriginalRenderSkin = pSkin->m_OriginalSkin; - aRenderInfo[i].m_ColorableRenderSkin = pSkin->m_ColorableSkin; - aRenderInfo[i].m_BloodColor = pSkin->m_BloodColor; - aRenderInfo[i].m_SkinMetrics = pSkin->m_Metrics; + aRenderInfo[i].m_aSixup[g_Config.m_ClDummy].Reset(); + + aRenderInfo[i].Apply(pSkin); aRenderInfo[i].m_CustomColoredSkin = IsTeamplay; if(!IsTeamplay) { @@ -863,16 +854,11 @@ void CPlayers::OnRender() } } } - - const CSkin *pSkin = m_pClient->m_Skins.Find("x_spec"); CTeeRenderInfo RenderInfoSpec; - RenderInfoSpec.m_OriginalRenderSkin = pSkin->m_OriginalSkin; - RenderInfoSpec.m_ColorableRenderSkin = pSkin->m_ColorableSkin; - RenderInfoSpec.m_BloodColor = pSkin->m_BloodColor; - RenderInfoSpec.m_SkinMetrics = pSkin->m_Metrics; + RenderInfoSpec.Apply(m_pClient->m_Skins.Find("x_spec")); RenderInfoSpec.m_CustomColoredSkin = false; RenderInfoSpec.m_Size = 64.0f; - const int LocalClientID = m_pClient->m_Snap.m_LocalClientID; + const int LocalClientId = m_pClient->m_Snap.m_LocalClientId; // get screen edges to avoid rendering offscreen float ScreenX0, ScreenY0, ScreenX1, ScreenY1; @@ -888,56 +874,55 @@ void CPlayers::OnRender() ScreenY1 += BorderBuffer; // render everyone else's hook, then our own - for(int ClientID = 0; ClientID < MAX_CLIENTS; ClientID++) + for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) { - if(ClientID == LocalClientID || !m_pClient->m_Snap.m_aCharacters[ClientID].m_Active || !IsPlayerInfoAvailable(ClientID)) + if(ClientId == LocalClientId || !m_pClient->m_Snap.m_aCharacters[ClientId].m_Active || !IsPlayerInfoAvailable(ClientId)) { continue; } - - RenderHook(&m_pClient->m_aClients[ClientID].m_RenderPrev, &m_pClient->m_aClients[ClientID].m_RenderCur, &aRenderInfo[ClientID], ClientID); + RenderHook(&m_pClient->m_aClients[ClientId].m_RenderPrev, &m_pClient->m_aClients[ClientId].m_RenderCur, &aRenderInfo[ClientId], ClientId); } - if(LocalClientID != -1 && m_pClient->m_Snap.m_aCharacters[LocalClientID].m_Active && IsPlayerInfoAvailable(LocalClientID)) + if(LocalClientId != -1 && m_pClient->m_Snap.m_aCharacters[LocalClientId].m_Active && IsPlayerInfoAvailable(LocalClientId)) { - const CGameClient::CClientData *pLocalClientData = &m_pClient->m_aClients[LocalClientID]; - RenderHook(&pLocalClientData->m_RenderPrev, &pLocalClientData->m_RenderCur, &aRenderInfo[LocalClientID], LocalClientID); + const CGameClient::CClientData *pLocalClientData = &m_pClient->m_aClients[LocalClientId]; + RenderHook(&pLocalClientData->m_RenderPrev, &pLocalClientData->m_RenderCur, &aRenderInfo[LocalClientId], LocalClientId); } // render spectating players - for(auto &m_aClient : m_pClient->m_aClients) + for(auto &Client : m_pClient->m_aClients) { - if(!m_aClient.m_SpecCharPresent) + if(!Client.m_SpecCharPresent) { continue; } - RenderTools()->RenderTee(CAnimState::GetIdle(), &RenderInfoSpec, EMOTE_BLINK, vec2(1, 0), m_aClient.m_SpecChar); + RenderTools()->RenderTee(CAnimState::GetIdle(), &RenderInfoSpec, EMOTE_BLINK, vec2(1, 0), Client.m_SpecChar); } // render everyone else's tee, then either our own or the tee we are spectating. - const int RenderLastID = (m_pClient->m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW && m_pClient->m_Snap.m_SpecInfo.m_Active) ? m_pClient->m_Snap.m_SpecInfo.m_SpectatorID : LocalClientID; + const int RenderLastId = (m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW && m_pClient->m_Snap.m_SpecInfo.m_Active) ? m_pClient->m_Snap.m_SpecInfo.m_SpectatorId : LocalClientId; - for(int ClientID = 0; ClientID < MAX_CLIENTS; ClientID++) + for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) { - if(ClientID == RenderLastID || !m_pClient->m_Snap.m_aCharacters[ClientID].m_Active || !IsPlayerInfoAvailable(ClientID)) + if(ClientId == RenderLastId || !m_pClient->m_Snap.m_aCharacters[ClientId].m_Active || !IsPlayerInfoAvailable(ClientId)) { continue; } - RenderHookCollLine(&m_pClient->m_aClients[ClientID].m_RenderPrev, &m_pClient->m_aClients[ClientID].m_RenderCur, ClientID); + RenderHookCollLine(&m_pClient->m_aClients[ClientId].m_RenderPrev, &m_pClient->m_aClients[ClientId].m_RenderCur, ClientId); // don't render offscreen - vec2 *pRenderPos = &m_pClient->m_aClients[ClientID].m_RenderPos; + vec2 *pRenderPos = &m_pClient->m_aClients[ClientId].m_RenderPos; if(pRenderPos->x < ScreenX0 || pRenderPos->x > ScreenX1 || pRenderPos->y < ScreenY0 || pRenderPos->y > ScreenY1) { continue; } - RenderPlayer(&m_pClient->m_aClients[ClientID].m_RenderPrev, &m_pClient->m_aClients[ClientID].m_RenderCur, &aRenderInfo[ClientID], ClientID); + RenderPlayer(&m_pClient->m_aClients[ClientId].m_RenderPrev, &m_pClient->m_aClients[ClientId].m_RenderCur, &aRenderInfo[ClientId], ClientId); } - if(RenderLastID != -1 && m_pClient->m_Snap.m_aCharacters[RenderLastID].m_Active && IsPlayerInfoAvailable(RenderLastID)) + if(RenderLastId != -1 && m_pClient->m_Snap.m_aCharacters[RenderLastId].m_Active && IsPlayerInfoAvailable(RenderLastId)) { - const CGameClient::CClientData *pClientData = &m_pClient->m_aClients[RenderLastID]; - RenderHookCollLine(&pClientData->m_RenderPrev, &pClientData->m_RenderCur, RenderLastID); - RenderPlayer(&pClientData->m_RenderPrev, &pClientData->m_RenderCur, &aRenderInfo[RenderLastID], RenderLastID); + const CGameClient::CClientData *pClientData = &m_pClient->m_aClients[RenderLastId]; + RenderHookCollLine(&pClientData->m_RenderPrev, &pClientData->m_RenderCur, RenderLastId); + RenderPlayer(&pClientData->m_RenderPrev, &pClientData->m_RenderCur, &aRenderInfo[RenderLastId], RenderLastId); } } @@ -1012,4 +997,4 @@ void CPlayers::OnInit() Graphics()->QuadsSetSubset(0.f, 0.f, 1.f, 1.f); Graphics()->QuadsSetRotation(0.f); -} \ No newline at end of file +} diff --git a/src/game/client/components/players.h b/src/game/client/components/players.h index 00c66d6ae3..b20df35185 100644 --- a/src/game/client/components/players.h +++ b/src/game/client/components/players.h @@ -11,34 +11,39 @@ class CPlayers : public CComponent { friend class CGhost; + void RenderHand6(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha = 1.0f); + void RenderHand7(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha = 1.0f); + void RenderHand(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha = 1.0f); void RenderPlayer( const CNetObj_Character *pPrevChar, const CNetObj_Character *pPlayerChar, const CTeeRenderInfo *pRenderInfo, - int ClientID, + int ClientId, float Intra = 0.f); void RenderHook( const CNetObj_Character *pPrevChar, const CNetObj_Character *pPlayerChar, const CTeeRenderInfo *pRenderInfo, - int ClientID, + int ClientId, float Intra = 0.f); void RenderHookCollLine( const CNetObj_Character *pPrevChar, const CNetObj_Character *pPlayerChar, - int ClientID, + int ClientId, float Intra = 0.f); - bool IsPlayerInfoAvailable(int ClientID) const; + bool IsPlayerInfoAvailable(int ClientId) const; int m_WeaponEmoteQuadContainerIndex; int m_aWeaponSpriteMuzzleQuadContainerIndex[NUM_WEAPONS]; + int64_t m_SkidSoundTime = 0; + public: float GetPlayerTargetAngle( const CNetObj_Character *pPrevChar, const CNetObj_Character *pPlayerChar, - int ClientID, + int ClientId, float Intra = 0.f); virtual int Sizeof() const override { return sizeof(*this); } diff --git a/src/game/client/components/race_demo.cpp b/src/game/client/components/race_demo.cpp index 4f57bc5b45..d1de5ef46a 100644 --- a/src/game/client/components/race_demo.cpp +++ b/src/game/client/components/race_demo.cpp @@ -29,7 +29,7 @@ struct CDemoListParam { const CRaceDemo *m_pThis; std::vector *m_pvDemos; - const char *pMap; + const char *m_pMap; }; CRaceDemo::CRaceDemo() : @@ -79,7 +79,7 @@ void CRaceDemo::OnNewSnapshot() vec2 PrevPos = vec2(m_pClient->m_Snap.m_pLocalPrevCharacter->m_X, m_pClient->m_Snap.m_pLocalPrevCharacter->m_Y); vec2 Pos = vec2(m_pClient->m_Snap.m_pLocalCharacter->m_X, m_pClient->m_Snap.m_pLocalCharacter->m_Y); - if(ForceStart || (!ServerControl && CRaceHelper::IsStart(m_pClient, PrevPos, Pos))) + if(ForceStart || (!ServerControl && GameClient()->RaceHelper()->IsStart(PrevPos, Pos))) { if(m_RaceState == RACE_STARTED) Client()->RaceRecord_Stop(); @@ -132,7 +132,7 @@ void CRaceDemo::OnMessage(int MsgType, void *pRawMsg) if(MsgType == NETMSGTYPE_SV_KILLMSG) { CNetMsg_Sv_KillMsg *pMsg = (CNetMsg_Sv_KillMsg *)pRawMsg; - if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalClientID && Client()->RaceRecord_IsRecording()) + if(pMsg->m_Victim == m_pClient->m_Snap.m_LocalClientId && Client()->RaceRecord_IsRecording()) StopRecord(m_Time); } else if(MsgType == NETMSGTYPE_SV_KILLMSGTEAM) @@ -140,18 +140,18 @@ void CRaceDemo::OnMessage(int MsgType, void *pRawMsg) CNetMsg_Sv_KillMsgTeam *pMsg = (CNetMsg_Sv_KillMsgTeam *)pRawMsg; for(int i = 0; i < MAX_CLIENTS; i++) { - if(m_pClient->m_Teams.Team(i) == pMsg->m_Team && i == m_pClient->m_Snap.m_LocalClientID && Client()->RaceRecord_IsRecording()) + if(m_pClient->m_Teams.Team(i) == pMsg->m_Team && i == m_pClient->m_Snap.m_LocalClientId && Client()->RaceRecord_IsRecording()) StopRecord(m_Time); } } else if(MsgType == NETMSGTYPE_SV_CHAT) { CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; - if(pMsg->m_ClientID == -1 && m_RaceState == RACE_STARTED) + if(pMsg->m_ClientId == -1 && m_RaceState == RACE_STARTED) { char aName[MAX_NAME_LENGTH]; int Time = CRaceHelper::TimeFromFinishMessage(pMsg->m_pMessage, aName, sizeof(aName)); - if(Time > 0 && m_pClient->m_Snap.m_LocalClientID >= 0 && str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientID].m_aName) == 0) + if(Time > 0 && m_pClient->m_Snap.m_LocalClientId >= 0 && str_comp(aName, m_pClient->m_aClients[m_pClient->m_Snap.m_LocalClientId].m_aName) == 0) { m_RaceState = RACE_FINISHED; m_RecordStopTick = Client()->GameTick(g_Config.m_ClDummy) + Client()->GameTickSpeed(); @@ -203,8 +203,8 @@ int CRaceDemo::RaceDemolistFetchCallback(const CFsFileInfo *pInfo, int IsDir, in { auto *pRealUser = (SRaceDemoFetchUser *)pUser; auto *pParam = pRealUser->m_pParam; - int MapLen = str_length(pParam->pMap); - if(IsDir || !str_endswith(pInfo->m_pName, ".demo") || !str_startswith(pInfo->m_pName, pParam->pMap) || pInfo->m_pName[MapLen] != '_') + int MapLen = str_length(pParam->m_pMap); + if(IsDir || !str_endswith(pInfo->m_pName, ".demo") || !str_startswith(pInfo->m_pName, pParam->m_pMap) || pInfo->m_pName[MapLen] != '_') return 0; CDemoItem Item; diff --git a/src/game/client/components/scoreboard.cpp b/src/game/client/components/scoreboard.cpp index 6be6adadbb..33d2fd4527 100644 --- a/src/game/client/components/scoreboard.cpp +++ b/src/game/client/components/scoreboard.cpp @@ -1,9 +1,12 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include "scoreboard.h" + #include #include #include #include + #include #include @@ -12,11 +15,10 @@ #include #include #include - +#include +#include #include -#include "scoreboard.h" - CScoreboard::CScoreboard() { OnReset(); @@ -24,10 +26,15 @@ CScoreboard::CScoreboard() void CScoreboard::ConKeyScoreboard(IConsole::IResult *pResult, void *pUserData) { - CScoreboard *pSelf = (CScoreboard *)pUserData; + CScoreboard *pSelf = static_cast(pUserData); pSelf->m_Active = pResult->GetInteger(0) != 0; } +void CScoreboard::OnConsoleInit() +{ + Console()->Register("+scoreboard", "", CFGFLAG_CLIENT, ConKeyScoreboard, this, "Show scoreboard"); +} + void CScoreboard::OnReset() { m_Active = false; @@ -41,216 +48,241 @@ void CScoreboard::OnRelease() void CScoreboard::OnMessage(int MsgType, void *pRawMsg) { - if(MsgType == NETMSGTYPE_SV_RECORD || MsgType == NETMSGTYPE_SV_RECORDLEGACY) + if(MsgType == NETMSGTYPE_SV_RECORD) { - CNetMsg_Sv_Record *pMsg = (CNetMsg_Sv_Record *)pRawMsg; - m_ServerRecord = (float)pMsg->m_ServerTimeBest / 100; + CNetMsg_Sv_Record *pMsg = static_cast(pRawMsg); + m_ServerRecord = pMsg->m_ServerTimeBest / 100.0f; + } + else if(MsgType == NETMSGTYPE_SV_RECORDLEGACY) + { + CNetMsg_Sv_RecordLegacy *pMsg = static_cast(pRawMsg); + m_ServerRecord = pMsg->m_ServerTimeBest / 100.0f; } } -void CScoreboard::OnConsoleInit() -{ - Console()->Register("+scoreboard", "", CFGFLAG_CLIENT, ConKeyScoreboard, this, "Show scoreboard"); -} - -void CScoreboard::RenderGoals(float x, float y, float w) +void CScoreboard::RenderTitle(CUIRect TitleBar, int Team, const char *pTitle) { - float h = 50.0f; + dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid"); - Graphics()->DrawRect(x, y, w, h, ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 10.0f); + const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; - // render goals - if(m_pClient->m_Snap.m_pGameInfoObj) + char aScore[128] = ""; + if(GameClient()->m_GameInfo.m_TimeScore) + { + if(m_ServerRecord > 0) + { + str_time_float(m_ServerRecord, TIME_HOURS, aScore, sizeof(aScore)); + } + } + else if(pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS)) { - if(m_pClient->m_Snap.m_pGameInfoObj->m_ScoreLimit) + const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj; + if(pGameDataObj) { - char aBuf[64]; - str_format(aBuf, sizeof(aBuf), "%s: %d", Localize("Score limit"), m_pClient->m_Snap.m_pGameInfoObj->m_ScoreLimit); - TextRender()->Text(x + 10.0f, y + (h - 20.f) / 2.f, 20.0f, aBuf, -1.0f); + str_format(aScore, sizeof(aScore), "%d", Team == TEAM_RED ? pGameDataObj->m_TeamscoreRed : pGameDataObj->m_TeamscoreBlue); } - if(m_pClient->m_Snap.m_pGameInfoObj->m_TimeLimit) + } + else + { + if(GameClient()->m_Snap.m_SpecInfo.m_Active && + GameClient()->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW && + GameClient()->m_Snap.m_apPlayerInfos[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId]) { - char aBuf[64]; - str_format(aBuf, sizeof(aBuf), Localize("Time limit: %d min"), m_pClient->m_Snap.m_pGameInfoObj->m_TimeLimit); - TextRender()->Text(x + 230.0f, y + (h - 20.f) / 2.f, 20.0f, aBuf, -1.0f); + str_format(aScore, sizeof(aScore), "%d", GameClient()->m_Snap.m_apPlayerInfos[GameClient()->m_Snap.m_SpecInfo.m_SpectatorId]->m_Score); } - if(m_pClient->m_Snap.m_pGameInfoObj->m_RoundNum && m_pClient->m_Snap.m_pGameInfoObj->m_RoundCurrent) + else if(GameClient()->m_Snap.m_pLocalInfo) { - char aBuf[64]; - str_format(aBuf, sizeof(aBuf), "%s %d/%d", Localize("Round"), m_pClient->m_Snap.m_pGameInfoObj->m_RoundCurrent, m_pClient->m_Snap.m_pGameInfoObj->m_RoundNum); - float tw = TextRender()->TextWidth(20.0f, aBuf, -1, -1.0f); - TextRender()->Text(x + w - tw - 10.0f, y + (h - 20.f) / 2.f, 20.0f, aBuf, -1.0f); + str_format(aScore, sizeof(aScore), "%d", GameClient()->m_Snap.m_pLocalInfo->m_Score); } } -} -void CScoreboard::RenderSpectators(float x, float y, float w, float h) -{ - // background - Graphics()->DrawRect(x, y, w, h, ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 10.0f); + const float TitleFontSize = 40.0f; + const float ScoreTextWidth = TextRender()->TextWidth(TitleFontSize, aScore); - // Headline - y += 10.0f; - TextRender()->Text(x + 10.0f, y + (30.f - 28.f) / 2.f, 28.0f, Localize("Spectators"), w - 20.0f); - - // spectator names - y += 30.0f; - bool Multiple = false; + TitleBar.VMargin(20.0f, &TitleBar); + CUIRect TitleLabel, ScoreLabel; + if(Team == TEAM_RED) + { + TitleBar.VSplitRight(ScoreTextWidth, &TitleLabel, &ScoreLabel); + TitleLabel.VSplitRight(10.0f, &TitleLabel, nullptr); + } + else + { + TitleBar.VSplitLeft(ScoreTextWidth, &ScoreLabel, &TitleLabel); + TitleLabel.VSplitLeft(10.0f, nullptr, &TitleLabel); + } - CTextCursor Cursor; - TextRender()->SetCursor(&Cursor, x + 10.0f, y, 22.0f, TEXTFLAG_RENDER); - Cursor.m_LineWidth = w - 20.0f; - Cursor.m_MaxLines = 4; + { + SLabelProperties Props; + Props.m_MaxWidth = TitleLabel.w; + Props.m_EllipsisAtEnd = true; + Ui()->DoLabel(&TitleLabel, pTitle, TitleFontSize, Team == TEAM_RED ? TEXTALIGN_ML : TEXTALIGN_MR, Props); + } - for(const auto *pInfo : m_pClient->m_Snap.m_apInfoByName) + if(aScore[0] != '\0') { - if(!pInfo || pInfo->m_Team != TEAM_SPECTATORS) - continue; + Ui()->DoLabel(&ScoreLabel, aScore, TitleFontSize, Team == TEAM_RED ? TEXTALIGN_MR : TEXTALIGN_ML); + } +} - if(Multiple) - TextRender()->TextEx(&Cursor, ", ", 2); +void CScoreboard::RenderGoals(CUIRect Goals) +{ + Goals.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 15.0f); + Goals.VMargin(10.0f, &Goals); - if(m_pClient->m_aClients[pInfo->m_ClientID].m_AuthLevel) - { - ColorRGBA Color = color_cast(ColorHSLA(g_Config.m_ClAuthedPlayerColor)); - TextRender()->TextColor(Color); - } + const float FontSize = 20.0f; + const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; + char aBuf[64]; - if(g_Config.m_ClShowIDs) - { - char aBuffer[5]; - int size = str_format(aBuffer, sizeof(aBuffer), "%d: ", pInfo->m_ClientID); - TextRender()->TextEx(&Cursor, aBuffer, size); - } - TextRender()->TextEx(&Cursor, m_pClient->m_aClients[pInfo->m_ClientID].m_aName, -1); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + if(pGameInfoObj->m_ScoreLimit) + { + str_format(aBuf, sizeof(aBuf), "%s: %d", Localize("Score limit"), pGameInfoObj->m_ScoreLimit); + Ui()->DoLabel(&Goals, aBuf, FontSize, TEXTALIGN_ML); + } + + if(pGameInfoObj->m_TimeLimit) + { + str_format(aBuf, sizeof(aBuf), Localize("Time limit: %d min"), pGameInfoObj->m_TimeLimit); + Ui()->DoLabel(&Goals, aBuf, FontSize, TEXTALIGN_MC); + } - Multiple = true; + if(pGameInfoObj->m_RoundNum && pGameInfoObj->m_RoundCurrent) + { + str_format(aBuf, sizeof(aBuf), Localize("Round %d/%d"), pGameInfoObj->m_RoundCurrent, pGameInfoObj->m_RoundNum); + Ui()->DoLabel(&Goals, aBuf, FontSize, TEXTALIGN_MR); } } -void CScoreboard::RenderScoreboard(float x, float y, float w, int Team, const char *pTitle, int NumPlayers) +void CScoreboard::RenderSpectators(CUIRect Spectators) { - if(Team == TEAM_SPECTATORS) - return; + Spectators.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 15.0f); + Spectators.Margin(10.0f, &Spectators); + + CTextCursor Cursor; + TextRender()->SetCursor(&Cursor, Spectators.x, Spectators.y, 22.0f, TEXTFLAG_RENDER); + Cursor.m_LineWidth = Spectators.w; + Cursor.m_MaxLines = round_truncate(Spectators.h / Cursor.m_FontSize); - bool lower16 = false; - bool upper16 = false; - bool lower24 = false; - bool upper24 = false; - bool lower32 = false; - bool upper32 = false; - - if(Team == -3) - upper16 = true; - else if(Team == -4) - lower32 = true; - else if(Team == -5) - upper32 = true; - else if(Team == -6) - lower16 = true; - else if(Team == -7) - lower24 = true; - else if(Team == -8) - upper24 = true; - - bool IsTeamplayTeam = Team > TEAM_SPECTATORS; - - if(Team < -1) - Team = 0; - - if(NumPlayers < 0) - NumPlayers = m_pClient->m_Snap.m_aTeamSize[Team]; - - float h = 760.0f; - - // background + int RemainingSpectators = 0; + for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByName) { - int Corners; - if(upper16 || upper32 || upper24) - Corners = IGraphics::CORNER_R; - else if(lower16 || lower32 || lower24) - Corners = IGraphics::CORNER_L; - else - Corners = IGraphics::CORNER_ALL; - Graphics()->DrawRect(x, y, w, h, ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), Corners, 17.0f); + if(!pInfo || pInfo->m_Team != TEAM_SPECTATORS) + continue; + ++RemainingSpectators; } - char aBuf[128] = {0}; + TextRender()->TextEx(&Cursor, Localize("Spectators")); - // render title - float TitleFontsize = 40.0f; - int TitleWidth = (lower32 || lower24 || lower16) ? 1140 : 440; - if(!pTitle) + if(RemainingSpectators > 0) { - if(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) - pTitle = Localize("Game over"); - else - { - str_copy(aBuf, Client()->GetCurrentMap()); - while(TextRender()->TextWidth(TitleFontsize, aBuf, -1, -1.0f) > TitleWidth) - aBuf[str_length(aBuf) - 1] = '\0'; - if(str_comp(aBuf, Client()->GetCurrentMap())) - str_append(aBuf, "…"); - pTitle = aBuf; - } + TextRender()->TextEx(&Cursor, ": "); } - TextRender()->Text(x + 20.0f, y + (50.f - TitleFontsize) / 2.f, TitleFontsize, pTitle, -1.0f); - if(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS) + bool CommaNeeded = false; + for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByName) { - if(m_pClient->m_Snap.m_pGameDataObj) + if(!pInfo || pInfo->m_Team != TEAM_SPECTATORS) + continue; + + if(CommaNeeded) { - int Score = Team == TEAM_RED ? m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreRed : m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreBlue; - str_from_int(Score, aBuf); + TextRender()->TextEx(&Cursor, ", "); } - } - else - { - if(m_pClient->m_Snap.m_SpecInfo.m_Active && m_pClient->m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW && - m_pClient->m_Snap.m_apPlayerInfos[m_pClient->m_Snap.m_SpecInfo.m_SpectatorID]) + + if(Cursor.m_LineCount == Cursor.m_MaxLines && RemainingSpectators >= 2) { - int Score = m_pClient->m_Snap.m_apPlayerInfos[m_pClient->m_Snap.m_SpecInfo.m_SpectatorID]->m_Score; - str_from_int(Score, aBuf); + // This is less expensive than checking with a separate invisible + // text cursor though we waste some space at the end of the line. + char aRemaining[64]; + str_format(aRemaining, sizeof(aRemaining), Localize("%d others…", "Spectators"), RemainingSpectators); + TextRender()->TextEx(&Cursor, aRemaining); + break; } - else if(m_pClient->m_Snap.m_pLocalInfo) + + if(g_Config.m_ClShowIds) { - int Score = m_pClient->m_Snap.m_pLocalInfo->m_Score; - str_from_int(Score, aBuf); + char aClientId[16]; + GameClient()->FormatClientId(pInfo->m_ClientId, aClientId, EClientIdFormat::NO_INDENT); + TextRender()->TextEx(&Cursor, aClientId); } - } - if(m_pClient->m_GameInfo.m_TimeScore && g_Config.m_ClDDRaceScoreBoard) - { - if(m_ServerRecord > 0) - str_time_float(m_ServerRecord, TIME_HOURS, aBuf, sizeof(aBuf)); - else - aBuf[0] = 0; - } + { + const char *pClanName = GameClient()->m_aClients[pInfo->m_ClientId].m_aClan; + + if(pClanName[0] != '\0') + { + if(str_comp(pClanName, GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_aClan) == 0) + { + TextRender()->TextColor(color_cast(ColorHSLA(g_Config.m_ClSameClanColor))); + } + else + { + TextRender()->TextColor(ColorRGBA(0.7f, 0.7f, 0.7f)); + } - float tw; + TextRender()->TextEx(&Cursor, pClanName); + TextRender()->TextEx(&Cursor, " "); - if(!lower16 && !lower32 && !lower24) - { - tw = TextRender()->TextWidth(TitleFontsize, aBuf, -1, -1.0f); - TextRender()->Text(x + w - tw - 20.0f, y + (50.f - TitleFontsize) / 2.f, TitleFontsize, aBuf, -1.0f); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + } + + if(GameClient()->m_aClients[pInfo->m_ClientId].m_AuthLevel) + { + TextRender()->TextColor(color_cast(ColorHSLA(g_Config.m_ClAuthedPlayerColor))); + } + + TextRender()->TextEx(&Cursor, GameClient()->m_aClients[pInfo->m_ClientId].m_aName); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + + CommaNeeded = true; + --RemainingSpectators; } +} + +void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, int CountEnd, CScoreboardRenderState &State) +{ + dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid"); + + const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; + const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj; + const bool TimeScore = GameClient()->m_GameInfo.m_TimeScore; + const int NumPlayers = CountEnd - CountStart; + const bool LowScoreboardWidth = Scoreboard.w < 700.0f; + + bool Race7 = Client()->IsSixup() && pGameInfoObj && pGameInfoObj->m_GameFlags & protocol7::GAMEFLAG_RACE; // calculate measurements - float LineHeight = 60.0f; - float TeeSizeMod = 1.0f; - float Spacing = 16.0f; - float RoundRadius = 15.0f; - float FontSize = 24.0f; - if(NumPlayers > 48) + float LineHeight; + float TeeSizeMod; + float Spacing; + float RoundRadius; + float FontSize; + if(NumPlayers <= 8) { - LineHeight = 20.0f; - TeeSizeMod = 0.4f; + LineHeight = 60.0f; + TeeSizeMod = 1.0f; + Spacing = 16.0f; + RoundRadius = 10.0f; + FontSize = 24.0f; + } + else if(NumPlayers <= 12) + { + LineHeight = 50.0f; + TeeSizeMod = 0.9f; + Spacing = 5.0f; + RoundRadius = 10.0f; + FontSize = 24.0f; + } + else if(NumPlayers <= 16) + { + LineHeight = 40.0f; + TeeSizeMod = 0.8f; Spacing = 0.0f; RoundRadius = 5.0f; - FontSize = 16.0f; + FontSize = 24.0f; } - else if(NumPlayers > 32) + else if(NumPlayers <= 24) { LineHeight = 27.0f; TeeSizeMod = 0.6f; @@ -258,308 +290,304 @@ void CScoreboard::RenderScoreboard(float x, float y, float w, int Team, const ch RoundRadius = 5.0f; FontSize = 20.0f; } - else if(NumPlayers > 12) + else if(NumPlayers <= 32) { - LineHeight = 40.0f; - TeeSizeMod = 0.8f; + LineHeight = 20.0f; + TeeSizeMod = 0.4f; Spacing = 0.0f; - RoundRadius = 15.0f; + RoundRadius = 5.0f; + FontSize = 16.0f; } - else if(NumPlayers > 8) + else if(LowScoreboardWidth) { - LineHeight = 50.0f; - TeeSizeMod = 0.9f; - Spacing = 5.0f; - RoundRadius = 15.0f; + LineHeight = 15.0f; + TeeSizeMod = 0.25f; + Spacing = 0.0f; + RoundRadius = 2.0f; + FontSize = 14.0f; + } + else + { + LineHeight = 10.0f; + TeeSizeMod = 0.2f; + Spacing = 0.0f; + RoundRadius = 2.0f; + FontSize = 10.0f; } - float ScoreOffset = x + 10.0f + 10.0f, ScoreLength = TextRender()->TextWidth(FontSize, "00:00:00", -1, -1.0f); - if(IsTeamplayTeam) - ScoreLength = TextRender()->TextWidth(FontSize, "99999", -1, -1.0f); - float TeeOffset = ScoreOffset + ScoreLength + 15.0f, TeeLength = 60 * TeeSizeMod; - float NameOffset = TeeOffset + TeeLength, NameLength = 300.0f - TeeLength; - float CountryLength = (LineHeight - Spacing - TeeSizeMod * 5.0f) * 2.0f; - float PingLength = 65.0f; - float PingOffset = x + w - PingLength - 10.0f - 10.0f; - float CountryOffset = PingOffset - CountryLength; - float ClanLength = w - ((NameOffset - x) + NameLength) - (w - (CountryOffset - x)); - float ClanOffset = CountryOffset - ClanLength; + const float ScoreOffset = Scoreboard.x + 40.0f; + const float ScoreLength = TextRender()->TextWidth(FontSize, TimeScore ? "00:00:00" : "99999"); + const float TeeOffset = ScoreOffset + ScoreLength + 20.0f; + const float TeeLength = 60.0f * TeeSizeMod; + const float NameOffset = TeeOffset + TeeLength; + const float NameLength = (LowScoreboardWidth ? 180.0f : 300.0f) - TeeLength; + const float CountryLength = (LineHeight - Spacing - TeeSizeMod * 5.0f) * 2.0f; + const float PingLength = 55.0f; + const float PingOffset = Scoreboard.x + Scoreboard.w - PingLength - 20.0f; + const float CountryOffset = PingOffset - CountryLength; + const float ClanOffset = NameOffset + NameLength + 5.0f; + const float ClanLength = CountryOffset - ClanOffset - 5.0f; // render headlines - x += 10.0f; - y += 50.0f; - float HeadlineFontsize = 22.0f; - const char *pScore = (m_pClient->m_GameInfo.m_TimeScore && g_Config.m_ClDDRaceScoreBoard) ? Localize("Time") : Localize("Score"); - tw = TextRender()->TextWidth(HeadlineFontsize, pScore, -1, -1.0f); - TextRender()->Text(ScoreOffset + ScoreLength - tw, y + (HeadlineFontsize * 2.f - HeadlineFontsize) / 2.f, HeadlineFontsize, pScore, -1.0f); - - TextRender()->Text(NameOffset, y + (HeadlineFontsize * 2.f - HeadlineFontsize) / 2.f, HeadlineFontsize, Localize("Name"), -1.0f); - - tw = TextRender()->TextWidth(HeadlineFontsize, Localize("Clan"), -1, -1.0f); - TextRender()->Text(ClanOffset + (ClanLength - tw) / 2, y + (HeadlineFontsize * 2.f - HeadlineFontsize) / 2.f, HeadlineFontsize, Localize("Clan"), -1.0f); - - tw = TextRender()->TextWidth(HeadlineFontsize, Localize("Ping"), -1, -1.0f); - TextRender()->Text(PingOffset + PingLength - tw, y + (HeadlineFontsize * 2.f - HeadlineFontsize) / 2.f, HeadlineFontsize, Localize("Ping"), -1.0f); + const float HeadlineFontsize = 22.0f; + CUIRect Headline; + Scoreboard.HSplitTop(HeadlineFontsize * 2.0f, &Headline, &Scoreboard); + const float HeadlineY = Headline.y + Headline.h / 2.0f - HeadlineFontsize / 2.0f; + const char *pScore = TimeScore ? Localize("Time") : Localize("Score"); + TextRender()->Text(ScoreOffset + ScoreLength - TextRender()->TextWidth(HeadlineFontsize, pScore), HeadlineY, HeadlineFontsize, pScore); + TextRender()->Text(NameOffset, HeadlineY, HeadlineFontsize, Localize("Name")); + const char *pClanLabel = Localize("Clan"); + TextRender()->Text(ClanOffset + (ClanLength - TextRender()->TextWidth(HeadlineFontsize, pClanLabel)) / 2.0f, HeadlineY, HeadlineFontsize, pClanLabel); + const char *pPingLabel = Localize("Ping"); + TextRender()->Text(PingOffset + PingLength - TextRender()->TextWidth(HeadlineFontsize, pPingLabel), HeadlineY, HeadlineFontsize, pPingLabel); // render player entries - y += HeadlineFontsize * 2.0f; - CTextCursor Cursor; - - int rendered = 0; - if(upper16) - rendered = -16; - if(upper32) - rendered = -32; - if(upper24) - rendered = -24; - - int OldDDTeam = -1; + int CountRendered = 0; + int PrevDDTeam = -1; + int &CurrentDDTeamSize = State.m_CurrentDDTeamSize; - int m_aTeamsCount[64]; - mem_zero(m_aTeamsCount, sizeof(int) * 64); + char aBuf[64]; + int MaxTeamSize = m_pClient->Config()->m_SvMaxTeamSize; - for(int i = 0; i < MAX_CLIENTS; i++) + for(int RenderDead = 0; RenderDead < 2; RenderDead++) { - const CNetObj_PlayerInfo *pInfo = m_pClient->m_Snap.m_apInfoByDDTeamScore[i]; - if(!pInfo) - continue; - - int DDTeam = m_pClient->m_Teams.Team(pInfo->m_ClientID); - if(DDTeam != -1 && DDTeam < TEAM_SUPER) - m_aTeamsCount[DDTeam]++; - } - - for(int i = 0; i < MAX_CLIENTS; i++) - { - // make sure that we render the correct team - const CNetObj_PlayerInfo *pInfo = m_pClient->m_Snap.m_apInfoByDDTeamScore[i]; - if(!pInfo || pInfo->m_Team != Team) - continue; - - if(rendered++ < 0) - continue; - - int DDTeam = m_pClient->m_Teams.Team(pInfo->m_ClientID); - int NextDDTeam = 0; - - for(int j = i + 1; j < MAX_CLIENTS; j++) + for(int i = 0; i < MAX_CLIENTS; i++) { - const CNetObj_PlayerInfo *pInfo2 = m_pClient->m_Snap.m_apInfoByDDTeamScore[j]; + // make sure that we render the correct team + const CNetObj_PlayerInfo *pInfo = GameClient()->m_Snap.m_apInfoByDDTeamScore[i]; + if(!pInfo || pInfo->m_Team != Team) + continue; - if(!pInfo2 || pInfo2->m_Team != Team) + if(CountRendered++ < CountStart) continue; - NextDDTeam = m_pClient->m_Teams.Team(pInfo2->m_ClientID); - break; - } + int DDTeam = GameClient()->m_Teams.Team(pInfo->m_ClientId); + int NextDDTeam = 0; + bool IsDead = Client()->m_TranslationContext.m_aClients[pInfo->m_ClientId].m_PlayerFlags7 & protocol7::PLAYERFLAG_DEAD; + if(!RenderDead && IsDead) + continue; + if(RenderDead && !IsDead) + continue; - if(OldDDTeam == -1) - { - for(int j = i - 1; j >= 0; j--) - { - const CNetObj_PlayerInfo *pInfo2 = m_pClient->m_Snap.m_apInfoByDDTeamScore[j]; + ColorRGBA TextColor = TextRender()->DefaultTextColor(); + TextColor.a = RenderDead ? 0.5f : 1.0f; + TextRender()->TextColor(TextColor); - if(!pInfo2 || pInfo2->m_Team != Team) + for(int j = i + 1; j < MAX_CLIENTS; j++) + { + const CNetObj_PlayerInfo *pInfoNext = GameClient()->m_Snap.m_apInfoByDDTeamScore[j]; + if(!pInfoNext || pInfoNext->m_Team != Team) continue; - OldDDTeam = m_pClient->m_Teams.Team(pInfo2->m_ClientID); + NextDDTeam = GameClient()->m_Teams.Team(pInfoNext->m_ClientId); break; } - } - if(DDTeam != TEAM_FLOCK) - { - const ColorRGBA Color = m_pClient->GetDDTeamColor(DDTeam).WithAlpha(0.5f); - int Corners = 0; - if(OldDDTeam != DDTeam) - Corners |= IGraphics::CORNER_TL | IGraphics::CORNER_TR; - if(NextDDTeam != DDTeam) - Corners |= IGraphics::CORNER_BL | IGraphics::CORNER_BR; - Graphics()->DrawRect(x - 10.0f, y, w, LineHeight + Spacing, Color, Corners, RoundRadius); - - if(NextDDTeam != DDTeam) + if(PrevDDTeam == -1) { - if(m_pClient->m_Snap.m_aTeamSize[0] > 8) - { - if(DDTeam == TEAM_SUPER) - str_copy(aBuf, Localize("Super")); - else - str_format(aBuf, sizeof(aBuf), "%d | %d", DDTeam, m_aTeamsCount[DDTeam]); - TextRender()->SetCursor(&Cursor, x - 10.0f, y + Spacing + FontSize - (FontSize / 1.5f), FontSize / 1.5f, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END); - Cursor.m_LineWidth = NameLength + 3; - } - else + for(int j = i - 1; j >= 0; j--) { - if(DDTeam == TEAM_SUPER) - str_copy(aBuf, Localize("Super")); - else - str_format(aBuf, sizeof(aBuf), Localize("Team %d | Players: %d"), DDTeam, m_aTeamsCount[DDTeam]); - tw = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); - TextRender()->SetCursor(&Cursor, ScoreOffset + w / 2.0f - tw / 2.0f, y + LineHeight, FontSize / 1.5f, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END); - Cursor.m_LineWidth = NameLength + 3; + const CNetObj_PlayerInfo *pInfoPrev = GameClient()->m_Snap.m_apInfoByDDTeamScore[j]; + if(!pInfoPrev || pInfoPrev->m_Team != Team) + continue; + + PrevDDTeam = GameClient()->m_Teams.Team(pInfoPrev->m_ClientId); + break; } - TextRender()->TextEx(&Cursor, aBuf, -1); } - } - OldDDTeam = DDTeam; - // background so it's easy to find the local player or the followed one in spectator mode - if((!m_pClient->m_Snap.m_SpecInfo.m_Active && pInfo->m_Local) || (m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == SPEC_FREEVIEW && pInfo->m_Local) || (m_pClient->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientID == m_pClient->m_Snap.m_SpecInfo.m_SpectatorID)) - { - Graphics()->DrawRect(x, y, w - 20.0f, LineHeight, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, RoundRadius); - } - - // score - if(m_pClient->m_GameInfo.m_TimeScore && g_Config.m_ClDDRaceScoreBoard) - { - if(pInfo->m_Score == -9999) - aBuf[0] = 0; - else - str_time((int64_t)absolute(pInfo->m_Score) * 100, TIME_HOURS, aBuf, sizeof(aBuf)); - } - else - str_from_int(clamp(pInfo->m_Score, -999, 99999), aBuf); - tw = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); - TextRender()->SetCursor(&Cursor, ScoreOffset + ScoreLength - tw, y + (LineHeight - FontSize) / 2.f, FontSize, TEXTFLAG_RENDER); - TextRender()->TextEx(&Cursor, aBuf, -1); - - // flag - if(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS && - m_pClient->m_Snap.m_pGameDataObj && (m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierRed == pInfo->m_ClientID || m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientID)) - { - Graphics()->BlendNormal(); - if(m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientID) - Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagBlue); - else - Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagRed); - - Graphics()->QuadsBegin(); - Graphics()->QuadsSetSubset(1, 0, 0, 1); - - float Size = LineHeight; - IGraphics::CQuadItem QuadItem(TeeOffset + 0.0f, y - 5.0f - Spacing / 2.0f, Size / 2.0f, Size); - Graphics()->QuadsDrawTL(&QuadItem, 1); - Graphics()->QuadsEnd(); - } - - // avatar - CTeeRenderInfo TeeInfo = m_pClient->m_aClients[pInfo->m_ClientID].m_RenderInfo; - TeeInfo.m_Size *= TeeSizeMod; - const CAnimState *pIdleState = CAnimState::GetIdle(); - vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); - vec2 TeeRenderPos(TeeOffset + TeeLength / 2, y + LineHeight / 2.0f + OffsetToMid.y); - - RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos); - // Own color - if(str_comp(m_pClient->m_aClients[pInfo->m_ClientID].m_aName, - m_pClient->m_aClients[GameClient()->m_aLocalIDs[g_Config.m_ClDummy]].m_aName) == 0) - { - ColorRGBA Color = color_cast(ColorHSLA(g_Config.m_ScPlayerOwnColor)); - TextRender()->TextColor(Color); - } - else - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + CUIRect RowAndSpacing, Row; + Scoreboard.HSplitTop(LineHeight + Spacing, &RowAndSpacing, &Scoreboard); + RowAndSpacing.HSplitTop(LineHeight, &Row, nullptr); + // team background + if(DDTeam != TEAM_FLOCK) + { + const ColorRGBA Color = GameClient()->GetDDTeamColor(DDTeam).WithAlpha(0.5f); + int TeamRectCorners = 0; + if(PrevDDTeam != DDTeam) + { + TeamRectCorners |= IGraphics::CORNER_T; + State.m_TeamStartX = Row.x; + State.m_TeamStartY = Row.y; + } + if(NextDDTeam != DDTeam) + TeamRectCorners |= IGraphics::CORNER_B; + RowAndSpacing.Draw(Color, TeamRectCorners, RoundRadius); - // Set the cursor for the name rendering - TextRender()->SetCursor(&Cursor, NameOffset, y + (LineHeight - FontSize) / 2.f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END); - + CurrentDDTeamSize++; - if(m_pClient->m_aClients[pInfo->m_ClientID].m_AuthLevel) - { - ColorRGBA Color = color_cast(ColorHSLA(g_Config.m_ClAuthedPlayerColor)); - TextRender()->TextColor(Color); - } - else if(m_pClient->m_aClients[pInfo->m_ClientID].m_Friend) - { - ColorRGBA Color = color_cast(ColorHSLA(g_Config.m_ScFriendColor)); - TextRender()->TextColor(Color); - } + if(NextDDTeam != DDTeam) + { + const float TeamFontSize = FontSize / 1.5f; + + if(NumPlayers > 8) + { + if(DDTeam == TEAM_SUPER) + str_copy(aBuf, Localize("Super")); + else if(CurrentDDTeamSize <= 1) + str_format(aBuf, sizeof(aBuf), "%d", DDTeam); + else + str_format(aBuf, sizeof(aBuf), Localize("%d\n(%d/%d)", "Team and size"), DDTeam, CurrentDDTeamSize, MaxTeamSize); + TextRender()->Text(State.m_TeamStartX, maximum(State.m_TeamStartY + Row.h / 2.0f - TeamFontSize, State.m_TeamStartY + 3.0f /* padding top */), TeamFontSize, aBuf); + } + else + { + if(DDTeam == TEAM_SUPER) + str_copy(aBuf, Localize("Super")); + else if(CurrentDDTeamSize > 1) + str_format(aBuf, sizeof(aBuf), Localize("Team %d (%d/%d)"), DDTeam, CurrentDDTeamSize, MaxTeamSize); + else + str_format(aBuf, sizeof(aBuf), Localize("Team %d"), DDTeam); + TextRender()->Text(Row.x + Row.w / 2.0f - TextRender()->TextWidth(TeamFontSize, aBuf) / 2.0f + 10.0f, Row.y + Row.h, TeamFontSize, aBuf); + } + + CurrentDDTeamSize = 0; + } + } + PrevDDTeam = DDTeam; - TextRender()->SetCursor(&Cursor, NameOffset, y + (LineHeight - FontSize) / 2.f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END); - if(m_pClient->m_aClients[pInfo->m_ClientID].m_Foe) - { - ColorRGBA Color = color_cast(ColorHSLA(g_Config.m_ScBlacklistPColor)); - TextRender()->TextColor(Color); - } + // background so it's easy to find the local player or the followed one in spectator mode + if((!GameClient()->m_Snap.m_SpecInfo.m_Active && pInfo->m_Local) || + (GameClient()->m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW && pInfo->m_Local) || + (GameClient()->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientId == GameClient()->m_Snap.m_SpecInfo.m_SpectatorId)) + { + Row.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, RoundRadius); + } - if(g_Config.m_ClShowIDs) - { - char aId[64] = ""; - if(pInfo->m_ClientID < 10) - str_format(aId, sizeof(aId), " %d: %s", pInfo->m_ClientID, m_pClient->m_aClients[pInfo->m_ClientID].m_aName); + // score + if(Race7) + { + if(pInfo->m_Score == -1) + { + aBuf[0] = '\0'; + } + else + { + // 0.7 uses milliseconds and ddnets str_time wants centiseconds + // 0.7 servers can also send the amount of precision the client should use + // we ignore that and always show 3 digit precision + str_time((int64_t)absolute(pInfo->m_Score / 10), TIME_MINS_CENTISECS, aBuf, sizeof(aBuf)); + } + } + else if(TimeScore) + { + if(pInfo->m_Score == -9999) + { + aBuf[0] = '\0'; + } + else + { + str_time((int64_t)absolute(pInfo->m_Score) * 100, TIME_HOURS, aBuf, sizeof(aBuf)); + } + } else - str_format(aId, sizeof(aId), "%d: %s", pInfo->m_ClientID, m_pClient->m_aClients[pInfo->m_ClientID].m_aName); + { + str_format(aBuf, sizeof(aBuf), "%d", clamp(pInfo->m_Score, -999, 99999)); + } + TextRender()->Text(ScoreOffset + ScoreLength - TextRender()->TextWidth(FontSize, aBuf), Row.y + (Row.h - FontSize) / 2.0f, FontSize, aBuf); - Cursor.m_LineWidth = NameLength; - TextRender()->TextEx(&Cursor, aId, -1); - } - else - { - Cursor.m_LineWidth = NameLength; - TextRender()->TextEx(&Cursor, m_pClient->m_aClients[pInfo->m_ClientID].m_aName, -1); - } + // CTF flag + if(pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) && + pGameDataObj && (pGameDataObj->m_FlagCarrierRed == pInfo->m_ClientId || pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientId)) + { + Graphics()->BlendNormal(); + Graphics()->TextureSet(pGameDataObj->m_FlagCarrierBlue == pInfo->m_ClientId ? GameClient()->m_GameSkin.m_SpriteFlagBlue : GameClient()->m_GameSkin.m_SpriteFlagRed); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetSubset(1.0f, 0.0f, 0.0f, 1.0f); + IGraphics::CQuadItem QuadItem(TeeOffset, Row.y - 5.0f - Spacing / 2.0f, Row.h / 2.0f, Row.h); + Graphics()->QuadsDrawTL(&QuadItem, 1); + Graphics()->QuadsEnd(); + } - // clan + const CGameClient::CClientData &ClientData = GameClient()->m_aClients[pInfo->m_ClientId]; - if(str_comp(m_pClient->m_aClients[pInfo->m_ClientID].m_aClan, - m_pClient->m_aClients[GameClient()->m_aLocalIDs[g_Config.m_ClDummy]].m_aClan) == 0) - { - ColorRGBA Color = color_cast(ColorHSLA(g_Config.m_ClSameClanColor)); - TextRender()->TextColor(Color); - ColorRGBA special = color_cast(ColorHSVA(round_to_int(LocalTime() * 20) % 255 / 255.f, 1.f, 1.f)); - - std::string innerpeace = "Inner peace"; - if(m_pClient->m_aClients[pInfo->m_ClientID].m_aClan == innerpeace) + // skin + if(RenderDead) { - TextRender()->TextColor(special); + Graphics()->BlendNormal(); + Graphics()->TextureSet(client_data7::g_pData->m_aImages[client_data7::IMAGE_DEADTEE].m_Id); + Graphics()->QuadsBegin(); + if(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS) + { + ColorRGBA Color = m_pClient->m_Skins7.GetTeamColor(true, 0, m_pClient->m_aClients[pInfo->m_ClientId].m_Team, protocol7::SKINPART_BODY); + Graphics()->SetColor(Color.r, Color.g, Color.b, Color.a); + } + CTeeRenderInfo TeeInfo = m_pClient->m_aClients[pInfo->m_ClientId].m_RenderInfo; + TeeInfo.m_Size *= TeeSizeMod; + IGraphics::CQuadItem QuadItem(TeeOffset, Row.y, TeeInfo.m_Size, TeeInfo.m_Size); + Graphics()->QuadsDrawTL(&QuadItem, 1); + Graphics()->QuadsEnd(); + } + else + { + CTeeRenderInfo TeeInfo = ClientData.m_RenderInfo; + TeeInfo.m_Size *= TeeSizeMod; + vec2 OffsetToMid; + CRenderTools::GetRenderTeeOffsetToRenderedTee(CAnimState::GetIdle(), &TeeInfo, OffsetToMid); + const vec2 TeeRenderPos = vec2(TeeOffset + TeeLength / 2, Row.y + Row.h / 2.0f + OffsetToMid.y); + RenderTools()->RenderTee(CAnimState::GetIdle(), &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos); } - } - else - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - + // name + { + CTextCursor Cursor; + TextRender()->SetCursor(&Cursor, NameOffset, Row.y + (Row.h - FontSize) / 2.0f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END); + Cursor.m_LineWidth = NameLength; + if(ClientData.m_AuthLevel) + { + TextRender()->TextColor(color_cast(ColorHSLA(g_Config.m_ClAuthedPlayerColor))); + } + if(g_Config.m_ClShowIds) + { + char aClientId[16]; + GameClient()->FormatClientId(pInfo->m_ClientId, aClientId, EClientIdFormat::INDENT_AUTO); + TextRender()->TextEx(&Cursor, aClientId); + } + TextRender()->TextEx(&Cursor, ClientData.m_aName); - tw = minimum(TextRender()->TextWidth(FontSize, m_pClient->m_aClients[pInfo->m_ClientID].m_aClan, -1, -1.0f), ClanLength); - TextRender()->SetCursor(&Cursor, ClanOffset + (ClanLength - tw) / 2, y + (LineHeight - FontSize) / 2.f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END); - Cursor.m_LineWidth = ClanLength; - TextRender()->TextEx(&Cursor, m_pClient->m_aClients[pInfo->m_ClientID].m_aClan, -1); + // ready / watching + if(Client()->IsSixup() && Client()->m_TranslationContext.m_aClients[pInfo->m_ClientId].m_PlayerFlags7 & protocol7::PLAYERFLAG_READY) + { + TextRender()->TextColor(0.1f, 1.0f, 0.1f, TextColor.a); + TextRender()->TextEx(&Cursor, "✓"); + } + } - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + // clan + { + if(str_comp(ClientData.m_aClan, GameClient()->m_aClients[GameClient()->m_aLocalIds[g_Config.m_ClDummy]].m_aClan) == 0) + { + TextRender()->TextColor(color_cast(ColorHSLA(g_Config.m_ClSameClanColor))); + } + else + { + TextRender()->TextColor(TextColor); + } + CTextCursor Cursor; + TextRender()->SetCursor(&Cursor, ClanOffset + (ClanLength - minimum(TextRender()->TextWidth(FontSize, ClientData.m_aClan), ClanLength)) / 2.0f, Row.y + (Row.h - FontSize) / 2.0f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END); + Cursor.m_LineWidth = ClanLength; + TextRender()->TextEx(&Cursor, ClientData.m_aClan); + } - // country flag - m_pClient->m_CountryFlags.Render(m_pClient->m_aClients[pInfo->m_ClientID].m_Country, ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), - CountryOffset, y + (Spacing + TeeSizeMod * 5.0f) / 2.0f, CountryLength, LineHeight - Spacing - TeeSizeMod * 5.0f); + // country flag + GameClient()->m_CountryFlags.Render(ClientData.m_Country, ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), + CountryOffset, Row.y + (Spacing + TeeSizeMod * 5.0f) / 2.0f, CountryLength, Row.h - Spacing - TeeSizeMod * 5.0f); - // ping - if(g_Config.m_ClEnablePingColor) - { - ColorRGBA rgb = color_cast(ColorHSLA((300.0f - clamp(pInfo->m_Latency, 0, 300)) / 1000.0f, 1.0f, 0.5f)); - TextRender()->TextColor(rgb); - } - str_from_int(clamp(pInfo->m_Latency, 0, 6000), aBuf); - tw = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); - TextRender()->SetCursor(&Cursor, PingOffset + PingLength - tw, y + (LineHeight - FontSize) / 2.f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END); - Cursor.m_LineWidth = PingLength; - TextRender()->TextEx(&Cursor, aBuf, -1); - - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + // ping + if(g_Config.m_ClEnablePingColor) + { + TextRender()->TextColor(color_cast(ColorHSLA((300.0f - clamp(pInfo->m_Latency, 0, 300)) / 1000.0f, 1.0f, 0.5f))); + } + else + { + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + str_format(aBuf, sizeof(aBuf), "%d", clamp(pInfo->m_Latency, 0, 999)); + TextRender()->Text(PingOffset + PingLength - TextRender()->TextWidth(FontSize, aBuf), Row.y + (Row.h - FontSize) / 2.0f, FontSize, aBuf); + TextRender()->TextColor(TextRender()->DefaultTextColor()); - y += LineHeight + Spacing; - if(lower32 || upper32) - { - if(rendered == 32) - break; - } - else if(lower24 || upper24) - { - if(rendered == 24) - break; - } - else - { - if(rendered == 16) + if(CountRendered == CountEnd) break; } } @@ -567,189 +595,251 @@ void CScoreboard::RenderScoreboard(float x, float y, float w, int Team, const ch void CScoreboard::RenderRecordingNotification(float x) { - char aBuf[64] = "\0"; - char aBuf2[64]; - char aTime[32]; + char aBuf[512] = ""; - if(m_pClient->DemoRecorder(RECORDER_MANUAL)->IsRecording()) - { - str_time((int64_t)m_pClient->DemoRecorder(RECORDER_MANUAL)->Length() * 100, TIME_HOURS, aTime, sizeof(aTime)); - str_format(aBuf2, sizeof(aBuf2), "%s %s ", Localize("Manual"), aTime); - str_append(aBuf, aBuf2); - } - if(m_pClient->DemoRecorder(RECORDER_RACE)->IsRecording()) - { - str_time((int64_t)m_pClient->DemoRecorder(RECORDER_RACE)->Length() * 100, TIME_HOURS, aTime, sizeof(aTime)); - str_format(aBuf2, sizeof(aBuf2), "%s %s ", Localize("Race"), aTime); - str_append(aBuf, aBuf2); - } - if(m_pClient->DemoRecorder(RECORDER_AUTO)->IsRecording()) - { - str_time((int64_t)m_pClient->DemoRecorder(RECORDER_AUTO)->Length() * 100, TIME_HOURS, aTime, sizeof(aTime)); - str_format(aBuf2, sizeof(aBuf2), "%s %s ", Localize("Auto"), aTime); - str_append(aBuf, aBuf2); - } - if(m_pClient->DemoRecorder(RECORDER_REPLAYS)->IsRecording()) - { - str_time((int64_t)m_pClient->DemoRecorder(RECORDER_REPLAYS)->Length() * 100, TIME_HOURS, aTime, sizeof(aTime)); - str_format(aBuf2, sizeof(aBuf2), "%s %s ", Localize("Replay"), aTime); - str_append(aBuf, aBuf2); - } + const auto &&AppendRecorderInfo = [&](int Recorder, const char *pName) { + if(GameClient()->DemoRecorder(Recorder)->IsRecording()) + { + char aTime[32]; + str_time((int64_t)GameClient()->DemoRecorder(Recorder)->Length() * 100, TIME_HOURS, aTime, sizeof(aTime)); + str_append(aBuf, pName); + str_append(aBuf, " "); + str_append(aBuf, aTime); + str_append(aBuf, " "); + } + }; - if(!aBuf[0]) + AppendRecorderInfo(RECORDER_MANUAL, Localize("Manual")); + AppendRecorderInfo(RECORDER_RACE, Localize("Race")); + AppendRecorderInfo(RECORDER_AUTO, Localize("Auto")); + AppendRecorderInfo(RECORDER_REPLAYS, Localize("Replay")); + + if(aBuf[0] == '\0') return; - float w = TextRender()->TextWidth(20.0f, aBuf, -1, -1.0f); + const float FontSize = 20.0f; - // draw the box - Graphics()->DrawRect(x, 0.0f, w + 60.0f, 50.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_B, 15.0f); + CUIRect Rect = {x, 0.0f, TextRender()->TextWidth(FontSize, aBuf) + 60.0f, 50.0f}; + Rect.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_B, 15.0f); + Rect.VSplitLeft(20.0f, nullptr, &Rect); + Rect.VSplitRight(10.0f, &Rect, nullptr); - // draw the red dot - Graphics()->DrawRect(x + 20, 15.0f, 20.0f, 20.0f, ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), IGraphics::CORNER_ALL, 10.0f); + CUIRect Circle; + Rect.VSplitLeft(20.0f, &Circle, &Rect); + Circle.HMargin((Circle.h - Circle.w) / 2.0f, &Circle); + Circle.Draw(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f), IGraphics::CORNER_ALL, Circle.h / 2.0f); - TextRender()->Text(x + 50.0f, (50.f - 20.f) / 2.f, 20.0f, aBuf, -1.0f); + Rect.VSplitLeft(10.0f, nullptr, &Rect); + Ui()->DoLabel(&Rect, aBuf, FontSize, TEXTALIGN_ML); } void CScoreboard::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if(!Active()) return; // if the score board is active, then we should clear the motd message as well - if(m_pClient->m_Motd.IsActive()) - m_pClient->m_Motd.Clear(); - - float Width = 400 * 3.0f * Graphics()->ScreenAspect(); - float Height = 400 * 3.0f; + if(GameClient()->m_Motd.IsActive()) + GameClient()->m_Motd.Clear(); + const float Height = 400.0f * 3.0f; + const float Width = Height * Graphics()->ScreenAspect(); Graphics()->MapScreen(0, 0, Width, Height); - float w = 750.0f; - float ExtraWidthSingle = 20.0f; + const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; + const bool Teams = pGameInfoObj && (pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS); + const auto &aTeamSize = GameClient()->m_Snap.m_aTeamSize; + const int NumPlayers = Teams ? maximum(aTeamSize[TEAM_RED], aTeamSize[TEAM_BLUE]) : aTeamSize[TEAM_RED]; + + const float ScoreboardSmallWidth = 750.0f + 20.0f; + const float ScoreboardWidth = !Teams && NumPlayers <= 16 ? ScoreboardSmallWidth : 1500.0f; + const float TitleHeight = 60.0f; - if(m_pClient->m_Snap.m_pGameInfoObj) + CUIRect Scoreboard = {(Width - ScoreboardWidth) / 2.0f, 150.0f, ScoreboardWidth, 710.0f + TitleHeight}; + CScoreboardRenderState RenderState{}; + + if(Teams) { - if(!(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS)) + const char *pRedTeamName = GetTeamName(TEAM_RED); + const char *pBlueTeamName = GetTeamName(TEAM_BLUE); + + // Game over title + const CNetObj_GameData *pGameDataObj = GameClient()->m_Snap.m_pGameDataObj; + if((pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) && pGameDataObj) { - if(m_pClient->m_Snap.m_aTeamSize[0] > 48) + char aTitle[256]; + if(pGameDataObj->m_TeamscoreRed > pGameDataObj->m_TeamscoreBlue) { - RenderScoreboard(Width / 2, 150.0f, w, -5, ""); - RenderScoreboard(Width / 2 - w, 150.0f, w, -4, 0); - } - else if(m_pClient->m_Snap.m_aTeamSize[0] > 32) - { - RenderScoreboard(Width / 2, 150.0f, w, -8, ""); - RenderScoreboard(Width / 2 - w, 150.0f, w, -7, 0); + TextRender()->TextColor(ColorRGBA(0.975f, 0.17f, 0.17f, 1.0f)); + if(pRedTeamName == nullptr) + { + str_copy(aTitle, Localize("Red team wins!")); + } + else + { + str_format(aTitle, sizeof(aTitle), Localize("%s wins!"), pRedTeamName); + } } - else if(m_pClient->m_Snap.m_aTeamSize[0] > 16) + else if(pGameDataObj->m_TeamscoreBlue > pGameDataObj->m_TeamscoreRed) { - RenderScoreboard(Width / 2, 150.0f, w, -3, ""); - RenderScoreboard(Width / 2 - w, 150.0f, w, -6, 0); + TextRender()->TextColor(ColorRGBA(0.17f, 0.46f, 0.975f, 1.0f)); + if(pBlueTeamName == nullptr) + { + str_copy(aTitle, Localize("Blue team wins!")); + } + else + { + str_format(aTitle, sizeof(aTitle), Localize("%s wins!"), pBlueTeamName); + } } else { - w += ExtraWidthSingle; - RenderScoreboard(Width / 2 - w / 2, 150.0f, w, -2, 0); + TextRender()->TextColor(ColorRGBA(0.91f, 0.78f, 0.33f, 1.0f)); + str_copy(aTitle, Localize("Draw!")); } + + const float TitleFontSize = 72.0f; + CUIRect GameOverTitle = {Scoreboard.x, Scoreboard.y - TitleFontSize - 12.0f, Scoreboard.w, TitleFontSize}; + Ui()->DoLabel(&GameOverTitle, aTitle, TitleFontSize, TEXTALIGN_MC); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + + CUIRect RedScoreboard, BlueScoreboard, RedTitle, BlueTitle; + Scoreboard.VSplitMid(&RedScoreboard, &BlueScoreboard, 15.0f); + RedScoreboard.HSplitTop(TitleHeight, &RedTitle, &RedScoreboard); + BlueScoreboard.HSplitTop(TitleHeight, &BlueTitle, &BlueScoreboard); + + RedTitle.Draw(ColorRGBA(0.975f, 0.17f, 0.17f, 0.5f), IGraphics::CORNER_T, 15.0f); + BlueTitle.Draw(ColorRGBA(0.17f, 0.46f, 0.975f, 0.5f), IGraphics::CORNER_T, 15.0f); + RedScoreboard.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_B, 15.0f); + BlueScoreboard.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_B, 15.0f); + + RenderTitle(RedTitle, TEAM_RED, pRedTeamName == nullptr ? Localize("Red team") : pRedTeamName); + RenderTitle(BlueTitle, TEAM_BLUE, pBlueTeamName == nullptr ? Localize("Blue team") : pBlueTeamName); + RenderScoreboard(RedScoreboard, TEAM_RED, 0, NumPlayers, RenderState); + RenderScoreboard(BlueScoreboard, TEAM_BLUE, 0, NumPlayers, RenderState); + } + else + { + Scoreboard.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 15.0f); + + const char *pTitle; + if(pGameInfoObj && (pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER)) + { + pTitle = Localize("Game over"); } else { - const char *pRedClanName = GetClanName(TEAM_RED); - const char *pBlueClanName = GetClanName(TEAM_BLUE); + pTitle = Client()->GetCurrentMap(); + } - if(m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER && m_pClient->m_Snap.m_pGameDataObj) - { - char aText[256]; - str_copy(aText, Localize("Draw!")); + CUIRect Title; + Scoreboard.HSplitTop(TitleHeight, &Title, &Scoreboard); + RenderTitle(Title, TEAM_RED, pTitle); - if(m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreRed > m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreBlue) - { - if(pRedClanName) - str_format(aText, sizeof(aText), Localize("%s wins!"), pRedClanName); - else - str_copy(aText, Localize("Red team wins!")); - } - else if(m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreBlue > m_pClient->m_Snap.m_pGameDataObj->m_TeamscoreRed) - { - if(pBlueClanName) - str_format(aText, sizeof(aText), Localize("%s wins!"), pBlueClanName); - else - str_copy(aText, Localize("Blue team wins!")); - } + if(NumPlayers <= 16) + { + RenderScoreboard(Scoreboard, TEAM_RED, 0, NumPlayers, RenderState); + } + else if(NumPlayers <= 64) + { + int PlayersPerSide; + if(NumPlayers <= 24) + PlayersPerSide = 12; + else if(NumPlayers <= 32) + PlayersPerSide = 16; + else if(NumPlayers <= 48) + PlayersPerSide = 24; + else + PlayersPerSide = 32; - float TextWidth = TextRender()->TextWidth(86.0f, aText, -1, -1.0f); - TextRender()->Text(Width / 2 - TextWidth / 2, 39, 86.0f, aText, -1.0f); + CUIRect LeftScoreboard, RightScoreboard; + Scoreboard.VSplitMid(&LeftScoreboard, &RightScoreboard); + RenderScoreboard(LeftScoreboard, TEAM_RED, 0, PlayersPerSide, RenderState); + RenderScoreboard(RightScoreboard, TEAM_RED, PlayersPerSide, 2 * PlayersPerSide, RenderState); + } + else + { + const int NumColumns = 3; + const int PlayersPerColumn = std::ceil(128.0f / NumColumns); + CUIRect RemainingScoreboard = Scoreboard; + for(int i = 0; i < NumColumns; ++i) + { + CUIRect Column; + RemainingScoreboard.VSplitLeft(Scoreboard.w / NumColumns, &Column, &RemainingScoreboard); + RenderScoreboard(Column, TEAM_RED, i * PlayersPerColumn, (i + 1) * PlayersPerColumn, RenderState); } - - //decrease width, because team games use additional offsets - w -= 10.0f; - - int NumPlayers = maximum(m_pClient->m_Snap.m_aTeamSize[TEAM_RED], m_pClient->m_Snap.m_aTeamSize[TEAM_BLUE]); - RenderScoreboard(Width / 2 - w - 5.0f, 150.0f, w, TEAM_RED, pRedClanName ? pRedClanName : Localize("Red team"), NumPlayers); - RenderScoreboard(Width / 2 + 5.0f, 150.0f, w, TEAM_BLUE, pBlueClanName ? pBlueClanName : Localize("Blue team"), NumPlayers); } } - if(m_pClient->m_Snap.m_pGameInfoObj && (m_pClient->m_Snap.m_pGameInfoObj->m_ScoreLimit || m_pClient->m_Snap.m_pGameInfoObj->m_TimeLimit || (m_pClient->m_Snap.m_pGameInfoObj->m_RoundNum && m_pClient->m_Snap.m_pGameInfoObj->m_RoundCurrent))) - { - RenderGoals(Width / 2 - w / 2, 150 + 760 + 10, w); - RenderSpectators(Width / 2 - w / 2, 150 + 760 + 10 + 50 + 10, w, 160.0f); - } - else + + CUIRect Spectators = {(Width - ScoreboardSmallWidth) / 2.0f, Scoreboard.y + Scoreboard.h + 10.0f, ScoreboardSmallWidth, 200.0f}; + if(pGameInfoObj && (pGameInfoObj->m_ScoreLimit || pGameInfoObj->m_TimeLimit || (pGameInfoObj->m_RoundNum && pGameInfoObj->m_RoundCurrent))) { - RenderSpectators(Width / 2 - w / 2, 150 + 760 + 10, w, 200.0f); + CUIRect Goals; + Spectators.HSplitTop(50.0f, &Goals, &Spectators); + Spectators.HSplitTop(10.0f, nullptr, &Spectators); + RenderGoals(Goals); } + RenderSpectators(Spectators); RenderRecordingNotification((Width / 7) * 4 + 20); } -bool CScoreboard::Active() +bool CScoreboard::Active() const { // if statboard is active don't show scoreboard - if(m_pClient->m_Statboard.IsActive()) + if(GameClient()->m_Statboard.IsActive()) return false; if(m_Active) return true; - if(m_pClient->m_Snap.m_pLocalInfo && !m_pClient->m_Snap.m_SpecInfo.m_Active) + const CNetObj_GameInfo *pGameInfoObj = GameClient()->m_Snap.m_pGameInfoObj; + if(GameClient()->m_Snap.m_pLocalInfo && !GameClient()->m_Snap.m_SpecInfo.m_Active) { - // we are not a spectator, check if we are dead - if(!m_pClient->m_Snap.m_pLocalCharacter && g_Config.m_ClScoreboardOnDeath) + // we are not a spectator, check if we are dead and the game isn't paused + if(!GameClient()->m_Snap.m_pLocalCharacter && g_Config.m_ClScoreboardOnDeath && + !(pGameInfoObj && pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_PAUSED)) return true; } // if the game is over - if(m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) + if(pGameInfoObj && pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) return true; return false; } -const char *CScoreboard::GetClanName(int Team) +const char *CScoreboard::GetTeamName(int Team) const { + dbg_assert(Team == TEAM_RED || Team == TEAM_BLUE, "Team invalid"); + int ClanPlayers = 0; - const char *pClanName = 0; - for(const auto *pInfo : m_pClient->m_Snap.m_apInfoByScore) + const char *pClanName = nullptr; + for(const CNetObj_PlayerInfo *pInfo : GameClient()->m_Snap.m_apInfoByScore) { if(!pInfo || pInfo->m_Team != Team) continue; if(!pClanName) { - pClanName = m_pClient->m_aClients[pInfo->m_ClientID].m_aClan; + pClanName = GameClient()->m_aClients[pInfo->m_ClientId].m_aClan; ClanPlayers++; } else { - if(str_comp(m_pClient->m_aClients[pInfo->m_ClientID].m_aClan, pClanName) == 0) + if(str_comp(GameClient()->m_aClients[pInfo->m_ClientId].m_aClan, pClanName) == 0) ClanPlayers++; else - return 0; + return nullptr; } } - if(ClanPlayers > 1 && pClanName[0]) + if(ClanPlayers > 1 && pClanName[0] != '\0') return pClanName; else - return 0; + return nullptr; } diff --git a/src/game/client/components/scoreboard.h b/src/game/client/components/scoreboard.h index e45b781cfd..09f064fab1 100644 --- a/src/game/client/components/scoreboard.h +++ b/src/game/client/components/scoreboard.h @@ -6,36 +6,42 @@ #include #include +#include class CScoreboard : public CComponent { - void RenderGoals(float x, float y, float w); - void RenderSpectators(float x, float y, float w, float h); - void RenderScoreboard(float x, float y, float w, int Team, const char *pTitle, int NumPlayers = -1); + struct CScoreboardRenderState + { + float m_TeamStartX; + float m_TeamStartY; + int m_CurrentDDTeamSize; + + CScoreboardRenderState() : + m_TeamStartX(0), m_TeamStartY(0), m_CurrentDDTeamSize(0) {} + }; + + void RenderTitle(CUIRect TitleBar, int Team, const char *pTitle); + void RenderGoals(CUIRect Goals); + void RenderSpectators(CUIRect Spectators); + void RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, int CountEnd, CScoreboardRenderState &State); void RenderRecordingNotification(float x); static void ConKeyScoreboard(IConsole::IResult *pResult, void *pUserData); - - const char *GetClanName(int Team); + const char *GetTeamName(int Team) const; bool m_Active; + float m_ServerRecord; public: CScoreboard(); virtual int Sizeof() const override { return sizeof(*this); } - virtual void OnReset() override; virtual void OnConsoleInit() override; + virtual void OnReset() override; virtual void OnRender() override; virtual void OnRelease() override; - - bool Active(); - - // DDRace - virtual void OnMessage(int MsgType, void *pRawMsg) override; -private: - float m_ServerRecord; + bool Active() const; }; #endif diff --git a/src/game/client/components/skins.cpp b/src/game/client/components/skins.cpp index 90296fc439..9e66d06516 100644 --- a/src/game/client/components/skins.cpp +++ b/src/game/client/components/skins.cpp @@ -1,10 +1,12 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include #include #include #include +#include #include #include #include @@ -16,20 +18,38 @@ #include "skins.h" +CSkins::CSkins() : + m_PlaceholderSkin("dummy") +{ + m_PlaceholderSkin.m_OriginalSkin.Reset(); + m_PlaceholderSkin.m_ColorableSkin.Reset(); + m_PlaceholderSkin.m_BloodColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + m_PlaceholderSkin.m_Metrics.m_Body.m_Width = 64; + m_PlaceholderSkin.m_Metrics.m_Body.m_Height = 64; + m_PlaceholderSkin.m_Metrics.m_Body.m_OffsetX = 16; + m_PlaceholderSkin.m_Metrics.m_Body.m_OffsetY = 16; + m_PlaceholderSkin.m_Metrics.m_Body.m_MaxWidth = 96; + m_PlaceholderSkin.m_Metrics.m_Body.m_MaxHeight = 96; + m_PlaceholderSkin.m_Metrics.m_Feet.m_Width = 32; + m_PlaceholderSkin.m_Metrics.m_Feet.m_Height = 16; + m_PlaceholderSkin.m_Metrics.m_Feet.m_OffsetX = 16; + m_PlaceholderSkin.m_Metrics.m_Feet.m_OffsetY = 8; + m_PlaceholderSkin.m_Metrics.m_Feet.m_MaxWidth = 64; + m_PlaceholderSkin.m_Metrics.m_Feet.m_MaxHeight = 32; +} + bool CSkins::IsVanillaSkin(const char *pName) { return std::any_of(std::begin(VANILLA_SKINS), std::end(VANILLA_SKINS), [pName](const char *pVanillaSkin) { return str_comp(pName, pVanillaSkin) == 0; }); } -int CSkins::CGetPngFile::OnCompletion(int State) +void CSkins::CGetPngFile::OnCompletion(EHttpState State) { - State = CHttpRequest::OnCompletion(State); - - if(State != HTTP_ERROR && State != HTTP_ABORTED && !m_pSkins->LoadSkinPNG(m_Info, Dest(), Dest(), IStorage::TYPE_SAVE)) + // Maybe this should start another thread to load the png in instead of stalling the curl thread + if(State == EHttpState::DONE) { - State = HTTP_ERROR; + m_pSkins->LoadSkinPng(m_Info, Dest(), Dest(), IStorage::TYPE_SAVE); } - return State; } CSkins::CGetPngFile::CGetPngFile(CSkins *pSkins, const char *pUrl, IStorage *pStorage, const char *pDest) : @@ -49,27 +69,31 @@ struct SSkinScanUser int CSkins::SkinScan(const char *pName, int IsDir, int DirType, void *pUser) { - auto *pUserReal = (SSkinScanUser *)pUser; + auto *pUserReal = static_cast(pUser); CSkins *pSelf = pUserReal->m_pThis; - if(IsDir || !str_endswith(pName, ".png")) + if(IsDir) return 0; - char aNameWithoutPng[128]; - str_copy(aNameWithoutPng, pName); - aNameWithoutPng[str_length(aNameWithoutPng) - 4] = 0; + const char *pSuffix = str_endswith(pName, ".png"); + if(pSuffix == nullptr) + return 0; - if(g_Config.m_ClVanillaSkinsOnly && !IsVanillaSkin(aNameWithoutPng)) + char aSkinName[IO_MAX_PATH_LENGTH]; + str_truncate(aSkinName, sizeof(aSkinName), pName, pSuffix - pName); + if(!CSkin::IsValidName(aSkinName)) + { + log_error("skins", "Skin name is not valid: %s", aSkinName); + log_error("skins", "%s", CSkin::m_aSkinNameRestrictions); return 0; + } - // Don't add duplicate skins (one from user's config directory, other from - // client itself) - if(pSelf->m_Skins.find(aNameWithoutPng) != pSelf->m_Skins.end()) + if(g_Config.m_ClVanillaSkinsOnly && !IsVanillaSkin(aSkinName)) return 0; - char aBuf[IO_MAX_PATH_LENGTH]; - str_format(aBuf, sizeof(aBuf), "skins/%s", pName); - pSelf->LoadSkin(aNameWithoutPng, aBuf, DirType); + char aPath[IO_MAX_PATH_LENGTH]; + str_format(aPath, sizeof(aPath), "skins/%s", pName); + pSelf->LoadSkin(aSkinName, aPath, DirType); pUserReal->m_SkinLoadedFunc((int)pSelf->m_Skins.size()); return 0; } @@ -112,18 +136,16 @@ static void CheckMetrics(CSkin::SSkinMetricVariable &Metrics, const uint8_t *pIm const CSkin *CSkins::LoadSkin(const char *pName, const char *pPath, int DirType) { CImageInfo Info; - if(!LoadSkinPNG(Info, pName, pPath, DirType)) - return 0; + if(!LoadSkinPng(Info, pName, pPath, DirType)) + return nullptr; return LoadSkin(pName, Info); } -bool CSkins::LoadSkinPNG(CImageInfo &Info, const char *pName, const char *pPath, int DirType) +bool CSkins::LoadSkinPng(CImageInfo &Info, const char *pName, const char *pPath, int DirType) { - char aBuf[512]; - if(!Graphics()->LoadPNG(&Info, pPath, DirType)) + if(!Graphics()->LoadPng(Info, pPath, DirType)) { - str_format(aBuf, sizeof(aBuf), "failed to load skin from %s", pName); - Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "game", aBuf); + log_error("skins", "Failed to load skin PNG: %s", pName); return false; } return true; @@ -131,18 +153,14 @@ bool CSkins::LoadSkinPNG(CImageInfo &Info, const char *pName, const char *pPath, const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info) { - char aBuf[512]; - if(!Graphics()->CheckImageDivisibility(pName, Info, g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridy, true)) { - str_format(aBuf, sizeof(aBuf), "skin failed image divisibility: %s", pName); - Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "game", aBuf); + log_error("skins", "Skin failed image divisibility: %s", pName); return nullptr; } - if(!Graphics()->IsImageFormatRGBA(pName, Info)) + if(!Graphics()->IsImageFormatRgba(pName, Info)) { - str_format(aBuf, sizeof(aBuf), "skin format is not RGBA: %s", pName); - Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "game", aBuf); + log_error("skins", "Skin format is not RGBA: %s", pName); return nullptr; } @@ -181,19 +199,20 @@ const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info) int BodyOutlineOffsetX = g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_X * BodyOutlineGridPixelsWidth; int BodyOutlineOffsetY = g_pData->m_aSprites[SPRITE_TEE_BODY_OUTLINE].m_Y * BodyOutlineGridPixelsHeight; - int BodyWidth = g_pData->m_aSprites[SPRITE_TEE_BODY].m_W * (Info.m_Width / g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridx); // body width - int BodyHeight = g_pData->m_aSprites[SPRITE_TEE_BODY].m_H * (Info.m_Height / g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridy); // body height + size_t BodyWidth = g_pData->m_aSprites[SPRITE_TEE_BODY].m_W * (Info.m_Width / g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridx); // body width + size_t BodyHeight = g_pData->m_aSprites[SPRITE_TEE_BODY].m_H * (Info.m_Height / g_pData->m_aSprites[SPRITE_TEE_BODY].m_pSet->m_Gridy); // body height if(BodyWidth > Info.m_Width || BodyHeight > Info.m_Height) return nullptr; - unsigned char *pData = (unsigned char *)Info.m_pData; + uint8_t *pData = Info.m_pData; const int PixelStep = 4; int Pitch = Info.m_Width * PixelStep; // dig out blood color { - int aColors[3] = {0}; - for(int y = 0; y < BodyHeight; y++) - for(int x = 0; x < BodyWidth; x++) + int64_t aColors[3] = {0}; + for(size_t y = 0; y < BodyHeight; y++) + { + for(size_t x = 0; x < BodyWidth; x++) { uint8_t AlphaValue = pData[y * Pitch + x * PixelStep + 3]; if(AlphaValue > 128) @@ -203,10 +222,9 @@ const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info) aColors[2] += pData[y * Pitch + x * PixelStep + 2]; } } - if(aColors[0] != 0 && aColors[1] != 0 && aColors[2] != 0) - Skin.m_BloodColor = ColorRGBA(normalize(vec3(aColors[0], aColors[1], aColors[2]))); - else - Skin.m_BloodColor = ColorRGBA(0, 0, 0, 1); + } + + Skin.m_BloodColor = ColorRGBA(normalize(vec3(aColors[0], aColors[1], aColors[2]))); } CheckMetrics(Skin.m_Metrics.m_Body, pData, Pitch, 0, 0, BodyWidth, BodyHeight); @@ -220,22 +238,15 @@ const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info) // get feet outline size CheckMetrics(Skin.m_Metrics.m_Feet, pData, Pitch, FeetOutlineOffsetX, FeetOutlineOffsetY, FeetOutlineWidth, FeetOutlineHeight); - // make the texture gray scale - for(int i = 0; i < Info.m_Width * Info.m_Height; i++) - { - int v = (pData[i * PixelStep] + pData[i * PixelStep + 1] + pData[i * PixelStep + 2]) / 3; - pData[i * PixelStep] = v; - pData[i * PixelStep + 1] = v; - pData[i * PixelStep + 2] = v; - } + ConvertToGrayscale(Info); int aFreq[256] = {0}; int OrgWeight = 0; int NewWeight = 192; // find most common frequency - for(int y = 0; y < BodyHeight; y++) - for(int x = 0; x < BodyWidth; x++) + for(size_t y = 0; y < BodyHeight; y++) + for(size_t x = 0; x < BodyWidth; x++) { if(pData[y * Pitch + x * PixelStep + 3] > 128) aFreq[pData[y * Pitch + x * PixelStep]]++; @@ -250,8 +261,8 @@ const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info) // reorder int InvOrgWeight = 255 - OrgWeight; int InvNewWeight = 255 - NewWeight; - for(int y = 0; y < BodyHeight; y++) - for(int x = 0; x < BodyWidth; x++) + for(size_t y = 0; y < BodyHeight; y++) + for(size_t x = 0; x < BodyWidth; x++) { int v = pData[y * Pitch + x * PixelStep]; if(v <= OrgWeight && OrgWeight == 0) @@ -277,13 +288,11 @@ const CSkin *CSkins::LoadSkin(const char *pName, CImageInfo &Info) for(int i = 0; i < 6; ++i) Skin.m_ColorableSkin.m_aEyes[i] = Graphics()->LoadSpriteTexture(Info, &g_pData->m_aSprites[SPRITE_TEE_EYE_NORMAL + i]); - Graphics()->FreePNG(&Info); + Info.Free(); - // set skin data if(g_Config.m_Debug) { - str_format(aBuf, sizeof(aBuf), "load skin %s", Skin.GetName()); - Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "game", aBuf); + log_trace("skins", "Loaded skin '%s'", Skin.GetName()); } auto &&pSkin = std::make_unique(std::move(Skin)); @@ -306,32 +315,16 @@ void CSkins::OnInit() // load skins; Refresh([this](int SkinCounter) { - GameClient()->m_Menus.RenderLoading(Localize("Rendering u'r skins..."), Localize("Loading skin files"), 0); + GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); }); } void CSkins::Refresh(TSkinLoadedCBFunc &&SkinLoadedFunc) { - for(const auto &SkinIt : m_Skins) + for(const auto &[_, pSkin] : m_Skins) { - const auto &pSkin = SkinIt.second; - Graphics()->UnloadTexture(&pSkin->m_OriginalSkin.m_Body); - Graphics()->UnloadTexture(&pSkin->m_OriginalSkin.m_BodyOutline); - Graphics()->UnloadTexture(&pSkin->m_OriginalSkin.m_Feet); - Graphics()->UnloadTexture(&pSkin->m_OriginalSkin.m_FeetOutline); - Graphics()->UnloadTexture(&pSkin->m_OriginalSkin.m_Hands); - Graphics()->UnloadTexture(&pSkin->m_OriginalSkin.m_HandsOutline); - for(auto &Eye : pSkin->m_OriginalSkin.m_aEyes) - Graphics()->UnloadTexture(&Eye); - - Graphics()->UnloadTexture(&pSkin->m_ColorableSkin.m_Body); - Graphics()->UnloadTexture(&pSkin->m_ColorableSkin.m_BodyOutline); - Graphics()->UnloadTexture(&pSkin->m_ColorableSkin.m_Feet); - Graphics()->UnloadTexture(&pSkin->m_ColorableSkin.m_FeetOutline); - Graphics()->UnloadTexture(&pSkin->m_ColorableSkin.m_Hands); - Graphics()->UnloadTexture(&pSkin->m_ColorableSkin.m_HandsOutline); - for(auto &Eye : pSkin->m_ColorableSkin.m_aEyes) - Graphics()->UnloadTexture(&Eye); + pSkin->m_OriginalSkin.Unload(Graphics()); + pSkin->m_ColorableSkin.Unload(Graphics()); } m_Skins.clear(); @@ -341,14 +334,6 @@ void CSkins::Refresh(TSkinLoadedCBFunc &&SkinLoadedFunc) SkinScanUser.m_pThis = this; SkinScanUser.m_SkinLoadedFunc = SkinLoadedFunc; Storage()->ListDirectory(IStorage::TYPE_ALL, "skins", SkinScan, &SkinScanUser); - if(m_Skins.empty()) - { - Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "gameclient", "failed to load skins. folder='skins/'"); - CSkin DummySkin{"dummy"}; - DummySkin.m_BloodColor = ColorRGBA(1.0f, 1.0f, 1.0f); - auto &&pDummySkin = std::make_unique(std::move(DummySkin)); - m_Skins.insert({pDummySkin->GetName(), std::move(pDummySkin)}); - } } int CSkins::Num() @@ -362,35 +347,34 @@ const CSkin *CSkins::Find(const char *pName) if(pSkin == nullptr) { pSkin = FindOrNullptr("default"); - if(pSkin == nullptr) - return m_Skins.begin()->second.get(); - else - return pSkin; } - else + if(pSkin == nullptr) { - return pSkin; + pSkin = &m_PlaceholderSkin; } + return pSkin; } const CSkin *CSkins::FindOrNullptr(const char *pName, bool IgnorePrefix) { - const char *pSkinPrefix = m_aEventSkinPrefix[0] ? m_aEventSkinPrefix : g_Config.m_ClSkinPrefix; if(g_Config.m_ClVanillaSkinsOnly && !IsVanillaSkin(pName)) { return nullptr; } - else if(pSkinPrefix && pSkinPrefix[0] && !IgnorePrefix) + + const char *pSkinPrefix = m_aEventSkinPrefix[0] != '\0' ? m_aEventSkinPrefix : g_Config.m_ClSkinPrefix; + if(!IgnorePrefix && pSkinPrefix[0] != '\0') { - char aBuf[24]; - str_format(aBuf, sizeof(aBuf), "%s_%s", pSkinPrefix, pName); + char aNameWithPrefix[48]; // Larger than skin name length to allow IsValidName to check if it's too long + str_format(aNameWithPrefix, sizeof(aNameWithPrefix), "%s_%s", pSkinPrefix, pName); // If we find something, use it, otherwise fall back to normal skins. - const auto *pResult = FindImpl(aBuf); + const auto *pResult = FindImpl(aNameWithPrefix); if(pResult != nullptr) { return pResult; } } + return FindImpl(pName); } @@ -406,13 +390,13 @@ const CSkin *CSkins::FindImpl(const char *pName) if(!g_Config.m_ClDownloadSkins) return nullptr; - if(str_find(pName, "/") != 0) + if(!CSkin::IsValidName(pName)) return nullptr; const auto SkinDownloadIt = m_DownloadSkins.find(pName); if(SkinDownloadIt != m_DownloadSkins.end()) { - if(SkinDownloadIt->second->m_pTask && SkinDownloadIt->second->m_pTask->State() == HTTP_DONE) + if(SkinDownloadIt->second->m_pTask && SkinDownloadIt->second->m_pTask->State() == EHttpState::DONE && SkinDownloadIt->second->m_pTask->m_Info.m_pData) { char aPath[IO_MAX_PATH_LENGTH]; str_format(aPath, sizeof(aPath), "downloadedskins/%s.png", SkinDownloadIt->second->GetName()); @@ -422,7 +406,7 @@ const CSkin *CSkins::FindImpl(const char *pName) --m_DownloadingSkins; return pSkin; } - if(SkinDownloadIt->second->m_pTask && (SkinDownloadIt->second->m_pTask->State() == HTTP_ERROR || SkinDownloadIt->second->m_pTask->State() == HTTP_ABORTED)) + if(SkinDownloadIt->second->m_pTask && (SkinDownloadIt->second->m_pTask->State() == EHttpState::ERROR || SkinDownloadIt->second->m_pTask->State() == EHttpState::ABORTED)) { SkinDownloadIt->second->m_pTask = nullptr; --m_DownloadingSkins; @@ -432,16 +416,62 @@ const CSkin *CSkins::FindImpl(const char *pName) CDownloadSkin Skin{pName}; - char aUrl[IO_MAX_PATH_LENGTH]; char aEscapedName[256]; EscapeUrl(aEscapedName, sizeof(aEscapedName), pName); + char aUrl[IO_MAX_PATH_LENGTH]; str_format(aUrl, sizeof(aUrl), "%s%s.png", g_Config.m_ClDownloadCommunitySkins != 0 ? g_Config.m_ClSkinCommunityDownloadUrl : g_Config.m_ClSkinDownloadUrl, aEscapedName); + char aBuf[IO_MAX_PATH_LENGTH]; str_format(Skin.m_aPath, sizeof(Skin.m_aPath), "downloadedskins/%s", IStorage::FormatTmpPath(aBuf, sizeof(aBuf), pName)); + Skin.m_pTask = std::make_shared(this, aUrl, Storage(), Skin.m_aPath); - m_pClient->Engine()->AddJob(Skin.m_pTask); + Http()->Run(Skin.m_pTask); + auto &&pDownloadSkin = std::make_unique(std::move(Skin)); m_DownloadSkins.insert({pDownloadSkin->GetName(), std::move(pDownloadSkin)}); ++m_DownloadingSkins; + return nullptr; } + +void CSkins::RandomizeSkin(int Dummy) +{ + static const float s_aSchemes[] = {1.0f / 2.0f, 1.0f / 3.0f, 1.0f / -3.0f, 1.0f / 12.0f, 1.0f / -12.0f}; // complementary, triadic, analogous + const bool UseCustomColor = Dummy ? g_Config.m_ClDummyUseCustomColor : g_Config.m_ClPlayerUseCustomColor; + if(UseCustomColor) + { + float GoalSat = random_float(0.3f, 1.0f); + float MaxBodyLht = 1.0f - GoalSat * GoalSat; // max allowed lightness before we start losing saturation + + ColorHSLA Body; + Body.h = random_float(); + Body.l = random_float(0.0f, MaxBodyLht); + Body.s = clamp(GoalSat * GoalSat / (1.0f - Body.l), 0.0f, 1.0f); + + ColorHSLA Feet; + Feet.h = std::fmod(Body.h + s_aSchemes[rand() % std::size(s_aSchemes)], 1.0f); + Feet.l = random_float(); + Feet.s = clamp(GoalSat * GoalSat / (1.0f - Feet.l), 0.0f, 1.0f); + + unsigned *pColorBody = Dummy ? &g_Config.m_ClDummyColorBody : &g_Config.m_ClPlayerColorBody; + unsigned *pColorFeet = Dummy ? &g_Config.m_ClDummyColorFeet : &g_Config.m_ClPlayerColorFeet; + + *pColorBody = Body.Pack(false); + *pColorFeet = Feet.Pack(false); + } + + const size_t SkinNameSize = Dummy ? sizeof(g_Config.m_ClDummySkin) : sizeof(g_Config.m_ClPlayerSkin); + char aRandomSkinName[24]; + str_copy(aRandomSkinName, "default", SkinNameSize); + if(!m_pClient->m_Skins.GetSkinsUnsafe().empty()) + { + do + { + auto it = m_pClient->m_Skins.GetSkinsUnsafe().begin(); + std::advance(it, rand() % m_pClient->m_Skins.GetSkinsUnsafe().size()); + str_copy(aRandomSkinName, (*it).second->GetName(), SkinNameSize); + } while(!str_comp(aRandomSkinName, "x_ninja") || !str_comp(aRandomSkinName, "x_spec")); + } + char *pSkinName = Dummy ? g_Config.m_ClDummySkin : g_Config.m_ClPlayerSkin; + str_copy(pSkinName, aRandomSkinName, SkinNameSize); +} diff --git a/src/game/client/components/skins.h b/src/game/client/components/skins.h index 677350fca4..cf648f6ad9 100644 --- a/src/game/client/components/skins.h +++ b/src/game/client/components/skins.h @@ -13,14 +13,14 @@ class CSkins : public CComponent { public: - CSkins() = default; + CSkins(); class CGetPngFile : public CHttpRequest { CSkins *m_pSkins; protected: - virtual int OnCompletion(int State) override; + virtual void OnCompletion(EHttpState State) override; public: CGetPngFile(CSkins *pSkins, const char *pUrl, IStorage *pStorage, const char *pDest); @@ -66,6 +66,7 @@ class CSkins : public CComponent std::unordered_map> &GetSkinsUnsafe() { return m_Skins; } const CSkin *FindOrNullptr(const char *pName, bool IgnorePrefix = false); const CSkin *Find(const char *pName); + void RandomizeSkin(int Dummy); bool IsDownloadingSkins() { return m_DownloadingSkins; } @@ -79,10 +80,11 @@ class CSkins : public CComponent private: std::unordered_map> m_Skins; std::unordered_map> m_DownloadSkins; + CSkin m_PlaceholderSkin; size_t m_DownloadingSkins = 0; char m_aEventSkinPrefix[24]; - bool LoadSkinPNG(CImageInfo &Info, const char *pName, const char *pPath, int DirType); + bool LoadSkinPng(CImageInfo &Info, const char *pName, const char *pPath, int DirType); const CSkin *LoadSkin(const char *pName, const char *pPath, int DirType); const CSkin *LoadSkin(const char *pName, CImageInfo &Info); const CSkin *FindImpl(const char *pName); diff --git a/src/game/client/components/skins7.cpp b/src/game/client/components/skins7.cpp new file mode 100644 index 0000000000..6dc5a18a20 --- /dev/null +++ b/src/game/client/components/skins7.cpp @@ -0,0 +1,579 @@ +/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ +/* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "menus.h" +#include "skins7.h" + +const char *const CSkins7::ms_apSkinPartNames[protocol7::NUM_SKINPARTS] = {"body", "marking", "decoration", "hands", "feet", "eyes"}; +const char *const CSkins7::ms_apSkinPartNamesLocalized[protocol7::NUM_SKINPARTS] = {Localizable("Body", "skins"), Localizable("Marking", "skins"), Localizable("Decoration", "skins"), Localizable("Hands", "skins"), Localizable("Feet", "skins"), Localizable("Eyes", "skins")}; +const char *const CSkins7::ms_apColorComponents[NUM_COLOR_COMPONENTS] = {"hue", "sat", "lgt", "alp"}; + +char *CSkins7::ms_apSkinNameVariables[NUM_DUMMIES] = {0}; +char *CSkins7::ms_apSkinVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{0}}; +int *CSkins7::ms_apUCCVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{0}}; +int unsigned *CSkins7::ms_apColorVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{0}}; + +#define SKINS_DIR "skins7" + +// TODO: uncomment +// const float MIN_EYE_BODY_COLOR_DIST = 80.f; // between body and eyes (LAB color space) + +int CSkins7::SkinPartScan(const char *pName, int IsDir, int DirType, void *pUser) +{ + CSkins7 *pSelf = (CSkins7 *)pUser; + if(IsDir || !str_endswith(pName, ".png")) + return 0; + + size_t PartNameSize, PartNameCount; + str_utf8_stats(pName, str_length(pName) - str_length(".png") + 1, IO_MAX_PATH_LENGTH, &PartNameSize, &PartNameCount); + if(PartNameSize >= protocol7::MAX_SKIN_ARRAY_SIZE || PartNameCount > protocol7::MAX_SKIN_LENGTH) + { + log_error("skins7", "Failed to load skin part '%s': name too long", pName); + return 0; + } + + CSkinPart Part; + str_copy(Part.m_aName, pName, minimum(PartNameSize + 1, sizeof(Part.m_aName))); + if(pSelf->FindSkinPart(pSelf->m_ScanningPart, Part.m_aName, true) != -1) + return 0; + + char aFilename[IO_MAX_PATH_LENGTH]; + str_format(aFilename, sizeof(aFilename), SKINS_DIR "/%s/%s", CSkins7::ms_apSkinPartNames[pSelf->m_ScanningPart], pName); + CImageInfo Info; + if(!pSelf->Graphics()->LoadPng(Info, aFilename, DirType)) + { + log_error("skins7", "Failed to load skin part '%s'", pName); + return 0; + } + if(!pSelf->Graphics()->IsImageFormatRgba(aFilename, Info)) + { + log_error("skins7", "Failed to load skin part '%s': must be RGBA format", pName); + Info.Free(); + return 0; + } + + Part.m_OrgTexture = pSelf->Graphics()->LoadTextureRaw(Info, 0, aFilename); + Part.m_BloodColor = ColorRGBA(1.0f, 1.0f, 1.0f); + + const int Step = 4; + unsigned char *pData = (unsigned char *)Info.m_pData; + + // dig out blood color + if(pSelf->m_ScanningPart == protocol7::SKINPART_BODY) + { + int Pitch = Info.m_Width * Step; + int PartX = Info.m_Width / 2; + int PartY = 0; + int PartWidth = Info.m_Width / 2; + int PartHeight = Info.m_Height / 2; + + int64_t aColors[3] = {0}; + for(int y = PartY; y < PartY + PartHeight; y++) + for(int x = PartX; x < PartX + PartWidth; x++) + if(pData[y * Pitch + x * Step + 3] > 128) + for(int c = 0; c < 3; c++) + aColors[c] += pData[y * Pitch + x * Step + c]; + + Part.m_BloodColor = ColorRGBA(normalize(vec3(aColors[0], aColors[1], aColors[2]))); + } + + ConvertToGrayscale(Info); + + Part.m_ColorTexture = pSelf->Graphics()->LoadTextureRawMove(Info, 0, aFilename); + + // set skin part data + Part.m_Flags = 0; + if(pName[0] == 'x' && pName[1] == '_') + Part.m_Flags |= SKINFLAG_SPECIAL; + if(DirType != IStorage::TYPE_SAVE) + Part.m_Flags |= SKINFLAG_STANDARD; + + if(pSelf->Config()->m_Debug) + { + log_trace("skins7", "Loaded skin part '%s'", Part.m_aName); + } + pSelf->m_avSkinParts[pSelf->m_ScanningPart].emplace_back(Part); + + return 0; +} + +int CSkins7::SkinScan(const char *pName, int IsDir, int DirType, void *pUser) +{ + if(IsDir || !str_endswith(pName, ".json")) + return 0; + + CSkins7 *pSelf = (CSkins7 *)pUser; + + // read file data into buffer + char aFilename[IO_MAX_PATH_LENGTH]; + str_format(aFilename, sizeof(aFilename), SKINS_DIR "/%s", pName); + void *pFileData; + unsigned JsonFileSize; + if(!pSelf->Storage()->ReadFile(aFilename, IStorage::TYPE_ALL, &pFileData, &JsonFileSize)) + { + return 0; + } + + // init + CSkin Skin = pSelf->m_DummySkin; + int NameLength = str_length(pName); + str_copy(Skin.m_aName, pName, 1 + NameLength - str_length(".json")); + bool SpecialSkin = pName[0] == 'x' && pName[1] == '_'; + + // parse json data + json_settings JsonSettings; + mem_zero(&JsonSettings, sizeof(JsonSettings)); + char aError[256]; + json_value *pJsonData = json_parse_ex(&JsonSettings, static_cast(pFileData), JsonFileSize, aError); + free(pFileData); + + if(pJsonData == nullptr) + { + log_error("skins7", "Failed to parse skin json file '%s': %s", aFilename, aError); + return 0; + } + + // extract data + const json_value &Start = (*pJsonData)["skin"]; + if(Start.type == json_object) + { + for(int PartIndex = 0; PartIndex < protocol7::NUM_SKINPARTS; ++PartIndex) + { + const json_value &Part = Start[(const char *)ms_apSkinPartNames[PartIndex]]; + if(Part.type != json_object) + continue; + + // filename + const json_value &Filename = Part["filename"]; + if(Filename.type == json_string) + { + int SkinPart = pSelf->FindSkinPart(PartIndex, (const char *)Filename, SpecialSkin); + if(SkinPart > -1) + Skin.m_apParts[PartIndex] = pSelf->GetSkinPart(PartIndex, SkinPart); + } + + // use custom colors + bool UseCustomColors = false; + const json_value &Color = Part["custom_colors"]; + if(Color.type == json_string) + UseCustomColors = str_comp((const char *)Color, "true") == 0; + else if(Color.type == json_boolean) + UseCustomColors = Color.u.boolean; + Skin.m_aUseCustomColors[PartIndex] = UseCustomColors; + + // color components + if(!UseCustomColors) + continue; + + for(int i = 0; i < NUM_COLOR_COMPONENTS; i++) + { + if(PartIndex != protocol7::SKINPART_MARKING && i == 3) + continue; + + const json_value &Component = Part[(const char *)ms_apColorComponents[i]]; + if(Component.type == json_integer) + { + switch(i) + { + case 0: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0xFF00FFFF) | (Component.u.integer << 16); break; + case 1: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0xFFFF00FF) | (Component.u.integer << 8); break; + case 2: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0xFFFFFF00) | Component.u.integer; break; + case 3: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0x00FFFFFF) | (Component.u.integer << 24); break; + } + } + } + } + } + + // clean up + json_value_free(pJsonData); + + // set skin data + Skin.m_Flags = SpecialSkin ? SKINFLAG_SPECIAL : 0; + if(DirType != IStorage::TYPE_SAVE) + Skin.m_Flags |= SKINFLAG_STANDARD; + + if(pSelf->Config()->m_Debug) + { + log_trace("skins7", "Loaded skin '%s'", Skin.m_aName); + } + pSelf->m_vSkins.insert(std::lower_bound(pSelf->m_vSkins.begin(), pSelf->m_vSkins.end(), Skin), Skin); + + return 0; +} + +int CSkins7::GetInitAmount() const +{ + return protocol7::NUM_SKINPARTS * 5 + 8; +} + +void CSkins7::OnInit() +{ + int Dummy = 0; + ms_apSkinNameVariables[Dummy] = Config()->m_ClPlayer7Skin; + ms_apSkinVariables[Dummy][protocol7::SKINPART_BODY] = Config()->m_ClPlayer7SkinBody; + ms_apSkinVariables[Dummy][protocol7::SKINPART_MARKING] = Config()->m_ClPlayer7SkinMarking; + ms_apSkinVariables[Dummy][protocol7::SKINPART_DECORATION] = Config()->m_ClPlayer7SkinDecoration; + ms_apSkinVariables[Dummy][protocol7::SKINPART_HANDS] = Config()->m_ClPlayer7SkinHands; + ms_apSkinVariables[Dummy][protocol7::SKINPART_FEET] = Config()->m_ClPlayer7SkinFeet; + ms_apSkinVariables[Dummy][protocol7::SKINPART_EYES] = Config()->m_ClPlayer7SkinEyes; + ms_apUCCVariables[Dummy][protocol7::SKINPART_BODY] = &Config()->m_ClPlayer7UseCustomColorBody; + ms_apUCCVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClPlayer7UseCustomColorMarking; + ms_apUCCVariables[Dummy][protocol7::SKINPART_DECORATION] = &Config()->m_ClPlayer7UseCustomColorDecoration; + ms_apUCCVariables[Dummy][protocol7::SKINPART_HANDS] = &Config()->m_ClPlayer7UseCustomColorHands; + ms_apUCCVariables[Dummy][protocol7::SKINPART_FEET] = &Config()->m_ClPlayer7UseCustomColorFeet; + ms_apUCCVariables[Dummy][protocol7::SKINPART_EYES] = &Config()->m_ClPlayer7UseCustomColorEyes; + ms_apColorVariables[Dummy][protocol7::SKINPART_BODY] = &Config()->m_ClPlayer7ColorBody; + ms_apColorVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClPlayer7ColorMarking; + ms_apColorVariables[Dummy][protocol7::SKINPART_DECORATION] = &Config()->m_ClPlayer7ColorDecoration; + ms_apColorVariables[Dummy][protocol7::SKINPART_HANDS] = &Config()->m_ClPlayer7ColorHands; + ms_apColorVariables[Dummy][protocol7::SKINPART_FEET] = &Config()->m_ClPlayer7ColorFeet; + ms_apColorVariables[Dummy][protocol7::SKINPART_EYES] = &Config()->m_ClPlayer7ColorEyes; + + Dummy = 1; + ms_apSkinNameVariables[Dummy] = Config()->m_ClDummy7Skin; + ms_apSkinVariables[Dummy][protocol7::SKINPART_BODY] = Config()->m_ClDummy7SkinBody; + ms_apSkinVariables[Dummy][protocol7::SKINPART_MARKING] = Config()->m_ClDummy7SkinMarking; + ms_apSkinVariables[Dummy][protocol7::SKINPART_DECORATION] = Config()->m_ClDummy7SkinDecoration; + ms_apSkinVariables[Dummy][protocol7::SKINPART_HANDS] = Config()->m_ClDummy7SkinHands; + ms_apSkinVariables[Dummy][protocol7::SKINPART_FEET] = Config()->m_ClDummy7SkinFeet; + ms_apSkinVariables[Dummy][protocol7::SKINPART_EYES] = Config()->m_ClDummy7SkinEyes; + ms_apUCCVariables[Dummy][protocol7::SKINPART_BODY] = &Config()->m_ClDummy7UseCustomColorBody; + ms_apUCCVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClDummy7UseCustomColorMarking; + ms_apUCCVariables[Dummy][protocol7::SKINPART_DECORATION] = &Config()->m_ClDummy7UseCustomColorDecoration; + ms_apUCCVariables[Dummy][protocol7::SKINPART_HANDS] = &Config()->m_ClDummy7UseCustomColorHands; + ms_apUCCVariables[Dummy][protocol7::SKINPART_FEET] = &Config()->m_ClDummy7UseCustomColorFeet; + ms_apUCCVariables[Dummy][protocol7::SKINPART_EYES] = &Config()->m_ClDummy7UseCustomColorEyes; + ms_apColorVariables[Dummy][protocol7::SKINPART_BODY] = &Config()->m_ClDummy7ColorBody; + ms_apColorVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClDummy7ColorMarking; + ms_apColorVariables[Dummy][protocol7::SKINPART_DECORATION] = &Config()->m_ClDummy7ColorDecoration; + ms_apColorVariables[Dummy][protocol7::SKINPART_HANDS] = &Config()->m_ClDummy7ColorHands; + ms_apColorVariables[Dummy][protocol7::SKINPART_FEET] = &Config()->m_ClDummy7ColorFeet; + ms_apColorVariables[Dummy][protocol7::SKINPART_EYES] = &Config()->m_ClDummy7ColorEyes; + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + m_avSkinParts[Part].clear(); + + // add none part + if(Part == protocol7::SKINPART_MARKING || Part == protocol7::SKINPART_DECORATION) + { + CSkinPart NoneSkinPart; + NoneSkinPart.m_Flags = SKINFLAG_STANDARD; + NoneSkinPart.m_aName[0] = '\0'; + NoneSkinPart.m_BloodColor = vec3(1.0f, 1.0f, 1.0f); + m_avSkinParts[Part].emplace_back(NoneSkinPart); + } + + // load skin parts + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), SKINS_DIR "/%s", ms_apSkinPartNames[Part]); + m_ScanningPart = Part; + Storage()->ListDirectory(IStorage::TYPE_ALL, aBuf, SkinPartScan, this); + + // add dummy skin part + if(m_avSkinParts[Part].empty()) + { + CSkinPart DummySkinPart; + DummySkinPart.m_Flags = SKINFLAG_STANDARD; + str_copy(DummySkinPart.m_aName, "dummy"); + DummySkinPart.m_BloodColor = vec3(1.0f, 1.0f, 1.0f); + m_avSkinParts[Part].emplace_back(DummySkinPart); + } + + GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); + } + + // create dummy skin + m_DummySkin.m_Flags = SKINFLAG_STANDARD; + str_copy(m_DummySkin.m_aName, "dummy"); + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + int Default; + if(Part == protocol7::SKINPART_MARKING || Part == protocol7::SKINPART_DECORATION) + Default = FindSkinPart(Part, "", false); + else + Default = FindSkinPart(Part, "standard", false); + if(Default < 0) + Default = 0; + m_DummySkin.m_apParts[Part] = GetSkinPart(Part, Default); + m_DummySkin.m_aPartColors[Part] = Part == protocol7::SKINPART_MARKING ? (255 << 24) + 65408 : 65408; + m_DummySkin.m_aUseCustomColors[Part] = 0; + } + GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); + + // load skins + m_vSkins.clear(); + Storage()->ListDirectory(IStorage::TYPE_ALL, SKINS_DIR, SkinScan, this); + GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); + + // add dummy skin + if(m_vSkins.empty()) + m_vSkins.emplace_back(m_DummySkin); + + LoadXmasHat(); + LoadBotDecoration(); + GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); +} + +void CSkins7::LoadXmasHat() +{ + const char *pFilename = SKINS_DIR "/xmas_hat.png"; + CImageInfo Info; + if(!Graphics()->LoadPng(Info, pFilename, IStorage::TYPE_ALL) || + !Graphics()->IsImageFormatRgba(pFilename, Info) || + !Graphics()->CheckImageDivisibility(pFilename, Info, 1, 4, false)) + { + log_error("skins7", "Failed to xmas hat '%s'", pFilename); + Info.Free(); + } + else + { + if(Config()->m_Debug) + { + log_trace("skins7", "Loaded xmas hat '%s'", pFilename); + } + m_XmasHatTexture = Graphics()->LoadTextureRawMove(Info, 0, pFilename); + } +} + +void CSkins7::LoadBotDecoration() +{ + const char *pFilename = SKINS_DIR "/bot.png"; + CImageInfo Info; + if(!Graphics()->LoadPng(Info, pFilename, IStorage::TYPE_ALL) || + !Graphics()->IsImageFormatRgba(pFilename, Info) || + !Graphics()->CheckImageDivisibility(pFilename, Info, 12, 5, false)) + { + log_error("skins7", "Failed to load bot '%s'", pFilename); + Info.Free(); + } + else + { + if(Config()->m_Debug) + { + log_trace("skins7", "Loaded bot '%s'", pFilename); + } + m_BotTexture = Graphics()->LoadTextureRawMove(Info, 0, pFilename); + } +} + +void CSkins7::AddSkin(const char *pSkinName, int Dummy) +{ + CSkin Skin = m_DummySkin; + Skin.m_Flags = 0; + str_copy(Skin.m_aName, pSkinName, sizeof(Skin.m_aName)); + for(int PartIndex = 0; PartIndex < protocol7::NUM_SKINPARTS; ++PartIndex) + { + int SkinPart = FindSkinPart(PartIndex, ms_apSkinVariables[Dummy][PartIndex], false); + if(SkinPart > -1) + Skin.m_apParts[PartIndex] = GetSkinPart(PartIndex, SkinPart); + Skin.m_aUseCustomColors[PartIndex] = *ms_apUCCVariables[Dummy][PartIndex]; + Skin.m_aPartColors[PartIndex] = *ms_apColorVariables[Dummy][PartIndex]; + } + int SkinIndex = Find(Skin.m_aName, false); + if(SkinIndex != -1) + m_vSkins[SkinIndex] = Skin; + else + m_vSkins.emplace_back(Skin); +} + +bool CSkins7::RemoveSkin(const CSkin *pSkin) +{ + char aBuf[IO_MAX_PATH_LENGTH]; + str_format(aBuf, sizeof(aBuf), SKINS_DIR "/%s.json", pSkin->m_aName); + if(!Storage()->RemoveFile(aBuf, IStorage::TYPE_SAVE)) + { + return false; + } + + auto Position = std::find(m_vSkins.begin(), m_vSkins.end(), *pSkin); + m_vSkins.erase(Position); + return true; +} + +int CSkins7::Num() +{ + return m_vSkins.size(); +} + +int CSkins7::NumSkinPart(int Part) +{ + return m_avSkinParts[Part].size(); +} + +const CSkins7::CSkin *CSkins7::Get(int Index) +{ + return &m_vSkins[maximum((std::size_t)0, Index % m_vSkins.size())]; +} + +int CSkins7::Find(const char *pName, bool AllowSpecialSkin) +{ + for(unsigned int i = 0; i < m_vSkins.size(); i++) + { + if(str_comp(m_vSkins[i].m_aName, pName) == 0 && ((m_vSkins[i].m_Flags & SKINFLAG_SPECIAL) == 0 || AllowSpecialSkin)) + return i; + } + return -1; +} + +const CSkins7::CSkinPart *CSkins7::GetSkinPart(int Part, int Index) +{ + int Size = m_avSkinParts[Part].size(); + return &m_avSkinParts[Part][maximum(0, Index % Size)]; +} + +int CSkins7::FindSkinPart(int Part, const char *pName, bool AllowSpecialPart) +{ + for(unsigned int i = 0; i < m_avSkinParts[Part].size(); i++) + { + if(str_comp(m_avSkinParts[Part][i].m_aName, pName) == 0 && ((m_avSkinParts[Part][i].m_Flags & SKINFLAG_SPECIAL) == 0 || AllowSpecialPart)) + return i; + } + return -1; +} + +void CSkins7::RandomizeSkin(int Dummy) +{ + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + int Hue = rand() % 255; + int Sat = rand() % 255; + int Lgt = rand() % 255; + int Alp = 0; + if(Part == protocol7::SKINPART_MARKING) + Alp = rand() % 255; + int ColorVariable = (Alp << 24) | (Hue << 16) | (Sat << 8) | Lgt; + *ms_apUCCVariables[Dummy][Part] = true; + *ms_apColorVariables[Dummy][Part] = ColorVariable; + } + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + const CSkinPart *pSkinPart = GetSkinPart(Part, rand() % NumSkinPart(Part)); + while(pSkinPart->m_Flags & SKINFLAG_SPECIAL) + pSkinPart = GetSkinPart(Part, rand() % NumSkinPart(Part)); + str_copy(ms_apSkinVariables[Dummy][Part], pSkinPart->m_aName, protocol7::MAX_SKIN_ARRAY_SIZE); + } + + ms_apSkinNameVariables[Dummy][0] = '\0'; +} + +ColorRGBA CSkins7::GetColor(int Value, bool UseAlpha) const +{ + return color_cast(ColorHSLA(Value, UseAlpha).UnclampLighting(ColorHSLA::DARKEST_LGT7)); +} + +ColorRGBA CSkins7::GetTeamColor(int UseCustomColors, int PartColor, int Team, int Part) const +{ + static const int s_aTeamColors[3] = {0xC4C34E, 0x00FF6B, 0x9BFF6B}; + + int TeamHue = (s_aTeamColors[Team + 1] >> 16) & 0xff; + int TeamSat = (s_aTeamColors[Team + 1] >> 8) & 0xff; + int TeamLgt = s_aTeamColors[Team + 1] & 0xff; + int PartSat = (PartColor >> 8) & 0xff; + int PartLgt = PartColor & 0xff; + + if(!UseCustomColors) + { + PartSat = 255; + PartLgt = 255; + } + + int MinSat = 160; + int MaxSat = 255; + + int h = TeamHue; + int s = clamp(mix(TeamSat, PartSat, 0.2), MinSat, MaxSat); + int l = clamp(mix(TeamLgt, PartLgt, 0.2), (int)ColorHSLA::DARKEST_LGT7, 200); + + int ColorVal = (h << 16) + (s << 8) + l; + + return GetColor(ColorVal, Part == protocol7::SKINPART_MARKING); +} + +bool CSkins7::ValidateSkinParts(char *apPartNames[protocol7::NUM_SKINPARTS], int *pUseCustomColors, int *pPartColors, int GameFlags) const +{ + // force standard (black) eyes on team skins + if(GameFlags & GAMEFLAG_TEAMS) + { + // TODO: adjust eye color here as well? + if(str_comp(apPartNames[protocol7::SKINPART_EYES], "colorable") == 0 || str_comp(apPartNames[protocol7::SKINPART_EYES], "negative") == 0) + { + str_copy(apPartNames[protocol7::SKINPART_EYES], "standard", protocol7::MAX_SKIN_ARRAY_SIZE); + return false; + } + } + return true; +} + +bool CSkins7::SaveSkinfile(const char *pSaveSkinName, int Dummy) +{ + char aBuf[IO_MAX_PATH_LENGTH]; + str_format(aBuf, sizeof(aBuf), SKINS_DIR "/%s.json", pSaveSkinName); + IOHANDLE File = Storage()->OpenFile(aBuf, IOFLAG_WRITE, IStorage::TYPE_SAVE); + if(!File) + return false; + + CJsonFileWriter Writer(File); + + Writer.BeginObject(); + Writer.WriteAttribute("skin"); + Writer.BeginObject(); + for(int PartIndex = 0; PartIndex < protocol7::NUM_SKINPARTS; PartIndex++) + { + if(!ms_apSkinVariables[Dummy][PartIndex][0]) + continue; + + // part start + Writer.WriteAttribute(ms_apSkinPartNames[PartIndex]); + Writer.BeginObject(); + { + Writer.WriteAttribute("filename"); + Writer.WriteStrValue(ms_apSkinVariables[Dummy][PartIndex]); + + const bool CustomColors = *ms_apUCCVariables[Dummy][PartIndex]; + Writer.WriteAttribute("custom_colors"); + Writer.WriteBoolValue(CustomColors); + + if(CustomColors) + { + for(int ColorComponent = 0; ColorComponent < NUM_COLOR_COMPONENTS - 1; ColorComponent++) + { + int Val = (*ms_apColorVariables[Dummy][PartIndex] >> (2 - ColorComponent) * 8) & 0xff; + Writer.WriteAttribute(ms_apColorComponents[ColorComponent]); + Writer.WriteIntValue(Val); + } + if(PartIndex == protocol7::SKINPART_MARKING) + { + int Val = (*ms_apColorVariables[Dummy][PartIndex] >> 24) & 0xff; + Writer.WriteAttribute(ms_apColorComponents[3]); + Writer.WriteIntValue(Val); + } + } + } + Writer.EndObject(); + } + Writer.EndObject(); + Writer.EndObject(); + + // add new skin to the skin list + AddSkin(pSaveSkinName, Dummy); + return true; +} diff --git a/src/game/client/components/skins7.h b/src/game/client/components/skins7.h new file mode 100644 index 0000000000..3cfd1f74fb --- /dev/null +++ b/src/game/client/components/skins7.h @@ -0,0 +1,100 @@ +/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ +/* If you are missing that file, acquire a complete release at teeworlds.com. */ +#ifndef GAME_CLIENT_COMPONENTS_SKINS7_H +#define GAME_CLIENT_COMPONENTS_SKINS7_H +#include +#include +#include +#include +#include +#include + +#include +#include + +class CSkins7 : public CComponent +{ +public: + enum + { + SKINFLAG_SPECIAL = 1 << 0, + SKINFLAG_STANDARD = 1 << 1, + + NUM_COLOR_COMPONENTS = 4, + + HAT_NUM = 2, + HAT_OFFSET_SIDE = 2, + }; + + struct CSkinPart + { + int m_Flags; + char m_aName[24]; + IGraphics::CTextureHandle m_OrgTexture; + IGraphics::CTextureHandle m_ColorTexture; + ColorRGBA m_BloodColor; + + bool operator<(const CSkinPart &Other) { return str_comp_nocase(m_aName, Other.m_aName) < 0; } + }; + + struct CSkin + { + int m_Flags; + char m_aName[24]; + const CSkinPart *m_apParts[protocol7::NUM_SKINPARTS]; + int m_aPartColors[protocol7::NUM_SKINPARTS]; + int m_aUseCustomColors[protocol7::NUM_SKINPARTS]; + + bool operator<(const CSkin &Other) const { return str_comp_nocase(m_aName, Other.m_aName) < 0; } + bool operator==(const CSkin &Other) const { return !str_comp(m_aName, Other.m_aName); } + }; + + static const char *const ms_apSkinPartNames[protocol7::NUM_SKINPARTS]; + static const char *const ms_apSkinPartNamesLocalized[protocol7::NUM_SKINPARTS]; + static const char *const ms_apColorComponents[NUM_COLOR_COMPONENTS]; + + static char *ms_apSkinNameVariables[NUM_DUMMIES]; + static char *ms_apSkinVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS]; + static int *ms_apUCCVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS]; // use custom color + static unsigned int *ms_apColorVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS]; + IGraphics::CTextureHandle m_XmasHatTexture; + IGraphics::CTextureHandle m_BotTexture; + + int GetInitAmount() const; + void OnInit() override; + + void AddSkin(const char *pSkinName, int Dummy); + bool RemoveSkin(const CSkin *pSkin); + + int Num(); + int NumSkinPart(int Part); + const CSkin *Get(int Index); + int Find(const char *pName, bool AllowSpecialSkin); + const CSkinPart *GetSkinPart(int Part, int Index); + int FindSkinPart(int Part, const char *pName, bool AllowSpecialPart); + void RandomizeSkin(int Dummy); + + ColorRGBA GetColor(int Value, bool UseAlpha) const; + ColorRGBA GetTeamColor(int UseCustomColors, int PartColor, int Team, int Part) const; + + // returns true if everything was valid and nothing changed + bool ValidateSkinParts(char *apPartNames[protocol7::NUM_SKINPARTS], int *pUseCustomColors, int *pPartColors, int GameFlags) const; + + bool SaveSkinfile(const char *pSaveSkinName, int Dummy); + + virtual int Sizeof() const override { return sizeof(*this); } + +private: + int m_ScanningPart; + std::vector m_avSkinParts[protocol7::NUM_SKINPARTS]; + std::vector m_vSkins; + CSkin m_DummySkin; + + static int SkinPartScan(const char *pName, int IsDir, int DirType, void *pUser); + static int SkinScan(const char *pName, int IsDir, int DirType, void *pUser); + + void LoadXmasHat(); + void LoadBotDecoration(); +}; + +#endif diff --git a/src/game/client/components/sounds.cpp b/src/game/client/components/sounds.cpp index d1af20ccb5..d04a20fdf6 100644 --- a/src/game/client/components/sounds.cpp +++ b/src/game/client/components/sounds.cpp @@ -16,17 +16,21 @@ CSoundLoading::CSoundLoading(CGameClient *pGameClient, bool Render) : m_pGameClient(pGameClient), m_Render(Render) { + Abortable(true); } void CSoundLoading::Run() { for(int s = 0; s < g_pData->m_NumSounds; s++) { - const char *pLoadingCaption = Localize("Loading StormA Client"); + const char *pLoadingCaption = Localize("Loading DDNet Client"); const char *pLoadingContent = Localize("Loading sound files"); for(int i = 0; i < g_pData->m_aSounds[s].m_NumSounds; i++) { + if(State() == IJob::STATE_ABORTED) + return; + int Id = m_pGameClient->Sound()->LoadWV(g_pData->m_aSounds[s].m_aSounds[i].m_pFilename); g_pData->m_aSounds[s].m_aSounds[i].m_Id = Id; // try to render a frame @@ -39,6 +43,38 @@ void CSoundLoading::Run() } } +void CSounds::UpdateChannels() +{ + const float NewGuiSoundVolume = g_Config.m_SndChatSoundVolume / 100.0f; + if(NewGuiSoundVolume != m_GuiSoundVolume) + { + m_GuiSoundVolume = NewGuiSoundVolume; + Sound()->SetChannel(CSounds::CHN_GUI, m_GuiSoundVolume, 0.0f); + } + + const float NewGameSoundVolume = g_Config.m_SndGameSoundVolume / 100.0f; + if(NewGameSoundVolume != m_GameSoundVolume) + { + m_GameSoundVolume = NewGameSoundVolume; + Sound()->SetChannel(CSounds::CHN_WORLD, 0.9f * m_GameSoundVolume, 1.0f); + Sound()->SetChannel(CSounds::CHN_GLOBAL, m_GameSoundVolume, 0.0f); + } + + const float NewMapSoundVolume = g_Config.m_SndMapSoundVolume / 100.0f; + if(NewMapSoundVolume != m_MapSoundVolume) + { + m_MapSoundVolume = NewMapSoundVolume; + Sound()->SetChannel(CSounds::CHN_MAPSOUND, m_MapSoundVolume, 1.0f); + } + + const float NewBackgroundMusicVolume = g_Config.m_SndBackgroundMusicVolume / 100.0f; + if(NewBackgroundMusicVolume != m_BackgroundMusicVolume) + { + m_BackgroundMusicVolume = NewBackgroundMusicVolume; + Sound()->SetChannel(CSounds::CHN_MUSIC, m_BackgroundMusicVolume, 1.0f); + } +} + int CSounds::GetSampleId(int SetId) { if(!g_Config.m_SndEnable || !Sound()->IsSoundEnabled() || m_WaitForSoundJob || SetId < 0 || SetId >= g_pData->m_NumSounds) @@ -63,20 +99,7 @@ int CSounds::GetSampleId(int SetId) void CSounds::OnInit() { - // setup sound channels - m_GuiSoundVolume = g_Config.m_SndChatSoundVolume / 100.0f; - m_GameSoundVolume = g_Config.m_SndGameSoundVolume / 100.0f; - m_MapSoundVolume = g_Config.m_SndMapSoundVolume / 100.0f; - m_BackgroundMusicVolume = g_Config.m_SndBackgroundMusicVolume / 100.0f; - - Sound()->SetChannel(CSounds::CHN_GUI, m_GuiSoundVolume, 0.0f); - Sound()->SetChannel(CSounds::CHN_MUSIC, m_BackgroundMusicVolume, 1.0f); - Sound()->SetChannel(CSounds::CHN_WORLD, 0.9f * m_GameSoundVolume, 1.0f); - Sound()->SetChannel(CSounds::CHN_GLOBAL, m_GameSoundVolume, 0.0f); - Sound()->SetChannel(CSounds::CHN_MAPSOUND, m_MapSoundVolume, 1.0f); - - Sound()->SetListenerPos(0.0f, 0.0f); - + UpdateChannels(); ClearQueue(); // load sounds @@ -114,44 +137,14 @@ void CSounds::OnRender() // check for sound initialisation if(m_WaitForSoundJob) { - if(m_pSoundJob->Status() == IJob::STATE_DONE) + if(m_pSoundJob->State() == IJob::STATE_DONE) m_WaitForSoundJob = false; else return; } - // set listener pos - Sound()->SetListenerPos(m_pClient->m_Camera.m_Center.x, m_pClient->m_Camera.m_Center.y); - - // update volume - float NewGuiSoundVol = g_Config.m_SndChatSoundVolume / 100.0f; - if(NewGuiSoundVol != m_GuiSoundVolume) - { - m_GuiSoundVolume = NewGuiSoundVol; - Sound()->SetChannel(CSounds::CHN_GUI, m_GuiSoundVolume, 1.0f); - } - - float NewGameSoundVol = g_Config.m_SndGameSoundVolume / 100.0f; - if(NewGameSoundVol != m_GameSoundVolume) - { - m_GameSoundVolume = NewGameSoundVol; - Sound()->SetChannel(CSounds::CHN_WORLD, 0.9f * m_GameSoundVolume, 1.0f); - Sound()->SetChannel(CSounds::CHN_GLOBAL, m_GameSoundVolume, 1.0f); - } - - float NewMapSoundVol = g_Config.m_SndMapSoundVolume / 100.0f; - if(NewMapSoundVol != m_MapSoundVolume) - { - m_MapSoundVolume = NewMapSoundVol; - Sound()->SetChannel(CSounds::CHN_MAPSOUND, m_MapSoundVolume, 1.0f); - } - - float NewBackgroundMusicVol = g_Config.m_SndBackgroundMusicVolume / 100.0f; - if(NewBackgroundMusicVol != m_BackgroundMusicVolume) - { - m_BackgroundMusicVolume = NewBackgroundMusicVol; - Sound()->SetChannel(CSounds::CHN_MUSIC, m_BackgroundMusicVolume, 1.0f); - } + Sound()->SetListenerPosition(m_pClient->m_Camera.m_Center); + UpdateChannels(); // play sound from queue if(m_QueuePos > 0) @@ -187,49 +180,26 @@ void CSounds::Enqueue(int Channel, int SetId) m_aQueue[m_QueuePos++].m_SetId = SetId; } -void CSounds::PlayAndRecord(int Channel, int SetId, float Vol, vec2 Pos) +void CSounds::PlayAndRecord(int Channel, int SetId, float Volume, vec2 Position) { + // TODO: Volume and position are currently not recorded for sounds played with this function + // TODO: This also causes desync sounds during demo playback of demos recorded on high ping servers: + // https://github.com/ddnet/ddnet/issues/1282 CNetMsg_Sv_SoundGlobal Msg; - Msg.m_SoundID = SetId; + Msg.m_SoundId = SetId; Client()->SendPackMsgActive(&Msg, MSGFLAG_NOSEND | MSGFLAG_RECORD); - Play(Channel, SetId, Vol); + PlayAt(Channel, SetId, Volume, Position); } -void CSounds::Play(int Channel, int SetId, float Vol) +void CSounds::Play(int Channel, int SetId, float Volume) { - if(m_pClient->m_SuppressEvents) - return; - if(Channel == CHN_MUSIC && !g_Config.m_SndMusic) - return; - - int SampleId = GetSampleId(SetId); - if(SampleId == -1) - return; - - int Flags = 0; - if(Channel == CHN_MUSIC) - Flags = ISound::FLAG_LOOP; - - Sound()->Play(Channel, SampleId, Flags); + PlaySample(Channel, GetSampleId(SetId), 0, Volume); } -void CSounds::PlayAt(int Channel, int SetId, float Vol, vec2 Pos) +void CSounds::PlayAt(int Channel, int SetId, float Volume, vec2 Position) { - if(m_pClient->m_SuppressEvents) - return; - if(Channel == CHN_MUSIC && !g_Config.m_SndMusic) - return; - - int SampleId = GetSampleId(SetId); - if(SampleId == -1) - return; - - int Flags = 0; - if(Channel == CHN_MUSIC) - Flags = ISound::FLAG_LOOP; - - Sound()->PlayAt(Channel, SampleId, Flags, Pos.x, Pos.y); + PlaySampleAt(Channel, GetSampleId(SetId), 0, Volume, Position); } void CSounds::Stop(int SetId) @@ -255,24 +225,24 @@ bool CSounds::IsPlaying(int SetId) return false; } -ISound::CVoiceHandle CSounds::PlaySample(int Channel, int SampleId, float Vol, int Flags) +ISound::CVoiceHandle CSounds::PlaySample(int Channel, int SampleId, int Flags, float Volume) { - if((Channel == CHN_MUSIC && !g_Config.m_SndMusic) || SampleId == -1) + if(m_pClient->m_SuppressEvents || (Channel == CHN_MUSIC && !g_Config.m_SndMusic) || SampleId == -1) return ISound::CVoiceHandle(); if(Channel == CHN_MUSIC) Flags |= ISound::FLAG_LOOP; - return Sound()->Play(Channel, SampleId, Flags); + return Sound()->Play(Channel, SampleId, Flags, Volume); } -ISound::CVoiceHandle CSounds::PlaySampleAt(int Channel, int SampleId, float Vol, vec2 Pos, int Flags) +ISound::CVoiceHandle CSounds::PlaySampleAt(int Channel, int SampleId, int Flags, float Volume, vec2 Position) { - if((Channel == CHN_MUSIC && !g_Config.m_SndMusic) || SampleId == -1) + if(m_pClient->m_SuppressEvents || (Channel == CHN_MUSIC && !g_Config.m_SndMusic) || SampleId == -1) return ISound::CVoiceHandle(); if(Channel == CHN_MUSIC) Flags |= ISound::FLAG_LOOP; - return Sound()->PlayAt(Channel, SampleId, Flags, Pos.x, Pos.y); + return Sound()->PlayAt(Channel, SampleId, Flags, Volume, Position); } diff --git a/src/game/client/components/sounds.h b/src/game/client/components/sounds.h index 4ee5df561a..8671f3bf82 100644 --- a/src/game/client/components/sounds.h +++ b/src/game/client/components/sounds.h @@ -34,12 +34,13 @@ class CSounds : public CComponent std::shared_ptr m_pSoundJob; bool m_WaitForSoundJob; + void UpdateChannels(); int GetSampleId(int SetId); - float m_GuiSoundVolume; - float m_GameSoundVolume; - float m_MapSoundVolume; - float m_BackgroundMusicVolume; + float m_GuiSoundVolume = -1.0f; + float m_GameSoundVolume = -1.0f; + float m_MapSoundVolume = -1.0f; + float m_BackgroundMusicVolume = -1.0f; public: // sound channels @@ -60,14 +61,14 @@ class CSounds : public CComponent void ClearQueue(); void Enqueue(int Channel, int SetId); - void Play(int Channel, int SetId, float Vol); - void PlayAt(int Channel, int SetId, float Vol, vec2 Pos); - void PlayAndRecord(int Channel, int SetId, float Vol, vec2 Pos); + void Play(int Channel, int SetId, float Volume); + void PlayAt(int Channel, int SetId, float Volume, vec2 Position); + void PlayAndRecord(int Channel, int SetId, float Volume, vec2 Position); void Stop(int SetId); bool IsPlaying(int SetId); - ISound::CVoiceHandle PlaySample(int Channel, int SampleId, float Vol, int Flags = 0); - ISound::CVoiceHandle PlaySampleAt(int Channel, int SampleId, float Vol, vec2 Pos, int Flags = 0); + ISound::CVoiceHandle PlaySample(int Channel, int SampleId, int Flags, float Volume); + ISound::CVoiceHandle PlaySampleAt(int Channel, int SampleId, int Flags, float Volume, vec2 Position); }; #endif diff --git a/src/game/client/components/spectator.cpp b/src/game/client/components/spectator.cpp index b6c981599f..1e29fccacb 100644 --- a/src/game/client/components/spectator.cpp +++ b/src/game/client/components/spectator.cpp @@ -1,7 +1,7 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ -#include +#include #include #include @@ -20,7 +20,7 @@ bool CSpectator::CanChangeSpectator() { - // Don't change SpectatorID when not spectating + // Don't change SpectatorId when not spectating return m_pClient->m_Snap.m_SpecInfo.m_Active; } @@ -29,12 +29,12 @@ void CSpectator::SpectateNext(bool Reverse) int CurIndex = -1; const CNetObj_PlayerInfo **paPlayerInfos = m_pClient->m_Snap.m_apInfoByDDTeamName; - // m_SpectatorID may be uninitialized if m_Active is false + // m_SpectatorId may be uninitialized if m_Active is false if(m_pClient->m_Snap.m_SpecInfo.m_Active) { for(int i = 0; i < MAX_CLIENTS; i++) { - if(paPlayerInfos[i] && paPlayerInfos[i]->m_ClientID == m_pClient->m_Snap.m_SpecInfo.m_SpectatorID) + if(paPlayerInfos[i] && paPlayerInfos[i]->m_ClientId == m_pClient->m_Snap.m_SpecInfo.m_SpectatorId) { CurIndex = i; break; @@ -70,7 +70,7 @@ void CSpectator::SpectateNext(bool Reverse) const CNetObj_PlayerInfo *pPlayerInfo = paPlayerInfos[PlayerIndex]; if(pPlayerInfo && pPlayerInfo->m_Team != TEAM_SPECTATORS) { - Spectate(pPlayerInfo->m_ClientID); + Spectate(pPlayerInfo->m_ClientId); break; } } @@ -120,33 +120,33 @@ void CSpectator::ConSpectateClosest(IConsole::IResult *pResult, void *pUserData) return; const CGameClient::CSnapState &Snap = pSelf->m_pClient->m_Snap; - int SpectatorID = Snap.m_SpecInfo.m_SpectatorID; + int SpectatorId = Snap.m_SpecInfo.m_SpectatorId; - int NewSpectatorID = -1; + int NewSpectatorId = -1; vec2 CurPosition(pSelf->m_pClient->m_Camera.m_Center); - if(SpectatorID != SPEC_FREEVIEW) + if(SpectatorId != SPEC_FREEVIEW) { - const CNetObj_Character &CurCharacter = Snap.m_aCharacters[SpectatorID].m_Cur; + const CNetObj_Character &CurCharacter = Snap.m_aCharacters[SpectatorId].m_Cur; CurPosition.x = CurCharacter.m_X; CurPosition.y = CurCharacter.m_Y; } - int ClosestDistance = INT_MAX; + int ClosestDistance = std::numeric_limits::max(); for(int i = 0; i < MAX_CLIENTS; i++) { - if(i == SpectatorID || !Snap.m_apPlayerInfos[i] || Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS || (SpectatorID == SPEC_FREEVIEW && i == Snap.m_LocalClientID)) + if(i == SpectatorId || !Snap.m_aCharacters[i].m_Active || !Snap.m_apPlayerInfos[i] || Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS || (SpectatorId == SPEC_FREEVIEW && i == Snap.m_LocalClientId)) continue; const CNetObj_Character &MaybeClosestCharacter = Snap.m_aCharacters[i].m_Cur; int Distance = distance(CurPosition, vec2(MaybeClosestCharacter.m_X, MaybeClosestCharacter.m_Y)); - if(NewSpectatorID == -1 || Distance < ClosestDistance) + if(NewSpectatorId == -1 || Distance < ClosestDistance) { - NewSpectatorID = i; + NewSpectatorId = i; ClosestDistance = Distance; } } - if(NewSpectatorID > -1) - pSelf->Spectate(NewSpectatorID); + if(NewSpectatorId > -1) + pSelf->Spectate(NewSpectatorId); } void CSpectator::ConMultiView(IConsole::IResult *pResult, void *pUserData) @@ -164,7 +164,6 @@ CSpectator::CSpectator() { m_SelectorMouse = vec2(0.0f, 0.0f); OnReset(); - m_OldMouseX = m_OldMouseY = 0.0f; } void CSpectator::OnConsoleInit() @@ -182,11 +181,38 @@ bool CSpectator::OnCursorMove(float x, float y, IInput::ECursorType CursorType) if(!m_Active) return false; - UI()->ConvertMouseMove(&x, &y, CursorType); + Ui()->ConvertMouseMove(&x, &y, CursorType); m_SelectorMouse += vec2(x, y); return true; } +bool CSpectator::OnInput(const IInput::CEvent &Event) +{ + if(IsActive() && Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_ESCAPE) + { + OnRelease(); + return true; + } + + if(g_Config.m_ClSpectatorMouseclicks) + { + if(m_pClient->m_Snap.m_SpecInfo.m_Active && !IsActive() && !GameClient()->m_MultiViewActivated && + !Ui()->IsPopupOpen() && m_pClient->m_GameConsole.IsClosed() && !m_pClient->m_Menus.IsActive()) + { + if(Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_MOUSE_1) + { + if(m_pClient->m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) + Spectate(SPEC_FREEVIEW); + else + SpectateClosest(); + return true; + } + } + } + + return false; +} + void CSpectator::OnRelease() { OnReset(); @@ -194,6 +220,9 @@ void CSpectator::OnRelease() void CSpectator::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if(!GameClient()->m_MultiViewActivated && m_MultiViewActivateDelay != 0.0f) { if(m_MultiViewActivateDelay <= Client()->LocalTime()) @@ -208,20 +237,20 @@ void CSpectator::OnRender() // closing the spectator menu if(m_WasActive) { - if(m_SelectedSpectatorID != NO_SELECTION) + if(m_SelectedSpectatorId != NO_SELECTION) { - if(m_SelectedSpectatorID == MULTI_VIEW) + if(m_SelectedSpectatorId == MULTI_VIEW) GameClient()->m_MultiViewActivated = true; - else if(m_SelectedSpectatorID == SPEC_FREEVIEW || m_SelectedSpectatorID == SPEC_FOLLOW) + else if(m_SelectedSpectatorId == SPEC_FREEVIEW || m_SelectedSpectatorId == SPEC_FOLLOW) GameClient()->m_MultiViewActivated = false; if(!GameClient()->m_MultiViewActivated) - Spectate(m_SelectedSpectatorID); + Spectate(m_SelectedSpectatorId); - if(GameClient()->m_MultiViewActivated && m_SelectedSpectatorID != MULTI_VIEW && m_pClient->m_Teams.Team(m_SelectedSpectatorID) != GameClient()->m_MultiViewTeam) + if(GameClient()->m_MultiViewActivated && m_SelectedSpectatorId != MULTI_VIEW && m_pClient->m_Teams.Team(m_SelectedSpectatorId) != GameClient()->m_MultiViewTeam) { GameClient()->ResetMultiView(); - Spectate(m_SelectedSpectatorID); + Spectate(m_SelectedSpectatorId); m_MultiViewActivateDelay = Client()->LocalTime() + 0.3f; } } @@ -230,24 +259,6 @@ void CSpectator::OnRender() return; } - if(m_SelectedSpectatorID != NO_SELECTION) - { - // clicking a component - if(m_Clicked) - { - if(!GameClient()->m_MultiViewActivated) - Spectate(m_SelectedSpectatorID); - - if(m_SelectedSpectatorID == MULTI_VIEW) - GameClient()->m_MultiViewActivated = true; - else if(m_SelectedSpectatorID == SPEC_FREEVIEW || m_SelectedSpectatorID == SPEC_FOLLOW) - GameClient()->m_MultiViewActivated = false; - - if(!GameClient()->m_MultiViewActivated && m_SelectedSpectatorID >= 0 && m_SelectedSpectatorID < MAX_CLIENTS) - m_Clicked = false; - } - } - if(!m_pClient->m_Snap.m_SpecInfo.m_Active && Client()->State() != IClient::STATE_DEMOPLAYBACK) { m_Active = false; @@ -256,7 +267,7 @@ void CSpectator::OnRender() } m_WasActive = true; - m_SelectedSpectatorID = NO_SELECTION; + m_SelectedSpectatorId = NO_SELECTION; // draw background float Width = 400 * 3.0f * Graphics()->ScreenAspect(); @@ -268,7 +279,6 @@ void CSpectator::OnRender() float LineHeight = 60.0f; float TeeSizeMod = 1.0f; float RoundRadius = 30.0f; - bool Selected = false; bool MultiViewSelected = false; int TotalPlayers = 0; int PerLine = 8; @@ -283,7 +293,17 @@ void CSpectator::OnRender() ++TotalPlayers; } - if(TotalPlayers > 32) + if(TotalPlayers > 64) + { + FontSize = 12.0f; + LineHeight = 15.0f; + TeeSizeMod = 0.3f; + PerLine = 32; + RoundRadius = 5.0f; + BoxMove = 3.0f; + BoxOffset = 6.0f; + } + else if(TotalPlayers > 32) { FontSize = 18.0f; LineHeight = 30.0f; @@ -298,17 +318,45 @@ void CSpectator::OnRender() ObjWidth = 600.0f; } + const vec2 ScreenSize = vec2(Width, Height); + const vec2 ScreenCenter = ScreenSize / 2.0f; + CUIRect SpectatorRect = {Width / 2.0f - ObjWidth, Height / 2.0f - 300.0f, ObjWidth * 2.0f, 600.0f}; + CUIRect SpectatorMouseRect; + SpectatorRect.Margin(20.0f, &SpectatorMouseRect); + + const bool WasTouchPressed = m_TouchState.m_AnyPressed; + Ui()->UpdateTouchState(m_TouchState); + if(m_TouchState.m_AnyPressed) + { + const vec2 TouchPos = (m_TouchState.m_PrimaryPosition - vec2(0.5f, 0.5f)) * ScreenSize; + if(SpectatorMouseRect.Inside(ScreenCenter + TouchPos)) + { + m_SelectorMouse = TouchPos; + } + } + else if(WasTouchPressed) + { + const vec2 TouchPos = (m_TouchState.m_PrimaryPosition - vec2(0.5f, 0.5f)) * ScreenSize; + if(!SpectatorRect.Inside(ScreenCenter + TouchPos)) + { + OnRelease(); + return; + } + } + Graphics()->MapScreen(0, 0, Width, Height); - Graphics()->DrawRect(Width / 2.0f - ObjWidth, Height / 2.0f - 300.0f, ObjWidth * 2, 600.0f, ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), IGraphics::CORNER_ALL, 20.0f); + SpectatorRect.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.3f), IGraphics::CORNER_ALL, 20.0f); // clamp mouse position to selector area m_SelectorMouse.x = clamp(m_SelectorMouse.x, -(ObjWidth - 20.0f), ObjWidth - 20.0f); m_SelectorMouse.y = clamp(m_SelectorMouse.y, -280.0f, 280.0f); + const bool MousePressed = Input()->KeyPress(KEY_MOUSE_1) || m_TouchState.m_PrimaryPressed; + // draw selections - if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecID == SPEC_FREEVIEW) || - (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == SPEC_FREEVIEW)) + if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecId == SPEC_FREEVIEW) || + (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW)) { Graphics()->DrawRect(Width / 2.0f - (ObjWidth - 20.0f), Height / 2.0f - 280.0f, ((ObjWidth * 2.0f) / 3.0f) - 40.0f, 60.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 20.0f); } @@ -318,39 +366,54 @@ void CSpectator::OnRender() Graphics()->DrawRect(Width / 2.0f - (ObjWidth - 20.0f) + (ObjWidth * 2.0f / 3.0f), Height / 2.0f - 280.0f, ((ObjWidth * 2.0f) / 3.0f) - 40.0f, 60.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 20.0f); } - if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_LocalClientID >= 0 && m_pClient->m_DemoSpecID == SPEC_FOLLOW) + if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_LocalClientId >= 0 && m_pClient->m_DemoSpecId == SPEC_FOLLOW) { Graphics()->DrawRect(Width / 2.0f - (ObjWidth - 20.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f), Height / 2.0f - 280.0f, ((ObjWidth * 2.0f) / 3.0f) - 40.0f, 60.0f, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 20.0f); } + bool FreeViewSelected = false; if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) && m_SelectorMouse.x <= -(ObjWidth - 20.0f) + ((ObjWidth * 2.0f) / 3.0f) - 40.0f && m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f) { - m_SelectedSpectatorID = SPEC_FREEVIEW; - Selected = true; + m_SelectedSpectatorId = SPEC_FREEVIEW; + FreeViewSelected = true; + if(MousePressed) + { + GameClient()->m_MultiViewActivated = false; + Spectate(m_SelectedSpectatorId); + } } - TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f); + TextRender()->TextColor(1.0f, 1.0f, 1.0f, FreeViewSelected ? 1.0f : 0.5f); TextRender()->Text(Width / 2.0f - (ObjWidth - 40.0f), Height / 2.0f - 280.f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Free-View"), -1.0f); if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f / 3.0f) && m_SelectorMouse.x <= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f / 3.0f) + ((ObjWidth * 2.0f) / 3.0f) - 40.0f && m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f) { - m_SelectedSpectatorID = MULTI_VIEW; + m_SelectedSpectatorId = MULTI_VIEW; MultiViewSelected = true; + if(MousePressed) + { + GameClient()->m_MultiViewActivated = true; + } } TextRender()->TextColor(1.0f, 1.0f, 1.0f, MultiViewSelected ? 1.0f : 0.5f); TextRender()->Text(Width / 2.0f - (ObjWidth - 40.0f) + (ObjWidth * 2.0f / 3.0f), Height / 2.0f - 280.f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Multi-View"), -1.0f); - if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_LocalClientID >= 0) + if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_LocalClientId >= 0) { - Selected = false; + bool FollowSelected = false; if(m_SelectorMouse.x >= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f) && m_SelectorMouse.x <= -(ObjWidth - 20.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f) + ((ObjWidth * 2.0f) / 3.0f) - 40.0f && m_SelectorMouse.y >= -280.0f && m_SelectorMouse.y <= -220.0f) { - m_SelectedSpectatorID = SPEC_FOLLOW; - Selected = true; + m_SelectedSpectatorId = SPEC_FOLLOW; + FollowSelected = true; + if(MousePressed) + { + GameClient()->m_MultiViewActivated = false; + Spectate(m_SelectedSpectatorId); + } } - TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f); + TextRender()->TextColor(1.0f, 1.0f, 1.0f, FollowSelected ? 1.0f : 0.5f); TextRender()->Text(Width / 2.0f - (ObjWidth - 40.0f) + (ObjWidth * 2.0f * 2.0f / 3.0f), Height / 2.0f - 280.0f + (60.f - BigFontSize) / 2.f, BigFontSize, Localize("Follow"), -1.0f); } @@ -372,7 +435,7 @@ void CSpectator::OnRender() } const CNetObj_PlayerInfo *pInfo = m_pClient->m_Snap.m_apInfoByDDTeamName[i]; - int DDTeam = m_pClient->m_Teams.Team(pInfo->m_ClientID); + int DDTeam = m_pClient->m_Teams.Team(pInfo->m_ClientId); int NextDDTeam = 0; for(int j = i + 1; j < MAX_CLIENTS; j++) @@ -382,7 +445,7 @@ void CSpectator::OnRender() if(!pInfo2 || pInfo2->m_Team == TEAM_SPECTATORS) continue; - NextDDTeam = m_pClient->m_Teams.Team(pInfo2->m_ClientID); + NextDDTeam = m_pClient->m_Teams.Team(pInfo2->m_ClientId); break; } @@ -395,7 +458,7 @@ void CSpectator::OnRender() if(!pInfo2 || pInfo2->m_Team == TEAM_SPECTATORS) continue; - OldDDTeam = m_pClient->m_Teams.Team(pInfo2->m_ClientID); + OldDDTeam = m_pClient->m_Teams.Team(pInfo2->m_ClientId); break; } } @@ -413,76 +476,91 @@ void CSpectator::OnRender() OldDDTeam = DDTeam; - if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecID == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID) || (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID)) + if((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_pClient->m_DemoSpecId == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId) || (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_pClient->m_Snap.m_SpecInfo.m_SpectatorId == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId)) { Graphics()->DrawRect(Width / 2.0f + x - 10.0f + BoxOffset, Height / 2.0f + y + BoxMove, 270.0f - BoxOffset, LineHeight, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, RoundRadius); } - Selected = false; + bool PlayerSelected = false; if(m_SelectorMouse.x >= x - 10.0f && m_SelectorMouse.x < x + 260.0f && m_SelectorMouse.y >= y - (LineHeight / 6.0f) && m_SelectorMouse.y < y + (LineHeight * 5.0f / 6.0f)) { - m_SelectedSpectatorID = m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID; - Selected = true; - if(GameClient()->m_MultiViewActivated && m_Clicked) + m_SelectedSpectatorId = m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId; + PlayerSelected = true; + if(MousePressed) { - if(GameClient()->m_MultiViewTeam == DDTeam) + if(GameClient()->m_MultiViewActivated) { - GameClient()->m_aMultiViewId[m_SelectedSpectatorID] = !GameClient()->m_aMultiViewId[m_SelectedSpectatorID]; - if(!GameClient()->m_aMultiViewId[m_pClient->m_Snap.m_SpecInfo.m_SpectatorID]) + if(GameClient()->m_MultiViewTeam == DDTeam) { - int NewClientID = GameClient()->FindFirstMultiViewId(); - if(NewClientID < MAX_CLIENTS && NewClientID >= 0) + GameClient()->m_aMultiViewId[m_SelectedSpectatorId] = !GameClient()->m_aMultiViewId[m_SelectedSpectatorId]; + if(!GameClient()->m_aMultiViewId[m_pClient->m_Snap.m_SpecInfo.m_SpectatorId]) { - GameClient()->CleanMultiViewId(NewClientID); - GameClient()->m_aMultiViewId[NewClientID] = true; - Spectate(NewClientID); + int NewClientId = GameClient()->FindFirstMultiViewId(); + if(NewClientId < MAX_CLIENTS && NewClientId >= 0) + { + GameClient()->CleanMultiViewId(NewClientId); + GameClient()->m_aMultiViewId[NewClientId] = true; + Spectate(NewClientId); + } } } + else + { + GameClient()->ResetMultiView(); + Spectate(m_SelectedSpectatorId); + m_MultiViewActivateDelay = Client()->LocalTime() + 0.3f; + } } else { - GameClient()->ResetMultiView(); - Spectate(m_SelectedSpectatorID); - m_MultiViewActivateDelay = Client()->LocalTime() + 0.3f; + Spectate(m_SelectedSpectatorId); } - m_Clicked = false; } } float TeeAlpha; if(Client()->State() == IClient::STATE_DEMOPLAYBACK && - !m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID].m_Active) + !m_pClient->m_Snap.m_aCharacters[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_Active) { TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.25f); TeeAlpha = 0.5f; } else { - TextRender()->TextColor(1.0f, 1.0f, 1.0f, Selected ? 1.0f : 0.5f); + TextRender()->TextColor(1.0f, 1.0f, 1.0f, PlayerSelected ? 1.0f : 0.5f); TeeAlpha = 1.0f; } - TextRender()->Text(Width / 2.0f + x + 50.0f, Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, FontSize, m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID].m_aName, 220.0f); + CTextCursor NameCursor; + TextRender()->SetCursor(&NameCursor, Width / 2.0f + x + 50.0f, Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END); + NameCursor.m_LineWidth = 180.0f; + if(g_Config.m_ClShowIds) + { + char aClientId[16]; + GameClient()->FormatClientId(m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId, aClientId, EClientIdFormat::INDENT_AUTO); + TextRender()->TextEx(&NameCursor, aClientId); + } + TextRender()->TextEx(&NameCursor, m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_aName); if(GameClient()->m_MultiViewActivated) { - if(GameClient()->m_aMultiViewId[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID]) + if(GameClient()->m_aMultiViewId[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId]) { - TextRender()->TextColor(0.1f, 1.0f, 0.1f, Selected ? 1.0f : 0.5f); + TextRender()->TextColor(0.1f, 1.0f, 0.1f, PlayerSelected ? 1.0f : 0.5f); TextRender()->Text(Width / 2.0f + x + 50.0f + 180.0f, Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, FontSize - 3, "⬤", 220.0f); } else if(GameClient()->m_MultiViewTeam == DDTeam) { - TextRender()->TextColor(1.0f, 0.1f, 0.1f, Selected ? 1.0f : 0.5f); + TextRender()->TextColor(1.0f, 0.1f, 0.1f, PlayerSelected ? 1.0f : 0.5f); TextRender()->Text(Width / 2.0f + x + 50.0f + 180.0f, Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, FontSize - 3, "◯", 220.0f); } } // flag if(m_pClient->m_Snap.m_pGameInfoObj && (m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS) && - m_pClient->m_Snap.m_pGameDataObj && (m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierRed == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID || m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID)) + m_pClient->m_Snap.m_pGameDataObj && (m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierRed == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId || m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId)) { Graphics()->BlendNormal(); - if(m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID) + if(m_pClient->m_Snap.m_pGameDataObj->m_FlagCarrierBlue == m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId) Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagBlue); else Graphics()->TextureSet(GameClient()->m_GameSkin.m_SpriteFlagRed); @@ -496,54 +574,70 @@ void CSpectator::OnRender() Graphics()->QuadsEnd(); } - CTeeRenderInfo TeeInfo = m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID].m_RenderInfo; + CTeeRenderInfo TeeInfo = m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_RenderInfo; TeeInfo.m_Size *= TeeSizeMod; const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); + CRenderTools::GetRenderTeeOffsetToRenderedTee(pIdleState, &TeeInfo, OffsetToMid); vec2 TeeRenderPos(Width / 2.0f + x + 20.0f, Height / 2.0f + y + BoxMove + LineHeight / 2.0f + OffsetToMid.y); RenderTools()->RenderTee(pIdleState, &TeeInfo, EMOTE_NORMAL, vec2(1.0f, 0.0f), TeeRenderPos, TeeAlpha); - if(m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientID].m_Friend) + if(m_pClient->m_aClients[m_pClient->m_Snap.m_apInfoByDDTeamName[i]->m_ClientId].m_Friend) { ColorRGBA rgb = color_cast(ColorHSLA(g_Config.m_ClMessageFriendColor)); TextRender()->TextColor(rgb.WithAlpha(1.f)); TextRender()->Text(Width / 2.0f + x - TeeInfo.m_Size / 2.0f, Height / 2.0f + y + BoxMove + (LineHeight - FontSize) / 2.f, FontSize, "♥", 220.0f); - TextRender()->TextColor(g_Config.m_ScFriendColor); + TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); } y += LineHeight; } TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - RenderTools()->RenderCursor(m_SelectorMouse + vec2(Width, Height) / 2, 48.0f); + RenderTools()->RenderCursor(ScreenCenter + m_SelectorMouse, 48.0f); } void CSpectator::OnReset() { m_WasActive = false; m_Active = false; - m_SelectedSpectatorID = NO_SELECTION; + m_SelectedSpectatorId = NO_SELECTION; } -void CSpectator::Spectate(int SpectatorID) +void CSpectator::Spectate(int SpectatorId) { if(Client()->State() == IClient::STATE_DEMOPLAYBACK) { - m_pClient->m_DemoSpecID = clamp(SpectatorID, (int)SPEC_FOLLOW, MAX_CLIENTS - 1); + m_pClient->m_DemoSpecId = clamp(SpectatorId, (int)SPEC_FOLLOW, MAX_CLIENTS - 1); // The tick must be rendered for the spectator mode to be updated, so we do it manually when demo playback is paused if(DemoPlayer()->BaseInfo()->m_Paused) GameClient()->m_Menus.DemoSeekTick(IDemoPlayer::TICK_CURRENT); return; } - if(m_pClient->m_Snap.m_SpecInfo.m_SpectatorID == SpectatorID) + if(m_pClient->m_Snap.m_SpecInfo.m_SpectatorId == SpectatorId) return; + if(Client()->IsSixup()) + { + protocol7::CNetMsg_Cl_SetSpectatorMode Msg; + if(SpectatorId == SPEC_FREEVIEW) + { + Msg.m_SpecMode = protocol7::SPEC_FREEVIEW; + Msg.m_SpectatorId = -1; + } + else + { + Msg.m_SpecMode = protocol7::SPEC_PLAYER; + Msg.m_SpectatorId = SpectatorId; + } + Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL, true); + return; + } CNetMsg_Cl_SetSpectatorMode Msg; - Msg.m_SpectatorID = SpectatorID; + Msg.m_SpectatorId = SpectatorId; Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); } @@ -551,18 +645,3 @@ void CSpectator::SpectateClosest() { ConSpectateClosest(NULL, this); } - -bool CSpectator::OnInput(const IInput::CEvent &Event) -{ - if(m_Active && Event.m_Flags & IInput::FLAG_PRESS && Event.m_Key == KEY_MOUSE_1) - { - m_Clicked = true; - return true; - } - else if(Event.m_Flags & IInput::FLAG_RELEASE && Event.m_Key == KEY_MOUSE_1) - { - m_Clicked = false; - return false; - } - return false; -} diff --git a/src/game/client/components/spectator.h b/src/game/client/components/spectator.h index 11b9e585b9..25dddfb60e 100644 --- a/src/game/client/components/spectator.h +++ b/src/game/client/components/spectator.h @@ -3,8 +3,10 @@ #ifndef GAME_CLIENT_COMPONENTS_SPECTATOR_H #define GAME_CLIENT_COMPONENTS_SPECTATOR_H #include +#include #include +#include class CSpectator : public CComponent { @@ -16,13 +18,11 @@ class CSpectator : public CComponent bool m_Active; bool m_WasActive; - bool m_Clicked; - int m_SelectedSpectatorID; + int m_SelectedSpectatorId; vec2 m_SelectorMouse; - float m_OldMouseX; - float m_OldMouseY; + CUi::CTouchState m_TouchState; float m_MultiViewActivateDelay; @@ -42,13 +42,15 @@ class CSpectator : public CComponent virtual void OnConsoleInit() override; virtual bool OnCursorMove(float x, float y, IInput::ECursorType CursorType) override; + virtual bool OnInput(const IInput::CEvent &Event) override; virtual void OnRender() override; virtual void OnRelease() override; virtual void OnReset() override; - virtual bool OnInput(const IInput::CEvent &Event) override; - void Spectate(int SpectatorID); + void Spectate(int SpectatorId); void SpectateClosest(); + + bool IsActive() const { return m_Active; } }; #endif diff --git a/src/game/client/components/statboard.cpp b/src/game/client/components/statboard.cpp index be9bed2195..2cfe85b0b9 100644 --- a/src/game/client/components/statboard.cpp +++ b/src/game/client/components/statboard.cpp @@ -90,7 +90,7 @@ void CStatboard::OnMessage(int MsgType, void *pRawMsg) else if(MsgType == NETMSGTYPE_SV_CHAT) { CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; - if(pMsg->m_ClientID < 0) + if(pMsg->m_ClientId < 0) { const char *p, *t; const char *pLookFor = "flag was captured by '"; @@ -102,7 +102,7 @@ void CStatboard::OnMessage(int MsgType, void *pRawMsg) if(t <= p) return; - str_utf8_truncate(aName, sizeof(aName), p, t - p); + str_truncate(aName, sizeof(aName), p, t - p); for(int i = 0; i < MAX_CLIENTS; i++) { @@ -122,6 +122,9 @@ void CStatboard::OnMessage(int MsgType, void *pRawMsg) void CStatboard::OnRender() { + if(Client()->State() != IClient::STATE_ONLINE && Client()->State() != IClient::STATE_DEMOPLAYBACK) + return; + if((g_Config.m_ClAutoStatboardScreenshot || g_Config.m_ClAutoCSV) && Client()->State() != IClient::STATE_DEMOPLAYBACK) { if(m_ScreenshotTime < 0 && m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER) @@ -155,7 +158,7 @@ void CStatboard::RenderGlobalStats() // sort red or dm players by score for(const auto *pInfo : m_pClient->m_Snap.m_apInfoByScore) { - if(!pInfo || !m_pClient->m_aStats[pInfo->m_ClientID].IsActive() || m_pClient->m_aClients[pInfo->m_ClientID].m_Team != TEAM_RED) + if(!pInfo || !m_pClient->m_aStats[pInfo->m_ClientId].IsActive() || m_pClient->m_aClients[pInfo->m_ClientId].m_Team != TEAM_RED) continue; apPlayers[NumPlayers] = pInfo; NumPlayers++; @@ -166,7 +169,7 @@ void CStatboard::RenderGlobalStats() { for(const auto *pInfo : m_pClient->m_Snap.m_apInfoByScore) { - if(!pInfo || !m_pClient->m_aStats[pInfo->m_ClientID].IsActive() || m_pClient->m_aClients[pInfo->m_ClientID].m_Team != TEAM_BLUE) + if(!pInfo || !m_pClient->m_aStats[pInfo->m_ClientId].IsActive() || m_pClient->m_aClients[pInfo->m_ClientId].m_Team != TEAM_BLUE) continue; apPlayers[NumPlayers] = pInfo; NumPlayers++; @@ -193,7 +196,7 @@ void CStatboard::RenderGlobalStats() bool aDisplayWeapon[NUM_WEAPONS] = {false}; for(int i = 0; i < NumPlayers; i++) { - const CGameClient::CClientStats *pStats = &m_pClient->m_aStats[apPlayers[i]->m_ClientID]; + const CGameClient::CClientStats *pStats = &m_pClient->m_aStats[apPlayers[i]->m_ClientId]; for(int j = 0; j < NUM_WEAPONS; j++) aDisplayWeapon[j] = aDisplayWeapon[j] || pStats->m_aFragsWith[j] || pStats->m_aDeathsFrom[j]; } @@ -279,20 +282,20 @@ void CStatboard::RenderGlobalStats() for(int j = 0; j < NumPlayers; j++) { const CNetObj_PlayerInfo *pInfo = apPlayers[j]; - const CGameClient::CClientStats *pStats = &m_pClient->m_aStats[pInfo->m_ClientID]; + const CGameClient::CClientStats *pStats = &m_pClient->m_aStats[pInfo->m_ClientId]; - if(m_pClient->m_Snap.m_LocalClientID == pInfo->m_ClientID || (m_pClient->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientID == m_pClient->m_Snap.m_SpecInfo.m_SpectatorID)) + if(m_pClient->m_Snap.m_LocalClientId == pInfo->m_ClientId || (m_pClient->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientId == m_pClient->m_Snap.m_SpecInfo.m_SpectatorId)) { // background so it's easy to find the local player Graphics()->DrawRect(x - 10, y + ContentLineOffset / 2, StatboardContentWidth, LineHeight - ContentLineOffset, ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_NONE, 0.0f); } - CTeeRenderInfo Teeinfo = m_pClient->m_aClients[pInfo->m_ClientID].m_RenderInfo; + CTeeRenderInfo Teeinfo = m_pClient->m_aClients[pInfo->m_ClientId].m_RenderInfo; Teeinfo.m_Size *= TeeSizemod; const CAnimState *pIdleState = CAnimState::GetIdle(); vec2 OffsetToMid; - RenderTools()->GetRenderTeeOffsetToRenderedTee(pIdleState, &Teeinfo, OffsetToMid); + CRenderTools::GetRenderTeeOffsetToRenderedTee(pIdleState, &Teeinfo, OffsetToMid); vec2 TeeRenderPos(x + Teeinfo.m_Size / 2, y + LineHeight / 2.0f + OffsetToMid.y); RenderTools()->RenderTee(pIdleState, &Teeinfo, EMOTE_NORMAL, vec2(1, 0), TeeRenderPos); @@ -301,20 +304,20 @@ void CStatboard::RenderGlobalStats() CTextCursor Cursor; TextRender()->SetCursor(&Cursor, x + 64, y + (LineHeight * 0.95f - FontSize) / 2.f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END); Cursor.m_LineWidth = 220; - TextRender()->TextEx(&Cursor, m_pClient->m_aClients[pInfo->m_ClientID].m_aName, -1); + TextRender()->TextEx(&Cursor, m_pClient->m_aClients[pInfo->m_ClientId].m_aName, -1); px = 325; // FRAGS { - str_from_int(pStats->m_Frags, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", pStats->m_Frags); tw = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); TextRender()->Text(x - tw + px, y + (LineHeight * 0.95f - FontSize) / 2.f, FontSize, aBuf, -1.0f); px += 85; } // DEATHS { - str_from_int(pStats->m_Deaths, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", pStats->m_Deaths); tw = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); TextRender()->Text(x - tw + px, y + (LineHeight * 0.95f - FontSize) / 2.f, FontSize, aBuf, -1.0f); px += 85; @@ -322,7 +325,7 @@ void CStatboard::RenderGlobalStats() // SUICIDES { px += 10; - str_from_int(pStats->m_Suicides, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", pStats->m_Suicides); tw = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); TextRender()->Text(x - tw + px, y + (LineHeight * 0.95f - FontSize) / 2.f, FontSize, aBuf, -1.0f); px += 85; @@ -354,14 +357,14 @@ void CStatboard::RenderGlobalStats() } // SPREE { - str_from_int(pStats->m_CurrentSpree, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", pStats->m_CurrentSpree); tw = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); TextRender()->Text(x - tw + px, y + (LineHeight * 0.95f - FontSize) / 2.f, FontSize, aBuf, -1.0f); px += 85; } // BEST SPREE { - str_from_int(pStats->m_BestSpree, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", pStats->m_BestSpree); tw = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); TextRender()->Text(x - tw + px, y + (LineHeight * 0.95f - FontSize) / 2.f, FontSize, aBuf, -1.0f); px += 85; @@ -369,7 +372,7 @@ void CStatboard::RenderGlobalStats() // GRABS if(GameWithFlags) { - str_from_int(pStats->m_FlagGrabs, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", pStats->m_FlagGrabs); tw = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); TextRender()->Text(x - tw + px, y + (LineHeight * 0.95f - FontSize) / 2.f, FontSize, aBuf, -1.0f); px += 85; @@ -389,7 +392,7 @@ void CStatboard::RenderGlobalStats() // FLAGS if(GameWithFlags) { - str_from_int(pStats->m_FlagCaptures, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", pStats->m_FlagCaptures); tw = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); TextRender()->Text(x - tw + px, y + (LineHeight * 0.95f - FontSize) / 2.f, FontSize, aBuf, -1.0f); } @@ -464,7 +467,7 @@ void CStatboard::FormatStats(char *pDest, size_t DestSize) // sort red or dm players by score for(const auto *pInfo : m_pClient->m_Snap.m_apInfoByScore) { - if(!pInfo || !m_pClient->m_aStats[pInfo->m_ClientID].IsActive() || m_pClient->m_aClients[pInfo->m_ClientID].m_Team != TEAM_RED) + if(!pInfo || !m_pClient->m_aStats[pInfo->m_ClientId].IsActive() || m_pClient->m_aClients[pInfo->m_ClientId].m_Team != TEAM_RED) continue; apPlayers[NumPlayers] = pInfo; NumPlayers++; @@ -475,7 +478,7 @@ void CStatboard::FormatStats(char *pDest, size_t DestSize) { for(const auto *pInfo : m_pClient->m_Snap.m_apInfoByScore) { - if(!pInfo || !m_pClient->m_aStats[pInfo->m_ClientID].IsActive() || m_pClient->m_aClients[pInfo->m_ClientID].m_Team != TEAM_BLUE) + if(!pInfo || !m_pClient->m_aStats[pInfo->m_ClientId].IsActive() || m_pClient->m_aClients[pInfo->m_ClientId].m_Team != TEAM_BLUE) continue; apPlayers[NumPlayers] = pInfo; NumPlayers++; @@ -487,7 +490,7 @@ void CStatboard::FormatStats(char *pDest, size_t DestSize) for(int i = 0; i < NumPlayers; i++) { const CNetObj_PlayerInfo *pInfo = apPlayers[i]; - const CGameClient::CClientStats *pStats = &m_pClient->m_aStats[pInfo->m_ClientID]; + const CGameClient::CClientStats *pStats = &m_pClient->m_aStats[pInfo->m_ClientId]; // Pre-formatting @@ -507,7 +510,7 @@ void CStatboard::FormatStats(char *pDest, size_t DestSize) fdratio = (float)(pStats->m_Frags) / pStats->m_Deaths; // Local player - bool localPlayer = (m_pClient->m_Snap.m_LocalClientID == pInfo->m_ClientID || (m_pClient->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientID == m_pClient->m_Snap.m_SpecInfo.m_SpectatorID)); + bool localPlayer = (m_pClient->m_Snap.m_LocalClientId == pInfo->m_ClientId || (m_pClient->m_Snap.m_SpecInfo.m_Active && pInfo->m_ClientId == m_pClient->m_Snap.m_SpecInfo.m_SpectatorId)); // Game with flags bool GameWithFlags = (m_pClient->m_Snap.m_pGameInfoObj && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_FLAGS); @@ -515,9 +518,9 @@ void CStatboard::FormatStats(char *pDest, size_t DestSize) char aBuf[1024]; str_format(aBuf, sizeof(aBuf), "%d,%d,%s,%s,%d,%d,%d,%d,%.2f,%i,%.1f,%d,%d,%s,%d,%d,%d\n", localPlayer ? 1 : 0, // Local player - m_pClient->m_aClients[pInfo->m_ClientID].m_Team, // Team - ReplaceCommata(m_pClient->m_aClients[pInfo->m_ClientID].m_aName).c_str(), // Name - ReplaceCommata(m_pClient->m_aClients[pInfo->m_ClientID].m_aClan).c_str(), // Clan + m_pClient->m_aClients[pInfo->m_ClientId].m_Team, // Team + ReplaceCommata(m_pClient->m_aClients[pInfo->m_ClientId].m_aName).c_str(), // Name + ReplaceCommata(m_pClient->m_aClients[pInfo->m_ClientId].m_aClan).c_str(), // Clan clamp(pInfo->m_Score, -999, 999), // Score pStats->m_Frags, // Frags pStats->m_Deaths, // Deaths diff --git a/src/game/client/components/tooltips.cpp b/src/game/client/components/tooltips.cpp index 4d7dd79701..ed1cbba9f7 100644 --- a/src/game/client/components/tooltips.cpp +++ b/src/game/client/components/tooltips.cpp @@ -26,11 +26,11 @@ inline void CTooltips::ClearActiveTooltip() m_PreviousTooltip.reset(); } -void CTooltips::DoToolTip(const void *pID, const CUIRect *pNearRect, const char *pText, float WidthHint) +void CTooltips::DoToolTip(const void *pId, const CUIRect *pNearRect, const char *pText, float WidthHint) { - uintptr_t ID = reinterpret_cast(pID); - const auto result = m_Tooltips.emplace(ID, CTooltip{ - pID, + uintptr_t Id = reinterpret_cast(pId); + const auto result = m_Tooltips.emplace(Id, CTooltip{ + pId, *pNearRect, pText, WidthHint, @@ -45,7 +45,7 @@ void CTooltips::DoToolTip(const void *pID, const CUIRect *pNearRect, const char Tooltip.m_OnScreen = true; - if(UI()->HotItem() == Tooltip.m_pID) + if(Ui()->HotItem() == Tooltip.m_pId) { SetActiveTooltip(Tooltip); } @@ -57,7 +57,7 @@ void CTooltips::OnRender() { CTooltip &Tooltip = m_ActiveTooltip.value(); - if(UI()->HotItem() != Tooltip.m_pID) + if(Ui()->HotItem() != Tooltip.m_pId) { Tooltip.m_OnScreen = false; ClearActiveTooltip(); @@ -91,33 +91,33 @@ void CTooltips::OnRender() Rect.w = BoundingBox.m_W + 2 * Padding; Rect.h = BoundingBox.m_H + 2 * Padding; - const CUIRect *pScreen = UI()->Screen(); + const CUIRect *pScreen = Ui()->Screen(); Rect.w = minimum(Rect.w, pScreen->w - 2 * Margin); Rect.h = minimum(Rect.h, pScreen->h - 2 * Margin); // Try the top side. if(Tooltip.m_Rect.y - Rect.h - Margin > pScreen->y) { - Rect.x = clamp(UI()->MouseX() - Rect.w / 2.0f, Margin, pScreen->w - Rect.w - Margin); + Rect.x = clamp(Ui()->MouseX() - Rect.w / 2.0f, Margin, pScreen->w - Rect.w - Margin); Rect.y = Tooltip.m_Rect.y - Rect.h - Margin; } // Try the bottom side. else if(Tooltip.m_Rect.y + Tooltip.m_Rect.h + Margin < pScreen->h) { - Rect.x = clamp(UI()->MouseX() - Rect.w / 2.0f, Margin, pScreen->w - Rect.w - Margin); + Rect.x = clamp(Ui()->MouseX() - Rect.w / 2.0f, Margin, pScreen->w - Rect.w - Margin); Rect.y = Tooltip.m_Rect.y + Tooltip.m_Rect.h + Margin; } // Try the right side. else if(Tooltip.m_Rect.x + Tooltip.m_Rect.w + Margin + Rect.w < pScreen->w) { Rect.x = Tooltip.m_Rect.x + Tooltip.m_Rect.w + Margin; - Rect.y = clamp(UI()->MouseY() - Rect.h / 2.0f, Margin, pScreen->h - Rect.h - Margin); + Rect.y = clamp(Ui()->MouseY() - Rect.h / 2.0f, Margin, pScreen->h - Rect.h - Margin); } // Try the left side. else if(Tooltip.m_Rect.x - Rect.w - Margin > pScreen->x) { Rect.x = Tooltip.m_Rect.x - Rect.w - Margin; - Rect.y = clamp(UI()->MouseY() - Rect.h / 2.0f, Margin, pScreen->h - Rect.h - Margin); + Rect.y = clamp(Ui()->MouseY() - Rect.h / 2.0f, Margin, pScreen->h - Rect.h - Margin); } Rect.Draw(ColorRGBA(0.2f, 0.2f, 0.2f, 0.8f * AlphaFactor), IGraphics::CORNER_ALL, Padding); diff --git a/src/game/client/components/tooltips.h b/src/game/client/components/tooltips.h index aaac1afec1..b2b98b363b 100644 --- a/src/game/client/components/tooltips.h +++ b/src/game/client/components/tooltips.h @@ -11,7 +11,7 @@ struct CTooltip { - const void *m_pID; + const void *m_pId; CUIRect m_Rect; const char *m_pText; float m_WidthHint; @@ -47,12 +47,12 @@ class CTooltips : public CComponent * On the first call to this function, the data passed is cached, afterwards the calls are used to detect if the tooltip should be activated. * If multiple tooltips cover the same rect or the rects intersect, then the tooltip that is added later has priority. * - * @param pID The ID of the tooltip. Usually a reference to some g_Config value. + * @param pId The ID of the tooltip. Usually a reference to some g_Config value. * @param pNearRect Place the tooltip near this rect. * @param pText The text to display in the tooltip. * @param WidthHint The maximum width of the tooltip, or -1.0f for unlimited. */ - void DoToolTip(const void *pID, const CUIRect *pNearRect, const char *pText, float WidthHint = -1.0f); + void DoToolTip(const void *pId, const CUIRect *pNearRect, const char *pText, float WidthHint = -1.0f); virtual void OnReset() override; virtual void OnRender() override; diff --git a/src/game/client/components/voting.cpp b/src/game/client/components/voting.cpp index aaab39dedd..462a14e8da 100644 --- a/src/game/client/components/voting.cpp +++ b/src/game/client/components/voting.cpp @@ -1,13 +1,18 @@ /* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ /* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include "voting.h" + +#include + #include +#include -#include "voting.h" +#include #include +#include #include #include - -#include +#include void CVoting::ConCallvote(IConsole::IResult *pResult, void *pUserData) { @@ -26,6 +31,16 @@ void CVoting::ConVote(IConsole::IResult *pResult, void *pUserData) void CVoting::Callvote(const char *pType, const char *pValue, const char *pReason) { + if(Client()->IsSixup()) + { + protocol7::CNetMsg_Cl_CallVote Msg; + Msg.m_pType = pType; + Msg.m_pValue = pValue; + Msg.m_pReason = pReason; + Msg.m_Force = false; + Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL, true); + return; + } CNetMsg_Cl_CallVote Msg = {0}; Msg.m_pType = pType; Msg.m_pValue = pValue; @@ -33,44 +48,44 @@ void CVoting::Callvote(const char *pType, const char *pValue, const char *pReaso Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); } -void CVoting::CallvoteSpectate(int ClientID, const char *pReason, bool ForceVote) +void CVoting::CallvoteSpectate(int ClientId, const char *pReason, bool ForceVote) { if(ForceVote) { char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "set_team %d -1", ClientID); + str_format(aBuf, sizeof(aBuf), "set_team %d -1", ClientId); Client()->Rcon(aBuf); } else { char aBuf[32]; - str_from_int(ClientID, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", ClientId); Callvote("spectate", aBuf, pReason); } } -void CVoting::CallvoteKick(int ClientID, const char *pReason, bool ForceVote) +void CVoting::CallvoteKick(int ClientId, const char *pReason, bool ForceVote) { if(ForceVote) { char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "force_vote kick %d %s", ClientID, pReason); + str_format(aBuf, sizeof(aBuf), "force_vote kick %d %s", ClientId, pReason); Client()->Rcon(aBuf); } else { char aBuf[32]; - str_from_int(ClientID, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", ClientId); Callvote("kick", aBuf, pReason); } } -void CVoting::CallvoteOption(int OptionID, const char *pReason, bool ForceVote) +void CVoting::CallvoteOption(int OptionId, const char *pReason, bool ForceVote) { CVoteOptionClient *pOption = m_pFirst; - while(pOption && OptionID >= 0) + while(pOption && OptionId >= 0) { - if(OptionID == 0) + if(OptionId == 0) { if(ForceVote) { @@ -89,17 +104,17 @@ void CVoting::CallvoteOption(int OptionID, const char *pReason, bool ForceVote) break; } - OptionID--; + OptionId--; pOption = pOption->m_pNext; } } -void CVoting::RemovevoteOption(int OptionID) +void CVoting::RemovevoteOption(int OptionId) { CVoteOptionClient *pOption = m_pFirst; - while(pOption && OptionID >= 0) + while(pOption && OptionId >= 0) { - if(OptionID == 0) + if(OptionId == 0) { char aBuf[128]; str_copy(aBuf, "remove_vote \""); @@ -110,7 +125,7 @@ void CVoting::RemovevoteOption(int OptionID) break; } - OptionID--; + OptionId--; pOption = pOption->m_pNext; } } @@ -136,15 +151,15 @@ void CVoting::Vote(int v) Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); } +int CVoting::SecondsLeft() const +{ + return (m_Closetime - time()) / time_freq(); +} + CVoting::CVoting() { ClearOptions(); - - m_Closetime = 0; - m_aDescription[0] = 0; - m_aReason[0] = 0; - m_Yes = m_No = m_Pass = m_Total = 0; - m_Voted = 0; + OnReset(); } void CVoting::AddOption(const char *pDescription) @@ -177,6 +192,37 @@ void CVoting::AddOption(const char *pDescription) ++m_NumVoteOptions; } +void CVoting::RemoveOption(const char *pDescription) +{ + for(CVoteOptionClient *pOption = m_pFirst; pOption; pOption = pOption->m_pNext) + { + if(str_comp(pOption->m_aDescription, pDescription) == 0) + { + // remove it from the list + if(m_pFirst == pOption) + m_pFirst = m_pFirst->m_pNext; + if(m_pLast == pOption) + m_pLast = m_pLast->m_pPrev; + if(pOption->m_pPrev) + pOption->m_pPrev->m_pNext = pOption->m_pNext; + if(pOption->m_pNext) + pOption->m_pNext->m_pPrev = pOption->m_pPrev; + --m_NumVoteOptions; + + // add it to recycle list + pOption->m_pNext = 0; + pOption->m_pPrev = m_pRecycleLast; + if(pOption->m_pPrev) + pOption->m_pPrev->m_pNext = pOption; + m_pRecycleLast = pOption; + if(!m_pRecycleFirst) + m_pRecycleLast = pOption; + + break; + } + } +} + void CVoting::ClearOptions() { m_Heap.Reset(); @@ -191,11 +237,12 @@ void CVoting::ClearOptions() void CVoting::OnReset() { - m_Closetime = 0; + m_Opentime = m_Closetime = 0; m_aDescription[0] = 0; m_aReason[0] = 0; m_Yes = m_No = m_Pass = m_Total = 0; m_Voted = 0; + m_ReceivingOptions = false; } void CVoting::OnConsoleInit() @@ -214,6 +261,7 @@ void CVoting::OnMessage(int MsgType, void *pRawMsg) { str_copy(m_aDescription, pMsg->m_pDescription); str_copy(m_aReason, pMsg->m_pReason); + m_Opentime = time(); m_Closetime = time() + time_freq() * pMsg->m_Timeout; if(Client()->RconAuthed()) @@ -221,7 +269,7 @@ void CVoting::OnMessage(int MsgType, void *pRawMsg) char aBuf[512]; str_format(aBuf, sizeof(aBuf), "%s (%s)", m_aDescription, m_aReason); Client()->Notify("DDNet Vote", aBuf); - m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_HIGHLIGHT, 0); + m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_CHAT_HIGHLIGHT, 1.0f); } } } @@ -271,98 +319,110 @@ void CVoting::OnMessage(int MsgType, void *pRawMsg) else if(MsgType == NETMSGTYPE_SV_VOTEOPTIONREMOVE) { CNetMsg_Sv_VoteOptionRemove *pMsg = (CNetMsg_Sv_VoteOptionRemove *)pRawMsg; - - for(CVoteOptionClient *pOption = m_pFirst; pOption; pOption = pOption->m_pNext) - { - if(str_comp(pOption->m_aDescription, pMsg->m_pDescription) == 0) - { - // remove it from the list - if(m_pFirst == pOption) - m_pFirst = m_pFirst->m_pNext; - if(m_pLast == pOption) - m_pLast = m_pLast->m_pPrev; - if(pOption->m_pPrev) - pOption->m_pPrev->m_pNext = pOption->m_pNext; - if(pOption->m_pNext) - pOption->m_pNext->m_pPrev = pOption->m_pPrev; - --m_NumVoteOptions; - - // add it to recycle list - pOption->m_pNext = 0; - pOption->m_pPrev = m_pRecycleLast; - if(pOption->m_pPrev) - pOption->m_pPrev->m_pNext = pOption; - m_pRecycleLast = pOption; - if(!m_pRecycleFirst) - m_pRecycleLast = pOption; - - break; - } - } + RemoveOption(pMsg->m_pDescription); } else if(MsgType == NETMSGTYPE_SV_YOURVOTE) { CNetMsg_Sv_YourVote *pMsg = (CNetMsg_Sv_YourVote *)pRawMsg; m_Voted = pMsg->m_Voted; } + else if(MsgType == NETMSGTYPE_SV_VOTEOPTIONGROUPSTART) + { + m_ReceivingOptions = true; + } + else if(MsgType == NETMSGTYPE_SV_VOTEOPTIONGROUPEND) + { + m_ReceivingOptions = false; + } } -void CVoting::OnRender() +void CVoting::Render() { + if((!g_Config.m_ClShowVotesAfterVoting && !m_pClient->m_Scoreboard.Active() && TakenChoice()) || !IsVoting() || Client()->State() == IClient::STATE_DEMOPLAYBACK) + return; + const int Seconds = SecondsLeft(); + if(Seconds < 0) + { + OnReset(); + return; + } + + CUIRect View = {0.0f, 60.0f, 120.0f, 38.0f}; + View.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_R, 3.0f); + View.Margin(3.0f, &View); + + SLabelProperties Props; + Props.m_EllipsisAtEnd = true; + + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), Localize("%ds left"), Seconds); + + CUIRect Row, LeftColumn, RightColumn, ProgressSpinner; + View.HSplitTop(6.0f, &Row, &View); + Row.VSplitRight(TextRender()->TextWidth(6.0f, aBuf), &LeftColumn, &RightColumn); + LeftColumn.VSplitRight(2.0f, &LeftColumn, nullptr); + LeftColumn.VSplitRight(6.0f, &LeftColumn, &ProgressSpinner); + LeftColumn.VSplitRight(2.0f, &LeftColumn, nullptr); + + SProgressSpinnerProperties ProgressProps; + ProgressProps.m_Progress = clamp((time() - m_Opentime) / (float)(m_Closetime - m_Opentime), 0.0f, 1.0f); + Ui()->RenderProgressSpinner(ProgressSpinner.Center(), ProgressSpinner.h / 2.0f, ProgressProps); + + Ui()->DoLabel(&RightColumn, aBuf, 6.0f, TEXTALIGN_MR); + + Props.m_MaxWidth = LeftColumn.w; + Ui()->DoLabel(&LeftColumn, VoteDescription(), 6.0f, TEXTALIGN_ML, Props); + + View.HSplitTop(3.0f, nullptr, &View); + View.HSplitTop(6.0f, &Row, &View); + str_format(aBuf, sizeof(aBuf), "%s %s", Localize("Reason:"), VoteReason()); + Props.m_MaxWidth = Row.w; + Ui()->DoLabel(&Row, aBuf, 6.0f, TEXTALIGN_ML, Props); + + View.HSplitTop(3.0f, nullptr, &View); + View.HSplitTop(4.0f, &Row, &View); + RenderBars(Row); + + View.HSplitTop(3.0f, nullptr, &View); + View.HSplitTop(6.0f, &Row, &View); + Row.VSplitMid(&LeftColumn, &RightColumn, 4.0f); + + char aKey[64]; + m_pClient->m_Binds.GetKey("vote yes", aKey, sizeof(aKey)); + str_format(aBuf, sizeof(aBuf), "%s - %s", aKey, Localize("Vote yes")); + TextRender()->TextColor(TakenChoice() == 1 ? ColorRGBA(0.2f, 0.9f, 0.2f, 0.85f) : TextRender()->DefaultTextColor()); + Ui()->DoLabel(&LeftColumn, aBuf, 6.0f, TEXTALIGN_ML); + + m_pClient->m_Binds.GetKey("vote no", aKey, sizeof(aKey)); + str_format(aBuf, sizeof(aBuf), "%s - %s", Localize("Vote no"), aKey); + TextRender()->TextColor(TakenChoice() == -1 ? ColorRGBA(0.95f, 0.25f, 0.25f, 0.85f) : TextRender()->DefaultTextColor()); + Ui()->DoLabel(&RightColumn, aBuf, 6.0f, TEXTALIGN_MR); + + TextRender()->TextColor(TextRender()->DefaultTextColor()); } -void CVoting::RenderBars(CUIRect Bars, bool Text) +void CVoting::RenderBars(CUIRect Bars) const { - Bars.Draw(ColorRGBA(0.8f, 0.8f, 0.8f, 0.5f), IGraphics::CORNER_ALL, Bars.h / 3); + Bars.Draw(ColorRGBA(0.8f, 0.8f, 0.8f, 0.5f), IGraphics::CORNER_ALL, Bars.h / 2.0f); - CUIRect Splitter = Bars; - Splitter.x = Splitter.x + Splitter.w / 2; - Splitter.w = Splitter.h / 2.0f; - Splitter.x -= Splitter.w / 2; - Splitter.Draw(ColorRGBA(0.4f, 0.4f, 0.4f, 0.5f), IGraphics::CORNER_ALL, Splitter.h / 4); + CUIRect Splitter; + Bars.VMargin((Bars.w - 2.0f) / 2.0f, &Splitter); + Splitter.Draw(ColorRGBA(0.4f, 0.4f, 0.4f, 0.5f), IGraphics::CORNER_NONE, 0.0f); if(m_Total) { - CUIRect PassArea = Bars; if(m_Yes) { - CUIRect YesArea = Bars; - YesArea.w *= m_Yes / (float)m_Total; - YesArea.Draw(ColorRGBA(0.2f, 0.9f, 0.2f, 0.85f), IGraphics::CORNER_ALL, Bars.h / 3); - - if(Text) - { - char aBuf[256]; - str_from_int(m_Yes, aBuf); - UI()->DoLabel(&YesArea, aBuf, Bars.h * 0.75f, TEXTALIGN_MC); - } - - PassArea.x += YesArea.w; - PassArea.w -= YesArea.w; + CUIRect YesArea; + Bars.VSplitLeft(Bars.w * m_Yes / m_Total, &YesArea, nullptr); + YesArea.Draw(ColorRGBA(0.2f, 0.9f, 0.2f, 0.85f), IGraphics::CORNER_ALL, YesArea.h / 2.0f); } if(m_No) { - CUIRect NoArea = Bars; - NoArea.w *= m_No / (float)m_Total; - NoArea.x = (Bars.x + Bars.w) - NoArea.w; - NoArea.Draw(ColorRGBA(0.9f, 0.2f, 0.2f, 0.85f), IGraphics::CORNER_ALL, Bars.h / 3); - - if(Text) - { - char aBuf[256]; - str_from_int(m_No, aBuf); - UI()->DoLabel(&NoArea, aBuf, Bars.h * 0.75f, TEXTALIGN_MC); - } - - PassArea.w -= NoArea.w; - } - - if(Text && m_Pass) - { - char aBuf[256]; - str_from_int(m_Pass, aBuf); - UI()->DoLabel(&PassArea, aBuf, Bars.h * 0.75f, TEXTALIGN_MC); + CUIRect NoArea; + Bars.VSplitRight(Bars.w * m_No / m_Total, nullptr, &NoArea); + NoArea.Draw(ColorRGBA(0.9f, 0.2f, 0.2f, 0.85f), IGraphics::CORNER_ALL, NoArea.h / 2.0f); } } } diff --git a/src/game/client/components/voting.h b/src/game/client/components/voting.h index f81959a68e..87abcb1ebb 100644 --- a/src/game/client/components/voting.h +++ b/src/game/client/components/voting.h @@ -18,16 +18,20 @@ class CVoting : public CComponent static void ConCallvote(IConsole::IResult *pResult, void *pUserData); static void ConVote(IConsole::IResult *pResult, void *pUserData); + int64_t m_Opentime; int64_t m_Closetime; char m_aDescription[VOTE_DESC_LENGTH]; char m_aReason[VOTE_REASON_LENGTH]; int m_Voted; int m_Yes, m_No, m_Pass, m_Total; + bool m_ReceivingOptions; - void AddOption(const char *pDescription); + void RemoveOption(const char *pDescription); void ClearOptions(); void Callvote(const char *pType, const char *pValue, const char *pReason); + void RenderBars(CUIRect Bars) const; + public: int m_NumVoteOptions; CVoteOptionClient *m_pFirst; @@ -41,23 +45,24 @@ class CVoting : public CComponent virtual void OnReset() override; virtual void OnConsoleInit() override; virtual void OnMessage(int Msgtype, void *pRawMsg) override; - virtual void OnRender() override; - void RenderBars(CUIRect Bars, bool Text); + void Render(); - void CallvoteSpectate(int ClientID, const char *pReason, bool ForceVote = false); - void CallvoteKick(int ClientID, const char *pReason, bool ForceVote = false); - void CallvoteOption(int OptionID, const char *pReason, bool ForceVote = false); - void RemovevoteOption(int OptionID); + void CallvoteSpectate(int ClientId, const char *pReason, bool ForceVote = false); + void CallvoteKick(int ClientId, const char *pReason, bool ForceVote = false); + void CallvoteOption(int OptionId, const char *pReason, bool ForceVote = false); + void RemovevoteOption(int OptionId); void AddvoteOption(const char *pDescription, const char *pCommand); + void AddOption(const char *pDescription); void Vote(int v); // -1 = no, 1 = yes - int SecondsLeft() { return (m_Closetime - time()) / time_freq(); } - bool IsVoting() { return m_Closetime != 0; } + int SecondsLeft() const; + bool IsVoting() const { return m_Closetime != 0; } int TakenChoice() const { return m_Voted; } const char *VoteDescription() const { return m_aDescription; } const char *VoteReason() const { return m_aReason; } + bool IsReceivingOptions() const { return m_ReceivingOptions; } }; #endif diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index ca0ae8c00e..cc5fc66b34 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -2,11 +2,10 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include -#include // for rand() #include -#include "engine/discord.h" #include +#include #include #include #include @@ -25,6 +24,7 @@ #include #include +#include #include #include #include @@ -38,6 +38,9 @@ #include #include +#include +#include + #include "components/background.h" #include "components/binds.h" #include "components/broadcast.h" @@ -62,12 +65,12 @@ #include "components/menus.h" #include "components/motd.h" #include "components/nameplates.h" -#include "components/parser.h" #include "components/particles.h" #include "components/players.h" #include "components/race_demo.h" #include "components/scoreboard.h" #include "components/skins.h" +#include "components/skins7.h" #include "components/sounds.h" #include "components/spectator.h" #include "components/statboard.h" @@ -79,8 +82,10 @@ using namespace std::chrono_literals; const char *CGameClient::Version() const { return GAME_VERSION; } const char *CGameClient::NetVersion() const { return GAME_NETVERSION; } +const char *CGameClient::NetVersion7() const { return GAME_NETVERSION7; } int CGameClient::DDNetVersion() const { return DDNET_VERSION_NUMBER; } const char *CGameClient::DDNetVersionStr() const { return m_aDDNetVersionStr; } +int CGameClient::ClientVersion7() const { return CLIENT_VERSION7; } const char *CGameClient::GetItemName(int Type) const { return m_NetObjHandler.GetObjName(Type); } void CGameClient::OnConsoleInit() @@ -103,17 +108,14 @@ void CGameClient::OnConsoleInit() #if defined(CONF_AUTOUPDATE) m_pUpdater = Kernel()->RequestInterface(); #endif - - m_Menus.SetMenuBackground(&m_MenuBackground); - - m_NamePlates.SetPlayers(&m_Players); + m_pHttp = Kernel()->RequestInterface(); // make a list of all the systems, make sure to add them in the correct render order m_vpAll.insert(m_vpAll.end(), {&m_Skins, + &m_Skins7, &m_CountryFlags, &m_MapImages, &m_Effects, // doesn't render anything, just updates effects - &m_SkinProfiles, &m_Binds, &m_Binds.m_SpecialBinds, &m_Controls, @@ -132,9 +134,6 @@ void CGameClient::OnConsoleInit() &m_MapLayersForeground, &m_Particles.m_RenderExplosions, &m_NamePlates, - //bindwheel - &m_BindWheel, - &m_Outlines, &m_Particles.m_RenderExtra, &m_Particles.m_RenderGeneral, &m_FreezeBars, @@ -148,7 +147,6 @@ void CGameClient::OnConsoleInit() &m_DebugHud, &m_Scoreboard, &m_Statboard, - &m_Stats, &m_Motd, &m_Menus, &m_Tooltips, @@ -162,39 +160,19 @@ void CGameClient::OnConsoleInit() &m_GameConsole, &m_Chat, // chat has higher prio, due to that you can quit it by pressing esc &m_Motd, // for pressing esc to remove it - &m_Menus, - //bindwheel - &m_BindWheel, &m_Spectator, &m_Emoticon, + &m_Menus, &m_Controls, &m_Binds}); - // add the some console commands + // add basic console commands Console()->Register("team", "i[team-id]", CFGFLAG_CLIENT, ConTeam, this, "Switch team"); Console()->Register("kill", "", CFGFLAG_CLIENT, ConKill, this, "Kill yourself to restart"); - - // register server dummy commands for tab completion - Console()->Register("tune", "s[tuning] ?i[value]", CFGFLAG_SERVER, 0, 0, "Tune variable to value or show current value"); - Console()->Register("tune_reset", "?s[tuning]", CFGFLAG_SERVER, 0, 0, "Reset all or one tuning variable to default"); - Console()->Register("tunes", "", CFGFLAG_SERVER, 0, 0, "List all tuning variables and their values"); - Console()->Register("change_map", "?r[map]", CFGFLAG_SERVER, 0, 0, "Change map"); - Console()->Register("restart", "?i[seconds]", CFGFLAG_SERVER, 0, 0, "Restart in x seconds"); - Console()->Register("broadcast", "r[message]", CFGFLAG_SERVER, 0, 0, "Broadcast message"); - Console()->Register("say", "r[message]", CFGFLAG_SERVER, 0, 0, "Say in chat"); - Console()->Register("set_team", "i[id] i[team-id] ?i[delay in minutes]", CFGFLAG_SERVER, 0, 0, "Set team of player to team"); - Console()->Register("set_team_all", "i[team-id]", CFGFLAG_SERVER, 0, 0, "Set team of all players to team"); - Console()->Register("add_vote", "s[name] r[command]", CFGFLAG_SERVER, 0, 0, "Add a voting option"); - Console()->Register("remove_vote", "s[name]", CFGFLAG_SERVER, 0, 0, "remove a voting option"); - Console()->Register("force_vote", "s[name] s[command] ?r[reason]", CFGFLAG_SERVER, 0, 0, "Force a voting option"); - Console()->Register("clear_votes", "", CFGFLAG_SERVER, 0, 0, "Clears the voting options"); - Console()->Register("add_map_votes", "", CFGFLAG_SERVER, 0, 0, "Automatically adds voting options for all maps"); - Console()->Register("vote", "r['yes'|'no']", CFGFLAG_SERVER, 0, 0, "Force a vote to yes/no"); - Console()->Register("swap_teams", "", CFGFLAG_SERVER, 0, 0, "Swap the current teams"); - Console()->Register("shuffle_teams", "", CFGFLAG_SERVER, 0, 0, "Shuffle the current teams"); + Console()->Register("ready_change", "", CFGFLAG_CLIENT, ConReadyChange7, this, "Change ready state (0.7 only)"); // register tune zone command to allow the client prediction to load tunezones from the map - Console()->Register("tune_zone", "i[zone] s[tuning] i[value]", CFGFLAG_CLIENT | CFGFLAG_GAME, ConTuneZone, this, "Tune in zone a variable to value"); + Console()->Register("tune_zone", "i[zone] s[tuning] f[value]", CFGFLAG_GAME, ConTuneZone, this, "Tune in zone a variable to value"); for(auto &pComponent : m_vpAll) pComponent->m_pClient = this; @@ -213,6 +191,26 @@ void CGameClient::OnConsoleInit() Console()->Chain("player_color_feet", ConchainSpecialInfoupdate, this); Console()->Chain("player_skin", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_skin", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_skin_body", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_skin_marking", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_skin_decoration", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_skin_hands", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_skin_feet", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_skin_eyes", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_color_body", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_color_marking", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_color_decoration", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_color_hands", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_color_feet", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_color_eyes", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_use_custom_color_body", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_use_custom_color_marking", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_use_custom_color_decoration", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_use_custom_color_hands", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_use_custom_color_feet", ConchainSpecialInfoupdate, this); + Console()->Chain("player7_use_custom_color_eyes", ConchainSpecialInfoupdate, this); + Console()->Chain("dummy_name", ConchainSpecialDummyInfoupdate, this); Console()->Chain("dummy_clan", ConchainSpecialDummyInfoupdate, this); Console()->Chain("dummy_country", ConchainSpecialDummyInfoupdate, this); @@ -221,19 +219,81 @@ void CGameClient::OnConsoleInit() Console()->Chain("dummy_color_feet", ConchainSpecialDummyInfoupdate, this); Console()->Chain("dummy_skin", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_skin", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_skin_body", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_skin_marking", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_skin_decoration", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_skin_hands", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_skin_feet", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_skin_eyes", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_color_body", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_color_marking", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_color_decoration", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_color_hands", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_color_feet", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_color_eyes", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_use_custom_color_body", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_use_custom_color_marking", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_use_custom_color_decoration", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_use_custom_color_hands", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_use_custom_color_feet", ConchainSpecialDummyInfoupdate, this); + Console()->Chain("dummy7_use_custom_color_eyes", ConchainSpecialDummyInfoupdate, this); + + Console()->Chain("cl_skin_download_url", ConchainRefreshSkins, this); + Console()->Chain("cl_skin_community_download_url", ConchainRefreshSkins, this); + Console()->Chain("cl_download_skins", ConchainRefreshSkins, this); + Console()->Chain("cl_download_community_skins", ConchainRefreshSkins, this); + Console()->Chain("cl_vanilla_skins_only", ConchainRefreshSkins, this); + Console()->Chain("cl_dummy", ConchainSpecialDummy, this); Console()->Chain("cl_text_entities_size", ConchainClTextEntitiesSize, this); Console()->Chain("cl_menu_map", ConchainMenuMap, this); +} - // - m_SuppressEvents = false; +static void GenerateTimeoutCode(char *pTimeoutCode) +{ + if(pTimeoutCode[0] == '\0' || str_comp(pTimeoutCode, "hGuEYnfxicsXGwFq") == 0) + { + for(unsigned int i = 0; i < 16; i++) + { + if(rand() % 2) + pTimeoutCode[i] = (char)((rand() % ('z' - 'a' + 1)) + 'a'); + else + pTimeoutCode[i] = (char)((rand() % ('Z' - 'A' + 1)) + 'A'); + } + } } void CGameClient::OnInit() { - Client()->SetMapLoadingCBFunc([this]() { - m_Menus.RenderLoading(DemoPlayer()->IsPlaying() ? Localize("Preparing demo playback") : Localize("Connected"), Localize("Loading map file from storage"), 0, false); + const int64_t OnInitStart = time_get(); + + Client()->SetLoadingCallback([this](IClient::ELoadingCallbackDetail Detail) { + const char *pTitle; + if(Detail == IClient::LOADING_CALLBACK_DETAIL_DEMO || DemoPlayer()->IsPlaying()) + { + pTitle = Localize("Preparing demo playback"); + } + else + { + pTitle = Localize("Connected"); + } + + const char *pMessage; + switch(Detail) + { + case IClient::LOADING_CALLBACK_DETAIL_MAP: + pMessage = Localize("Loading map file from storage"); + break; + case IClient::LOADING_CALLBACK_DETAIL_DEMO: + pMessage = Localize("Loading demo file from storage"); + break; + default: + dbg_assert(false, "Invalid callback loading detail"); + dbg_break(); + } + m_Menus.RenderLoading(pTitle, pMessage, 0, false); }); m_pGraphics = Kernel()->RequestInterface(); @@ -242,8 +302,6 @@ void CGameClient::OnInit() m_UI.Init(Kernel()); m_RenderTools.Init(Graphics(), TextRender()); - int64_t Start = time_get(); - if(GIT_SHORTREV_HASH) { str_format(m_aDDNetVersionStr, sizeof(m_aDDNetVersionStr), "%s %s (%s)", GAME_NAME, GAME_RELEASE_VERSION, GIT_SHORTREV_HASH); @@ -263,6 +321,11 @@ void CGameClient::OnInit() // setup item sizes for(int i = 0; i < NUM_NETOBJTYPES; i++) Client()->SnapSetStaticsize(i, m_NetObjHandler.GetObjSize(i)); + // HACK: only set static size for items, which were available in the first 0.7 release + // so new items don't break the snapshot delta + static const int OLD_NUM_NETOBJTYPES = 23; + for(int i = 0; i < OLD_NUM_NETOBJTYPES; i++) + Client()->SnapSetStaticsize7(i, m_NetObjHandler7.GetObjSize(i)); TextRender()->LoadFonts(); TextRender()->SetFontLanguageVariant(g_Config.m_ClLanguagefile); @@ -270,21 +333,24 @@ void CGameClient::OnInit() // update and swap after font loading, they are quite huge Client()->UpdateAndSwap(); - const char *pLoadingDDNetCaption = Localize("StA Client Loading"); + const char *pLoadingDDNetCaption = Localize("Loading DDNet Client"); + const char *pLoadingMessageComponents = Localize("Initializing components"); + const char *pLoadingMessageComponentsSpecial = Localize("Why are you slowmo replaying to read this?"); + char aLoadingMessage[256]; // init all components - int SkippedComps = 0; - int CompCounter = 0; - for(int i = m_vpAll.size() - 1; i >= 0; --i) + int SkippedComps = 1; + int CompCounter = 1; + const int NumComponents = ComponentCount(); + for(int i = NumComponents - 1; i >= 0; --i) { m_vpAll[i]->OnInit(); // try to render a frame after each component, also flushes GPU uploads if(m_Menus.IsInit()) { - char aBuff[256]; - str_format(aBuff, std::size(aBuff), "%s [%d/%d]", CompCounter == 40 ? Localize("Why are you slowmo replaying to read this?") : Localize("Initializing components"), (CompCounter + 1), (int)ComponentCount()); - m_Menus.RenderLoading(pLoadingDDNetCaption, aBuff, 1 + SkippedComps); - SkippedComps = 0; + str_format(aLoadingMessage, std::size(aLoadingMessage), "%s [%d/%d]", CompCounter == NumComponents ? pLoadingMessageComponentsSpecial : pLoadingMessageComponents, CompCounter, NumComponents); + m_Menus.RenderLoading(pLoadingDDNetCaption, aLoadingMessage, SkippedComps); + SkippedComps = 1; } else { @@ -293,14 +359,13 @@ void CGameClient::OnInit() ++CompCounter; } - char aBuf[256]; - m_GameSkinLoaded = false; m_ParticlesSkinLoaded = false; m_EmoticonsSkinLoaded = false; m_HudSkinLoaded = false; // setup load amount, load textures + const char *pLoadingMessageAssets = Localize("Initializing assets"); for(int i = 0; i < g_pData->m_NumImages; i++) { if(i == IMAGE_GAME) @@ -317,56 +382,26 @@ void CGameClient::OnInit() g_pData->m_aImages[i].m_Id = IGraphics::CTextureHandle(); else g_pData->m_aImages[i].m_Id = Graphics()->LoadTexture(g_pData->m_aImages[i].m_pFilename, IStorage::TYPE_ALL); + m_Menus.RenderLoading(pLoadingDDNetCaption, pLoadingMessageAssets, 1); + } + for(int i = 0; i < client_data7::g_pData->m_NumImages; i++) + { + if(client_data7::g_pData->m_aImages[i].m_pFilename[0] == '\0') // handle special null image without filename + client_data7::g_pData->m_aImages[i].m_Id = IGraphics::CTextureHandle(); + else if(i == client_data7::IMAGE_DEADTEE) + client_data7::g_pData->m_aImages[i].m_Id = Graphics()->LoadTexture(client_data7::g_pData->m_aImages[i].m_pFilename, IStorage::TYPE_ALL, 0); m_Menus.RenderLoading(pLoadingDDNetCaption, Localize("Initializing assets"), 1); } - for(auto &pComponent : m_vpAll) - pComponent->OnReset(); - - m_ServerMode = SERVERMODE_PURE; - - m_aDDRaceMsgSent[0] = false; - m_aDDRaceMsgSent[1] = false; - m_aShowOthers[0] = SHOW_OTHERS_NOT_SET; - m_aShowOthers[1] = SHOW_OTHERS_NOT_SET; - m_aSwitchStateTeam[0] = -1; - m_aSwitchStateTeam[1] = -1; - - m_LastZoom = .0; - m_LastScreenAspect = .0; - m_LastDummyConnected = false; + m_GameWorld.m_pCollision = Collision(); + m_GameWorld.m_pTuningList = m_aTuningList; + OnReset(); // Set free binds to DDRace binds if it's active m_Binds.SetDDRaceBinds(true); - if(g_Config.m_ClTimeoutCode[0] == '\0' || str_comp(g_Config.m_ClTimeoutCode, "hGuEYnfxicsXGwFq") == 0) - { - for(unsigned int i = 0; i < 16; i++) - { - if(rand() % 2) - g_Config.m_ClTimeoutCode[i] = (char)((rand() % 26) + 97); - else - g_Config.m_ClTimeoutCode[i] = (char)((rand() % 26) + 65); - } - } - - if(g_Config.m_ClDummyTimeoutCode[0] == '\0' || str_comp(g_Config.m_ClDummyTimeoutCode, "hGuEYnfxicsXGwFq") == 0) - { - for(unsigned int i = 0; i < 16; i++) - { - if(rand() % 2) - g_Config.m_ClDummyTimeoutCode[i] = (char)((rand() % 26) + 97); - else - g_Config.m_ClDummyTimeoutCode[i] = (char)((rand() % 26) + 65); - } - } - - int64_t End = time_get(); - str_format(aBuf, sizeof(aBuf), "initialisation finished after %.2fms", ((End - Start) * 1000) / (float)time_freq()); - Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "gameclient", aBuf); - - m_GameWorld.m_pCollision = Collision(); - m_GameWorld.m_pTuningList = m_aTuningList; + GenerateTimeoutCode(g_Config.m_ClTimeoutCode); + GenerateTimeoutCode(g_Config.m_ClDummyTimeoutCode); m_MapImages.SetTextureScale(g_Config.m_ClTextEntitiesSize); @@ -386,13 +421,15 @@ void CGameClient::OnInit() int Size = m_vpAll[i]->Sizeof(); pChecksum->m_aComponentsChecksum[i] = Size; } + + log_trace("gameclient", "initialization finished after %.2fms", (time_get() - OnInitStart) * 1000.0f / (float)time_freq()); } void CGameClient::OnUpdate() { HandleLanguageChanged(); - CUIElementBase::Init(UI()); // update static pointer because game and editor use separate UI + CUIElementBase::Init(Ui()); // update static pointer because game and editor use separate UI // handle mouse movement float x = 0.0f, y = 0.0f; @@ -407,55 +444,21 @@ void CGameClient::OnUpdate() } // handle key presses - for(size_t i = 0; i < Input()->NumEvents(); i++) - { - const IInput::CEvent &Event = Input()->GetEvent(i); - if(!Input()->IsEventValid(Event)) - continue; - + Input()->ConsumeEvents([&](const IInput::CEvent &Event) { for(auto &pComponent : m_vpInput) { - if(pComponent->OnInput(Event)) + // Events with flag `FLAG_RELEASE` must always be forwarded to all components so keys being + // released can be handled in all components also after some components have been disabled. + if(pComponent->OnInput(Event) && (Event.m_Flags & ~IInput::FLAG_RELEASE) != 0) break; } - } + }); if(g_Config.m_ClSubTickAiming && m_Binds.m_MouseOnAction) { m_Controls.m_aMousePosOnAction[g_Config.m_ClDummy] = m_Controls.m_aMousePos[g_Config.m_ClDummy]; m_Binds.m_MouseOnAction = false; } - static float OldUpdateTime = Client()->LocalTime(); - if(OldUpdateTime + 1 < Client()->LocalTime()) - { - OldUpdateTime = Client()->LocalTime(); - IDiscord *discord = Kernel()->RequestInterface(); - int LocalID = m_aLocalIDs[g_Config.m_ClDummy]; - bool Afk = m_aClients[LocalID].m_Afk; - if(!discord) - return; - - const char *pText; - const char *pImage; - - if(Afk) - { - pText = "Idleing..."; - pImage = "idle"; - } - else if(Client()->State() != IClient::STATE_ONLINE) - { - pText = "Chilling in menus"; - pImage = "menu1"; - } - else - { - pText = "Playing"; - pImage = "greenline"; - } - - discord->SetGameInfo(Client()->ServerAddress(), (Client()->State() != IClient::STATE_ONLINE) ? "" : Client()->GetCurrentMap(), false, pText, pImage, Client()->PlayerName()); - } } void CGameClient::OnDummySwap() @@ -512,7 +515,7 @@ int CGameClient::OnSnapInput(int *pData, bool Dummy, bool Force) } vec2 MainPos = m_LocalCharacterPos; - vec2 DummyPos = m_aClients[m_aLocalIDs[!g_Config.m_ClDummy]].m_Predicted.m_Pos; + vec2 DummyPos = m_aClients[m_aLocalIds[!g_Config.m_ClDummy]].m_Predicted.m_Pos; vec2 Dir = MainPos - DummyPos; m_HammerInput.m_TargetX = (int)(Dir.x); m_HammerInput.m_TargetY = (int)(Dir.y); @@ -531,21 +534,7 @@ void CGameClient::OnConnected() m_Layers.Init(Kernel()); m_Collision.Init(Layers()); m_GameWorld.m_Core.InitSwitchers(m_Collision.m_HighestSwitchNumber); - - CRaceHelper::ms_aFlagIndex[0] = -1; - CRaceHelper::ms_aFlagIndex[1] = -1; - - CTile *pGameTiles = static_cast(Layers()->Map()->GetData(Layers()->GameLayer()->m_Data)); - - // get flag positions - for(int i = 0; i < m_Collision.GetWidth() * m_Collision.GetHeight(); i++) - { - if(pGameTiles[i].m_Index - ENTITY_OFFSET == ENTITY_FLAGSTAND_RED) - CRaceHelper::ms_aFlagIndex[TEAM_RED] = i; - else if(pGameTiles[i].m_Index - ENTITY_OFFSET == ENTITY_FLAGSTAND_BLUE) - CRaceHelper::ms_aFlagIndex[TEAM_BLUE] = i; - i += pGameTiles[i].m_Skip; - } + m_RaceHelper.Init(this); // render loading before going through all components m_Menus.RenderLoading(pConnectCaption, pLoadMapContent, 0, false); @@ -558,8 +547,6 @@ void CGameClient::OnConnected() Client()->SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_GETTING_READY); m_Menus.RenderLoading(pConnectCaption, Localize("Sending initial client info"), 0, false); - m_ServerMode = SERVERMODE_PURE; - // send the initial info SendInfo(true); // we should keep this in for now, because otherwise you can't spectate @@ -567,10 +554,6 @@ void CGameClient::OnConnected() // snap Client()->Rcon("crashmeplx"); - m_GameWorld.Clear(); - m_GameWorld.m_WorldConfig.m_InfiniteAmmo = true; - mem_zero(&m_GameInfo, sizeof(m_GameInfo)); - m_PredictedDummyID = -1; ConfigManager()->ResetGameSettings(); LoadMapSettings(); @@ -580,48 +563,100 @@ void CGameClient::OnConnected() void CGameClient::OnReset() { - m_aLastNewPredictedTick[0] = -1; - m_aLastNewPredictedTick[1] = -1; + InvalidateSnapshot(); - m_aLocalTuneZone[0] = 0; - m_aLocalTuneZone[1] = 0; + m_EditorMovementDelay = 5; - m_aExpectingTuningForZone[0] = -1; - m_aExpectingTuningForZone[1] = -1; + m_PredictedTick = -1; + std::fill(std::begin(m_aLastNewPredictedTick), std::end(m_aLastNewPredictedTick), -1); - m_aReceivedTuning[0] = false; - m_aReceivedTuning[1] = false; + m_LastRoundStartTick = -1; + m_LastFlagCarrierRed = -4; + m_LastFlagCarrierBlue = -4; - InvalidateSnapshot(); + std::fill(std::begin(m_aCheckInfo), std::end(m_aCheckInfo), -1); - for(auto &Client : m_aClients) - Client.Reset(); + // m_aDDNetVersionStr is initialized once in OnInit - for(auto &pComponent : m_vpAll) - pComponent->OnReset(); + std::fill(std::begin(m_aLastPos), std::end(m_aLastPos), vec2(0.0f, 0.0f)); + std::fill(std::begin(m_aLastActive), std::end(m_aLastActive), false); + + m_GameOver = false; + m_GamePaused = false; + m_PrevLocalId = -1; + + m_SuppressEvents = false; + m_NewTick = false; + m_NewPredictedTick = false; - m_DemoSpecID = SPEC_FOLLOW; m_aFlagDropTick[TEAM_RED] = 0; m_aFlagDropTick[TEAM_BLUE] = 0; - m_LastRoundStartTick = -1; - m_LastFlagCarrierRed = -4; - m_LastFlagCarrierBlue = -4; - m_aTuning[g_Config.m_ClDummy] = CTuningParams(); + + m_ServerMode = SERVERMODE_PURE; + mem_zero(&m_GameInfo, sizeof(m_GameInfo)); + + m_DemoSpecId = SPEC_FOLLOW; + m_LocalCharacterPos = vec2(0.0f, 0.0f); + + m_PredictedPrevChar.Reset(); + m_PredictedChar.Reset(); + + // m_Snap was cleared in InvalidateSnapshot + + std::fill(std::begin(m_aLocalTuneZone), std::end(m_aLocalTuneZone), 0); + std::fill(std::begin(m_aReceivedTuning), std::end(m_aReceivedTuning), false); + std::fill(std::begin(m_aExpectingTuningForZone), std::end(m_aExpectingTuningForZone), -1); + std::fill(std::begin(m_aExpectingTuningSince), std::end(m_aExpectingTuningSince), 0); + std::fill(std::begin(m_aTuning), std::end(m_aTuning), CTuningParams()); + + for(auto &Client : m_aClients) + Client.Reset(); + + for(auto &Stats : m_aStats) + Stats.Reset(); + + m_NextChangeInfo = 0; + std::fill(std::begin(m_aLocalIds), std::end(m_aLocalIds), -1); + m_DummyInput = {}; + m_HammerInput = {}; + m_DummyFire = 0; + m_ReceivedDDNetPlayer = false; m_Teams.Reset(); - m_aDDRaceMsgSent[0] = false; - m_aDDRaceMsgSent[1] = false; - m_aShowOthers[0] = SHOW_OTHERS_NOT_SET; - m_aShowOthers[1] = SHOW_OTHERS_NOT_SET; + m_GameWorld.Clear(); + m_GameWorld.m_WorldConfig.m_InfiniteAmmo = true; + m_PredictedWorld.CopyWorld(&m_GameWorld); + m_PrevPredictedWorld.CopyWorld(&m_PredictedWorld); - m_LastZoom = .0; - m_LastScreenAspect = .0; + m_vSnapEntities.clear(); + + std::fill(std::begin(m_aDDRaceMsgSent), std::end(m_aDDRaceMsgSent), false); + std::fill(std::begin(m_aShowOthers), std::end(m_aShowOthers), SHOW_OTHERS_NOT_SET); + std::fill(std::begin(m_aLastUpdateTick), std::end(m_aLastUpdateTick), 0); + + m_PredictedDummyId = -1; + m_IsDummySwapping = false; + m_CharOrder.Reset(); + std::fill(std::begin(m_aSwitchStateTeam), std::end(m_aSwitchStateTeam), -1); + + // m_aTuningList is reset in LoadMapSettings + + m_LastZoom = 0.0f; + m_LastScreenAspect = 0.0f; m_LastDummyConnected = false; - m_ReceivedDDNetPlayer = false; + m_MultiViewPersonalZoom = 0; + m_MultiViewActivated = false; + m_MultiView.m_IsInit = false; + + for(auto &pComponent : m_vpAll) + pComponent->OnReset(); Editor()->ResetMentions(); Editor()->ResetIngameMoved(); + + Collision()->Unload(); + Layers()->Unload(); } void CGameClient::UpdatePositions() @@ -663,17 +698,17 @@ void CGameClient::UpdatePositions() { HandleMultiView(); } - else if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_DemoSpecID != SPEC_FOLLOW && m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW) + else if(Client()->State() == IClient::STATE_DEMOPLAYBACK && m_DemoSpecId != SPEC_FOLLOW && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) { m_Snap.m_SpecInfo.m_Position = mix( - vec2(m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorID].m_Prev.m_X, m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorID].m_Prev.m_Y), - vec2(m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorID].m_Cur.m_X, m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorID].m_Cur.m_Y), + vec2(m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorId].m_Prev.m_X, m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorId].m_Prev.m_Y), + vec2(m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorId].m_Cur.m_X, m_Snap.m_aCharacters[m_Snap.m_SpecInfo.m_SpectatorId].m_Cur.m_Y), Client()->IntraGameTick(g_Config.m_ClDummy)); m_Snap.m_SpecInfo.m_UsePosition = true; } - else if(m_Snap.m_pSpectatorInfo && ((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_DemoSpecID == SPEC_FOLLOW) || (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW))) + else if(m_Snap.m_pSpectatorInfo && ((Client()->State() == IClient::STATE_DEMOPLAYBACK && m_DemoSpecId == SPEC_FOLLOW) || (Client()->State() != IClient::STATE_DEMOPLAYBACK && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW))) { - if(m_Snap.m_pPrevSpectatorInfo && m_Snap.m_pPrevSpectatorInfo->m_SpectatorID == m_Snap.m_pSpectatorInfo->m_SpectatorID) + if(m_Snap.m_pPrevSpectatorInfo && m_Snap.m_pPrevSpectatorInfo->m_SpectatorId == m_Snap.m_pSpectatorInfo->m_SpectatorId) m_Snap.m_SpecInfo.m_Position = mix(vec2(m_Snap.m_pPrevSpectatorInfo->m_X, m_Snap.m_pPrevSpectatorInfo->m_Y), vec2(m_Snap.m_pSpectatorInfo->m_X, m_Snap.m_pSpectatorInfo->m_Y), Client()->IntraGameTick(g_Config.m_ClDummy)); else @@ -690,17 +725,12 @@ void CGameClient::UpdatePositions() void CGameClient::OnRender() { - if(g_Config.m_ClAutoVerify) - { - m_Verify.OnRender(); - } - // check if multi view got activated if(!m_MultiView.m_IsInit && m_MultiViewActivated) { int TeamId = 0; - if(m_Snap.m_SpecInfo.m_SpectatorID >= 0) - TeamId = m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorID); + if(m_Snap.m_SpecInfo.m_SpectatorId >= 0) + TeamId = m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorId); if(TeamId > MAX_CLIENTS || TeamId < 0) TeamId = 0; @@ -720,7 +750,7 @@ void CGameClient::OnRender() { if(pWarning != nullptr && m_Menus.CanDisplayWarning()) { - m_Menus.PopupWarning(pWarning->m_aWarningTitle[0] == '\0' ? Localize("Warning") : pWarning->m_aWarningTitle, pWarning->m_aWarningMsg, "Ok", pWarning->m_AutoHide ? 10s : 0s); + m_Menus.PopupWarning(pWarning->m_aWarningTitle[0] == '\0' ? Localize("Warning") : pWarning->m_aWarningTitle, pWarning->m_aWarningMsg, Localize("Ok"), pWarning->m_AutoHide ? 10s : 0s); pWarning->m_WasShown = true; } } @@ -746,17 +776,27 @@ void CGameClient::OnRender() { if(m_aCheckInfo[0] == 0) { - if( - str_comp(m_aClients[m_aLocalIDs[0]].m_aName, Client()->PlayerName()) || - str_comp(m_aClients[m_aLocalIDs[0]].m_aClan, g_Config.m_PlayerClan) || - m_aClients[m_aLocalIDs[0]].m_Country != g_Config.m_PlayerCountry || - str_comp(m_aClients[m_aLocalIDs[0]].m_aSkinName, g_Config.m_ClPlayerSkin) || - m_aClients[m_aLocalIDs[0]].m_UseCustomColor != g_Config.m_ClPlayerUseCustomColor || - m_aClients[m_aLocalIDs[0]].m_ColorBody != (int)g_Config.m_ClPlayerColorBody || - m_aClients[m_aLocalIDs[0]].m_ColorFeet != (int)g_Config.m_ClPlayerColorFeet) - SendInfo(false); + if(m_pClient->IsSixup()) + { + if(!GotWantedSkin7(false)) + SendSkinChange7(false); + else + m_aCheckInfo[0] = -1; + } else - m_aCheckInfo[0] = -1; + { + if( + str_comp(m_aClients[m_aLocalIds[0]].m_aName, Client()->PlayerName()) || + str_comp(m_aClients[m_aLocalIds[0]].m_aClan, g_Config.m_PlayerClan) || + m_aClients[m_aLocalIds[0]].m_Country != g_Config.m_PlayerCountry || + str_comp(m_aClients[m_aLocalIds[0]].m_aSkinName, g_Config.m_ClPlayerSkin) || + m_aClients[m_aLocalIds[0]].m_UseCustomColor != g_Config.m_ClPlayerUseCustomColor || + m_aClients[m_aLocalIds[0]].m_ColorBody != (int)g_Config.m_ClPlayerColorBody || + m_aClients[m_aLocalIds[0]].m_ColorFeet != (int)g_Config.m_ClPlayerColorFeet) + SendInfo(false); + else + m_aCheckInfo[0] = -1; + } } if(m_aCheckInfo[0] > 0) @@ -766,17 +806,27 @@ void CGameClient::OnRender() { if(m_aCheckInfo[1] == 0) { - if( - str_comp(m_aClients[m_aLocalIDs[1]].m_aName, Client()->DummyName()) || - str_comp(m_aClients[m_aLocalIDs[1]].m_aClan, g_Config.m_ClDummyClan) || - m_aClients[m_aLocalIDs[1]].m_Country != g_Config.m_ClDummyCountry || - str_comp(m_aClients[m_aLocalIDs[1]].m_aSkinName, g_Config.m_ClDummySkin) || - m_aClients[m_aLocalIDs[1]].m_UseCustomColor != g_Config.m_ClDummyUseCustomColor || - m_aClients[m_aLocalIDs[1]].m_ColorBody != (int)g_Config.m_ClDummyColorBody || - m_aClients[m_aLocalIDs[1]].m_ColorFeet != (int)g_Config.m_ClDummyColorFeet) - SendDummyInfo(false); + if(m_pClient->IsSixup()) + { + if(!GotWantedSkin7(true)) + SendSkinChange7(true); + else + m_aCheckInfo[1] = -1; + } else - m_aCheckInfo[1] = -1; + { + if( + str_comp(m_aClients[m_aLocalIds[1]].m_aName, Client()->DummyName()) || + str_comp(m_aClients[m_aLocalIds[1]].m_aClan, g_Config.m_ClDummyClan) || + m_aClients[m_aLocalIds[1]].m_Country != g_Config.m_ClDummyCountry || + str_comp(m_aClients[m_aLocalIds[1]].m_aSkinName, g_Config.m_ClDummySkin) || + m_aClients[m_aLocalIds[1]].m_UseCustomColor != g_Config.m_ClDummyUseCustomColor || + m_aClients[m_aLocalIds[1]].m_ColorBody != (int)g_Config.m_ClDummyColorBody || + m_aClients[m_aLocalIds[1]].m_ColorFeet != (int)g_Config.m_ClDummyColorFeet) + SendDummyInfo(false); + else + m_aCheckInfo[1] = -1; + } } if(m_aCheckInfo[1] > 0) @@ -790,7 +840,7 @@ void CGameClient::OnDummyDisconnect() m_aDDRaceMsgSent[1] = false; m_aShowOthers[1] = SHOW_OTHERS_NOT_SET; m_aLastNewPredictedTick[1] = -1; - m_PredictedDummyID = -1; + m_PredictedDummyId = -1; } int CGameClient::GetLastRaceTick() const @@ -825,6 +875,32 @@ ColorRGBA CGameClient::GetDDTeamColor(int DDTeam, float Lightness) const return color_cast(ColorHSLA(Hue, 1.0f, Lightness)); } +void CGameClient::FormatClientId(int ClientId, char (&aClientId)[16], EClientIdFormat Format) const +{ + if(Format == EClientIdFormat::NO_INDENT) + { + str_format(aClientId, sizeof(aClientId), "%d", ClientId); + } + else + { + const int HighestClientId = Format == EClientIdFormat::INDENT_AUTO ? m_Snap.m_HighestClientId : 64; + const char *pFigureSpace = " "; + char aNumber[8]; + str_format(aNumber, sizeof(aNumber), "%d", ClientId); + aClientId[0] = '\0'; + if(ClientId < 100 && HighestClientId >= 100) + { + str_append(aClientId, pFigureSpace); + } + if(ClientId < 10 && HighestClientId >= 10) + { + str_append(aClientId, pFigureSpace); + } + str_append(aClientId, aNumber); + } + str_append(aClientId, ": "); +} + void CGameClient::OnRelease() { // release all systems @@ -835,16 +911,18 @@ void CGameClient::OnRelease() void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dummy) { // special messages + static_assert((int)NETMSGTYPE_SV_TUNEPARAMS == (int)protocol7::NETMSGTYPE_SV_TUNEPARAMS, "0.6 and 0.7 tune message id do not match"); if(MsgId == NETMSGTYPE_SV_TUNEPARAMS) { // unpack the new tuning CTuningParams NewTuning; int *pParams = (int *)&NewTuning; - // No jetpack on DDNet incompatible servers: - NewTuning.m_JetpackStrength = 0; for(unsigned i = 0; i < sizeof(CTuningParams) / sizeof(int); i++) { - int value = pUnpacker->GetInt(); + // 31 is the magic number index of laser_damage + // which was removed in 0.7 + // also in 0.6 it is unsed so we just set it to 0 + int value = (Client()->IsSixup() && i == 30) ? 0 : pUnpacker->GetInt(); // check for unpacking errors if(pUnpacker->Error()) @@ -853,6 +931,9 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm pParams[i] = value; } + // No jetpack on DDNet incompatible servers: + NewTuning.m_JetpackStrength = 0; + m_ServerMode = SERVERMODE_PURE; m_aReceivedTuning[Conn] = true; @@ -861,22 +942,28 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm return; } - void *pRawMsg = m_NetObjHandler.SecureUnpackMsg(MsgId, pUnpacker); + void *pRawMsg = TranslateGameMsg(&MsgId, pUnpacker, Conn); + if(!pRawMsg) { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "dropped weird message '%s' (%d), failed on '%s'", m_NetObjHandler.GetMsgName(MsgId), MsgId, m_NetObjHandler.FailedMsgOn()); - Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf); + // the 0.7 version of this error message is printed on translation + // in sixup/translate_game.cpp + if(!Client()->IsSixup()) + { + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "dropped weird message '%s' (%d), failed on '%s'", m_NetObjHandler.GetMsgName(MsgId), MsgId, m_NetObjHandler.FailedMsgOn()); + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf); + } return; } if(Dummy) { - if(MsgId == NETMSGTYPE_SV_CHAT && m_aLocalIDs[0] >= 0 && m_aLocalIDs[1] >= 0) + if(MsgId == NETMSGTYPE_SV_CHAT && m_aLocalIds[0] >= 0 && m_aLocalIds[1] >= 0) { CNetMsg_Sv_Chat *pMsg = (CNetMsg_Sv_Chat *)pRawMsg; - if((pMsg->m_Team == 1 && (m_aClients[m_aLocalIDs[0]].m_Team != m_aClients[m_aLocalIDs[1]].m_Team || m_Teams.Team(m_aLocalIDs[0]) != m_Teams.Team(m_aLocalIDs[1]))) || pMsg->m_Team > 1) + if((pMsg->m_Team == 1 && (m_aClients[m_aLocalIds[0]].m_Team != m_aClients[m_aLocalIds[1]].m_Team || m_Teams.Team(m_aLocalIds[0]) != m_Teams.Team(m_aLocalIds[1]))) || pMsg->m_Team > 1) { m_Chat.OnMessage(MsgId, pRawMsg); } @@ -897,9 +984,9 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm CNetMsg_Sv_Emoticon *pMsg = (CNetMsg_Sv_Emoticon *)pRawMsg; // apply - m_aClients[pMsg->m_ClientID].m_Emoticon = pMsg->m_Emoticon; - m_aClients[pMsg->m_ClientID].m_EmoticonStartTick = Client()->GameTick(Conn); - m_aClients[pMsg->m_ClientID].m_EmoticonStartFraction = Client()->IntraGameTickSincePrev(Conn); + m_aClients[pMsg->m_ClientId].m_Emoticon = pMsg->m_Emoticon; + m_aClients[pMsg->m_ClientId].m_EmoticonStartTick = Client()->GameTick(Conn); + m_aClients[pMsg->m_ClientId].m_EmoticonStartFraction = Client()->IntraGameTickSincePrev(Conn); } else if(MsgId == NETMSGTYPE_SV_SOUNDGLOBAL) { @@ -908,17 +995,17 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm // don't enqueue pseudo-global sounds from demos (created by PlayAndRecord) CNetMsg_Sv_SoundGlobal *pMsg = (CNetMsg_Sv_SoundGlobal *)pRawMsg; - if(pMsg->m_SoundID == SOUND_CTF_DROP || pMsg->m_SoundID == SOUND_CTF_RETURN || - pMsg->m_SoundID == SOUND_CTF_CAPTURE || pMsg->m_SoundID == SOUND_CTF_GRAB_EN || - pMsg->m_SoundID == SOUND_CTF_GRAB_PL) + if(pMsg->m_SoundId == SOUND_CTF_DROP || pMsg->m_SoundId == SOUND_CTF_RETURN || + pMsg->m_SoundId == SOUND_CTF_CAPTURE || pMsg->m_SoundId == SOUND_CTF_GRAB_EN || + pMsg->m_SoundId == SOUND_CTF_GRAB_PL) { if(g_Config.m_SndGame) - m_Sounds.Enqueue(CSounds::CHN_GLOBAL, pMsg->m_SoundID); + m_Sounds.Enqueue(CSounds::CHN_GLOBAL, pMsg->m_SoundId); } else { if(g_Config.m_SndGame) - m_Sounds.Play(CSounds::CHN_GLOBAL, pMsg->m_SoundID, 1.0f); + m_Sounds.Play(CSounds::CHN_GLOBAL, pMsg->m_SoundId, 1.0f); } } else if(MsgId == NETMSGTYPE_SV_TEAMSSTATE || MsgId == NETMSGTYPE_SV_TEAMSSTATELEGACY) @@ -950,13 +1037,14 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm if(!(m_GameWorld.m_WorldConfig.m_IsFNG && pMsg->m_Weapon == WEAPON_LASER)) { m_CharOrder.GiveWeak(pMsg->m_Victim); - if(CCharacter *pChar = m_GameWorld.GetCharacterByID(pMsg->m_Victim)) + if(CCharacter *pChar = m_GameWorld.GetCharacterById(pMsg->m_Victim)) pChar->ResetPrediction(); m_GameWorld.ReleaseHooked(pMsg->m_Victim); } // if we are spectating a static id set (team 0) and somebody killed, and its not a guy in solo, we remove him from the list - if(IsMultiViewIdSet() && m_MultiViewTeam == 0 && m_aMultiViewId[pMsg->m_Victim] && !m_aClients[pMsg->m_Victim].m_Spec && !m_MultiView.m_Solo) + // never remove players from the list if it is a pvp server + if(IsMultiViewIdSet() && m_MultiViewTeam == 0 && m_aMultiViewId[pMsg->m_Victim] && !m_aClients[pMsg->m_Victim].m_Spec && !m_MultiView.m_Solo && !m_GameInfo.m_Pvp) { m_aMultiViewId[pMsg->m_Victim] = false; @@ -966,14 +1054,14 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm else { // the "main" tee killed, search a new one - if(m_Snap.m_SpecInfo.m_SpectatorID == pMsg->m_Victim) + if(m_Snap.m_SpecInfo.m_SpectatorId == pMsg->m_Victim) { - int NewClientID = FindFirstMultiViewId(); - if(NewClientID < MAX_CLIENTS && NewClientID >= 0) + int NewClientId = FindFirstMultiViewId(); + if(NewClientId < MAX_CLIENTS && NewClientId >= 0) { - CleanMultiViewId(NewClientID); - m_aMultiViewId[NewClientID] = true; - m_Spectator.Spectate(NewClientID); + CleanMultiViewId(NewClientId); + m_aMultiViewId[NewClientId] = true; + m_Spectator.Spectate(NewClientId); } } } @@ -989,20 +1077,36 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm { if(m_Teams.Team(i) == pMsg->m_Team) { - if(CCharacter *pChar = m_GameWorld.GetCharacterByID(i)) + if(CCharacter *pChar = m_GameWorld.GetCharacterById(i)) { pChar->ResetPrediction(); - vStrongWeakSorted.emplace_back(i, pMsg->m_First == i ? MAX_CLIENTS : pChar ? pChar->GetStrongWeakID() : 0); + vStrongWeakSorted.emplace_back(i, pMsg->m_First == i ? MAX_CLIENTS : pChar ? pChar->GetStrongWeakId() : 0); } m_GameWorld.ReleaseHooked(i); } } std::stable_sort(vStrongWeakSorted.begin(), vStrongWeakSorted.end(), [](auto &Left, auto &Right) { return Left.second > Right.second; }); - for(auto ID : vStrongWeakSorted) + for(auto Id : vStrongWeakSorted) { - m_CharOrder.GiveWeak(ID.first); + m_CharOrder.GiveWeak(Id.first); } } + else if(MsgId == NETMSGTYPE_SV_CHANGEINFOCOOLDOWN) + { + CNetMsg_Sv_ChangeInfoCooldown *pMsg = (CNetMsg_Sv_ChangeInfoCooldown *)pRawMsg; + m_NextChangeInfo = pMsg->m_WaitUntil; + } + else if(MsgId == NETMSGTYPE_SV_MAPSOUNDGLOBAL) + { + if(m_SuppressEvents) + return; + + if(!g_Config.m_SndGame) + return; + + CNetMsg_Sv_MapSoundGlobal *pMsg = (CNetMsg_Sv_MapSoundGlobal *)pRawMsg; + m_MapSounds.Play(CSounds::CHN_GLOBAL, pMsg->m_SoundId); + } } void CGameClient::OnStateChange(int NewState, int OldState) @@ -1018,15 +1122,12 @@ void CGameClient::OnStateChange(int NewState, int OldState) void CGameClient::OnShutdown() { - RenderShutdownMessage(); - for(auto &pComponent : m_vpAll) pComponent->OnShutdown(); } void CGameClient::OnEnterGame() { - m_Effects.ResetDamageIndicator(); } void CGameClient::OnGameOver() @@ -1053,9 +1154,9 @@ void CGameClient::OnStartRound() m_RaceDemo.OnReset(); } -void CGameClient::OnFlagGrab(int TeamID) +void CGameClient::OnFlagGrab(int TeamId) { - if(TeamID == TEAM_RED) + if(TeamId == TEAM_RED) m_aStats[m_Snap.m_pGameDataObj->m_FlagCarrierRed].m_FlagGrabs++; else m_aStats[m_Snap.m_pGameDataObj->m_FlagCarrierBlue].m_FlagGrabs++; @@ -1066,7 +1167,7 @@ void CGameClient::OnWindowResize() for(auto &pComponent : m_vpAll) pComponent->OnWindowResize(); - UI()->OnWindowResize(); + Ui()->OnWindowResize(); } void CGameClient::OnLanguageChange() @@ -1093,20 +1194,8 @@ void CGameClient::HandleLanguageChanged() void CGameClient::RenderShutdownMessage() { const char *pMessage = nullptr; - if(Client()->State() == IClient::STATE_QUITTING) - { - // Randomly decide whether to display "Goodbye..." (10% chance) - if(rand() % 20 == 0) - { - pMessage = "Hope -StormAx gonna kill himself soon…"; - } - else - { - pMessage = Localize("Quitting. Please wait…"); - } - } - + pMessage = Localize("Quitting. Please wait…"); else if(Client()->State() == IClient::STATE_RESTARTING) pMessage = Localize("Restarting. Please wait…"); else @@ -1114,9 +1203,9 @@ void CGameClient::RenderShutdownMessage() // This function only gets called after the render loop has already terminated, so we have to call Swap manually. Graphics()->Clear(0.0f, 0.0f, 0.0f); - UI()->MapScreen(); + Ui()->MapScreen(); TextRender()->TextColor(TextRender()->DefaultTextColor()); - UI()->DoLabel(UI()->Screen(), pMessage, 16.0f, TEXTALIGN_MC); + Ui()->DoLabel(Ui()->Screen(), pMessage, 16.0f, TEXTALIGN_MC); Graphics()->Swap(); Graphics()->Clear(0.0f, 0.0f, 0.0f); } @@ -1140,52 +1229,69 @@ void CGameClient::ProcessEvents() int Num = Client()->SnapNumItems(SnapType); for(int Index = 0; Index < Num; Index++) { - IClient::CSnapItem Item; - const void *pData = Client()->SnapGetItem(SnapType, Index, &Item); + const IClient::CSnapItem Item = Client()->SnapGetItem(SnapType, Index); // We don't have enough info about us, others, to know a correct alpha value. float Alpha = 1.0f; if(Item.m_Type == NETEVENTTYPE_DAMAGEIND) { - CNetEvent_DamageInd *pEvent = (CNetEvent_DamageInd *)pData; + const CNetEvent_DamageInd *pEvent = (const CNetEvent_DamageInd *)Item.m_pData; m_Effects.DamageIndicator(vec2(pEvent->m_X, pEvent->m_Y), direction(pEvent->m_Angle / 256.0f), Alpha); } else if(Item.m_Type == NETEVENTTYPE_EXPLOSION) { - CNetEvent_Explosion *pEvent = (CNetEvent_Explosion *)pData; + const CNetEvent_Explosion *pEvent = (const CNetEvent_Explosion *)Item.m_pData; m_Effects.Explosion(vec2(pEvent->m_X, pEvent->m_Y), Alpha); } else if(Item.m_Type == NETEVENTTYPE_HAMMERHIT) { - CNetEvent_HammerHit *pEvent = (CNetEvent_HammerHit *)pData; + const CNetEvent_HammerHit *pEvent = (const CNetEvent_HammerHit *)Item.m_pData; m_Effects.HammerHit(vec2(pEvent->m_X, pEvent->m_Y), Alpha); } + else if(Item.m_Type == NETEVENTTYPE_BIRTHDAY) + { + const CNetEvent_Birthday *pEvent = (const CNetEvent_Birthday *)Item.m_pData; + m_Effects.Confetti(vec2(pEvent->m_X, pEvent->m_Y), Alpha); + } + else if(Item.m_Type == NETEVENTTYPE_FINISH) + { + const CNetEvent_Finish *pEvent = (const CNetEvent_Finish *)Item.m_pData; + m_Effects.Confetti(vec2(pEvent->m_X, pEvent->m_Y), Alpha); + } else if(Item.m_Type == NETEVENTTYPE_SPAWN) { - CNetEvent_Spawn *pEvent = (CNetEvent_Spawn *)pData; + const CNetEvent_Spawn *pEvent = (const CNetEvent_Spawn *)Item.m_pData; m_Effects.PlayerSpawn(vec2(pEvent->m_X, pEvent->m_Y), Alpha); } else if(Item.m_Type == NETEVENTTYPE_DEATH) { - CNetEvent_Death *pEvent = (CNetEvent_Death *)pData; - m_Effects.PlayerDeath(vec2(pEvent->m_X, pEvent->m_Y), pEvent->m_ClientID, Alpha); + const CNetEvent_Death *pEvent = (const CNetEvent_Death *)Item.m_pData; + m_Effects.PlayerDeath(vec2(pEvent->m_X, pEvent->m_Y), pEvent->m_ClientId, Alpha); } else if(Item.m_Type == NETEVENTTYPE_SOUNDWORLD) { - CNetEvent_SoundWorld *pEvent = (CNetEvent_SoundWorld *)pData; + const CNetEvent_SoundWorld *pEvent = (const CNetEvent_SoundWorld *)Item.m_pData; if(!Config()->m_SndGame) continue; - if(m_GameInfo.m_RaceSounds && ((pEvent->m_SoundID == SOUND_GUN_FIRE && !g_Config.m_SndGun) || (pEvent->m_SoundID == SOUND_PLAYER_PAIN_LONG && !g_Config.m_SndLongPain))) + if(m_GameInfo.m_RaceSounds && ((pEvent->m_SoundId == SOUND_GUN_FIRE && !g_Config.m_SndGun) || (pEvent->m_SoundId == SOUND_PLAYER_PAIN_LONG && !g_Config.m_SndLongPain))) + continue; + + m_Sounds.PlayAt(CSounds::CHN_WORLD, pEvent->m_SoundId, 1.0f, vec2(pEvent->m_X, pEvent->m_Y)); + } + else if(Item.m_Type == NETEVENTTYPE_MAPSOUNDWORLD) + { + CNetEvent_MapSoundWorld *pEvent = (CNetEvent_MapSoundWorld *)Item.m_pData; + if(!Config()->m_SndGame) continue; - m_Sounds.PlayAt(CSounds::CHN_WORLD, pEvent->m_SoundID, 1.0f, vec2(pEvent->m_X, pEvent->m_Y)); + m_MapSounds.PlayAt(CSounds::CHN_WORLD, pEvent->m_SoundId, vec2(pEvent->m_X, pEvent->m_Y)); } } } -static CGameInfo GetGameInfo(const CNetObj_GameInfoEx *pInfoEx, int InfoExSize, CServerInfo *pFallbackServerInfo) +static CGameInfo GetGameInfo(const CNetObj_GameInfoEx *pInfoEx, int InfoExSize, const CServerInfo *pFallbackServerInfo) { int Version = -1; if(InfoExSize >= 12) @@ -1277,6 +1383,7 @@ static CGameInfo GetGameInfo(const CNetObj_GameInfoEx *pInfoEx, int InfoExSize, Info.m_EntitiesVanilla = Vanilla; Info.m_EntitiesBW = BlockWorlds; Info.m_Race = Race; + Info.m_Pvp = !Race; Info.m_DontMaskEntities = !DDNet; Info.m_AllowXSkins = false; Info.m_EntitiesFDDrace = FDDrace; @@ -1352,7 +1459,7 @@ void CGameClient::InvalidateSnapshot() { // clear all pointers mem_zero(&m_Snap, sizeof(m_Snap)); - m_Snap.m_LocalClientID = -1; + m_Snap.m_LocalClientId = -1; SnapCollectEntities(); } @@ -1394,14 +1501,14 @@ void CGameClient::OnNewSnapshot() aMessage[i] = (char)('a' + (rand() % ('z' - 'a'))); aMessage[MsgLen] = 0; - CNetMsg_Cl_Say Msg; - Msg.m_Team = rand() & 1; - Msg.m_pMessage = aMessage; - Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); + m_Chat.SendChat(rand() & 1, aMessage); } } #endif + CServerInfo ServerInfo; + Client()->GetServerInfo(&ServerInfo); + bool FoundGameInfoEx = false; bool GotSwitchStateTeam = false; m_aSwitchStateTeam[g_Config.m_ClDummy] = -1; @@ -1418,21 +1525,23 @@ void CGameClient::OnNewSnapshot() int Num = Client()->SnapNumItems(IClient::SNAP_CURRENT); for(int i = 0; i < Num; i++) { - IClient::CSnapItem Item; - const void *pData = Client()->SnapGetItem(IClient::SNAP_CURRENT, i, &Item); + const IClient::CSnapItem Item = Client()->SnapGetItem(IClient::SNAP_CURRENT, i); if(Item.m_Type == NETOBJTYPE_CLIENTINFO) { - const CNetObj_ClientInfo *pInfo = (const CNetObj_ClientInfo *)pData; - int ClientID = Item.m_ID; - if(ClientID < MAX_CLIENTS) + const CNetObj_ClientInfo *pInfo = (const CNetObj_ClientInfo *)Item.m_pData; + int ClientId = Item.m_Id; + if(ClientId < MAX_CLIENTS) { - CClientData *pClient = &m_aClients[ClientID]; + CClientData *pClient = &m_aClients[ClientId]; - IntsToStr(&pInfo->m_Name0, 4, pClient->m_aName); - IntsToStr(&pInfo->m_Clan0, 3, pClient->m_aClan); + if(!IntsToStr(&pInfo->m_Name0, 4, pClient->m_aName, std::size(pClient->m_aName))) + { + str_copy(pClient->m_aName, "nameless tee"); + } + IntsToStr(&pInfo->m_Clan0, 3, pClient->m_aClan, std::size(pClient->m_aClan)); pClient->m_Country = pInfo->m_Country; - IntsToStr(&pInfo->m_Skin0, 6, pClient->m_aSkinName); + IntsToStr(&pInfo->m_Skin0, 6, pClient->m_aSkinName, std::size(pClient->m_aSkinName)); pClient->m_UseCustomColor = pInfo->m_UseCustomColor; pClient->m_ColorBody = pInfo->m_ColorBody; @@ -1442,16 +1551,12 @@ void CGameClient::OnNewSnapshot() if(!m_GameInfo.m_AllowXSkins && (pClient->m_aSkinName[0] == 'x' && pClient->m_aSkinName[1] == '_')) str_copy(pClient->m_aSkinName, "default"); - pClient->m_SkinInfo.m_ColorBody = color_cast(ColorHSLA(pClient->m_ColorBody).UnclampLighting()); - pClient->m_SkinInfo.m_ColorFeet = color_cast(ColorHSLA(pClient->m_ColorFeet).UnclampLighting()); + pClient->m_SkinInfo.m_ColorBody = color_cast(ColorHSLA(pClient->m_ColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT)); + pClient->m_SkinInfo.m_ColorFeet = color_cast(ColorHSLA(pClient->m_ColorFeet).UnclampLighting(ColorHSLA::DARKEST_LGT)); pClient->m_SkinInfo.m_Size = 64; // find new skin - const CSkin *pSkin = m_Skins.Find(pClient->m_aSkinName); - pClient->m_SkinInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin; - pClient->m_SkinInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin; - pClient->m_SkinInfo.m_SkinMetrics = pSkin->m_Metrics; - pClient->m_SkinInfo.m_BloodColor = pSkin->m_BloodColor; + pClient->m_SkinInfo.Apply(m_Skins.Find(pClient->m_aSkinName)); pClient->m_SkinInfo.m_CustomColoredSkin = pClient->m_UseCustomColor; if(!pClient->m_UseCustomColor) @@ -1465,18 +1570,18 @@ void CGameClient::OnNewSnapshot() } else if(Item.m_Type == NETOBJTYPE_PLAYERINFO) { - const CNetObj_PlayerInfo *pInfo = (const CNetObj_PlayerInfo *)pData; + const CNetObj_PlayerInfo *pInfo = (const CNetObj_PlayerInfo *)Item.m_pData; - if(pInfo->m_ClientID < MAX_CLIENTS && pInfo->m_ClientID == Item.m_ID) + if(pInfo->m_ClientId < MAX_CLIENTS && pInfo->m_ClientId == Item.m_Id) { - m_aClients[pInfo->m_ClientID].m_Team = pInfo->m_Team; - m_aClients[pInfo->m_ClientID].m_Active = true; - m_Snap.m_apPlayerInfos[pInfo->m_ClientID] = pInfo; + m_aClients[pInfo->m_ClientId].m_Team = pInfo->m_Team; + m_aClients[pInfo->m_ClientId].m_Active = true; + m_Snap.m_apPlayerInfos[pInfo->m_ClientId] = pInfo; m_Snap.m_NumPlayers++; if(pInfo->m_Local) { - m_Snap.m_LocalClientID = pInfo->m_ClientID; + m_Snap.m_LocalClientId = pInfo->m_ClientId; m_Snap.m_pLocalInfo = pInfo; if(pInfo->m_Team == TEAM_SPECTATORS) @@ -1485,29 +1590,31 @@ void CGameClient::OnNewSnapshot() } } + m_Snap.m_HighestClientId = maximum(m_Snap.m_HighestClientId, pInfo->m_ClientId); + // calculate team-balance if(pInfo->m_Team != TEAM_SPECTATORS) { m_Snap.m_aTeamSize[pInfo->m_Team]++; - if(!m_aStats[pInfo->m_ClientID].IsActive()) - m_aStats[pInfo->m_ClientID].JoinGame(Client()->GameTick(g_Config.m_ClDummy)); + if(!m_aStats[pInfo->m_ClientId].IsActive()) + m_aStats[pInfo->m_ClientId].JoinGame(Client()->GameTick(g_Config.m_ClDummy)); } - else if(m_aStats[pInfo->m_ClientID].IsActive()) - m_aStats[pInfo->m_ClientID].JoinSpec(Client()->GameTick(g_Config.m_ClDummy)); + else if(m_aStats[pInfo->m_ClientId].IsActive()) + m_aStats[pInfo->m_ClientId].JoinSpec(Client()->GameTick(g_Config.m_ClDummy)); } } else if(Item.m_Type == NETOBJTYPE_DDNETPLAYER) { m_ReceivedDDNetPlayer = true; - const CNetObj_DDNetPlayer *pInfo = (const CNetObj_DDNetPlayer *)pData; - if(Item.m_ID < MAX_CLIENTS) + const CNetObj_DDNetPlayer *pInfo = (const CNetObj_DDNetPlayer *)Item.m_pData; + if(Item.m_Id < MAX_CLIENTS) { - m_aClients[Item.m_ID].m_AuthLevel = pInfo->m_AuthLevel; - m_aClients[Item.m_ID].m_Afk = pInfo->m_Flags & EXPLAYERFLAG_AFK; - m_aClients[Item.m_ID].m_Paused = pInfo->m_Flags & EXPLAYERFLAG_PAUSED; - m_aClients[Item.m_ID].m_Spec = pInfo->m_Flags & EXPLAYERFLAG_SPEC; + m_aClients[Item.m_Id].m_AuthLevel = pInfo->m_AuthLevel; + m_aClients[Item.m_Id].m_Afk = pInfo->m_Flags & EXPLAYERFLAG_AFK; + m_aClients[Item.m_Id].m_Paused = pInfo->m_Flags & EXPLAYERFLAG_PAUSED; + m_aClients[Item.m_Id].m_Spec = pInfo->m_Flags & EXPLAYERFLAG_SPEC; - if(Item.m_ID == m_Snap.m_LocalClientID && (m_aClients[Item.m_ID].m_Paused || m_aClients[Item.m_ID].m_Spec)) + if(Item.m_Id == m_Snap.m_LocalClientId && (m_aClients[Item.m_Id].m_Paused || m_aClients[Item.m_Id].m_Spec)) { m_Snap.m_SpecInfo.m_Active = true; } @@ -1515,57 +1622,57 @@ void CGameClient::OnNewSnapshot() } else if(Item.m_Type == NETOBJTYPE_CHARACTER) { - if(Item.m_ID < MAX_CLIENTS) + if(Item.m_Id < MAX_CLIENTS) { - const void *pOld = Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_CHARACTER, Item.m_ID); - m_Snap.m_aCharacters[Item.m_ID].m_Cur = *((const CNetObj_Character *)pData); + const void *pOld = Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_CHARACTER, Item.m_Id); + m_Snap.m_aCharacters[Item.m_Id].m_Cur = *((const CNetObj_Character *)Item.m_pData); if(pOld) { - m_Snap.m_aCharacters[Item.m_ID].m_Active = true; - m_Snap.m_aCharacters[Item.m_ID].m_Prev = *((const CNetObj_Character *)pOld); + m_Snap.m_aCharacters[Item.m_Id].m_Active = true; + m_Snap.m_aCharacters[Item.m_Id].m_Prev = *((const CNetObj_Character *)pOld); // limit evolving to 3 seconds - bool EvolvePrev = Client()->PrevGameTick(g_Config.m_ClDummy) - m_Snap.m_aCharacters[Item.m_ID].m_Prev.m_Tick <= 3 * Client()->GameTickSpeed(); - bool EvolveCur = Client()->GameTick(g_Config.m_ClDummy) - m_Snap.m_aCharacters[Item.m_ID].m_Cur.m_Tick <= 3 * Client()->GameTickSpeed(); + bool EvolvePrev = Client()->PrevGameTick(g_Config.m_ClDummy) - m_Snap.m_aCharacters[Item.m_Id].m_Prev.m_Tick <= 3 * Client()->GameTickSpeed(); + bool EvolveCur = Client()->GameTick(g_Config.m_ClDummy) - m_Snap.m_aCharacters[Item.m_Id].m_Cur.m_Tick <= 3 * Client()->GameTickSpeed(); // reuse the result from the previous evolve if the snapped character didn't change since the previous snapshot - if(EvolveCur && m_aClients[Item.m_ID].m_Evolved.m_Tick == Client()->PrevGameTick(g_Config.m_ClDummy)) + if(EvolveCur && m_aClients[Item.m_Id].m_Evolved.m_Tick == Client()->PrevGameTick(g_Config.m_ClDummy)) { - if(mem_comp(&m_Snap.m_aCharacters[Item.m_ID].m_Prev, &m_aClients[Item.m_ID].m_Snapped, sizeof(CNetObj_Character)) == 0) - m_Snap.m_aCharacters[Item.m_ID].m_Prev = m_aClients[Item.m_ID].m_Evolved; - if(mem_comp(&m_Snap.m_aCharacters[Item.m_ID].m_Cur, &m_aClients[Item.m_ID].m_Snapped, sizeof(CNetObj_Character)) == 0) - m_Snap.m_aCharacters[Item.m_ID].m_Cur = m_aClients[Item.m_ID].m_Evolved; + if(mem_comp(&m_Snap.m_aCharacters[Item.m_Id].m_Prev, &m_aClients[Item.m_Id].m_Snapped, sizeof(CNetObj_Character)) == 0) + m_Snap.m_aCharacters[Item.m_Id].m_Prev = m_aClients[Item.m_Id].m_Evolved; + if(mem_comp(&m_Snap.m_aCharacters[Item.m_Id].m_Cur, &m_aClients[Item.m_Id].m_Snapped, sizeof(CNetObj_Character)) == 0) + m_Snap.m_aCharacters[Item.m_Id].m_Cur = m_aClients[Item.m_Id].m_Evolved; } - if(EvolvePrev && m_Snap.m_aCharacters[Item.m_ID].m_Prev.m_Tick) - Evolve(&m_Snap.m_aCharacters[Item.m_ID].m_Prev, Client()->PrevGameTick(g_Config.m_ClDummy)); - if(EvolveCur && m_Snap.m_aCharacters[Item.m_ID].m_Cur.m_Tick) - Evolve(&m_Snap.m_aCharacters[Item.m_ID].m_Cur, Client()->GameTick(g_Config.m_ClDummy)); + if(EvolvePrev && m_Snap.m_aCharacters[Item.m_Id].m_Prev.m_Tick) + Evolve(&m_Snap.m_aCharacters[Item.m_Id].m_Prev, Client()->PrevGameTick(g_Config.m_ClDummy)); + if(EvolveCur && m_Snap.m_aCharacters[Item.m_Id].m_Cur.m_Tick) + Evolve(&m_Snap.m_aCharacters[Item.m_Id].m_Cur, Client()->GameTick(g_Config.m_ClDummy)); - m_aClients[Item.m_ID].m_Snapped = *((const CNetObj_Character *)pData); - m_aClients[Item.m_ID].m_Evolved = m_Snap.m_aCharacters[Item.m_ID].m_Cur; + m_aClients[Item.m_Id].m_Snapped = *((const CNetObj_Character *)Item.m_pData); + m_aClients[Item.m_Id].m_Evolved = m_Snap.m_aCharacters[Item.m_Id].m_Cur; } else { - m_aClients[Item.m_ID].m_Evolved.m_Tick = -1; + m_aClients[Item.m_Id].m_Evolved.m_Tick = -1; } } } else if(Item.m_Type == NETOBJTYPE_DDNETCHARACTER) { - const CNetObj_DDNetCharacter *pCharacterData = (const CNetObj_DDNetCharacter *)pData; + const CNetObj_DDNetCharacter *pCharacterData = (const CNetObj_DDNetCharacter *)Item.m_pData; - if(Item.m_ID < MAX_CLIENTS) + if(Item.m_Id < MAX_CLIENTS) { - m_Snap.m_aCharacters[Item.m_ID].m_ExtendedData = *pCharacterData; - m_Snap.m_aCharacters[Item.m_ID].m_PrevExtendedData = (const CNetObj_DDNetCharacter *)Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_DDNETCHARACTER, Item.m_ID); - m_Snap.m_aCharacters[Item.m_ID].m_HasExtendedData = true; - m_Snap.m_aCharacters[Item.m_ID].m_HasExtendedDisplayInfo = false; + m_Snap.m_aCharacters[Item.m_Id].m_ExtendedData = *pCharacterData; + m_Snap.m_aCharacters[Item.m_Id].m_PrevExtendedData = (const CNetObj_DDNetCharacter *)Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_DDNETCHARACTER, Item.m_Id); + m_Snap.m_aCharacters[Item.m_Id].m_HasExtendedData = true; + m_Snap.m_aCharacters[Item.m_Id].m_HasExtendedDisplayInfo = false; if(pCharacterData->m_JumpedTotal != -1) { - m_Snap.m_aCharacters[Item.m_ID].m_HasExtendedDisplayInfo = true; + m_Snap.m_aCharacters[Item.m_Id].m_HasExtendedDisplayInfo = true; } - CClientData *pClient = &m_aClients[Item.m_ID]; + CClientData *pClient = &m_aClients[Item.m_Id]; // Collision pClient->m_Solo = pCharacterData->m_Flags & CHARACTERFLAG_SOLO; pClient->m_Jetpack = pCharacterData->m_Flags & CHARACTERFLAG_JETPACK; @@ -1593,16 +1700,16 @@ void CGameClient::OnNewSnapshot() pClient->m_Predicted.ReadDDNet(pCharacterData); - m_Teams.SetSolo(Item.m_ID, pClient->m_Solo); + m_Teams.SetSolo(Item.m_Id, pClient->m_Solo); } } else if(Item.m_Type == NETOBJTYPE_SPECCHAR) { - const CNetObj_SpecChar *pSpecCharData = (const CNetObj_SpecChar *)pData; + const CNetObj_SpecChar *pSpecCharData = (const CNetObj_SpecChar *)Item.m_pData; - if(Item.m_ID < MAX_CLIENTS) + if(Item.m_Id < MAX_CLIENTS) { - CClientData *pClient = &m_aClients[Item.m_ID]; + CClientData *pClient = &m_aClients[Item.m_Id]; pClient->m_SpecCharPresent = true; pClient->m_SpecChar.x = pSpecCharData->m_X; pClient->m_SpecChar.y = pSpecCharData->m_Y; @@ -1610,14 +1717,18 @@ void CGameClient::OnNewSnapshot() } else if(Item.m_Type == NETOBJTYPE_SPECTATORINFO) { - m_Snap.m_pSpectatorInfo = (const CNetObj_SpectatorInfo *)pData; - m_Snap.m_pPrevSpectatorInfo = (const CNetObj_SpectatorInfo *)Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_SPECTATORINFO, Item.m_ID); - - m_Snap.m_SpecInfo.m_SpectatorID = m_Snap.m_pSpectatorInfo->m_SpectatorID; + m_Snap.m_pSpectatorInfo = (const CNetObj_SpectatorInfo *)Item.m_pData; + m_Snap.m_pPrevSpectatorInfo = (const CNetObj_SpectatorInfo *)Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_SPECTATORINFO, Item.m_Id); + + // needed for 0.7 survival + // to auto spec players when dead + if(Client()->IsSixup()) + m_Snap.m_SpecInfo.m_Active = true; + m_Snap.m_SpecInfo.m_SpectatorId = m_Snap.m_pSpectatorInfo->m_SpectatorId; } else if(Item.m_Type == NETOBJTYPE_GAMEINFO) { - m_Snap.m_pGameInfoObj = (const CNetObj_GameInfo *)pData; + m_Snap.m_pGameInfoObj = (const CNetObj_GameInfo *)Item.m_pData; bool CurrentTickGameOver = (bool)(m_Snap.m_pGameInfoObj->m_GameStateFlags & GAMESTATEFLAG_GAMEOVER); if(!m_GameOver && CurrentTickGameOver) OnGameOver(); @@ -1638,14 +1749,12 @@ void CGameClient::OnNewSnapshot() continue; } FoundGameInfoEx = true; - CServerInfo ServerInfo; - Client()->GetServerInfo(&ServerInfo); - m_GameInfo = GetGameInfo((const CNetObj_GameInfoEx *)pData, Client()->SnapItemSize(IClient::SNAP_CURRENT, i), &ServerInfo); + m_GameInfo = GetGameInfo((const CNetObj_GameInfoEx *)Item.m_pData, Item.m_DataSize, &ServerInfo); } else if(Item.m_Type == NETOBJTYPE_GAMEDATA) { - m_Snap.m_pGameDataObj = (const CNetObj_GameData *)pData; - m_Snap.m_GameDataSnapID = Item.m_ID; + m_Snap.m_pGameDataObj = (const CNetObj_GameData *)Item.m_pData; + m_Snap.m_GameDataSnapId = Item.m_Id; if(m_Snap.m_pGameDataObj->m_FlagCarrierRed == FLAG_TAKEN) { if(m_aFlagDropTick[TEAM_RED] == 0) @@ -1669,15 +1778,15 @@ void CGameClient::OnNewSnapshot() m_LastFlagCarrierBlue = m_Snap.m_pGameDataObj->m_FlagCarrierBlue; } else if(Item.m_Type == NETOBJTYPE_FLAG) - m_Snap.m_apFlags[Item.m_ID % 2] = (const CNetObj_Flag *)pData; + m_Snap.m_apFlags[Item.m_Id % 2] = (const CNetObj_Flag *)Item.m_pData; else if(Item.m_Type == NETOBJTYPE_SWITCHSTATE) { if(Item.m_DataSize < 36) { continue; } - const CNetObj_SwitchState *pSwitchStateData = (const CNetObj_SwitchState *)pData; - int Team = clamp(Item.m_ID, (int)TEAM_FLOCK, (int)TEAM_SUPER - 1); + const CNetObj_SwitchState *pSwitchStateData = (const CNetObj_SwitchState *)Item.m_pData; + int Team = clamp(Item.m_Id, (int)TEAM_FLOCK, (int)TEAM_SUPER - 1); int HighestSwitchNumber = clamp(pSwitchStateData->m_HighestSwitchNumber, 0, 255); if(HighestSwitchNumber != maximum(0, (int)Switchers().size() - 1)) @@ -1725,17 +1834,15 @@ void CGameClient::OnNewSnapshot() if(!FoundGameInfoEx) { - CServerInfo ServerInfo; - Client()->GetServerInfo(&ServerInfo); m_GameInfo = GetGameInfo(0, 0, &ServerInfo); } // setup local pointers - if(m_Snap.m_LocalClientID >= 0) + if(m_Snap.m_LocalClientId >= 0) { - m_aLocalIDs[g_Config.m_ClDummy] = m_Snap.m_LocalClientID; + m_aLocalIds[g_Config.m_ClDummy] = m_Snap.m_LocalClientId; - CSnapState::CCharacterInfo *pChr = &m_Snap.m_aCharacters[m_Snap.m_LocalClientID]; + CSnapState::CCharacterInfo *pChr = &m_Snap.m_aCharacters[m_Snap.m_LocalClientId]; if(pChr->m_Active) { if(!m_Snap.m_SpecInfo.m_Active) @@ -1745,7 +1852,7 @@ void CGameClient::OnNewSnapshot() m_LocalCharacterPos = vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y); } } - else if(Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_CHARACTER, m_Snap.m_LocalClientID)) + else if(Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_CHARACTER, m_Snap.m_LocalClientId)) { // player died m_Controls.OnPlayerDeath(); @@ -1753,15 +1860,19 @@ void CGameClient::OnNewSnapshot() } if(Client()->State() == IClient::STATE_DEMOPLAYBACK) { - if(m_Snap.m_LocalClientID == -1 && m_DemoSpecID == SPEC_FOLLOW) - m_DemoSpecID = SPEC_FREEVIEW; - if(m_DemoSpecID != SPEC_FOLLOW) + if(m_Snap.m_LocalClientId == -1 && m_DemoSpecId == SPEC_FOLLOW) + { + // TODO: can this be done in the translation layer? + if(!Client()->IsSixup()) + m_DemoSpecId = SPEC_FREEVIEW; + } + if(m_DemoSpecId != SPEC_FOLLOW) { m_Snap.m_SpecInfo.m_Active = true; - if(m_DemoSpecID > SPEC_FREEVIEW && m_Snap.m_aCharacters[m_DemoSpecID].m_Active) - m_Snap.m_SpecInfo.m_SpectatorID = m_DemoSpecID; + if(m_DemoSpecId > SPEC_FREEVIEW && m_Snap.m_aCharacters[m_DemoSpecId].m_Active) + m_Snap.m_SpecInfo.m_SpectatorId = m_DemoSpecId; else - m_Snap.m_SpecInfo.m_SpectatorID = SPEC_FREEVIEW; + m_Snap.m_SpecInfo.m_SpectatorId = SPEC_FREEVIEW; } } @@ -1778,10 +1889,10 @@ void CGameClient::OnNewSnapshot() for(int i = 0; i < MAX_CLIENTS; ++i) { // update friend state - m_aClients[i].m_Friend = !(i == m_Snap.m_LocalClientID || !m_Snap.m_apPlayerInfos[i] || !Friends()->IsFriend(m_aClients[i].m_aName, m_aClients[i].m_aClan, true)); + m_aClients[i].m_Friend = !(i == m_Snap.m_LocalClientId || !m_Snap.m_apPlayerInfos[i] || !Friends()->IsFriend(m_aClients[i].m_aName, m_aClients[i].m_aClan, true)); // update foe state - m_aClients[i].m_Foe = !(i == m_Snap.m_LocalClientID || !m_Snap.m_apPlayerInfos[i] || !Foes()->IsFriend(m_aClients[i].m_aName, m_aClients[i].m_aClan, true)); + m_aClients[i].m_Foe = !(i == m_Snap.m_LocalClientId || !m_Snap.m_apPlayerInfos[i] || !Foes()->IsFriend(m_aClients[i].m_aName, m_aClients[i].m_aClan, true)); } // sort player infos by name @@ -1792,22 +1903,34 @@ void CGameClient::OnNewSnapshot() return static_cast(p1); if(!p1) return false; - return str_comp_nocase(m_aClients[p1->m_ClientID].m_aName, m_aClients[p2->m_ClientID].m_aName) < 0; + return str_comp_nocase(m_aClients[p1->m_ClientId].m_aName, m_aClients[p2->m_ClientId].m_aName) < 0; }); bool TimeScore = m_GameInfo.m_TimeScore; + bool Race7 = Client()->IsSixup() && m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameFlags & protocol7::GAMEFLAG_RACE; // sort player infos by score mem_copy(m_Snap.m_apInfoByScore, m_Snap.m_apInfoByName, sizeof(m_Snap.m_apInfoByScore)); - std::stable_sort(m_Snap.m_apInfoByScore, m_Snap.m_apInfoByScore + MAX_CLIENTS, - [TimeScore](const CNetObj_PlayerInfo *p1, const CNetObj_PlayerInfo *p2) -> bool { - if(!p2) - return static_cast(p1); - if(!p1) - return false; - return (((TimeScore && p1->m_Score == -9999) ? std::numeric_limits::min() : p1->m_Score) > - ((TimeScore && p2->m_Score == -9999) ? std::numeric_limits::min() : p2->m_Score)); - }); + if(Race7) + std::stable_sort(m_Snap.m_apInfoByScore, m_Snap.m_apInfoByScore + MAX_CLIENTS, + [](const CNetObj_PlayerInfo *p1, const CNetObj_PlayerInfo *p2) -> bool { + if(!p2) + return static_cast(p1); + if(!p1) + return false; + return (((p1->m_Score == -1) ? std::numeric_limits::max() : p1->m_Score) < + ((p2->m_Score == -1) ? std::numeric_limits::max() : p2->m_Score)); + }); + else + std::stable_sort(m_Snap.m_apInfoByScore, m_Snap.m_apInfoByScore + MAX_CLIENTS, + [TimeScore](const CNetObj_PlayerInfo *p1, const CNetObj_PlayerInfo *p2) -> bool { + if(!p2) + return static_cast(p1); + if(!p1) + return false; + return (((TimeScore && p1->m_Score == -9999) ? std::numeric_limits::min() : p1->m_Score) > + ((TimeScore && p2->m_Score == -9999) ? std::numeric_limits::min() : p2->m_Score)); + }); // sort player infos by DDRace Team (and score between) int Index = 0; @@ -1815,7 +1938,7 @@ void CGameClient::OnNewSnapshot() { for(int i = 0; i < MAX_CLIENTS && Index < MAX_CLIENTS; ++i) { - if(m_Snap.m_apInfoByScore[i] && m_Teams.Team(m_Snap.m_apInfoByScore[i]->m_ClientID) == Team) + if(m_Snap.m_apInfoByScore[i] && m_Teams.Team(m_Snap.m_apInfoByScore[i]->m_ClientId) == Team) m_Snap.m_apInfoByDDTeamScore[Index++] = m_Snap.m_apInfoByScore[i]; } } @@ -1826,17 +1949,15 @@ void CGameClient::OnNewSnapshot() { for(int i = 0; i < MAX_CLIENTS && Index < MAX_CLIENTS; ++i) { - if(m_Snap.m_apInfoByName[i] && m_Teams.Team(m_Snap.m_apInfoByName[i]->m_ClientID) == Team) + if(m_Snap.m_apInfoByName[i] && m_Teams.Team(m_Snap.m_apInfoByName[i]->m_ClientId) == Team) m_Snap.m_apInfoByDDTeamName[Index++] = m_Snap.m_apInfoByName[i]; } } - CServerInfo CurrentServerInfo; - Client()->GetServerInfo(&CurrentServerInfo); CTuningParams StandardTuning; - if(CurrentServerInfo.m_aGameType[0] != '0') + if(ServerInfo.m_aGameType[0] != '0') { - if(str_comp(CurrentServerInfo.m_aGameType, "DM") != 0 && str_comp(CurrentServerInfo.m_aGameType, "TDM") != 0 && str_comp(CurrentServerInfo.m_aGameType, "CTF") != 0) + if(str_comp(ServerInfo.m_aGameType, "DM") != 0 && str_comp(ServerInfo.m_aGameType, "TDM") != 0 && str_comp(ServerInfo.m_aGameType, "CTF") != 0) m_ServerMode = SERVERMODE_MOD; else if(mem_comp(&StandardTuning, &m_aTuning[g_Config.m_ClDummy], 33) == 0) m_ServerMode = SERVERMODE_PURE; @@ -1936,21 +2057,47 @@ void CGameClient::OnNewSnapshot() // detect air jump for other players for(int i = 0; i < MAX_CLIENTS; i++) if(m_Snap.m_aCharacters[i].m_Active && (m_Snap.m_aCharacters[i].m_Cur.m_Jumped & 2) && !(m_Snap.m_aCharacters[i].m_Prev.m_Jumped & 2)) - if(!Predict() || (i != m_Snap.m_LocalClientID && (!AntiPingPlayers() || i != m_PredictedDummyID))) + if(!Predict() || (i != m_Snap.m_LocalClientId && (!AntiPingPlayers() || i != m_PredictedDummyId))) { vec2 Pos = mix(vec2(m_Snap.m_aCharacters[i].m_Prev.m_X, m_Snap.m_aCharacters[i].m_Prev.m_Y), vec2(m_Snap.m_aCharacters[i].m_Cur.m_X, m_Snap.m_aCharacters[i].m_Cur.m_Y), Client()->IntraGameTick(g_Config.m_ClDummy)); float Alpha = 1.0f; - bool SameTeam = m_Teams.SameTeam(m_Snap.m_LocalClientID, i); - if(!SameTeam || m_aClients[i].m_Solo || m_aClients[m_Snap.m_LocalClientID].m_Solo) + bool SameTeam = m_Teams.SameTeam(m_Snap.m_LocalClientId, i); + if(!SameTeam || m_aClients[i].m_Solo || m_aClients[m_Snap.m_LocalClientId].m_Solo) Alpha = g_Config.m_ClShowOthersAlpha / 100.0f; m_Effects.AirJump(Pos, Alpha); } - if(m_Snap.m_LocalClientID != m_PrevLocalID) - m_PredictedDummyID = m_PrevLocalID; - m_PrevLocalID = m_Snap.m_LocalClientID; + if(g_Config.m_ClFreezeStars && !m_SuppressEvents) + { + for(auto &Character : m_Snap.m_aCharacters) + { + if(Character.m_Active && Character.m_HasExtendedData && Character.m_PrevExtendedData) + { + int FreezeTimeNow = Character.m_ExtendedData.m_FreezeEnd - Client()->GameTick(g_Config.m_ClDummy); + int FreezeTimePrev = Character.m_PrevExtendedData->m_FreezeEnd - Client()->PrevGameTick(g_Config.m_ClDummy); + vec2 Pos = vec2(Character.m_Cur.m_X, Character.m_Cur.m_Y); + int StarsNow = (FreezeTimeNow + 1) / Client()->GameTickSpeed(); + int StarsPrev = (FreezeTimePrev + 1) / Client()->GameTickSpeed(); + if(StarsNow < StarsPrev || (StarsPrev == 0 && StarsNow > 0)) + { + int Amount = StarsNow + 1; + float Mid = 3 * pi / 2; + float Min = Mid - pi / 3; + float Max = Mid + pi / 3; + for(int j = 0; j < Amount; j++) + { + float Angle = mix(Min, Max, (j + 1) / (float)(Amount + 2)); + m_Effects.DamageIndicator(Pos, direction(Angle)); + } + } + } + } + } + if(m_Snap.m_LocalClientId != m_PrevLocalId) + m_PredictedDummyId = m_PrevLocalId; + m_PrevLocalId = m_Snap.m_LocalClientId; m_IsDummySwapping = 0; SnapCollectEntities(); // creates a collection that associates EntityEx snap items with the entities they belong to @@ -1984,7 +2131,7 @@ void CGameClient::OnPredict() CCharacterCore BeforeChar = m_PredictedChar; // we can't predict without our own id or own character - if(m_Snap.m_LocalClientID == -1 || !m_Snap.m_aCharacters[m_Snap.m_LocalClientID].m_Active) + if(m_Snap.m_LocalClientId == -1 || !m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_Active) return; // don't predict anything if we are paused @@ -2013,7 +2160,7 @@ void CGameClient::OnPredict() // don't predict inactive players, or entities from other teams for(int i = 0; i < MAX_CLIENTS; i++) - if(CCharacter *pChar = m_PredictedWorld.GetCharacterByID(i)) + if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(i)) if((!m_Snap.m_aCharacters[i].m_Active && pChar->m_SnapTicks > 10) || IsOtherTeam(i)) pChar->Destroy(); @@ -2027,12 +2174,12 @@ void CGameClient::OnPredict() } } - CCharacter *pLocalChar = m_PredictedWorld.GetCharacterByID(m_Snap.m_LocalClientID); + CCharacter *pLocalChar = m_PredictedWorld.GetCharacterById(m_Snap.m_LocalClientId); if(!pLocalChar) return; CCharacter *pDummyChar = 0; if(PredictDummy()) - pDummyChar = m_PredictedWorld.GetCharacterByID(m_PredictedDummyID); + pDummyChar = m_PredictedWorld.GetCharacterById(m_PredictedDummyId); // predict for(int Tick = Client()->GameTick(g_Config.m_ClDummy) + 1; Tick <= Client()->PredGameTick(g_Config.m_ClDummy); Tick++) @@ -2043,7 +2190,7 @@ void CGameClient::OnPredict() m_PrevPredictedWorld.CopyWorld(&m_PredictedWorld); m_PredictedPrevChar = pLocalChar->GetCore(); for(int i = 0; i < MAX_CLIENTS; i++) - if(CCharacter *pChar = m_PredictedWorld.GetCharacterByID(i)) + if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(i)) m_aClients[i].m_PrevPredicted = pChar->GetCore(); } @@ -2054,7 +2201,7 @@ void CGameClient::OnPredict() // apply inputs and tick CNetObj_PlayerInput *pInputData = (CNetObj_PlayerInput *)Client()->GetInput(Tick, m_IsDummySwapping); CNetObj_PlayerInput *pDummyInputData = !pDummyChar ? 0 : (CNetObj_PlayerInput *)Client()->GetInput(Tick, m_IsDummySwapping ^ 1); - bool DummyFirst = pInputData && pDummyInputData && pDummyChar->GetCID() < pLocalChar->GetCID(); + bool DummyFirst = pInputData && pDummyInputData && pDummyChar->GetCid() < pLocalChar->GetCid(); if(DummyFirst) pDummyChar->OnDirectInput(pDummyInputData); @@ -2074,12 +2221,12 @@ void CGameClient::OnPredict() { m_PredictedChar = pLocalChar->GetCore(); for(int i = 0; i < MAX_CLIENTS; i++) - if(CCharacter *pChar = m_PredictedWorld.GetCharacterByID(i)) + if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(i)) m_aClients[i].m_Predicted = pChar->GetCore(); } for(int i = 0; i < MAX_CLIENTS; i++) - if(CCharacter *pChar = m_PredictedWorld.GetCharacterByID(i)) + if(CCharacter *pChar = m_PredictedWorld.GetCharacterById(i)) { m_aClients[i].m_aPredPos[Tick % 200] = pChar->Core()->m_Pos; m_aClients[i].m_aPredTick[Tick % 200] = Tick; @@ -2119,7 +2266,7 @@ void CGameClient::OnPredict() } // detect mispredictions of other players and make corrections smoother when possible - if(g_Config.m_ClAntiPingSmooth && Predict() && AntiPingPlayers() && m_NewTick && absolute(m_PredictedTick - Client()->PredGameTick(g_Config.m_ClDummy)) <= 1 && absolute(Client()->GameTick(g_Config.m_ClDummy) - Client()->PrevGameTick(g_Config.m_ClDummy)) <= 2) + if(g_Config.m_ClAntiPingSmooth && Predict() && AntiPingPlayers() && m_NewTick && m_PredictedTick >= MIN_TICK && absolute(m_PredictedTick - Client()->PredGameTick(g_Config.m_ClDummy)) <= 1 && absolute(Client()->GameTick(g_Config.m_ClDummy) - Client()->PrevGameTick(g_Config.m_ClDummy)) <= 2) { int PredTime = clamp(Client()->GetPredictionTime(), 0, 800); float SmoothPace = 4 - 1.5f * PredTime / 800.f; // smoothing pace (a lower value will make the smoothing quicker) @@ -2127,7 +2274,7 @@ void CGameClient::OnPredict() for(int i = 0; i < MAX_CLIENTS; i++) { - if(!m_Snap.m_aCharacters[i].m_Active || i == m_Snap.m_LocalClientID || !m_aLastActive[i]) + if(!m_Snap.m_aCharacters[i].m_Active || i == m_Snap.m_LocalClientId || !m_aLastActive[i]) continue; vec2 NewPos = (m_PredictedTick == Client()->PredGameTick(g_Config.m_ClDummy)) ? m_aClients[i].m_Predicted.m_Pos : m_aClients[i].m_PrevPredicted.m_Pos; vec2 PredErr = (m_aLastPos[i] - NewPos) / (float)minimum(Client()->GetPredictionTime(), 200); @@ -2228,16 +2375,15 @@ void CGameClient::CClientStats::Reset() m_JoinTick = 0; m_IngameTicks = 0; m_Active = false; + + std::fill(std::begin(m_aFragsWith), std::end(m_aFragsWith), 0); + std::fill(std::begin(m_aDeathsFrom), std::end(m_aDeathsFrom), 0); m_Frags = 0; m_Deaths = 0; m_Suicides = 0; m_BestSpree = 0; m_CurrentSpree = 0; - for(int j = 0; j < NUM_WEAPONS; j++) - { - m_aFragsWith[j] = 0; - m_aDeathsFrom[j] = 0; - } + m_FlagGrabs = 0; m_FlagCaptures = 0; } @@ -2255,41 +2401,50 @@ void CGameClient::CClientData::UpdateRenderInfo(bool IsTeamPlay) { m_RenderInfo.m_ColorBody = color_cast(ColorHSLA(aTeamColors[m_Team])); m_RenderInfo.m_ColorFeet = color_cast(ColorHSLA(aTeamColors[m_Team])); + + // 0.7 + for(auto &Sixup : m_RenderInfo.m_aSixup) + { + const ColorRGBA aTeamColorsSixup[2] = { + ColorRGBA(0.753f, 0.318f, 0.318f, 1.0f), + ColorRGBA(0.318f, 0.471f, 0.753f, 1.0f)}; + const ColorRGBA aMarkingColorsSixup[2] = { + ColorRGBA(0.824f, 0.345f, 0.345f, 1.0f), + ColorRGBA(0.345f, 0.514f, 0.824f, 1.0f)}; + float MarkingAlpha = Sixup.m_aColors[protocol7::SKINPART_MARKING].a; + for(auto &Color : Sixup.m_aColors) + Color = aTeamColorsSixup[m_Team]; + if(MarkingAlpha > 0.1f) + Sixup.m_aColors[protocol7::SKINPART_MARKING] = aMarkingColorsSixup[m_Team]; + } } else { m_RenderInfo.m_ColorBody = color_cast(ColorHSLA(12829350)); m_RenderInfo.m_ColorFeet = color_cast(ColorHSLA(12829350)); + for(auto &Sixup : m_RenderInfo.m_aSixup) + for(auto &Color : Sixup.m_aColors) + Color = color_cast(ColorHSLA(12829350)); } } } void CGameClient::CClientData::Reset() { - m_aName[0] = 0; - m_aClan[0] = 0; + m_UseCustomColor = 0; + m_ColorBody = 0; + m_ColorFeet = 0; + + m_aName[0] = '\0'; + m_aClan[0] = '\0'; m_Country = -1; + m_aSkinName[0] = '\0'; + m_SkinColor = 0; + m_Team = 0; - m_Angle = 0; m_Emoticon = 0; - m_EmoticonStartTick = -1; m_EmoticonStartFraction = 0; - m_Active = false; - m_ChatIgnore = false; - m_EmoticonIgnore = false; - m_Friend = false; - m_Foe = false; - m_AuthLevel = AUTHED_NO; - m_Afk = false; - m_Paused = false; - m_Spec = false; - m_SkinInfo.m_BloodColor = ColorRGBA(1, 1, 1); - m_SkinInfo.m_ColorableRenderSkin.Reset(); - m_SkinInfo.m_OriginalRenderSkin.Reset(); - m_SkinInfo.m_CustomColoredSkin = false; - m_SkinInfo.m_ColorBody = ColorRGBA(1, 1, 1); - m_SkinInfo.m_ColorFeet = ColorRGBA(1, 1, 1); - m_SkinInfo.m_SkinMetrics.Reset(); + m_EmoticonStartTick = -1; m_Solo = false; m_Jetpack = false; @@ -2309,28 +2464,146 @@ void CGameClient::CClientData::Reset() m_DeepFrozen = false; m_LiveFrozen = false; + m_Predicted.Reset(); + m_PrevPredicted.Reset(); + + m_SkinInfo.Reset(); + m_RenderInfo.Reset(); + + m_Angle = 0.0f; + m_Active = false; + m_ChatIgnore = false; + m_EmoticonIgnore = false; + m_Friend = false; + m_Foe = false; + + m_AuthLevel = AUTHED_NO; + m_Afk = false; + m_Paused = false; + m_Spec = false; + + std::fill(std::begin(m_aSwitchStates), std::end(m_aSwitchStates), 0); + + m_Snapped.m_Tick = -1; m_Evolved.m_Tick = -1; - m_SpecChar = vec2(0, 0); + m_RenderCur.m_Tick = -1; + m_RenderPrev.m_Tick = -1; + m_RenderPos = vec2(0.0f, 0.0f); + m_IsPredicted = false; + m_IsPredictedLocal = false; + std::fill(std::begin(m_aSmoothStart), std::end(m_aSmoothStart), 0); + std::fill(std::begin(m_aSmoothLen), std::end(m_aSmoothLen), 0); + std::fill(std::begin(m_aPredPos), std::end(m_aPredPos), vec2(0.0f, 0.0f)); + std::fill(std::begin(m_aPredTick), std::end(m_aPredTick), 0); m_SpecCharPresent = false; + m_SpecChar = vec2(0.0f, 0.0f); - mem_zero(m_aSwitchStates, sizeof(m_aSwitchStates)); + for(auto &Info : m_aSixup) + Info.Reset(); +} - UpdateRenderInfo(false); +void CGameClient::CClientData::CSixup::Reset() +{ + for(int i = 0; i < protocol7::NUM_SKINPARTS; ++i) + { + m_aaSkinPartNames[i][0] = '\0'; + m_aUseCustomColors[i] = 0; + m_aSkinPartColors[i] = 0; + } } -void CGameClient::SendSwitchTeam(int Team) +void CGameClient::SendSwitchTeam(int Team) const { CNetMsg_Cl_SetTeam Msg; Msg.m_Team = Team; Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); +} + +void CGameClient::SendStartInfo7(bool Dummy) const +{ + protocol7::CNetMsg_Cl_StartInfo Msg; + Msg.m_pName = Dummy ? Client()->DummyName() : Client()->PlayerName(); + Msg.m_pClan = Dummy ? Config()->m_ClDummyClan : Config()->m_PlayerClan; + Msg.m_Country = Dummy ? Config()->m_ClDummyCountry : Config()->m_PlayerCountry; + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) + { + Msg.m_apSkinPartNames[p] = CSkins7::ms_apSkinVariables[(int)Dummy][p]; + Msg.m_aUseCustomColors[p] = *CSkins7::ms_apUCCVariables[(int)Dummy][p]; + Msg.m_aSkinPartColors[p] = *CSkins7::ms_apColorVariables[(int)Dummy][p]; + } + CMsgPacker Packer(&Msg, false, true); + if(Msg.Pack(&Packer)) + return; + Client()->SendMsg((int)Dummy, &Packer, MSGFLAG_VITAL | MSGFLAG_FLUSH); +} + +void CGameClient::SendSkinChange7(bool Dummy) +{ + protocol7::CNetMsg_Cl_SkinChange Msg; + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) + { + Msg.m_apSkinPartNames[p] = CSkins7::ms_apSkinVariables[(int)Dummy][p]; + Msg.m_aUseCustomColors[p] = *CSkins7::ms_apUCCVariables[(int)Dummy][p]; + Msg.m_aSkinPartColors[p] = *CSkins7::ms_apColorVariables[(int)Dummy][p]; + } + CMsgPacker Packer(&Msg, false, true); + if(Msg.Pack(&Packer)) + return; + Client()->SendMsg((int)Dummy, &Packer, MSGFLAG_VITAL | MSGFLAG_FLUSH); + m_aCheckInfo[(int)Dummy] = Client()->GameTickSpeed(); +} + +bool CGameClient::GotWantedSkin7(bool Dummy) +{ + // validate the wanted skinparts before comparison + // because the skin parts we compare against are also validated + // otherwise it tries to resend the skin info when the eyes are set to "negative" + // in team based modes + char aSkinParts[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_ARRAY_SIZE]; + char *apSkinPartsPtr[protocol7::NUM_SKINPARTS]; + int aUCCVars[protocol7::NUM_SKINPARTS]; + int aColorVars[protocol7::NUM_SKINPARTS]; + for(int SkinPart = 0; SkinPart < protocol7::NUM_SKINPARTS; SkinPart++) + { + str_copy(aSkinParts[SkinPart], CSkins7::ms_apSkinVariables[(int)Dummy][SkinPart], protocol7::MAX_SKIN_ARRAY_SIZE); + apSkinPartsPtr[SkinPart] = aSkinParts[SkinPart]; + aUCCVars[SkinPart] = *CSkins7::ms_apUCCVariables[(int)Dummy][SkinPart]; + aColorVars[SkinPart] = *CSkins7::ms_apColorVariables[(int)Dummy][SkinPart]; + } + m_Skins7.ValidateSkinParts(apSkinPartsPtr, aUCCVars, aColorVars, m_pClient->m_TranslationContext.m_GameFlags); + + for(int SkinPart = 0; SkinPart < protocol7::NUM_SKINPARTS; SkinPart++) + { + if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_aSixup[g_Config.m_ClDummy].m_aaSkinPartNames[SkinPart], apSkinPartsPtr[SkinPart])) + return false; + if(m_aClients[m_aLocalIds[(int)Dummy]].m_aSixup[g_Config.m_ClDummy].m_aUseCustomColors[SkinPart] != aUCCVars[SkinPart]) + return false; + if(m_aClients[m_aLocalIds[(int)Dummy]].m_aSixup[g_Config.m_ClDummy].m_aSkinPartColors[SkinPart] != aColorVars[SkinPart]) + return false; + } - if(Team != TEAM_SPECTATORS) - m_Camera.OnReset(); + // TODO: add name change ddnet extension to 0.7 protocol + // if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_aName, Dummy ? Client()->DummyName() : Client()->PlayerName())) + // return false; + // if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_aClan, Dummy ? g_Config.m_ClDummyClan : g_Config.m_PlayerClan)) + // return false; + // if(m_aClients[m_aLocalIds[(int)Dummy]].m_Country != (Dummy ? g_Config.m_ClDummyCountry : g_Config.m_PlayerCountry)) + // return false; + + return true; } void CGameClient::SendInfo(bool Start) { + if(m_pClient->IsSixup()) + { + if(Start) + SendStartInfo7(false); + else + SendSkinChange7(false); + return; + } if(Start) { CNetMsg_Cl_StartInfo Msg; @@ -2365,6 +2638,14 @@ void CGameClient::SendInfo(bool Start) void CGameClient::SendDummyInfo(bool Start) { + if(m_pClient->IsSixup()) + { + if(Start) + SendStartInfo7(true); + else + SendSkinChange7(true); + return; + } if(Start) { CNetMsg_Cl_StartInfo Msg; @@ -2397,7 +2678,7 @@ void CGameClient::SendDummyInfo(bool Start) } } -void CGameClient::SendKill(int ClientID) const +void CGameClient::SendKill(int ClientId) const { CNetMsg_Cl_Kill Msg; Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); @@ -2409,6 +2690,17 @@ void CGameClient::SendKill(int ClientID) const } } +void CGameClient::SendReadyChange7() +{ + if(!Client()->IsSixup()) + { + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", "Error you have to be connected to a 0.7 server to use ready_change"); + return; + } + protocol7::CNetMsg_Cl_ReadyChange Msg; + Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL, true); +} + void CGameClient::ConTeam(IConsole::IResult *pResult, void *pUserData) { ((CGameClient *)pUserData)->SendSwitchTeam(pResult->GetInteger(0)); @@ -2419,11 +2711,22 @@ void CGameClient::ConKill(IConsole::IResult *pResult, void *pUserData) ((CGameClient *)pUserData)->SendKill(-1); } +void CGameClient::ConReadyChange7(IConsole::IResult *pResult, void *pUserData) +{ + CGameClient *pClient = static_cast(pUserData); + if(pClient->Client()->State() == IClient::STATE_ONLINE) + pClient->SendReadyChange7(); +} + void CGameClient::ConchainLanguageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) { + CGameClient *pThis = static_cast(pUserData); + const bool Changed = pThis->Client()->GlobalTime() && pResult->NumArguments() && str_comp(pResult->GetString(0), g_Config.m_ClLanguagefile) != 0; pfnCallback(pResult, pCallbackUserData); - if(pResult->NumArguments()) - ((CGameClient *)pUserData)->OnLanguageChange(); + if(Changed) + { + pThis->OnLanguageChange(); + } } void CGameClient::ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) @@ -2444,8 +2747,10 @@ void CGameClient::ConchainSpecialDummy(IConsole::IResult *pResult, void *pUserDa { pfnCallback(pResult, pCallbackUserData); if(pResult->NumArguments()) + { if(g_Config.m_ClDummy && !((CGameClient *)pUserData)->Client()->DummyConnected()) g_Config.m_ClDummy = 0; + } } void CGameClient::ConchainClTextEntitiesSize(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) @@ -2464,21 +2769,21 @@ IGameClient *CreateGameClient() return new CGameClient(); } -int CGameClient::IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, int ownID) +int CGameClient::IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, int OwnId) { float Distance = 0.0f; - int ClosestID = -1; + int ClosestId = -1; - const CClientData &OwnClientData = m_aClients[ownID]; + const CClientData &OwnClientData = m_aClients[OwnId]; for(int i = 0; i < MAX_CLIENTS; i++) { - if(i == ownID) + if(i == OwnId) continue; - const CClientData &cData = m_aClients[i]; + const CClientData &Data = m_aClients[i]; - if(!cData.m_Active) + if(!Data.m_Active) continue; CNetObj_Character Prev = m_Snap.m_aCharacters[i].m_Prev; @@ -2486,10 +2791,10 @@ int CGameClient::IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, in vec2 Position = mix(vec2(Prev.m_X, Prev.m_Y), vec2(Player.m_X, Player.m_Y), Client()->IntraGameTick(g_Config.m_ClDummy)); - bool IsOneSuper = cData.m_Super || OwnClientData.m_Super; - bool IsOneSolo = cData.m_Solo || OwnClientData.m_Solo; + bool IsOneSuper = Data.m_Super || OwnClientData.m_Super; + bool IsOneSolo = Data.m_Solo || OwnClientData.m_Solo; - if(!IsOneSuper && (!m_Teams.SameTeam(i, ownID) || IsOneSolo || OwnClientData.m_HookHitDisabled)) + if(!IsOneSuper && (!m_Teams.SameTeam(i, OwnId) || IsOneSolo || OwnClientData.m_HookHitDisabled)) continue; vec2 ClosestPoint; @@ -2497,17 +2802,17 @@ int CGameClient::IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, in { if(distance(Position, ClosestPoint) < CCharacterCore::PhysicalSize() + 2.0f) { - if(ClosestID == -1 || distance(HookPos, Position) < Distance) + if(ClosestId == -1 || distance(HookPos, Position) < Distance) { NewPos2 = ClosestPoint; - ClosestID = i; + ClosestId = i; Distance = distance(HookPos, Position); } } } } - return ClosestID; + return ClosestId; } ColorRGBA CalculateNameColor(ColorHSLA TextColorHSL) @@ -2534,14 +2839,14 @@ void CGameClient::UpdatePrediction() if(!m_Snap.m_pLocalCharacter) { - if(CCharacter *pLocalChar = m_GameWorld.GetCharacterByID(m_Snap.m_LocalClientID)) + if(CCharacter *pLocalChar = m_GameWorld.GetCharacterById(m_Snap.m_LocalClientId)) pLocalChar->Destroy(); return; } if(m_Snap.m_pLocalCharacter->m_AmmoCount > 0 && m_Snap.m_pLocalCharacter->m_Weapon != WEAPON_NINJA) m_GameWorld.m_WorldConfig.m_InfiniteAmmo = false; - m_GameWorld.m_WorldConfig.m_IsSolo = !m_Snap.m_aCharacters[m_Snap.m_LocalClientID].m_HasExtendedData && !m_aTuning[g_Config.m_ClDummy].m_PlayerCollision && !m_aTuning[g_Config.m_ClDummy].m_PlayerHooking; + m_GameWorld.m_WorldConfig.m_IsSolo = !m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_HasExtendedData && !m_aTuning[g_Config.m_ClDummy].m_PlayerCollision && !m_aTuning[g_Config.m_ClDummy].m_PlayerHooking; // update the tuning/tunezone at the local character position with the latest tunings received before the new snapshot vec2 LocalCharPos = vec2(m_Snap.m_pLocalCharacter->m_X, m_Snap.m_pLocalCharacter->m_Y); @@ -2596,40 +2901,40 @@ void CGameClient::UpdatePrediction() } // if ddnetcharacter is available, ignore server-wide tunings for hook and collision - if(m_Snap.m_aCharacters[m_Snap.m_LocalClientID].m_HasExtendedData) + if(m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_HasExtendedData) { m_GameWorld.m_Core.m_aTuning[g_Config.m_ClDummy].m_PlayerCollision = 1; m_GameWorld.m_Core.m_aTuning[g_Config.m_ClDummy].m_PlayerHooking = 1; } - CCharacter *pLocalChar = m_GameWorld.GetCharacterByID(m_Snap.m_LocalClientID); + CCharacter *pLocalChar = m_GameWorld.GetCharacterById(m_Snap.m_LocalClientId); CCharacter *pDummyChar = 0; if(PredictDummy()) - pDummyChar = m_GameWorld.GetCharacterByID(m_PredictedDummyID); + pDummyChar = m_GameWorld.GetCharacterById(m_PredictedDummyId); // update strong and weak hook if(pLocalChar && !m_Snap.m_SpecInfo.m_Active && Client()->State() != IClient::STATE_DEMOPLAYBACK && (m_aTuning[g_Config.m_ClDummy].m_PlayerCollision || m_aTuning[g_Config.m_ClDummy].m_PlayerHooking)) { - if(m_Snap.m_aCharacters[m_Snap.m_LocalClientID].m_HasExtendedData) + if(m_Snap.m_aCharacters[m_Snap.m_LocalClientId].m_HasExtendedData) { - int aIDs[MAX_CLIENTS]; - for(int &ID : aIDs) - ID = -1; + int aIds[MAX_CLIENTS]; + for(int &Id : aIds) + Id = -1; for(int i = 0; i < MAX_CLIENTS; i++) - if(CCharacter *pChar = m_GameWorld.GetCharacterByID(i)) - aIDs[pChar->GetStrongWeakID()] = i; - for(int ID : aIDs) - if(ID >= 0) - m_CharOrder.GiveStrong(ID); + if(CCharacter *pChar = m_GameWorld.GetCharacterById(i)) + aIds[pChar->GetStrongWeakId()] = i; + for(int Id : aIds) + if(Id >= 0) + m_CharOrder.GiveStrong(Id); } else { // manual detection DetectStrongHook(); } - for(int i : m_CharOrder.m_IDs) + for(int i : m_CharOrder.m_Ids) { - if(CCharacter *pChar = m_GameWorld.GetCharacterByID(i)) + if(CCharacter *pChar = m_GameWorld.GetCharacterById(i)) { m_GameWorld.RemoveEntity(pChar); m_GameWorld.InsertEntity(pChar); @@ -2658,7 +2963,7 @@ void CGameClient::UpdatePrediction() m_GameWorld.Tick(); for(int i = 0; i < MAX_CLIENTS; i++) - if(CCharacter *pChar = m_GameWorld.GetCharacterByID(i)) + if(CCharacter *pChar = m_GameWorld.GetCharacterById(i)) { m_aClients[i].m_aPredPos[Tick % 200] = pChar->Core()->m_Pos; m_aClients[i].m_aPredTick[Tick % 200] = Tick; @@ -2678,19 +2983,19 @@ void CGameClient::UpdatePrediction() } for(int i = 0; i < MAX_CLIENTS; i++) - if(CCharacter *pChar = m_GameWorld.GetCharacterByID(i)) + if(CCharacter *pChar = m_GameWorld.GetCharacterById(i)) { m_aClients[i].m_aPredPos[Client()->GameTick(g_Config.m_ClDummy) % 200] = pChar->Core()->m_Pos; m_aClients[i].m_aPredTick[Client()->GameTick(g_Config.m_ClDummy) % 200] = Client()->GameTick(g_Config.m_ClDummy); } // update the local gameworld with the new snapshot - m_GameWorld.NetObjBegin(m_Teams, m_Snap.m_LocalClientID); + m_GameWorld.NetObjBegin(m_Teams, m_Snap.m_LocalClientId); for(int i = 0; i < MAX_CLIENTS; i++) if(m_Snap.m_aCharacters[i].m_Active) { - bool IsLocal = (i == m_Snap.m_LocalClientID || (PredictDummy() && i == m_PredictedDummyID)); + bool IsLocal = (i == m_Snap.m_LocalClientId || (PredictDummy() && i == m_PredictedDummyId)); int GameTeam = (m_Snap.m_pGameInfoObj && (m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS)) ? m_aClients[i].m_Team : i; m_GameWorld.NetCharAdd(i, &m_Snap.m_aCharacters[i].m_Cur, m_Snap.m_aCharacters[i].m_HasExtendedData ? &m_Snap.m_aCharacters[i].m_ExtendedData : 0, @@ -2698,7 +3003,7 @@ void CGameClient::UpdatePrediction() } for(const CSnapEntities &EntData : SnapEntities()) - m_GameWorld.NetObjAdd(EntData.m_Item.m_ID, EntData.m_Item.m_Type, EntData.m_pData, EntData.m_pDataEx); + m_GameWorld.NetObjAdd(EntData.m_Item.m_Id, EntData.m_Item.m_Type, EntData.m_Item.m_pData, EntData.m_pDataEx); m_GameWorld.NetObjEnd(); } @@ -2719,8 +3024,8 @@ void CGameClient::UpdateRenderedCharacters() Client()->IntraGameTick(g_Config.m_ClDummy)); vec2 Pos = UnpredPos; - CCharacter *pChar = m_PredictedWorld.GetCharacterByID(i); - if(Predict() && (i == m_Snap.m_LocalClientID || (AntiPingPlayers() && !IsOtherTeam(i))) && pChar) + CCharacter *pChar = m_PredictedWorld.GetCharacterById(i); + if(Predict() && (i == m_Snap.m_LocalClientId || (AntiPingPlayers() && !IsOtherTeam(i))) && pChar) { m_aClients[i].m_Predicted.Write(&m_aClients[i].m_RenderCur); m_aClients[i].m_PrevPredicted.Write(&m_aClients[i].m_RenderPrev); @@ -2732,7 +3037,7 @@ void CGameClient::UpdateRenderedCharacters() vec2(m_aClients[i].m_RenderCur.m_X, m_aClients[i].m_RenderCur.m_Y), m_aClients[i].m_IsPredicted ? Client()->PredIntraGameTick(g_Config.m_ClDummy) : Client()->IntraGameTick(g_Config.m_ClDummy)); - if(i == m_Snap.m_LocalClientID) + if(i == m_Snap.m_LocalClientId) { m_aClients[i].m_IsPredictedLocal = true; if(AntiPingGunfire() && ((pChar->m_NinjaJetpack && pChar->m_FreezeTime == 0) || m_Snap.m_aCharacters[i].m_Cur.m_Weapon != WEAPON_NINJA || m_Snap.m_aCharacters[i].m_Cur.m_Weapon == m_aClients[i].m_Predicted.m_ActiveWeapon)) @@ -2754,7 +3059,7 @@ void CGameClient::UpdateRenderedCharacters() } m_Snap.m_aCharacters[i].m_Position = Pos; m_aClients[i].m_RenderPos = Pos; - if(Predict() && i == m_Snap.m_LocalClientID) + if(Predict() && i == m_Snap.m_LocalClientId) m_LocalCharacterPos = Pos; } } @@ -2774,8 +3079,8 @@ void CGameClient::DetectStrongHook() if(m_Snap.m_aCharacters[FromPlayer].m_Prev.m_Direction != m_Snap.m_aCharacters[FromPlayer].m_Cur.m_Direction || m_Snap.m_aCharacters[ToPlayer].m_Prev.m_Direction != m_Snap.m_aCharacters[ToPlayer].m_Cur.m_Direction) continue; - CCharacter *pFromCharWorld = m_GameWorld.GetCharacterByID(FromPlayer); - CCharacter *pToCharWorld = m_GameWorld.GetCharacterByID(ToPlayer); + CCharacter *pFromCharWorld = m_GameWorld.GetCharacterById(FromPlayer); + CCharacter *pToCharWorld = m_GameWorld.GetCharacterById(ToPlayer); if(!pFromCharWorld || !pToCharWorld) continue; @@ -2825,7 +3130,7 @@ void CGameClient::DetectStrongHook() { if(m_CharOrder.HasStrongAgainst(ToPlayer, FromPlayer)) { - if(ToPlayer != m_Snap.m_LocalClientID) + if(ToPlayer != m_Snap.m_LocalClientId) m_CharOrder.GiveWeak(ToPlayer); else m_CharOrder.GiveStrong(FromPlayer); @@ -2835,7 +3140,7 @@ void CGameClient::DetectStrongHook() { if(m_CharOrder.HasStrongAgainst(FromPlayer, ToPlayer)) { - if(ToPlayer != m_Snap.m_LocalClientID) + if(ToPlayer != m_Snap.m_LocalClientId) m_CharOrder.GiveStrong(ToPlayer); else m_CharOrder.GiveWeak(FromPlayer); @@ -2844,22 +3149,22 @@ void CGameClient::DetectStrongHook() } } -vec2 CGameClient::GetSmoothPos(int ClientID) +vec2 CGameClient::GetSmoothPos(int ClientId) { - vec2 Pos = mix(m_aClients[ClientID].m_PrevPredicted.m_Pos, m_aClients[ClientID].m_Predicted.m_Pos, Client()->PredIntraGameTick(g_Config.m_ClDummy)); + vec2 Pos = mix(m_aClients[ClientId].m_PrevPredicted.m_Pos, m_aClients[ClientId].m_Predicted.m_Pos, Client()->PredIntraGameTick(g_Config.m_ClDummy)); int64_t Now = time_get(); for(int i = 0; i < 2; i++) { - int64_t Len = clamp(m_aClients[ClientID].m_aSmoothLen[i], (int64_t)1, time_freq()); - int64_t TimePassed = Now - m_aClients[ClientID].m_aSmoothStart[i]; + int64_t Len = clamp(m_aClients[ClientId].m_aSmoothLen[i], (int64_t)1, time_freq()); + int64_t TimePassed = Now - m_aClients[ClientId].m_aSmoothStart[i]; if(in_range(TimePassed, (int64_t)0, Len - 1)) { float MixAmount = 1.f - std::pow(1.f - TimePassed / (float)Len, 1.2f); int SmoothTick; float SmoothIntra; Client()->GetSmoothTick(&SmoothTick, &SmoothIntra, MixAmount); - if(SmoothTick > 0 && m_aClients[ClientID].m_aPredTick[(SmoothTick - 1) % 200] >= Client()->PrevGameTick(g_Config.m_ClDummy) && m_aClients[ClientID].m_aPredTick[SmoothTick % 200] <= Client()->PredGameTick(g_Config.m_ClDummy)) - Pos[i] = mix(m_aClients[ClientID].m_aPredPos[(SmoothTick - 1) % 200][i], m_aClients[ClientID].m_aPredPos[SmoothTick % 200][i], SmoothIntra); + if(SmoothTick > 0 && m_aClients[ClientId].m_aPredTick[(SmoothTick - 1) % 200] >= Client()->PrevGameTick(g_Config.m_ClDummy) && m_aClients[ClientId].m_aPredTick[SmoothTick % 200] <= Client()->PredGameTick(g_Config.m_ClDummy)) + Pos[i] = mix(m_aClients[ClientId].m_aPredPos[(SmoothTick - 1) % 200][i], m_aClients[ClientId].m_aPredPos[SmoothTick % 200][i], SmoothIntra); } } return Pos; @@ -2870,45 +3175,45 @@ void CGameClient::Echo(const char *pString) m_Chat.Echo(pString); } -bool CGameClient::IsOtherTeam(int ClientID) const +bool CGameClient::IsOtherTeam(int ClientId) const { - bool Local = m_Snap.m_LocalClientID == ClientID; + bool Local = m_Snap.m_LocalClientId == ClientId; - if(m_Snap.m_LocalClientID < 0) + if(m_Snap.m_LocalClientId < 0) return false; - else if((m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorID == SPEC_FREEVIEW) || ClientID < 0) + else if((m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorId == SPEC_FREEVIEW) || ClientId < 0) return false; - else if(m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW) + else if(m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) { - if(m_Teams.Team(ClientID) == TEAM_SUPER || m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorID) == TEAM_SUPER) + if(m_Teams.Team(ClientId) == TEAM_SUPER || m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorId) == TEAM_SUPER) return false; - return m_Teams.Team(ClientID) != m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorID); + return m_Teams.Team(ClientId) != m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorId); } - else if((m_aClients[m_Snap.m_LocalClientID].m_Solo || m_aClients[ClientID].m_Solo) && !Local) + else if((m_aClients[m_Snap.m_LocalClientId].m_Solo || m_aClients[ClientId].m_Solo) && !Local) return true; - if(m_Teams.Team(ClientID) == TEAM_SUPER || m_Teams.Team(m_Snap.m_LocalClientID) == TEAM_SUPER) + if(m_Teams.Team(ClientId) == TEAM_SUPER || m_Teams.Team(m_Snap.m_LocalClientId) == TEAM_SUPER) return false; - return m_Teams.Team(ClientID) != m_Teams.Team(m_Snap.m_LocalClientID); + return m_Teams.Team(ClientId) != m_Teams.Team(m_Snap.m_LocalClientId); } int CGameClient::SwitchStateTeam() const { if(m_aSwitchStateTeam[g_Config.m_ClDummy] >= 0) return m_aSwitchStateTeam[g_Config.m_ClDummy]; - else if(m_Snap.m_LocalClientID < 0) + else if(m_Snap.m_LocalClientId < 0) return 0; - else if(m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorID != SPEC_FREEVIEW) - return m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorID); - return m_Teams.Team(m_Snap.m_LocalClientID); + else if(m_Snap.m_SpecInfo.m_Active && m_Snap.m_SpecInfo.m_SpectatorId != SPEC_FREEVIEW) + return m_Teams.Team(m_Snap.m_SpecInfo.m_SpectatorId); + return m_Teams.Team(m_Snap.m_LocalClientId); } bool CGameClient::IsLocalCharSuper() const { - if(m_Snap.m_LocalClientID < 0) + if(m_Snap.m_LocalClientId < 0) return false; - return m_aClients[m_Snap.m_LocalClientID].m_Super; + return m_aClients[m_Snap.m_LocalClientId].m_Super; } void CGameClient::LoadGameSkin(const char *pPath, bool AsDir) @@ -3033,7 +3338,7 @@ void CGameClient::LoadGameSkin(const char *pPath, bool AsDir) } CImageInfo ImgInfo; - bool PngLoaded = Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL); + bool PngLoaded = Graphics()->LoadPng(ImgInfo, aPath, IStorage::TYPE_ALL); if(!PngLoaded && !IsDefault) { if(AsDir) @@ -3041,7 +3346,7 @@ void CGameClient::LoadGameSkin(const char *pPath, bool AsDir) else LoadGameSkin(pPath, true); } - else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_HEALTH_FULL].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_HEALTH_FULL].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRGBA(aPath, ImgInfo)) + else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_HEALTH_FULL].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_HEALTH_FULL].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRgba(aPath, ImgInfo)) { m_GameSkin.m_SpriteHealthFull = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HEALTH_FULL]); m_GameSkin.m_SpriteHealthEmpty = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HEALTH_EMPTY]); @@ -3164,9 +3469,8 @@ void CGameClient::LoadGameSkin(const char *pPath, bool AsDir) } m_GameSkinLoaded = true; - - Graphics()->FreePNG(&ImgInfo); } + ImgInfo.Free(); } void CGameClient::LoadEmoticonsSkin(const char *pPath, bool AsDir) @@ -3195,7 +3499,7 @@ void CGameClient::LoadEmoticonsSkin(const char *pPath, bool AsDir) } CImageInfo ImgInfo; - bool PngLoaded = Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL); + bool PngLoaded = Graphics()->LoadPng(ImgInfo, aPath, IStorage::TYPE_ALL); if(!PngLoaded && !IsDefault) { if(AsDir) @@ -3203,14 +3507,14 @@ void CGameClient::LoadEmoticonsSkin(const char *pPath, bool AsDir) else LoadEmoticonsSkin(pPath, true); } - else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_OOP].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_OOP].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRGBA(aPath, ImgInfo)) + else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_OOP].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_OOP].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRgba(aPath, ImgInfo)) { for(int i = 0; i < 16; ++i) m_EmoticonsSkin.m_aSpriteEmoticons[i] = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_OOP + i]); m_EmoticonsSkinLoaded = true; - Graphics()->FreePNG(&ImgInfo); } + ImgInfo.Free(); } void CGameClient::LoadParticlesSkin(const char *pPath, bool AsDir) @@ -3249,7 +3553,7 @@ void CGameClient::LoadParticlesSkin(const char *pPath, bool AsDir) } CImageInfo ImgInfo; - bool PngLoaded = Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL); + bool PngLoaded = Graphics()->LoadPng(ImgInfo, aPath, IStorage::TYPE_ALL); if(!PngLoaded && !IsDefault) { if(AsDir) @@ -3257,7 +3561,7 @@ void CGameClient::LoadParticlesSkin(const char *pPath, bool AsDir) else LoadParticlesSkin(pPath, true); } - else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_PART_SLICE].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_PART_SLICE].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRGBA(aPath, ImgInfo)) + else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_PART_SLICE].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_PART_SLICE].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRgba(aPath, ImgInfo)) { m_ParticlesSkin.m_SpriteParticleSlice = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_SLICE]); m_ParticlesSkin.m_SpriteParticleBall = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_BALL]); @@ -3280,8 +3584,8 @@ void CGameClient::LoadParticlesSkin(const char *pPath, bool AsDir) m_ParticlesSkin.m_aSpriteParticles[9] = m_ParticlesSkin.m_SpriteParticleHit; m_ParticlesSkinLoaded = true; - free(ImgInfo.m_pData); } + ImgInfo.Free(); } void CGameClient::LoadHudSkin(const char *pPath, bool AsDir) @@ -3315,6 +3619,8 @@ void CGameClient::LoadHudSkin(const char *pPath, bool AsDir) Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudTeleportGun); Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudTeleportLaser); Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudPracticeMode); + Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudLockMode); + Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudTeam0Mode); Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudDummyHammer); Graphics()->UnloadTexture(&m_HudSkin.m_SpriteHudDummyCopy); m_HudSkinLoaded = false; @@ -3336,7 +3642,7 @@ void CGameClient::LoadHudSkin(const char *pPath, bool AsDir) } CImageInfo ImgInfo; - bool PngLoaded = Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL); + bool PngLoaded = Graphics()->LoadPng(ImgInfo, aPath, IStorage::TYPE_ALL); if(!PngLoaded && !IsDefault) { if(AsDir) @@ -3344,7 +3650,7 @@ void CGameClient::LoadHudSkin(const char *pPath, bool AsDir) else LoadHudSkin(pPath, true); } - else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_HUD_AIRJUMP].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_HUD_AIRJUMP].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRGBA(aPath, ImgInfo)) + else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_HUD_AIRJUMP].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_HUD_AIRJUMP].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRgba(aPath, ImgInfo)) { m_HudSkin.m_SpriteHudAirjump = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_AIRJUMP]); m_HudSkin.m_SpriteHudAirjumpEmpty = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_AIRJUMP_EMPTY]); @@ -3373,12 +3679,14 @@ void CGameClient::LoadHudSkin(const char *pPath, bool AsDir) m_HudSkin.m_SpriteHudTeleportGun = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_GUN]); m_HudSkin.m_SpriteHudTeleportLaser = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_TELEPORT_LASER]); m_HudSkin.m_SpriteHudPracticeMode = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_PRACTICE_MODE]); + m_HudSkin.m_SpriteHudLockMode = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_LOCK_MODE]); + m_HudSkin.m_SpriteHudTeam0Mode = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_TEAM0_MODE]); m_HudSkin.m_SpriteHudDummyHammer = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_DUMMY_HAMMER]); m_HudSkin.m_SpriteHudDummyCopy = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_HUD_DUMMY_COPY]); m_HudSkinLoaded = true; - free(ImgInfo.m_pData); } + ImgInfo.Free(); } void CGameClient::LoadExtrasSkin(const char *pPath, bool AsDir) @@ -3409,7 +3717,7 @@ void CGameClient::LoadExtrasSkin(const char *pPath, bool AsDir) } CImageInfo ImgInfo; - bool PngLoaded = Graphics()->LoadPNG(&ImgInfo, aPath, IStorage::TYPE_ALL); + bool PngLoaded = Graphics()->LoadPng(ImgInfo, aPath, IStorage::TYPE_ALL); if(!PngLoaded && !IsDefault) { if(AsDir) @@ -3417,26 +3725,31 @@ void CGameClient::LoadExtrasSkin(const char *pPath, bool AsDir) else LoadExtrasSkin(pPath, true); } - else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRGBA(aPath, ImgInfo)) + else if(PngLoaded && Graphics()->CheckImageDivisibility(aPath, ImgInfo, g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE].m_pSet->m_Gridx, g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE].m_pSet->m_Gridy, true) && Graphics()->IsImageFormatRgba(aPath, ImgInfo)) { m_ExtrasSkin.m_SpriteParticleSnowflake = Graphics()->LoadSpriteTexture(ImgInfo, &g_pData->m_aSprites[SPRITE_PART_SNOWFLAKE]); m_ExtrasSkin.m_aSpriteParticles[0] = m_ExtrasSkin.m_SpriteParticleSnowflake; m_ExtrasSkinLoaded = true; - free(ImgInfo.m_pData); } + ImgInfo.Free(); } -void CGameClient::RefindSkins() +void CGameClient::RefreshSkins() { + const auto SkinStartLoadTime = time_get_nanoseconds(); + m_Skins.Refresh([&](int) { + // if skin refreshing takes to long, swap to a loading screen + if(time_get_nanoseconds() - SkinStartLoadTime > 500ms) + { + m_Menus.RenderLoading(Localize("Loading skin files"), "", 0, false); + } + }); + for(auto &Client : m_aClients) { - Client.m_SkinInfo.m_OriginalRenderSkin.Reset(); - Client.m_SkinInfo.m_ColorableRenderSkin.Reset(); if(Client.m_aSkinName[0] != '\0') { - const CSkin *pSkin = m_Skins.Find(Client.m_aSkinName); - Client.m_SkinInfo.m_OriginalRenderSkin = pSkin->m_OriginalSkin; - Client.m_SkinInfo.m_ColorableRenderSkin = pSkin->m_ColorableSkin; + Client.m_SkinInfo.Apply(m_Skins.Find(Client.m_aSkinName)); } else { @@ -3445,9 +3758,19 @@ void CGameClient::RefindSkins() } Client.UpdateRenderInfo(IsTeamPlay()); } - m_Ghost.RefindSkins(); - m_Chat.RefindSkins(); - m_InfoMessages.RefindSkins(); + + for(auto &pComponent : m_vpAll) + pComponent->OnRefreshSkins(); +} + +void CGameClient::ConchainRefreshSkins(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) +{ + CGameClient *pThis = static_cast(pUserData); + pfnCallback(pResult, pCallbackUserData); + if(pResult->NumArguments() && pThis->m_Menus.IsInit()) + { + pThis->RefreshSkins(); + } } static bool UnknownMapSettingCallback(const char *pCommand, void *pUser) @@ -3475,10 +3798,10 @@ void CGameClient::LoadMapSettings() pMap->GetType(MAPITEMTYPE_INFO, &Start, &Num); for(int i = Start; i < Start + Num; i++) { - int ItemID; - CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(i, nullptr, &ItemID); + int ItemId; + CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(i, nullptr, &ItemId); int ItemSize = pMap->GetItemSize(i); - if(!pItem || ItemID != 0) + if(!pItem || ItemId != 0) continue; if(ItemSize < (int)sizeof(CMapItemInfoSettings)) @@ -3548,14 +3871,14 @@ bool CGameClient::CanDisplayWarning() const return m_Menus.CanDisplayWarning(); } -bool CGameClient::IsDisplayingWarning() const +CNetObjHandler *CGameClient::GetNetObjHandler() { - return m_Menus.GetCurPopup() == CMenus::POPUP_WARNING; + return &m_NetObjHandler; } -CNetObjHandler *CGameClient::GetNetObjHandler() +protocol7::CNetObjHandler *CGameClient::GetNetObjHandler7() { - return &m_NetObjHandler; + return &m_NetObjHandler7; } void CGameClient::SnapCollectEntities() @@ -3567,21 +3890,20 @@ void CGameClient::SnapCollectEntities() for(int Index = 0; Index < NumSnapItems; Index++) { - IClient::CSnapItem Item; - const void *pData = Client()->SnapGetItem(IClient::SNAP_CURRENT, Index, &Item); + const IClient::CSnapItem Item = Client()->SnapGetItem(IClient::SNAP_CURRENT, Index); if(Item.m_Type == NETOBJTYPE_ENTITYEX) - vItemEx.push_back({Item, pData, 0}); + vItemEx.push_back({Item, nullptr}); else if(Item.m_Type == NETOBJTYPE_PICKUP || Item.m_Type == NETOBJTYPE_DDNETPICKUP || Item.m_Type == NETOBJTYPE_LASER || Item.m_Type == NETOBJTYPE_DDNETLASER || Item.m_Type == NETOBJTYPE_PROJECTILE || Item.m_Type == NETOBJTYPE_DDRACEPROJECTILE || Item.m_Type == NETOBJTYPE_DDNETPROJECTILE) - vItemData.push_back({Item, pData, 0}); + vItemData.push_back({Item, nullptr}); } // sort by id class CEntComparer { public: - bool operator()(const CSnapEntities &lhs, const CSnapEntities &rhs) const + bool operator()(const CSnapEntities &Lhs, const CSnapEntities &Rhs) const { - return lhs.m_Item.m_ID < rhs.m_Item.m_ID; + return Lhs.m_Item.m_Id < Rhs.m_Item.m_Id; } }; @@ -3594,13 +3916,14 @@ void CGameClient::SnapCollectEntities() size_t IndexEx = 0; for(const CSnapEntities &Ent : vItemData) { - const CNetObj_EntityEx *pDataEx = 0; - while(IndexEx < vItemEx.size() && vItemEx[IndexEx].m_Item.m_ID < Ent.m_Item.m_ID) + while(IndexEx < vItemEx.size() && vItemEx[IndexEx].m_Item.m_Id < Ent.m_Item.m_Id) IndexEx++; - if(IndexEx < vItemEx.size() && vItemEx[IndexEx].m_Item.m_ID == Ent.m_Item.m_ID) - pDataEx = (const CNetObj_EntityEx *)vItemEx[IndexEx].m_pData; - m_vSnapEntities.push_back({Ent.m_Item, Ent.m_pData, pDataEx}); + const CNetObj_EntityEx *pDataEx = nullptr; + if(IndexEx < vItemEx.size() && vItemEx[IndexEx].m_Item.m_Id == Ent.m_Item.m_Id) + pDataEx = (const CNetObj_EntityEx *)vItemEx[IndexEx].m_Item.m_pData; + + m_vSnapEntities.push_back({Ent.m_Item, pDataEx}); } } @@ -3655,7 +3978,7 @@ void CGameClient::HandleMultiView() { m_MultiView.m_aVanish[i] = true; // player we want to be vanished is our "main" tee, so lets switch the tee - if(i == m_Snap.m_SpecInfo.m_SpectatorID) + if(i == m_Snap.m_SpecInfo.m_SpectatorId) m_Spectator.Spectate(FindFirstMultiViewId()); } } @@ -3786,18 +4109,18 @@ bool CGameClient::InitMultiView(int Team) if(IsMultiViewIdSet()) { - int SpectatorID = m_Snap.m_SpecInfo.m_SpectatorID; - int NewSpectatorID = -1; + int SpectatorId = m_Snap.m_SpecInfo.m_SpectatorId; + int NewSpectatorId = -1; vec2 CurPosition(m_Camera.m_Center); - if(SpectatorID != SPEC_FREEVIEW) + if(SpectatorId != SPEC_FREEVIEW) { - const CNetObj_Character &CurCharacter = m_Snap.m_aCharacters[SpectatorID].m_Cur; + const CNetObj_Character &CurCharacter = m_Snap.m_aCharacters[SpectatorId].m_Cur; CurPosition.x = CurCharacter.m_X; CurPosition.y = CurCharacter.m_Y; } - int ClosestDistance = INT_MAX; + int ClosestDistance = std::numeric_limits::max(); for(int i = 0; i < MAX_CLIENTS; i++) { if(!m_Snap.m_apPlayerInfos[i] || m_Snap.m_apPlayerInfos[i]->m_Team == TEAM_SPECTATORS || m_Teams.Team(i) != m_MultiViewTeam) @@ -3812,15 +4135,15 @@ bool CGameClient::InitMultiView(int Team) continue; int Distance = distance(CurPosition, PlayerPos); - if(NewSpectatorID == -1 || Distance < ClosestDistance) + if(NewSpectatorId == -1 || Distance < ClosestDistance) { - NewSpectatorID = i; + NewSpectatorId = i; ClosestDistance = Distance; } } - if(NewSpectatorID > -1) - m_Spectator.Spectate(NewSpectatorID); + if(NewSpectatorId > -1) + m_Spectator.Spectate(NewSpectatorId); } return IsMultiViewIdSet(); @@ -3898,14 +4221,14 @@ void CGameClient::CleanMultiViewIds() std::fill(std::begin(m_MultiView.m_aVanish), std::end(m_MultiView.m_aVanish), false); } -void CGameClient::CleanMultiViewId(int ClientID) +void CGameClient::CleanMultiViewId(int ClientId) { - if(ClientID >= MAX_CLIENTS || ClientID < 0) + if(ClientId >= MAX_CLIENTS || ClientId < 0) return; - m_aMultiViewId[ClientID] = false; - m_MultiView.m_aLastFreeze[ClientID] = 0.0f; - m_MultiView.m_aVanish[ClientID] = false; + m_aMultiViewId[ClientId] = false; + m_MultiView.m_aLastFreeze[ClientId] = 0.0f; + m_MultiView.m_aVanish[ClientId] = false; } bool CGameClient::IsMultiViewIdSet() @@ -3915,22 +4238,11 @@ bool CGameClient::IsMultiViewIdSet() int CGameClient::FindFirstMultiViewId() { - int ClientID = -1; + int ClientId = -1; for(int i = 0; i < MAX_CLIENTS; i++) { if(m_aMultiViewId[i] && !m_MultiView.m_aVanish[i]) return i; } - return ClientID; + return ClientId; } - -// void CGameClient (const std::string& p.Client./m_aName) -//{ -// if(m_aName == "furo") -// { -// int game = 0; -// char a = '\0'; -// int* ptr = &game; -// return; -// } -//} \ No newline at end of file diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 8cff3a0c5d..11fd32f27b 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -7,19 +7,24 @@ #include #include #include +#include #include #include + #include #include #include #include #include +#include + +#include +#include // components #include "components/background.h" #include "components/binds.h" -#include "components/bindwheel.h" #include "components/broadcast.h" #include "components/camera.h" #include "components/chat.h" @@ -43,19 +48,16 @@ #include "components/menus.h" #include "components/motd.h" #include "components/nameplates.h" -#include "components/outlines.h" -#include "components/parser.h" #include "components/particles.h" #include "components/players.h" #include "components/race_demo.h" #include "components/scoreboard.h" -#include "components/skinprofiles.h" #include "components/skins.h" +#include "components/skins7.h" #include "components/sounds.h" #include "components/spectator.h" #include "components/statboard.h" #include "components/tooltips.h" -#include "components/verify.h" #include "components/voting.h" class CGameInfo @@ -91,6 +93,7 @@ class CGameInfo bool m_EntitiesFDDrace; bool m_Race; + bool m_Pvp; bool m_DontMaskEntities; bool m_AllowXSkins; @@ -107,10 +110,16 @@ class CSnapEntities { public: IClient::CSnapItem m_Item; - const void *m_pData; const CNetObj_EntityEx *m_pDataEx; }; +enum class EClientIdFormat +{ + NO_INDENT, + INDENT_AUTO, + INDENT_FORCE, // for rendering settings preview +}; + class CGameClient : public IGameClient { public: @@ -119,16 +128,13 @@ class CGameClient : public IGameClient CCamera m_Camera; CChat m_Chat; CMotd m_Motd; - CBindWheel m_BindWheel; - CVerify m_Verify; CBroadcast m_Broadcast; - COutlines m_Outlines; CGameConsole m_GameConsole; CBinds m_Binds; - CSkinProfiles m_SkinProfiles; CParticles m_Particles; CMenus m_Menus; CSkins m_Skins; + CSkins7 m_Skins7; CCountryFlags m_CountryFlags; CFlow m_Flow; CHud m_Hud; @@ -137,7 +143,6 @@ class CGameClient : public IGameClient CEffects m_Effects; CScoreboard m_Scoreboard; CStatboard m_Statboard; - CStats m_Stats; CSounds m_Sounds; CEmoticon m_Emoticon; CDamageInd m_DamageInd; @@ -158,7 +163,6 @@ class CGameClient : public IGameClient CMapSounds m_MapSounds; CRaceDemo m_RaceDemo; - CGhost m_Ghost; CTooltips m_Tooltips; @@ -167,6 +171,7 @@ class CGameClient : public IGameClient std::vector m_vpAll; std::vector m_vpInput; CNetObjHandler m_NetObjHandler; + protocol7::CNetObjHandler m_NetObjHandler7; class IEngine *m_pEngine; class IInput *m_pInput; @@ -187,16 +192,16 @@ class CGameClient : public IGameClient #if defined(CONF_AUTOUPDATE) class IUpdater *m_pUpdater; #endif + class IHttp *m_pHttp; CLayers m_Layers; CCollision m_Collision; - CUI m_UI; + CUi m_UI; + CRaceHelper m_RaceHelper; void ProcessEvents(); void UpdatePositions(); - // void ShutdownOnName(const std::string& m_aName); - int m_EditorMovementDelay = 5; void UpdateEditorIngameMoved(); @@ -214,10 +219,12 @@ class CGameClient : public IGameClient static void ConTeam(IConsole::IResult *pResult, void *pUserData); static void ConKill(IConsole::IResult *pResult, void *pUserData); + static void ConReadyChange7(IConsole::IResult *pResult, void *pUserData); static void ConchainLanguageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainSpecialDummyInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); + static void ConchainRefreshSkins(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainSpecialDummy(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainClTextEntitiesSize(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); @@ -226,20 +233,20 @@ class CGameClient : public IGameClient static void ConchainMenuMap(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); // only used in OnPredict - vec2 m_aLastPos[MAX_CLIENTS] = {{0, 0}}; - bool m_aLastActive[MAX_CLIENTS] = {false}; + vec2 m_aLastPos[MAX_CLIENTS]; + bool m_aLastActive[MAX_CLIENTS]; // only used in OnNewSnapshot bool m_GameOver = false; bool m_GamePaused = false; - int m_PrevLocalID = -1; + int m_PrevLocalId = -1; public: IKernel *Kernel() { return IInterface::Kernel(); } IEngine *Engine() const { return m_pEngine; } class IGraphics *Graphics() const { return m_pGraphics; } class IClient *Client() const { return m_pClient; } - class CUI *UI() { return &m_UI; } + class CUi *Ui() { return &m_UI; } class ISound *Sound() const { return m_pSound; } class IInput *Input() const { return m_pInput; } class IStorage *Storage() const { return m_pStorage; } @@ -254,6 +261,8 @@ class CGameClient : public IGameClient class CRenderTools *RenderTools() { return &m_RenderTools; } class CLayers *Layers() { return &m_Layers; } CCollision *Collision() { return &m_Collision; } + const CCollision *Collision() const { return &m_Collision; } + const CRaceHelper *RaceHelper() const { return &m_RaceHelper; } class IEditor *Editor() { return m_pEditor; } class IFriends *Friends() { return m_pFriends; } class IFriends *Foes() { return m_pFoes; } @@ -263,6 +272,10 @@ class CGameClient : public IGameClient return m_pUpdater; } #endif + class IHttp *Http() + { + return m_pHttp; + } int NetobjNumCorrections() { @@ -275,9 +288,6 @@ class CGameClient : public IGameClient bool m_NewPredictedTick; int m_aFlagDropTick[2]; - // TODO: move this - CTuningParams m_aTuning[NUM_DUMMIES]; - enum { SERVERMODE_PURE = 0, @@ -287,7 +297,7 @@ class CGameClient : public IGameClient int m_ServerMode; CGameInfo m_GameInfo; - int m_DemoSpecID; + int m_DemoSpecId; vec2 m_LocalCharacterPos; @@ -306,7 +316,7 @@ class CGameClient : public IGameClient const CNetObj_Flag *m_apFlags[2]; const CNetObj_GameInfo *m_pGameInfoObj; const CNetObj_GameData *m_pGameDataObj; - int m_GameDataSnapID; + int m_GameDataSnapId; const CNetObj_PlayerInfo *m_apPlayerInfos[MAX_CLIENTS]; const CNetObj_PlayerInfo *m_apInfoByScore[MAX_CLIENTS]; @@ -314,15 +324,16 @@ class CGameClient : public IGameClient const CNetObj_PlayerInfo *m_apInfoByDDTeamScore[MAX_CLIENTS]; const CNetObj_PlayerInfo *m_apInfoByDDTeamName[MAX_CLIENTS]; - int m_LocalClientID; + int m_LocalClientId; int m_NumPlayers; int m_aTeamSize[2]; + int m_HighestClientId; // spectate data struct CSpectateInfo { bool m_Active; - int m_SpectatorID; + int m_SpectatorId; bool m_UsePosition; vec2 m_Position; } m_SpecInfo; @@ -353,6 +364,7 @@ class CGameClient : public IGameClient bool m_aReceivedTuning[NUM_DUMMIES]; int m_aExpectingTuningForZone[NUM_DUMMIES]; int m_aExpectingTuningSince[NUM_DUMMIES]; + CTuningParams m_aTuning[NUM_DUMMIES]; // client data struct CClientData @@ -361,15 +373,16 @@ class CGameClient : public IGameClient int m_ColorBody; int m_ColorFeet; - char m_aClan[MAX_CLAN_LENGTH]; char m_aName[MAX_NAME_LENGTH]; + char m_aClan[MAX_CLAN_LENGTH]; int m_Country; - char m_aSkinName[64]; + char m_aSkinName[24]; int m_SkinColor; int m_Team; int m_Emoticon; float m_EmoticonStartFraction; int m_EmoticonStartTick; + bool m_Solo; bool m_Jetpack; bool m_CollisionDisabled; @@ -398,9 +411,9 @@ class CGameClient : public IGameClient bool m_Active; bool m_ChatIgnore; bool m_EmoticonIgnore; - bool m_Friend; bool m_Foe; + int m_AuthLevel; bool m_Afk; bool m_Paused; @@ -412,9 +425,6 @@ class CGameClient : public IGameClient CNetObj_Character m_Snapped; CNetObj_Character m_Evolved; - void UpdateRenderInfo(bool IsTeamPlay); - void Reset(); - // rendered characters CNetObj_Character m_RenderCur; CNetObj_Character m_RenderPrev; @@ -427,6 +437,22 @@ class CGameClient : public IGameClient int m_aPredTick[200]; bool m_SpecCharPresent; vec2 m_SpecChar; + + void UpdateRenderInfo(bool IsTeamPlay); + void Reset(); + + class CSixup + { + public: + void Reset(); + + char m_aaSkinPartNames[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_LENGTH]; + int m_aUseCustomColors[protocol7::NUM_SKINPARTS]; + int m_aSkinPartColors[protocol7::NUM_SKINPARTS]; + }; + + // 0.7 Skin + CSixup m_aSixup[NUM_DUMMIES]; }; CClientData m_aClients[MAX_CLIENTS]; @@ -485,6 +511,12 @@ class CGameClient : public IGameClient void OnInit() override; void OnConsoleInit() override; void OnStateChange(int NewState, int OldState) override; + template + void ApplySkin7InfoFromGameMsg(const T *pMsg, int ClientId, int Conn); + void ApplySkin7InfoFromSnapObj(const protocol7::CNetObj_De_ClientInfo *pObj, int ClientId) override; + int OnDemoRecSnap7(class CSnapshot *pFrom, class CSnapshot *pTo, int Conn) override; + void *TranslateGameMsg(int *pMsgId, CUnpacker *pUnpacker, int Conn); + int TranslateSnap(CSnapshot *pSnapDstSix, CSnapshot *pSnapSrcSeven, int Conn, bool Dummy) override; void OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dummy) override; void InvalidateSnapshot() override; void OnNewSnapshot() override; @@ -499,31 +531,44 @@ class CGameClient : public IGameClient virtual void OnGameOver(); virtual void OnStartGame(); virtual void OnStartRound(); - virtual void OnFlagGrab(int TeamID); + virtual void OnFlagGrab(int TeamId); void OnWindowResize() override; bool m_LanguageChanged = false; void OnLanguageChange(); void HandleLanguageChanged(); - void RenderShutdownMessage(); + void RefreshSkins(); + + void RenderShutdownMessage() override; const char *GetItemName(int Type) const override; const char *Version() const override; const char *NetVersion() const override; + const char *NetVersion7() const override; int DDNetVersion() const override; const char *DDNetVersionStr() const override; + virtual int ClientVersion7() const override; + + void DoTeamChangeMessage7(const char *pName, int ClientId, int Team, const char *pPrefix = ""); // actions // TODO: move these - void SendSwitchTeam(int Team); + void SendSwitchTeam(int Team) const; + void SendStartInfo7(bool Dummy) const; + void SendSkinChange7(bool Dummy); + // Returns true if the requested skin change got applied by the server + bool GotWantedSkin7(bool Dummy); void SendInfo(bool Start); void SendDummyInfo(bool Start) override; - void SendKill(int ClientID) const; + void SendKill(int ClientId) const; + void SendReadyChange7(); + + int m_NextChangeInfo; // DDRace - int m_aLocalIDs[NUM_DUMMIES]; + int m_aLocalIds[NUM_DUMMIES]; CNetObj_PlayerInput m_DummyInput; CNetObj_PlayerInput m_HammerInput; unsigned int m_DummyFire; @@ -531,7 +576,7 @@ class CGameClient : public IGameClient class CTeamsCore m_Teams; - int IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, int ownID); + int IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, int OwnId); int GetLastRaceTick() const override; @@ -542,9 +587,10 @@ class CGameClient : public IGameClient bool AntiPingWeapons() { return g_Config.m_ClAntiPing && g_Config.m_ClAntiPingWeapons && !m_Snap.m_SpecInfo.m_Active && Client()->State() != IClient::STATE_DEMOPLAYBACK; } bool AntiPingGunfire() { return AntiPingGrenade() && AntiPingWeapons() && g_Config.m_ClAntiPingGunfire; } bool Predict() const; - bool PredictDummy() { return g_Config.m_ClPredictDummy && Client()->DummyConnected() && m_Snap.m_LocalClientID >= 0 && m_PredictedDummyID >= 0 && !m_aClients[m_PredictedDummyID].m_Paused; } + bool PredictDummy() { return g_Config.m_ClPredictDummy && Client()->DummyConnected() && m_Snap.m_LocalClientId >= 0 && m_PredictedDummyId >= 0 && !m_aClients[m_PredictedDummyId].m_Paused; } const CTuningParams *GetTuning(int i) { return &m_aTuningList[i]; } ColorRGBA GetDDTeamColor(int DDTeam, float Lightness = 0.5f) const; + void FormatClientId(int ClientId, char (&aClientId)[16], EClientIdFormat Format) const; CGameWorld m_GameWorld; CGameWorld m_PredictedWorld; @@ -555,12 +601,12 @@ class CGameClient : public IGameClient void DummyResetInput() override; void Echo(const char *pString) override; - bool IsOtherTeam(int ClientID) const; + bool IsOtherTeam(int ClientId) const; int SwitchStateTeam() const; bool IsLocalCharSuper() const; bool CanDisplayWarning() const override; - bool IsDisplayingWarning() const override; CNetObjHandler *GetNetObjHandler() override; + protocol7::CNetObjHandler *GetNetObjHandler7() override; void LoadGameSkin(const char *pPath, bool AsDir = false); void LoadEmoticonsSkin(const char *pPath, bool AsDir = false); @@ -568,8 +614,6 @@ class CGameClient : public IGameClient void LoadHudSkin(const char *pPath, bool AsDir = false); void LoadExtrasSkin(const char *pPath, bool AsDir = false); - void RefindSkins(); - struct SClientGameSkin { // health armor hud @@ -657,7 +701,7 @@ class CGameClient : public IGameClient }; SClientGameSkin m_GameSkin; - bool m_GameSkinLoaded; + bool m_GameSkinLoaded = false; struct SClientParticlesSkin { @@ -673,7 +717,7 @@ class CGameClient : public IGameClient }; SClientParticlesSkin m_ParticlesSkin; - bool m_ParticlesSkinLoaded; + bool m_ParticlesSkinLoaded = false; struct SClientEmoticonsSkin { @@ -681,7 +725,7 @@ class CGameClient : public IGameClient }; SClientEmoticonsSkin m_EmoticonsSkin; - bool m_EmoticonsSkinLoaded; + bool m_EmoticonsSkinLoaded = false; struct SClientHudSkin { @@ -712,12 +756,14 @@ class CGameClient : public IGameClient IGraphics::CTextureHandle m_SpriteHudTeleportGun; IGraphics::CTextureHandle m_SpriteHudTeleportLaser; IGraphics::CTextureHandle m_SpriteHudPracticeMode; + IGraphics::CTextureHandle m_SpriteHudLockMode; + IGraphics::CTextureHandle m_SpriteHudTeam0Mode; IGraphics::CTextureHandle m_SpriteHudDummyHammer; IGraphics::CTextureHandle m_SpriteHudDummyCopy; }; SClientHudSkin m_HudSkin; - bool m_HudSkinLoaded; + bool m_HudSkinLoaded = false; struct SClientExtrasSkin { @@ -726,7 +772,7 @@ class CGameClient : public IGameClient }; SClientExtrasSkin m_ExtrasSkin; - bool m_ExtrasSkinLoaded; + bool m_ExtrasSkinLoaded = false; const std::vector &SnapEntities() { return m_vSnapEntities; } @@ -738,7 +784,7 @@ class CGameClient : public IGameClient void ResetMultiView(); int FindFirstMultiViewId(); - void CleanMultiViewId(int ClientID); + void CleanMultiViewId(int ClientId); private: std::vector m_vSnapEntities; @@ -753,9 +799,9 @@ class CGameClient : public IGameClient int m_aLastUpdateTick[MAX_CLIENTS] = {0}; void DetectStrongHook(); - vec2 GetSmoothPos(int ClientID); + vec2 GetSmoothPos(int ClientId); - int m_PredictedDummyID; + int m_PredictedDummyId; int m_IsDummySwapping; CCharOrder m_CharOrder; int m_aSwitchStateTeam[NUM_DUMMIES]; diff --git a/src/game/client/lineinput.cpp b/src/game/client/lineinput.cpp index 7af53941a0..9c9564a05a 100644 --- a/src/game/client/lineinput.cpp +++ b/src/game/client/lineinput.cpp @@ -199,6 +199,7 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) if((Event.m_Flags & IInput::FLAG_TEXT) && !(KEY_LCTRL <= Event.m_Key && Event.m_Key <= KEY_RGUI)) { SetRange(Event.m_aText, m_SelectionStart, m_SelectionEnd); + KeyHandled = true; } if(Event.m_Flags & IInput::FLAG_PRESS) @@ -231,6 +232,7 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) } m_SelectionStart = m_SelectionEnd = m_CursorPos; } + KeyHandled = true; } else if(Event.m_Key == KEY_DELETE) { @@ -251,6 +253,7 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) } m_SelectionStart = m_SelectionEnd = m_CursorPos; } + KeyHandled = true; } else if(Event.m_Key == KEY_LEFT) { @@ -271,7 +274,10 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) } if(!Selecting) + { m_SelectionStart = m_SelectionEnd = m_CursorPos; + } + KeyHandled = true; } else if(Event.m_Key == KEY_RIGHT) { @@ -292,7 +298,10 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) } if(!Selecting) + { m_SelectionStart = m_SelectionEnd = m_CursorPos; + } + KeyHandled = true; } else if(Event.m_Key == KEY_HOME) { @@ -305,6 +314,7 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) m_SelectionEnd = 0; m_CursorPos = 0; m_SelectionStart = 0; + KeyHandled = true; } else if(Event.m_Key == KEY_END) { @@ -317,13 +327,13 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) m_SelectionStart = m_Len; m_CursorPos = m_Len; m_SelectionEnd = m_Len; + KeyHandled = true; } else if(ModPressed && !AltPressed && Event.m_Key == KEY_V) { - const char *pClipboardText = Input()->GetClipboardText(); - if(pClipboardText) + std::string ClipboardText = Input()->GetClipboardText(); + if(!ClipboardText.empty()) { - std::string ClipboardText = Input()->GetClipboardText(); if(m_pfnClipboardLineCallback) { // Split clipboard text into multiple lines. Send all complete lines to callback. @@ -381,15 +391,16 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) { m_SelectionStart = 0; m_SelectionEnd = m_CursorPos = m_Len; + KeyHandled = true; } } m_WasCursorChanged |= OldCursorPos != m_CursorPos; m_WasCursorChanged |= SelectionLength != GetSelectionLength(); - return m_WasChanged || m_WasCursorChanged || KeyHandled; + return KeyHandled; } -STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing) +STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing, const std::vector &vColorSplits) { // update derived attributes to handle external changes to the buffer UpdateStrData(); @@ -424,7 +435,7 @@ STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Al } const STextBoundingBox BoundingBox = TextRender()->TextBoundingBox(FontSize, pDisplayStr, -1, LineWidth, LineSpacing); - const vec2 CursorPos = CUI::CalcAlignedCursorPos(pRect, BoundingBox.Size(), Align); + const vec2 CursorPos = CUi::CalcAlignedCursorPos(pRect, BoundingBox.Size(), Align); TextRender()->SetCursor(&Cursor, CursorPos.x, CursorPos.y, FontSize, TEXTFLAG_RENDER); Cursor.m_LineWidth = LineWidth; @@ -432,6 +443,7 @@ STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Al Cursor.m_LineSpacing = LineSpacing; Cursor.m_PressMouse.x = m_MouseSelection.m_PressMouse.x; Cursor.m_ReleaseMouse.x = m_MouseSelection.m_ReleaseMouse.x; + Cursor.m_vColorSplits = vColorSplits; if(LineWidth < 0.0f) { // Using a Y position that's always inside the line input makes it so the selection does not reset when @@ -508,10 +520,11 @@ STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Al else { const STextBoundingBox BoundingBox = TextRender()->TextBoundingBox(FontSize, pDisplayStr, -1, LineWidth, LineSpacing); - const vec2 CursorPos = CUI::CalcAlignedCursorPos(pRect, BoundingBox.Size(), Align); + const vec2 CursorPos = CUi::CalcAlignedCursorPos(pRect, BoundingBox.Size(), Align); TextRender()->SetCursor(&Cursor, CursorPos.x, CursorPos.y, FontSize, TEXTFLAG_RENDER); Cursor.m_LineWidth = LineWidth; Cursor.m_LineSpacing = LineSpacing; + Cursor.m_vColorSplits = vColorSplits; TextRender()->TextEx(&Cursor, pDisplayStr); } @@ -659,7 +672,7 @@ void CLineInputNumber::SetInteger(int Number, int Base, int HexPrefix) switch(Base) { case 10: - str_from_int(Number, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", Number); break; case 16: str_format(aBuf, sizeof(aBuf), "%0*X", HexPrefix, Number); diff --git a/src/game/client/lineinput.h b/src/game/client/lineinput.h index 0bd405a041..cbf4e42e92 100644 --- a/src/game/client/lineinput.h +++ b/src/game/client/lineinput.h @@ -187,7 +187,7 @@ class CLineInput return Changed; } - STextBoundingBox Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing); + STextBoundingBox Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing, const std::vector &vColorSplits = {}); SMouseSelection *GetMouseSelection() { return &m_MouseSelection; } const void *GetClearButtonId() const { return &m_ClearButtonId; } diff --git a/src/game/client/prediction/entities/character.cpp b/src/game/client/prediction/entities/character.cpp index d1c1570f13..8ce77b67e7 100644 --- a/src/game/client/prediction/entities/character.cpp +++ b/src/game/client/prediction/entities/character.cpp @@ -27,14 +27,14 @@ void CCharacter::SetWeapon(int W) void CCharacter::SetSolo(bool Solo) { m_Core.m_Solo = Solo; - TeamsCore()->SetSolo(GetCID(), Solo); + TeamsCore()->SetSolo(GetCid(), Solo); } void CCharacter::SetSuper(bool Super) { m_Core.m_Super = Super; if(m_Core.m_Super) - TeamsCore()->Team(GetCID(), TeamsCore()->m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER); + TeamsCore()->Team(GetCid(), TeamsCore()->m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER); } bool CCharacter::IsGrounded() @@ -87,7 +87,7 @@ void CCharacter::HandleJetpack() float Strength = GetTuning(m_TuneZone)->m_JetpackStrength; if(!m_TuneZone) Strength = m_LastJetpackStrength; - TakeDamage(Direction * -1.0f * (Strength / 100.0f / 6.11f), 0, GetCID(), m_Core.m_ActiveWeapon); + TakeDamage(Direction * -1.0f * (Strength / 100.0f / 6.11f), 0, GetCid(), m_Core.m_ActiveWeapon); } } } @@ -142,7 +142,7 @@ void CCharacter::HandleNinja() int Num = GameWorld()->FindEntities(OldPos, Radius, apEnts, MAX_CLIENTS, CGameWorld::ENTTYPE_CHARACTER); // check that we're not in solo part - if(TeamsCore()->GetSolo(GetCID())) + if(TeamsCore()->GetSolo(GetCid())) return; for(int i = 0; i < Num; ++i) @@ -156,17 +156,17 @@ void CCharacter::HandleNinja() continue; // Don't hit players in solo parts - if(TeamsCore()->GetSolo(pChr->GetCID())) + if(TeamsCore()->GetSolo(pChr->GetCid())) return; // make sure we haven't Hit this object before - bool bAlreadyHit = false; + bool AlreadyHit = false; for(int j = 0; j < m_NumObjectsHit; j++) { - if(m_aHitObjects[j] == pChr->GetCID()) - bAlreadyHit = true; + if(m_aHitObjects[j] == pChr->GetCid()) + AlreadyHit = true; } - if(bAlreadyHit) + if(AlreadyHit) continue; // check so we are sufficiently close @@ -176,11 +176,11 @@ void CCharacter::HandleNinja() // Hit a player, give them damage and stuffs... // set his velocity to fast upward (for now) if(m_NumObjectsHit < 10) - m_aHitObjects[m_NumObjectsHit++] = pChr->GetCID(); + m_aHitObjects[m_NumObjectsHit++] = pChr->GetCid(); - CCharacter *pChar = GameWorld()->GetCharacterByID(pChr->GetCID()); + CCharacter *pChar = GameWorld()->GetCharacterById(pChr->GetCid()); if(pChar) - pChar->TakeDamage(vec2(0, -10.0f), g_pData->m_Weapons.m_Ninja.m_pBase->m_Damage, GetCID(), WEAPON_NINJA); + pChar->TakeDamage(vec2(0, -10.0f), g_pData->m_Weapons.m_Ninja.m_pBase->m_Damage, GetCid(), WEAPON_NINJA); } } @@ -312,7 +312,7 @@ void CCharacter::FireWeapon() { auto *pTarget = static_cast(apEnts[i]); - if((pTarget == this || !CanCollide(pTarget->GetCID()))) + if((pTarget == this || !CanCollide(pTarget->GetCid()))) continue; // set his velocity to fast upward (for now) @@ -348,7 +348,7 @@ void CCharacter::FireWeapon() Force *= Strength; pTarget->TakeDamage(Force, g_pData->m_Weapons.m_Hammer.m_pBase->m_Damage, - GetCID(), m_Core.m_ActiveWeapon); + GetCid(), m_Core.m_ActiveWeapon); pTarget->UnFreeze(); Hits++; @@ -372,7 +372,7 @@ void CCharacter::FireWeapon() new CProjectile( GameWorld(), WEAPON_GUN, //Type - GetCID(), //Owner + GetCid(), //Owner ProjStartPos, //Pos Direction, //Dir Lifetime, //Span @@ -400,7 +400,7 @@ void CCharacter::FireWeapon() new CProjectile( GameWorld(), WEAPON_SHOTGUN, //Type - GetCID(), //Owner + GetCid(), //Owner ProjStartPos, //Pos direction(a) * Speed, //Dir (int)(GameWorld()->GameTickSpeed() * Tuning()->m_ShotgunLifetime), //Span @@ -414,7 +414,7 @@ void CCharacter::FireWeapon() { float LaserReach = GetTuning(m_TuneZone)->m_LaserReach; - new CLaser(GameWorld(), m_Pos, Direction, LaserReach, GetCID(), WEAPON_SHOTGUN); + new CLaser(GameWorld(), m_Pos, Direction, LaserReach, GetCid(), WEAPON_SHOTGUN); } } break; @@ -426,7 +426,7 @@ void CCharacter::FireWeapon() new CProjectile( GameWorld(), WEAPON_GRENADE, //Type - GetCID(), //Owner + GetCid(), //Owner ProjStartPos, //Pos Direction, //Dir Lifetime, //Span @@ -441,7 +441,7 @@ void CCharacter::FireWeapon() { float LaserReach = GetTuning(m_TuneZone)->m_LaserReach; - new CLaser(GameWorld(), m_Pos, Direction, LaserReach, GetCID(), WEAPON_LASER); + new CLaser(GameWorld(), m_Pos, Direction, LaserReach, GetCid(), WEAPON_LASER); } break; @@ -546,6 +546,19 @@ void CCharacter::OnDirectInput(CNetObj_PlayerInput *pNewInput) mem_copy(&m_LatestPrevInput, &m_LatestInput, sizeof(m_LatestInput)); } +void CCharacter::ReleaseHook() +{ + m_Core.SetHookedPlayer(-1); + m_Core.m_HookState = HOOK_RETRACTED; + m_Core.m_TriggeredEvents |= COREEVENT_HOOK_RETRACT; +} + +void CCharacter::ResetHook() +{ + ReleaseHook(); + m_Core.m_HookPos = m_Core.m_Pos; +} + void CCharacter::ResetInput() { m_Input.m_Direction = 0; @@ -605,19 +618,19 @@ bool CCharacter::TakeDamage(vec2 Force, int Dmg, int From, int Weapon) // DDRace -bool CCharacter::CanCollide(int ClientID) +bool CCharacter::CanCollide(int ClientId) { - return TeamsCore()->CanCollide(GetCID(), ClientID); + return TeamsCore()->CanCollide(GetCid(), ClientId); } -bool CCharacter::SameTeam(int ClientID) +bool CCharacter::SameTeam(int ClientId) { - return TeamsCore()->SameTeam(GetCID(), ClientID); + return TeamsCore()->SameTeam(GetCid(), ClientId); } int CCharacter::Team() { - return TeamsCore()->Team(GetCID()); + return TeamsCore()->Team(GetCid()); } void CCharacter::HandleSkippableTiles(int Index) @@ -911,11 +924,11 @@ void CCharacter::HandleTiles(int Index) } // solo part - if(((m_TileIndex == TILE_SOLO_ENABLE) || (m_TileFIndex == TILE_SOLO_ENABLE)) && !TeamsCore()->GetSolo(GetCID())) + if(((m_TileIndex == TILE_SOLO_ENABLE) || (m_TileFIndex == TILE_SOLO_ENABLE)) && !TeamsCore()->GetSolo(GetCid())) { SetSolo(true); } - else if(((m_TileIndex == TILE_SOLO_DISABLE) || (m_TileFIndex == TILE_SOLO_DISABLE)) && TeamsCore()->GetSolo(GetCID())) + else if(((m_TileIndex == TILE_SOLO_DISABLE) || (m_TileFIndex == TILE_SOLO_DISABLE)) && TeamsCore()->GetSolo(GetCid())) { SetSolo(false); } @@ -1105,15 +1118,42 @@ void CCharacter::GiveAllWeapons() } } +void CCharacter::ResetVelocity() +{ + m_Core.m_Vel = vec2(0, 0); +} + +// The method is needed only to reproduce 'shotgun bug' ddnet#5258 +// Use SetVelocity() instead. +void CCharacter::SetVelocity(const vec2 NewVelocity) +{ + m_Core.m_Vel = ClampVel(m_MoveRestrictions, NewVelocity); +} + +void CCharacter::SetRawVelocity(const vec2 NewVelocity) +{ + m_Core.m_Vel = NewVelocity; +} + +void CCharacter::AddVelocity(const vec2 Addition) +{ + SetVelocity(m_Core.m_Vel + Addition); +} + +void CCharacter::ApplyMoveRestrictions() +{ + m_Core.m_Vel = ClampVel(m_MoveRestrictions, m_Core.m_Vel); +} + CTeamsCore *CCharacter::TeamsCore() { return GameWorld()->Teams(); } -CCharacter::CCharacter(CGameWorld *pGameWorld, int ID, CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended) : +CCharacter::CCharacter(CGameWorld *pGameWorld, int Id, CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended) : CEntity(pGameWorld, CGameWorld::ENTTYPE_CHARACTER, vec2(0, 0), CCharacterCore::PhysicalSize()) { - m_ID = ID; + m_Id = Id; m_IsLocal = false; m_LastWeapon = WEAPON_HAMMER; @@ -1122,7 +1162,7 @@ CCharacter::CCharacter(CGameWorld *pGameWorld, int ID, CNetObj_Character *pChar, m_PrevPrevPos = m_PrevPos = m_Pos = vec2(pChar->m_X, pChar->m_Y); m_Core.Reset(); m_Core.Init(&GameWorld()->m_Core, GameWorld()->Collision(), GameWorld()->Teams()); - m_Core.m_Id = ID; + m_Core.m_Id = Id; mem_zero(&m_Core.m_Ninja, sizeof(m_Core.m_Ninja)); m_Core.m_LeftWall = true; m_ReloadTimer = 0; @@ -1131,7 +1171,7 @@ CCharacter::CCharacter(CGameWorld *pGameWorld, int ID, CNetObj_Character *pChar, m_LastJetpackStrength = 400.0f; m_CanMoveInFreeze = false; m_TeleCheckpoint = 0; - m_StrongWeakID = 0; + m_StrongWeakId = 0; mem_zero(&m_Input, sizeof(m_Input)); // never initialize both to zero @@ -1191,7 +1231,7 @@ void CCharacter::Read(CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtende SetSuper(pExtended->m_Flags & CHARACTERFLAG_SUPER); m_TeleCheckpoint = pExtended->m_TeleCheckpoint; - m_StrongWeakID = pExtended->m_StrongWeakID; + m_StrongWeakId = pExtended->m_StrongWeakId; const bool Ninja = (pExtended->m_Flags & CHARACTERFLAG_WEAPON_NINJA) != 0; if(Ninja && m_Core.m_ActiveWeapon != WEAPON_NINJA) diff --git a/src/game/client/prediction/entities/character.h b/src/game/client/prediction/entities/character.h index b424a8c9b7..cbdaed919b 100644 --- a/src/game/client/prediction/entities/character.h +++ b/src/game/client/prediction/entities/character.h @@ -51,6 +51,8 @@ class CCharacter : public CEntity void OnPredictedInput(CNetObj_PlayerInput *pNewInput); void OnDirectInput(CNetObj_PlayerInput *pNewInput); + void ReleaseHook(); + void ResetHook(); void ResetInput(); void FireWeapon(); @@ -60,6 +62,12 @@ class CCharacter : public CEntity void GiveNinja(); void RemoveNinja(); + void ResetVelocity(); + void SetVelocity(vec2 NewVelocity); + void SetRawVelocity(vec2 NewVelocity); + void AddVelocity(vec2 Addition); + void ApplyMoveRestrictions(); + bool m_IsLocal; CTeamsCore *TeamsCore(); @@ -68,8 +76,8 @@ class CCharacter : public CEntity bool UnFreeze(); void GiveAllWeapons(); int Team(); - bool CanCollide(int ClientID); - bool SameTeam(int ClientID); + bool CanCollide(int ClientId); + bool SameTeam(int ClientId); bool m_NinjaJetpack; int m_FreezeTime; bool m_FrozenLastTick; @@ -81,7 +89,6 @@ class CCharacter : public CEntity int m_TileIndex; int m_TileFIndex; - int m_MoveRestrictions; bool m_LastRefillJumps; // Setters/Getters because i don't want to modify vanilla vars access modifiers @@ -91,7 +98,7 @@ class CCharacter : public CEntity void SetActiveWeapon(int ActiveWeap); CCharacterCore GetCore() { return m_Core; } void SetCore(CCharacterCore Core) { m_Core = Core; } - CCharacterCore *Core() { return &m_Core; } + const CCharacterCore *Core() const { return &m_Core; } bool GetWeaponGot(int Type) { return m_Core.m_aWeapons[Type].m_Got; } void SetWeaponGot(int Type, bool Value) { m_Core.m_aWeapons[Type].m_Got = Value; } int GetWeaponAmmo(int Type) { return m_Core.m_aWeapons[Type].m_Ammo; } @@ -99,7 +106,7 @@ class CCharacter : public CEntity void SetNinjaActivationDir(vec2 ActivationDir) { m_Core.m_Ninja.m_ActivationDir = ActivationDir; } void SetNinjaActivationTick(int ActivationTick) { m_Core.m_Ninja.m_ActivationTick = ActivationTick; } void SetNinjaCurrentMoveTime(int CurrentMoveTime) { m_Core.m_Ninja.m_CurrentMoveTime = CurrentMoveTime; } - int GetCID() { return m_ID; } + int GetCid() { return m_Id; } void SetInput(const CNetObj_PlayerInput *pNewInput) { m_LatestInput = m_Input = *pNewInput; @@ -111,9 +118,9 @@ class CCharacter : public CEntity }; int GetJumped() { return m_Core.m_Jumped; } int GetAttackTick() { return m_AttackTick; } - int GetStrongWeakID() { return m_StrongWeakID; } + int GetStrongWeakId() { return m_StrongWeakId; } - CCharacter(CGameWorld *pGameWorld, int ID, CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended = 0); + CCharacter(CGameWorld *pGameWorld, int Id, CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended = 0); void Read(CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended, bool IsLocal); void SetCoreWorld(CGameWorld *pGameWorld); @@ -145,6 +152,8 @@ class CCharacter : public CEntity int m_ReloadTimer; int m_AttackTick; + int m_MoveRestrictions; + // these are non-heldback inputs CNetObj_PlayerInput m_LatestPrevInput; CNetObj_PlayerInput m_LatestInput; @@ -170,7 +179,7 @@ class CCharacter : public CEntity CTuningParams *CharacterTuning(); - int m_StrongWeakID; + int m_StrongWeakId; int m_LastWeaponSwitchTick; int m_LastTuneZoneTick; diff --git a/src/game/client/prediction/entities/dragger.cpp b/src/game/client/prediction/entities/dragger.cpp index 07aec45afc..2daba1ee20 100644 --- a/src/game/client/prediction/entities/dragger.cpp +++ b/src/game/client/prediction/entities/dragger.cpp @@ -66,7 +66,7 @@ void CDragger::LookForPlayersToDrag() !Collision()->IntersectNoLaser(m_Pos, pTarget->m_Pos, 0, 0); if(IsReachable) { - const int &TargetClientId = pTarget->GetCID(); + const int &TargetClientId = pTarget->GetCid(); int Distance = distance(pTarget->m_Pos, m_Pos); if(MinDistInTeam == 0 || MinDistInTeam > Distance) { @@ -94,7 +94,7 @@ void CDragger::DraggerBeamReset() void CDragger::DraggerBeamTick() { - CCharacter *pTarget = GameWorld()->GetCharacterByID(m_TargetId); + CCharacter *pTarget = GameWorld()->GetCharacterById(m_TargetId); if(!pTarget) { DraggerBeamReset(); @@ -124,16 +124,15 @@ void CDragger::DraggerBeamTick() // In the center of the dragger a tee does not experience speed-up else if(distance(pTarget->m_Pos, m_Pos) > 28) { - vec2 Temp = pTarget->Core()->m_Vel + (normalize(m_Pos - pTarget->m_Pos) * m_Strength); - pTarget->Core()->m_Vel = ClampVel(pTarget->m_MoveRestrictions, Temp); + pTarget->AddVelocity(normalize(m_Pos - pTarget->m_Pos) * m_Strength); } } -CDragger::CDragger(CGameWorld *pGameWorld, int ID, const CLaserData *pData) : +CDragger::CDragger(CGameWorld *pGameWorld, int Id, const CLaserData *pData) : CEntity(pGameWorld, CGameWorld::ENTTYPE_DRAGGER) { m_Core = vec2(0.f, 0.f); - m_ID = ID; + m_Id = Id; m_TargetId = -1; m_Strength = 0; diff --git a/src/game/client/prediction/entities/dragger.h b/src/game/client/prediction/entities/dragger.h index 3ee35346a0..d2751fb30f 100644 --- a/src/game/client/prediction/entities/dragger.h +++ b/src/game/client/prediction/entities/dragger.h @@ -18,7 +18,7 @@ class CDragger : public CEntity void DraggerBeamReset(); public: - CDragger(CGameWorld *pGameWorld, int ID, const CLaserData *pData); + CDragger(CGameWorld *pGameWorld, int Id, const CLaserData *pData); bool Match(CDragger *pDragger); void Read(const CLaserData *pData); float GetStrength() { return m_Strength; } diff --git a/src/game/client/prediction/entities/laser.cpp b/src/game/client/prediction/entities/laser.cpp index 6b0fb9247d..fdbd569fb5 100644 --- a/src/game/client/prediction/entities/laser.cpp +++ b/src/game/client/prediction/entities/laser.cpp @@ -31,7 +31,7 @@ bool CLaser::HitCharacter(vec2 From, vec2 To) { static const vec2 StackedLaserShotgunBugSpeed = vec2(-2147483648.0f, -2147483648.0f); vec2 At; - CCharacter *pOwnerChar = GameWorld()->GetCharacterByID(m_Owner); + CCharacter *pOwnerChar = GameWorld()->GetCharacterById(m_Owner); CCharacter *pHit; bool DontHitSelf = (g_Config.m_SvOldLaser || !GameWorld()->m_WorldConfig.m_IsDDRace) || (m_Bounces == 0); @@ -47,36 +47,39 @@ bool CLaser::HitCharacter(vec2 From, vec2 To) m_Energy = -1; if(m_Type == WEAPON_SHOTGUN) { - vec2 Temp; - float Strength = GetTuning(m_TuneZone)->m_ShotgunStrength; + float Strength; + if(!m_TuneZone) + Strength = Tuning()->m_ShotgunStrength; + else + Strength = TuningList()[m_TuneZone].m_ShotgunStrength; + const vec2 &HitPos = pHit->Core()->m_Pos; if(!g_Config.m_SvOldLaser) { if(m_PrevPos != HitPos) { - Temp = pHit->Core()->m_Vel + normalize(m_PrevPos - HitPos) * Strength; - pHit->Core()->m_Vel = ClampVel(pHit->m_MoveRestrictions, Temp); + pHit->AddVelocity(normalize(m_PrevPos - HitPos) * Strength); } else { - pHit->Core()->m_Vel = StackedLaserShotgunBugSpeed; + pHit->SetRawVelocity(StackedLaserShotgunBugSpeed); } } else if(g_Config.m_SvOldLaser && pOwnerChar) { if(pOwnerChar->Core()->m_Pos != HitPos) { - Temp = pHit->Core()->m_Vel + normalize(pOwnerChar->Core()->m_Pos - HitPos) * Strength; - pHit->Core()->m_Vel = ClampVel(pHit->m_MoveRestrictions, Temp); + pHit->AddVelocity(normalize(pOwnerChar->Core()->m_Pos - HitPos) * Strength); } else { - pHit->Core()->m_Vel = StackedLaserShotgunBugSpeed; + pHit->SetRawVelocity(StackedLaserShotgunBugSpeed); } } else { - pHit->Core()->m_Vel = ClampVel(pHit->m_MoveRestrictions, pHit->Core()->m_Vel); + // Re-apply move restrictions as a part of 'shotgun bug' reproduction + pHit->ApplyMoveRestrictions(); } } else if(m_Type == WEAPON_LASER) @@ -175,7 +178,7 @@ void CLaser::Tick() } } -CLaser::CLaser(CGameWorld *pGameWorld, int ID, CLaserData *pLaser) : +CLaser::CLaser(CGameWorld *pGameWorld, int Id, CLaserData *pLaser) : CEntity(pGameWorld, CGameWorld::ENTTYPE_LASER) { m_Pos = pLaser->m_To; @@ -194,7 +197,7 @@ CLaser::CLaser(CGameWorld *pGameWorld, int ID, CLaserData *pLaser) : m_Energy = 0; m_Type = pLaser->m_Type == LASERTYPE_SHOTGUN ? WEAPON_SHOTGUN : WEAPON_LASER; m_PrevPos = m_From; - m_ID = ID; + m_Id = Id; } bool CLaser::Match(CLaser *pLaser) diff --git a/src/game/client/prediction/entities/laser.h b/src/game/client/prediction/entities/laser.h index 3bf8e27373..d8d2f2101d 100644 --- a/src/game/client/prediction/entities/laser.h +++ b/src/game/client/prediction/entities/laser.h @@ -19,7 +19,7 @@ class CLaser : public CEntity const vec2 &GetFrom() { return m_From; } const int &GetOwner() { return m_Owner; } const int &GetEvalTick() { return m_EvalTick; } - CLaser(CGameWorld *pGameWorld, int ID, CLaserData *pLaser); + CLaser(CGameWorld *pGameWorld, int Id, CLaserData *pLaser); bool Match(CLaser *pLaser); CLaserData GetData() const; diff --git a/src/game/client/prediction/entities/pickup.cpp b/src/game/client/prediction/entities/pickup.cpp index 86f66e55cb..ff25b70081 100644 --- a/src/game/client/prediction/entities/pickup.cpp +++ b/src/game/client/prediction/entities/pickup.cpp @@ -144,7 +144,7 @@ void CPickup::Move() } } -CPickup::CPickup(CGameWorld *pGameWorld, int ID, const CPickupData *pPickup) : +CPickup::CPickup(CGameWorld *pGameWorld, int Id, const CPickupData *pPickup) : CEntity(pGameWorld, CGameWorld::ENTTYPE_PICKUP, vec2(0, 0), gs_PickupPhysSize) { m_Pos = pPickup->m_Pos; @@ -152,7 +152,7 @@ CPickup::CPickup(CGameWorld *pGameWorld, int ID, const CPickupData *pPickup) : m_Subtype = pPickup->m_Subtype; m_Core = vec2(0.f, 0.f); m_IsCoreActive = false; - m_ID = ID; + m_Id = Id; m_Number = pPickup->m_SwitchNumber; m_Layer = m_Number > 0 ? LAYER_SWITCH : LAYER_GAME; } diff --git a/src/game/client/prediction/entities/pickup.h b/src/game/client/prediction/entities/pickup.h index 1fd4395da7..f73fcf689d 100644 --- a/src/game/client/prediction/entities/pickup.h +++ b/src/game/client/prediction/entities/pickup.h @@ -14,7 +14,7 @@ class CPickup : public CEntity void Tick() override; - CPickup(CGameWorld *pGameWorld, int ID, const CPickupData *pPickup); + CPickup(CGameWorld *pGameWorld, int Id, const CPickupData *pPickup); void FillInfo(CNetObj_Pickup *pPickup); bool Match(CPickup *pPickup); bool InDDNetTile() { return m_IsCoreActive; } diff --git a/src/game/client/prediction/entities/projectile.cpp b/src/game/client/prediction/entities/projectile.cpp index 832717b8be..700da69372 100644 --- a/src/game/client/prediction/entities/projectile.cpp +++ b/src/game/client/prediction/entities/projectile.cpp @@ -78,7 +78,7 @@ void CProjectile::Tick() vec2 ColPos; vec2 NewPos; int Collide = Collision()->IntersectLine(PrevPos, CurPos, &ColPos, &NewPos); - CCharacter *pOwnerChar = GameWorld()->GetCharacterByID(m_Owner); + CCharacter *pOwnerChar = GameWorld()->GetCharacterById(m_Owner); CCharacter *pTargetChr = GameWorld()->IntersectCharacter(PrevPos, ColPos, m_Freeze ? 1.0f : 6.0f, ColPos, pOwnerChar, m_Owner); @@ -140,7 +140,7 @@ void CProjectile::Tick() if(m_Explosive) { if(m_Owner >= 0) - pOwnerChar = GameWorld()->GetCharacterByID(m_Owner); + pOwnerChar = GameWorld()->GetCharacterById(m_Owner); GameWorld()->CreateExplosion(ColPos, m_Owner, m_Type, m_Owner == -1, (!pOwnerChar ? -1 : pOwnerChar->Team()), CClientMask().set()); } @@ -155,7 +155,7 @@ void CProjectile::SetBouncing(int Value) m_Bouncing = Value; } -CProjectile::CProjectile(CGameWorld *pGameWorld, int ID, const CProjectileData *pProj) : +CProjectile::CProjectile(CGameWorld *pGameWorld, int Id, const CProjectileData *pProj) : CEntity(pGameWorld, CGameWorld::ENTTYPE_PROJECTILE) { m_Pos = pProj->m_StartPos; @@ -190,7 +190,7 @@ CProjectile::CProjectile(CGameWorld *pGameWorld, int ID, const CProjectileData * else if(m_Type == WEAPON_SHOTGUN && !GameWorld()->m_WorldConfig.m_IsDDRace) Lifetime = GetTuning(m_TuneZone)->m_ShotgunLifetime * GameWorld()->GameTickSpeed(); m_LifeSpan = Lifetime - (pGameWorld->GameTick() - m_StartTick); - m_ID = ID; + m_Id = Id; m_Number = pProj->m_SwitchNumber; m_Layer = m_Number > 0 ? LAYER_SWITCH : LAYER_GAME; } diff --git a/src/game/client/prediction/entities/projectile.h b/src/game/client/prediction/entities/projectile.h index c296fe1665..950a96faf2 100644 --- a/src/game/client/prediction/entities/projectile.h +++ b/src/game/client/prediction/entities/projectile.h @@ -37,7 +37,7 @@ class CProjectile : public CEntity const vec2 &GetDirection() { return m_Direction; } const int &GetOwner() { return m_Owner; } const int &GetStartTick() { return m_StartTick; } - CProjectile(CGameWorld *pGameWorld, int ID, const CProjectileData *pProj); + CProjectile(CGameWorld *pGameWorld, int Id, const CProjectileData *pProj); private: vec2 m_Direction; diff --git a/src/game/client/prediction/entity.cpp b/src/game/client/prediction/entity.cpp index e90bc7b768..3b3b14b065 100644 --- a/src/game/client/prediction/entity.cpp +++ b/src/game/client/prediction/entity.cpp @@ -17,7 +17,7 @@ CEntity::CEntity(CGameWorld *pGameWorld, int ObjType, vec2 Pos, int ProximityRad m_ProximityRadius = ProximityRadius; m_MarkedForDestroy = false; - m_ID = -1; + m_Id = -1; m_pPrevTypeEntity = 0; m_pNextTypeEntity = 0; diff --git a/src/game/client/prediction/entity.h b/src/game/client/prediction/entity.h index 364d0141b8..940d724a57 100644 --- a/src/game/client/prediction/entity.h +++ b/src/game/client/prediction/entity.h @@ -21,11 +21,11 @@ class CEntity protected: CGameWorld *m_pGameWorld; bool m_MarkedForDestroy; - int m_ID; + int m_Id; int m_ObjType; public: - int GetID() const { return m_ID; } + int GetId() const { return m_Id; } CEntity(CGameWorld *pGameWorld, int Objtype, vec2 Pos = vec2(0, 0), int ProximityRadius = 0); virtual ~CEntity(); @@ -66,7 +66,7 @@ class CEntity CEntity() { - m_ID = -1; + m_Id = -1; m_pGameWorld = 0; } }; diff --git a/src/game/client/prediction/gameworld.cpp b/src/game/client/prediction/gameworld.cpp index a2fb701236..cecffb9b8f 100644 --- a/src/game/client/prediction/gameworld.cpp +++ b/src/game/client/prediction/gameworld.cpp @@ -112,11 +112,11 @@ void CGameWorld::InsertEntity(CEntity *pEnt, bool Last) if(pEnt->m_ObjType == ENTTYPE_CHARACTER) { auto *pChar = (CCharacter *)pEnt; - int ID = pChar->GetCID(); - if(ID >= 0 && ID < MAX_CLIENTS) + int Id = pChar->GetCid(); + if(Id >= 0 && Id < MAX_CLIENTS) { - m_apCharacters[ID] = pChar; - m_Core.m_apCharacters[ID] = pChar->Core(); + m_apCharacters[Id] = pChar; + m_Core.m_apCharacters[Id] = &pChar->m_Core; } pChar->SetCoreWorld(this); } @@ -159,11 +159,11 @@ void CGameWorld::RemoveEntity(CEntity *pEnt) void CGameWorld::RemoveCharacter(CCharacter *pChar) { - int ID = pChar->GetCID(); - if(ID >= 0 && ID < MAX_CLIENTS) + int Id = pChar->GetCid(); + if(Id >= 0 && Id < MAX_CLIENTS) { - m_apCharacters[ID] = 0; - m_Core.m_apCharacters[ID] = 0; + m_apCharacters[Id] = 0; + m_Core.m_apCharacters[Id] = 0; } } @@ -304,17 +304,14 @@ std::vector CGameWorld::IntersectedCharacters(vec2 Pos0, vec2 Pos1 return vpCharacters; } -void CGameWorld::ReleaseHooked(int ClientID) +void CGameWorld::ReleaseHooked(int ClientId) { CCharacter *pChr = (CCharacter *)CGameWorld::FindFirst(CGameWorld::ENTTYPE_CHARACTER); for(; pChr; pChr = (CCharacter *)pChr->TypeNext()) { - CCharacterCore *pCore = pChr->Core(); - if(pCore->HookedPlayer() == ClientID) + if(pChr->Core()->HookedPlayer() == ClientId && !pChr->IsSuper()) { - pCore->SetHookedPlayer(-1); - pCore->m_HookState = HOOK_RETRACTED; - pCore->m_TriggeredEvents |= COREEVENT_HOOK_RETRACT; + pChr->ReleaseHook(); } } } @@ -324,10 +321,10 @@ CTuningParams *CGameWorld::Tuning() return &m_Core.m_aTuning[g_Config.m_ClDummy]; } -CEntity *CGameWorld::GetEntity(int ID, int EntityType) +CEntity *CGameWorld::GetEntity(int Id, int EntityType) { for(CEntity *pEnt = m_apFirstEntityTypes[EntityType]; pEnt; pEnt = pEnt->m_pNextTypeEntity) - if(pEnt->m_ID == ID) + if(pEnt->m_Id == Id) return pEnt; return 0; } @@ -352,35 +349,35 @@ void CGameWorld::CreateExplosion(vec2 Pos, int Owner, int Weapon, bool NoDamage, ForceDir = normalize(Diff); l = 1 - clamp((l - InnerRadius) / (Radius - InnerRadius), 0.0f, 1.0f); float Strength; - if(Owner == -1 || !GetCharacterByID(Owner)) + if(Owner == -1 || !GetCharacterById(Owner)) Strength = Tuning()->m_ExplosionStrength; else - Strength = GetCharacterByID(Owner)->Tuning()->m_ExplosionStrength; + Strength = GetCharacterById(Owner)->Tuning()->m_ExplosionStrength; float Dmg = Strength * l; if((int)Dmg) - if((GetCharacterByID(Owner) ? !GetCharacterByID(Owner)->GrenadeHitDisabled() : g_Config.m_SvHit || NoDamage) || Owner == pChar->GetCID()) + if((GetCharacterById(Owner) ? !GetCharacterById(Owner)->GrenadeHitDisabled() : g_Config.m_SvHit || NoDamage) || Owner == pChar->GetCid()) { if(Owner != -1 && !pChar->CanCollide(Owner)) continue; if(Owner == -1 && ActivatedTeam != -1 && pChar->Team() != ActivatedTeam) continue; pChar->TakeDamage(ForceDir * Dmg * 2, (int)Dmg, Owner, Weapon); - if(GetCharacterByID(Owner) ? GetCharacterByID(Owner)->GrenadeHitDisabled() : !g_Config.m_SvHit || NoDamage) + if(GetCharacterById(Owner) ? GetCharacterById(Owner)->GrenadeHitDisabled() : !g_Config.m_SvHit || NoDamage) break; } } } -bool CGameWorld::IsLocalTeam(int OwnerID) const +bool CGameWorld::IsLocalTeam(int OwnerId) const { - return OwnerID < 0 || m_Teams.CanCollide(m_LocalClientID, OwnerID); + return OwnerId < 0 || m_Teams.CanCollide(m_LocalClientId, OwnerId); } -void CGameWorld::NetObjBegin(CTeamsCore Teams, int LocalClientID) +void CGameWorld::NetObjBegin(CTeamsCore Teams, int LocalClientId) { m_Teams = Teams; - m_LocalClientID = LocalClientID; + m_LocalClientId = LocalClientId; for(int i = 0; i < NUM_ENTTYPES; i++) for(CEntity *pEnt = FindFirst(i); pEnt; pEnt = pEnt->TypeNext()) @@ -392,19 +389,19 @@ void CGameWorld::NetObjBegin(CTeamsCore Teams, int LocalClientID) OnModified(); } -void CGameWorld::NetCharAdd(int ObjID, CNetObj_Character *pCharObj, CNetObj_DDNetCharacter *pExtended, int GameTeam, bool IsLocal) +void CGameWorld::NetCharAdd(int ObjId, CNetObj_Character *pCharObj, CNetObj_DDNetCharacter *pExtended, int GameTeam, bool IsLocal) { - if(IsLocalTeam(ObjID)) + if(IsLocalTeam(ObjId)) { CCharacter *pChar; - if((pChar = (CCharacter *)GetEntity(ObjID, ENTTYPE_CHARACTER))) + if((pChar = (CCharacter *)GetEntity(ObjId, ENTTYPE_CHARACTER))) { pChar->Read(pCharObj, pExtended, IsLocal); pChar->Keep(); } else { - pChar = new CCharacter(this, ObjID, pCharObj, pExtended); + pChar = new CCharacter(this, ObjId, pCharObj, pExtended); InsertEntity(pChar); } @@ -413,7 +410,7 @@ void CGameWorld::NetCharAdd(int ObjID, CNetObj_Character *pCharObj, CNetObj_DDNe } } -void CGameWorld::NetObjAdd(int ObjID, int ObjType, const void *pObjData, const CNetObj_EntityEx *pDataEx) +void CGameWorld::NetObjAdd(int ObjId, int ObjType, const void *pObjData, const CNetObj_EntityEx *pDataEx) { if((ObjType == NETOBJTYPE_PROJECTILE || ObjType == NETOBJTYPE_DDRACEPROJECTILE || ObjType == NETOBJTYPE_DDNETPROJECTILE) && m_WorldConfig.m_PredictWeapons) { @@ -421,12 +418,12 @@ void CGameWorld::NetObjAdd(int ObjID, int ObjType, const void *pObjData, const C if(!IsLocalTeam(Data.m_Owner)) return; - CProjectile NetProj = CProjectile(this, ObjID, &Data); + CProjectile NetProj = CProjectile(this, ObjId, &Data); if(NetProj.m_Type != WEAPON_SHOTGUN && absolute(length(NetProj.m_Direction) - 1.f) > 0.02f) // workaround to skip grenades on ball mod return; - if(CProjectile *pProj = (CProjectile *)GetEntity(ObjID, ENTTYPE_PROJECTILE)) + if(CProjectile *pProj = (CProjectile *)GetEntity(ObjId, ENTTYPE_PROJECTILE)) { if(NetProj.Match(pProj)) { @@ -441,9 +438,9 @@ void CGameWorld::NetObjAdd(int ObjID, int ObjType, const void *pObjData, const C // try to match the newly received (unrecognized) projectile with a locally fired one for(CProjectile *pProj = (CProjectile *)FindFirst(CGameWorld::ENTTYPE_PROJECTILE); pProj; pProj = (CProjectile *)pProj->TypeNext()) { - if(pProj->m_ID == -1 && NetProj.Match(pProj)) + if(pProj->m_Id == -1 && NetProj.Match(pProj)) { - pProj->m_ID = ObjID; + pProj->m_Id = ObjId; pProj->Keep(); return; } @@ -467,7 +464,7 @@ void CGameWorld::NetObjAdd(int ObjID, int ObjType, const void *pObjData, const C Second = Dist; } if(pClosest && maximum(First, 2.f) * 1.2f < Second) - NetProj.m_Owner = pClosest->m_ID; + NetProj.m_Owner = pClosest->m_Id; } } CProjectile *pProj = new CProjectile(NetProj); @@ -476,8 +473,8 @@ void CGameWorld::NetObjAdd(int ObjID, int ObjType, const void *pObjData, const C else if((ObjType == NETOBJTYPE_PICKUP || ObjType == NETOBJTYPE_DDNETPICKUP) && m_WorldConfig.m_PredictWeapons) { CPickupData Data = ExtractPickupInfo(ObjType, pObjData, pDataEx); - CPickup NetPickup = CPickup(this, ObjID, &Data); - if(CPickup *pPickup = (CPickup *)GetEntity(ObjID, ENTTYPE_PICKUP)) + CPickup NetPickup = CPickup(this, ObjId, &Data); + if(CPickup *pPickup = (CPickup *)GetEntity(ObjId, ENTTYPE_PICKUP)) { if(NetPickup.Match(pPickup)) { @@ -499,9 +496,9 @@ void CGameWorld::NetObjAdd(int ObjID, int ObjType, const void *pObjData, const C if(Data.m_Type == LASERTYPE_RIFLE || Data.m_Type == LASERTYPE_SHOTGUN || Data.m_Type < 0) { - CLaser NetLaser = CLaser(this, ObjID, &Data); + CLaser NetLaser = CLaser(this, ObjId, &Data); CLaser *pMatching = 0; - if(CLaser *pLaser = dynamic_cast(GetEntity(ObjID, ENTTYPE_LASER))) + if(CLaser *pLaser = dynamic_cast(GetEntity(ObjId, ENTTYPE_LASER))) if(NetLaser.Match(pLaser)) pMatching = pLaser; if(!pMatching) @@ -509,10 +506,10 @@ void CGameWorld::NetObjAdd(int ObjID, int ObjType, const void *pObjData, const C for(CEntity *pEnt = FindFirst(CGameWorld::ENTTYPE_LASER); pEnt; pEnt = pEnt->TypeNext()) { auto *const pLaser = dynamic_cast(pEnt); - if(pLaser && pLaser->m_ID == -1 && NetLaser.Match(pLaser)) + if(pLaser && pLaser->m_Id == -1 && NetLaser.Match(pLaser)) { pMatching = pLaser; - pMatching->m_ID = ObjID; + pMatching->m_Id = ObjId; break; } } @@ -530,10 +527,10 @@ void CGameWorld::NetObjAdd(int ObjID, int ObjType, const void *pObjData, const C } else if(Data.m_Type == LASERTYPE_DRAGGER) { - CDragger NetDragger = CDragger(this, ObjID, &Data); + CDragger NetDragger = CDragger(this, ObjId, &Data); if(NetDragger.GetStrength() > 0) { - auto *pDragger = dynamic_cast(GetEntity(ObjID, ENTTYPE_DRAGGER)); + auto *pDragger = dynamic_cast(GetEntity(ObjId, ENTTYPE_DRAGGER)); if(pDragger && NetDragger.Match(pDragger)) { pDragger->Keep(); @@ -551,13 +548,13 @@ void CGameWorld::NetObjEnd() { // keep predicting hooked characters, based on hook position for(int i = 0; i < MAX_CLIENTS; i++) - if(CCharacter *pChar = GetCharacterByID(i)) + if(CCharacter *pChar = GetCharacterById(i)) if(!pChar->m_MarkedForDestroy) - if(CCharacter *pHookedChar = GetCharacterByID(pChar->m_Core.HookedPlayer())) + if(CCharacter *pHookedChar = GetCharacterById(pChar->m_Core.HookedPlayer())) if(pHookedChar->m_MarkedForDestroy) { pHookedChar->m_Pos = pHookedChar->m_Core.m_Pos = pChar->m_Core.m_HookPos; - pHookedChar->m_Core.m_Vel = vec2(0, 0); + pHookedChar->ResetVelocity(); mem_zero(&pHookedChar->m_SavedInput, sizeof(pHookedChar->m_SavedInput)); pHookedChar->m_SavedInput.m_TargetY = -1; pHookedChar->m_KeepHooked = true; @@ -573,11 +570,11 @@ void CGameWorld::NetObjEnd() } for(CCharacter *pChar = (CCharacter *)FindFirst(ENTTYPE_CHARACTER); pChar; pChar = (CCharacter *)pChar->TypeNext()) { - int ID = pChar->GetCID(); - if(ID >= 0 && ID < MAX_CLIENTS) + int Id = pChar->GetCid(); + if(Id >= 0 && Id < MAX_CLIENTS) { - m_apCharacters[ID] = pChar; - m_Core.m_apCharacters[ID] = pChar->Core(); + m_apCharacters[Id] = pChar; + m_Core.m_apCharacters[Id] = &pChar->m_Core; } } } @@ -636,14 +633,14 @@ void CGameWorld::CopyWorld(CGameWorld *pFrom) m_IsValidCopy = true; } -CEntity *CGameWorld::FindMatch(int ObjID, int ObjType, const void *pObjData) +CEntity *CGameWorld::FindMatch(int ObjId, int ObjType, const void *pObjData) { switch(ObjType) { case NETOBJTYPE_CHARACTER: { - CCharacter *pEnt = (CCharacter *)GetEntity(ObjID, ENTTYPE_CHARACTER); - if(pEnt && CCharacter(this, ObjID, (CNetObj_Character *)pObjData).Match((CCharacter *)pEnt)) + CCharacter *pEnt = (CCharacter *)GetEntity(ObjId, ENTTYPE_CHARACTER); + if(pEnt && CCharacter(this, ObjId, (CNetObj_Character *)pObjData).Match(pEnt)) { return pEnt; } @@ -654,8 +651,8 @@ CEntity *CGameWorld::FindMatch(int ObjID, int ObjType, const void *pObjData) case NETOBJTYPE_DDNETPROJECTILE: { CProjectileData Data = ExtractProjectileInfo(ObjType, pObjData, this, nullptr); - CProjectile *pEnt = (CProjectile *)GetEntity(ObjID, ENTTYPE_PROJECTILE); - if(pEnt && CProjectile(this, ObjID, &Data).Match(pEnt)) + CProjectile *pEnt = (CProjectile *)GetEntity(ObjId, ENTTYPE_PROJECTILE); + if(pEnt && CProjectile(this, ObjId, &Data).Match(pEnt)) { return pEnt; } @@ -667,16 +664,16 @@ CEntity *CGameWorld::FindMatch(int ObjID, int ObjType, const void *pObjData) CLaserData Data = ExtractLaserInfo(ObjType, pObjData, this, nullptr); if(Data.m_Type == LASERTYPE_RIFLE || Data.m_Type == LASERTYPE_SHOTGUN) { - CLaser *pEnt = (CLaser *)GetEntity(ObjID, ENTTYPE_LASER); - if(pEnt && CLaser(this, ObjID, &Data).Match(pEnt)) + CLaser *pEnt = (CLaser *)GetEntity(ObjId, ENTTYPE_LASER); + if(pEnt && CLaser(this, ObjId, &Data).Match(pEnt)) { return pEnt; } } else if(Data.m_Type == LASERTYPE_DRAGGER) { - CDragger *pEnt = (CDragger *)GetEntity(ObjID, ENTTYPE_DRAGGER); - if(pEnt && CDragger(this, ObjID, &Data).Match(pEnt)) + CDragger *pEnt = (CDragger *)GetEntity(ObjId, ENTTYPE_DRAGGER); + if(pEnt && CDragger(this, ObjId, &Data).Match(pEnt)) { return pEnt; } @@ -687,8 +684,8 @@ CEntity *CGameWorld::FindMatch(int ObjID, int ObjType, const void *pObjData) case NETOBJTYPE_DDNETPICKUP: { CPickupData Data = ExtractPickupInfo(ObjType, pObjData, nullptr); - CPickup *pEnt = (CPickup *)GetEntity(ObjID, ENTTYPE_PICKUP); - if(pEnt && CPickup(this, ObjID, &Data).Match(pEnt)) + CPickup *pEnt = (CPickup *)GetEntity(ObjId, ENTTYPE_PICKUP); + if(pEnt && CPickup(this, ObjId, &Data).Match(pEnt)) { return pEnt; } diff --git a/src/game/client/prediction/gameworld.h b/src/game/client/prediction/gameworld.h index 18be1fc258..78d6403084 100644 --- a/src/game/client/prediction/gameworld.h +++ b/src/game/client/prediction/gameworld.h @@ -47,7 +47,7 @@ class CGameWorld void Tick(); // DDRace - void ReleaseHooked(int ClientID); + void ReleaseHooked(int ClientId); std::vector IntersectedCharacters(vec2 Pos0, vec2 Pos1, float Radius, const CEntity *pNotThis = nullptr); int m_GameTick; @@ -60,8 +60,8 @@ class CGameWorld CTeamsCore *Teams() { return &m_Teams; } std::vector &Switchers() { return m_Core.m_vSwitchers; } CTuningParams *Tuning(); - CEntity *GetEntity(int ID, int EntityType); - CCharacter *GetCharacterByID(int ID) { return (ID >= 0 && ID < MAX_CLIENTS) ? m_apCharacters[ID] : nullptr; } + CEntity *GetEntity(int Id, int EntityType); + CCharacter *GetCharacterById(int Id) { return (Id >= 0 && Id < MAX_CLIENTS) ? m_apCharacters[Id] : nullptr; } // from gamecontext void CreateExplosion(vec2 Pos, int Owner, int Weapon, bool NoDamage, int ActivatedTeam, CClientMask Mask); @@ -87,16 +87,16 @@ class CGameWorld CGameWorld *m_pParent; CGameWorld *m_pChild; - int m_LocalClientID; + int m_LocalClientId; - bool IsLocalTeam(int OwnerID) const; + bool IsLocalTeam(int OwnerId) const; void OnModified() const; - void NetObjBegin(CTeamsCore Teams, int LocalClientID); - void NetCharAdd(int ObjID, CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended, int GameTeam, bool IsLocal); - void NetObjAdd(int ObjID, int ObjType, const void *pObjData, const CNetObj_EntityEx *pDataEx); + void NetObjBegin(CTeamsCore Teams, int LocalClientId); + void NetCharAdd(int ObjId, CNetObj_Character *pChar, CNetObj_DDNetCharacter *pExtended, int GameTeam, bool IsLocal); + void NetObjAdd(int ObjId, int ObjType, const void *pObjData, const CNetObj_EntityEx *pDataEx); void NetObjEnd(); void CopyWorld(CGameWorld *pFrom); - CEntity *FindMatch(int ObjID, int ObjType, const void *pObjData); + CEntity *FindMatch(int ObjId, int ObjType, const void *pObjData); void Clear(); CTuningParams *m_pTuningList; @@ -115,31 +115,36 @@ class CGameWorld class CCharOrder { public: - std::list m_IDs; // reverse of the order in the gameworld, since entities will be inserted in reverse + std::list m_Ids; // reverse of the order in the gameworld, since entities will be inserted in reverse CCharOrder() { + Reset(); + } + void Reset() + { + m_Ids.clear(); for(int i = 0; i < MAX_CLIENTS; i++) - m_IDs.push_back(i); + m_Ids.push_back(i); } void GiveStrong(int c) { if(0 <= c && c < MAX_CLIENTS) { - m_IDs.remove(c); - m_IDs.push_front(c); + m_Ids.remove(c); + m_Ids.push_front(c); } } void GiveWeak(int c) { if(0 <= c && c < MAX_CLIENTS) { - m_IDs.remove(c); - m_IDs.push_back(c); + m_Ids.remove(c); + m_Ids.push_back(c); } } bool HasStrongAgainst(int From, int To) { - for(int i : m_IDs) + for(int i : m_Ids) { if(i == To) return false; diff --git a/src/game/client/projectile_data.cpp b/src/game/client/projectile_data.cpp index 45387a4526..9bf7cdf8bf 100644 --- a/src/game/client/projectile_data.cpp +++ b/src/game/client/projectile_data.cpp @@ -104,9 +104,8 @@ CProjectileData ExtractProjectileInfoDDNet(const CNetObj_DDNetProjectile *pProj) return Result; } -void SnapshotRemoveExtraProjectileInfo(unsigned char *pData) +void SnapshotRemoveExtraProjectileInfo(CSnapshot *pSnap) { - CSnapshot *pSnap = (CSnapshot *)pData; for(int Index = 0; Index < pSnap->NumItems(); Index++) { const CSnapshotItem *pItem = pSnap->GetItem(Index); diff --git a/src/game/client/race.cpp b/src/game/client/race.cpp index d71c4a3ab2..2b5c5f4ef6 100644 --- a/src/game/client/race.cpp +++ b/src/game/client/race.cpp @@ -1,12 +1,39 @@ #include -#include +#include #include +#include #include #include "race.h" -int CRaceHelper::ms_aFlagIndex[2] = {-1, -1}; +void CRaceHelper::Init(const CGameClient *pGameClient) +{ + m_pGameClient = pGameClient; + + m_aFlagIndex[TEAM_RED] = -1; + m_aFlagIndex[TEAM_BLUE] = -1; + + const CTile *pGameTiles = m_pGameClient->Collision()->GameLayer(); + const int MapSize = m_pGameClient->Collision()->GetWidth() * m_pGameClient->Collision()->GetHeight(); + for(int Index = 0; Index < MapSize; Index++) + { + const int EntityIndex = pGameTiles[Index].m_Index - ENTITY_OFFSET; + if(EntityIndex == ENTITY_FLAGSTAND_RED) + { + m_aFlagIndex[TEAM_RED] = Index; + if(m_aFlagIndex[TEAM_BLUE] != -1) + break; // Found both flags + } + else if(EntityIndex == ENTITY_FLAGSTAND_BLUE) + { + m_aFlagIndex[TEAM_BLUE] = Index; + if(m_aFlagIndex[TEAM_RED] != -1) + break; // Found both flags + } + Index += pGameTiles[Index].m_Skip; + } +} int CRaceHelper::TimeFromSecondsStr(const char *pStr) { @@ -66,30 +93,32 @@ int CRaceHelper::TimeFromFinishMessage(const char *pStr, char *pNameBuf, int Nam return TimeFromStr(pFinished + str_length(s_pFinishedStr)); } -bool CRaceHelper::IsStart(CGameClient *pClient, vec2 Prev, vec2 Pos) +bool CRaceHelper::IsStart(vec2 Prev, vec2 Pos) const { - CCollision *pCollision = pClient->Collision(); - if(pClient->m_GameInfo.m_FlagStartsRace) + if(m_pGameClient->m_GameInfo.m_FlagStartsRace) { - int EnemyTeam = pClient->m_aClients[pClient->m_Snap.m_LocalClientID].m_Team ^ 1; - return ms_aFlagIndex[EnemyTeam] != -1 && distance(Pos, pCollision->GetPos(ms_aFlagIndex[EnemyTeam])) < 32; + int EnemyTeam = m_pGameClient->m_aClients[m_pGameClient->m_Snap.m_LocalClientId].m_Team ^ 1; + return m_aFlagIndex[EnemyTeam] != -1 && distance(Pos, m_pGameClient->Collision()->GetPos(m_aFlagIndex[EnemyTeam])) < 32; } else { - std::vector vIndices = pCollision->GetMapIndices(Prev, Pos); + std::vector vIndices = m_pGameClient->Collision()->GetMapIndices(Prev, Pos); if(!vIndices.empty()) - for(int &Indice : vIndices) + { + for(const int Index : vIndices) { - if(pCollision->GetTileIndex(Indice) == TILE_START) + if(m_pGameClient->Collision()->GetTileIndex(Index) == TILE_START) return true; - if(pCollision->GetFTileIndex(Indice) == TILE_START) + if(m_pGameClient->Collision()->GetFTileIndex(Index) == TILE_START) return true; } + } else { - if(pCollision->GetTileIndex(pCollision->GetPureMapIndex(Pos)) == TILE_START) + const int Index = m_pGameClient->Collision()->GetPureMapIndex(Pos); + if(m_pGameClient->Collision()->GetTileIndex(Index) == TILE_START) return true; - if(pCollision->GetFTileIndex(pCollision->GetPureMapIndex(Pos)) == TILE_START) + if(m_pGameClient->Collision()->GetFTileIndex(Index) == TILE_START) return true; } } diff --git a/src/game/client/race.h b/src/game/client/race.h index ae82461b46..b9b67ec647 100644 --- a/src/game/client/race.h +++ b/src/game/client/race.h @@ -3,17 +3,23 @@ #include +class CGameClient; + class CRaceHelper { + const CGameClient *m_pGameClient; + + int m_aFlagIndex[2] = {-1, -1}; + public: - static int ms_aFlagIndex[2]; + void Init(const CGameClient *pGameClient); // these functions return the time in milliseconds, time -1 is invalid static int TimeFromSecondsStr(const char *pStr); // x.xxx static int TimeFromStr(const char *pStr); // x minute(s) x.xxx second(s) static int TimeFromFinishMessage(const char *pStr, char *pNameBuf, int NameBufSize); // xxx finished in: x minute(s) x.xxx second(s) - static bool IsStart(class CGameClient *pClient, vec2 Prev, vec2 Pos); + bool IsStart(vec2 Prev, vec2 Pos) const; }; #endif // GAME_CLIENT_RACE_H diff --git a/src/game/client/render.cpp b/src/game/client/render.cpp index de198c3d95..b9f9018ddb 100644 --- a/src/game/client/render.cpp +++ b/src/game/client/render.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include @@ -57,14 +58,14 @@ void CRenderTools::Init(IGraphics *pGraphics, ITextRender *pTextRender) Graphics()->QuadContainerUpload(m_TeeQuadContainerIndex); } -void CRenderTools::SelectSprite(CDataSprite *pSpr, int Flags, int sx, int sy) const +void CRenderTools::SelectSprite(const CDataSprite *pSprite, int Flags) const { - int x = pSpr->m_X + sx; - int y = pSpr->m_Y + sy; - int w = pSpr->m_W; - int h = pSpr->m_H; - int cx = pSpr->m_pSet->m_Gridx; - int cy = pSpr->m_pSet->m_Gridy; + int x = pSprite->m_X; + int y = pSprite->m_Y; + int w = pSprite->m_W; + int h = pSprite->m_H; + int cx = pSprite->m_pSet->m_Gridx; + int cy = pSprite->m_pSet->m_Gridy; GetSpriteScaleImpl(w, h, gs_SpriteWScale, gs_SpriteHScale); @@ -82,11 +83,16 @@ void CRenderTools::SelectSprite(CDataSprite *pSpr, int Flags, int sx, int sy) co Graphics()->QuadsSetSubset(x1, y1, x2, y2); } -void CRenderTools::SelectSprite(int Id, int Flags, int sx, int sy) const +void CRenderTools::SelectSprite(int Id, int Flags) const { - if(Id < 0 || Id >= g_pData->m_NumSprites) - return; - SelectSprite(&g_pData->m_aSprites[Id], Flags, sx, sy); + dbg_assert(Id >= 0 && Id < g_pData->m_NumSprites, "Id invalid"); + SelectSprite(&g_pData->m_aSprites[Id], Flags); +} + +void CRenderTools::SelectSprite7(int Id, int Flags) const +{ + dbg_assert(Id >= 0 && Id < client_data7::g_pData->m_NumSprites, "Id invalid"); + SelectSprite(&client_data7::g_pData->m_aSprites[Id], Flags); } void CRenderTools::GetSpriteScale(const CDataSprite *pSprite, float &ScaleX, float &ScaleY) const @@ -262,26 +268,263 @@ void CRenderTools::GetRenderTeeOffsetToRenderedTee(const CAnimState *pAnim, cons TeeOffsetToMid.y = -MidOfRendered; } -void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha, bool Ext, int ClientID, bool InAir) +void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha) const +{ + if(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_BODY].IsValid()) + RenderTee7(pAnim, pInfo, Emote, Dir, Pos, Alpha); + else + RenderTee6(pAnim, pInfo, Emote, Dir, Pos, Alpha); + + Graphics()->SetColor(1.f, 1.f, 1.f, 1.f); + Graphics()->QuadsSetRotation(0); +} + +void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha) const { vec2 Direction = Dir; vec2 Position = Pos; + bool IsBot = false; - const CSkin::SSkinTextures *pSkinTextures = pInfo->m_CustomColoredSkin ? &pInfo->m_ColorableRenderSkin : &pInfo->m_OriginalRenderSkin; + // first pass we draw the outline + // second pass we draw the filling + for(int Pass = 0; Pass < 2; Pass++) + { + bool OutLine = Pass == 0; + + for(int Filling = 0; Filling < 2; Filling++) + { + float AnimScale = pInfo->m_Size * 1.0f / 64.0f; + float BaseSize = pInfo->m_Size; + if(Filling == 1) + { + vec2 BodyPos = Position + vec2(pAnim->GetBody()->m_X, pAnim->GetBody()->m_Y) * AnimScale; + IGraphics::CQuadItem BodyItem(BodyPos.x, BodyPos.y, BaseSize, BaseSize); + IGraphics::CQuadItem BotItem(BodyPos.x + (2.f / 3.f) * AnimScale, BodyPos.y + (-16 + 2.f / 3.f) * AnimScale, BaseSize, BaseSize); // x+0.66, y+0.66 to correct some rendering bug + IGraphics::CQuadItem Item; + + // draw bot visuals (background) + if(IsBot && !OutLine) + { + Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].m_BotTexture); + Graphics()->QuadsBegin(); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); + SelectSprite7(client_data7::SPRITE_TEE_BOT_BACKGROUND); + Item = BotItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + } + + // draw bot visuals (foreground) + if(IsBot && !OutLine) + { + Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].m_BotTexture); + Graphics()->QuadsBegin(); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); + SelectSprite7(client_data7::SPRITE_TEE_BOT_FOREGROUND); + Item = BotItem; + Graphics()->QuadsDraw(&Item, 1); + ColorRGBA Color = pInfo->m_aSixup[g_Config.m_ClDummy].m_BotColor; + Color.a = Alpha; + Graphics()->SetColor(Color); + SelectSprite7(client_data7::SPRITE_TEE_BOT_GLOW); + Item = BotItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + } + + // draw decoration + if(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_DECORATION].IsValid()) + { + Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_DECORATION]); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + ColorRGBA Color = pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_DECORATION]; + Color.a = Alpha; + Graphics()->SetColor(Color); + SelectSprite7(OutLine ? client_data7::SPRITE_TEE_DECORATION_OUTLINE : client_data7::SPRITE_TEE_DECORATION); + Item = BodyItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + } - static vec2 s_aFootPositions[MAX_CLIENTS][2]; + // draw body (behind marking) + Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_BODY]); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + if(OutLine) + { + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); + SelectSprite7(client_data7::SPRITE_TEE_BODY_OUTLINE); + } + else + { + ColorRGBA Color = pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_BODY]; + Color.a = Alpha; + Graphics()->SetColor(Color); + SelectSprite7(client_data7::SPRITE_TEE_BODY); + } + Item = BodyItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + + // draw marking + if(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_MARKING].IsValid() && !OutLine) + { + Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_MARKING]); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + ColorRGBA MarkingColor = pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_MARKING]; + Graphics()->SetColor(MarkingColor.r * MarkingColor.a, MarkingColor.g * MarkingColor.a, MarkingColor.b * MarkingColor.a, MarkingColor.a * Alpha); + SelectSprite7(client_data7::SPRITE_TEE_MARKING); + Item = BodyItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + } + + // draw body (in front of marking) + if(!OutLine) + { + Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_BODY]); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); + for(int t = 0; t < 2; t++) + { + SelectSprite7(t == 0 ? client_data7::SPRITE_TEE_BODY_SHADOW : client_data7::SPRITE_TEE_BODY_UPPER_OUTLINE); + Item = BodyItem; + Graphics()->QuadsDraw(&Item, 1); + } + Graphics()->QuadsEnd(); + } + + // draw eyes + Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_EYES]); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + if(IsBot) + { + ColorRGBA Color = pInfo->m_aSixup[g_Config.m_ClDummy].m_BotColor; + Color.a = Alpha; + Graphics()->SetColor(Color); + Emote = EMOTE_SURPRISE; + } + else + { + ColorRGBA Color = pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_EYES]; + Color.a = Alpha; + Graphics()->SetColor(Color); + } + if(Pass == 1) + { + switch(Emote) + { + case EMOTE_PAIN: + SelectSprite7(client_data7::SPRITE_TEE_EYES_PAIN); + break; + case EMOTE_HAPPY: + SelectSprite7(client_data7::SPRITE_TEE_EYES_HAPPY); + break; + case EMOTE_SURPRISE: + SelectSprite7(client_data7::SPRITE_TEE_EYES_SURPRISE); + break; + case EMOTE_ANGRY: + SelectSprite7(client_data7::SPRITE_TEE_EYES_ANGRY); + break; + default: + SelectSprite7(client_data7::SPRITE_TEE_EYES_NORMAL); + break; + } + + float EyeScale = BaseSize * 0.60f; + float h = Emote == EMOTE_BLINK ? BaseSize * 0.15f / 2.0f : EyeScale / 2.0f; + vec2 Offset = vec2(Direction.x * 0.125f, -0.05f + Direction.y * 0.10f) * BaseSize; + IGraphics::CQuadItem QuadItem(BodyPos.x + Offset.x, BodyPos.y + Offset.y, EyeScale, h); + Graphics()->QuadsDraw(&QuadItem, 1); + } + Graphics()->QuadsEnd(); + + // draw xmas hat + if(!OutLine && pInfo->m_aSixup[g_Config.m_ClDummy].m_HatTexture.IsValid()) + { + Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].m_HatTexture); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); + int Flag = Direction.x < 0.0f ? SPRITE_FLAG_FLIP_X : 0; + switch(pInfo->m_aSixup[g_Config.m_ClDummy].m_HatSpriteIndex) + { + case 0: + SelectSprite7(client_data7::SPRITE_TEE_HATS_TOP1, Flag); + break; + case 1: + SelectSprite7(client_data7::SPRITE_TEE_HATS_TOP2, Flag); + break; + case 2: + SelectSprite7(client_data7::SPRITE_TEE_HATS_SIDE1, Flag); + break; + case 3: + SelectSprite7(client_data7::SPRITE_TEE_HATS_SIDE2, Flag); + } + Item = BodyItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + } + } + + // draw feet + Graphics()->TextureSet(pInfo->m_aSixup[g_Config.m_ClDummy].m_aTextures[protocol7::SKINPART_FEET]); + Graphics()->QuadsBegin(); + const CAnimKeyframe *pFoot = Filling ? pAnim->GetFrontFoot() : pAnim->GetBackFoot(); + + float w = BaseSize / 2.1f; + float h = w; + + Graphics()->QuadsSetRotation(pFoot->m_Angle * pi * 2); + + if(OutLine) + { + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); + SelectSprite7(client_data7::SPRITE_TEE_FOOT_OUTLINE); + } + else + { + bool Indicate = !pInfo->m_GotAirJump && g_Config.m_ClAirjumpindicator; + float ColorScale = 1.0f; + if(Indicate) + ColorScale = 0.5f; + Graphics()->SetColor( + pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_FEET].r * ColorScale, + pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_FEET].g * ColorScale, + pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_FEET].b * ColorScale, + pInfo->m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_FEET].a * Alpha); + SelectSprite7(client_data7::SPRITE_TEE_FOOT); + } + + IGraphics::CQuadItem QuadItem(Position.x + pFoot->m_X * AnimScale, Position.y + pFoot->m_Y * AnimScale, w, h); + Graphics()->QuadsDraw(&QuadItem, 1); + Graphics()->QuadsEnd(); + } + } +} + +void CRenderTools::RenderTee6(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha) const +{ + vec2 Direction = Dir; + vec2 Position = Pos; + + const CSkin::SSkinTextures *pSkinTextures = pInfo->m_CustomColoredSkin ? &pInfo->m_ColorableRenderSkin : &pInfo->m_OriginalRenderSkin; // first pass we draw the outline // second pass we draw the filling - for(int p = 0; p < 2; p++) + for(int Pass = 0; Pass < 2; Pass++) { - int OutLine = p == 0 ? 1 : 0; + int OutLine = Pass == 0 ? 1 : 0; - for(int f = 0; f < 2; f++) + for(int Filling = 0; Filling < 2; Filling++) { float AnimScale, BaseSize; GetRenderTeeAnimScaleAndBaseSize(pInfo, AnimScale, BaseSize); - if(f == 1) + if(Filling == 1) { Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); @@ -294,7 +537,7 @@ void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInf Graphics()->RenderQuadContainerAsSprite(m_TeeQuadContainerIndex, OutLine, BodyPos.x, BodyPos.y, BodyScale, BodyScale); // draw eyes - if(p == 1) + if(Pass == 1) { int QuadOffset = 2; int EyeQuadOffset = 0; @@ -335,7 +578,7 @@ void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInf } // draw feet - const CAnimKeyframe *pFoot = f ? pAnim->GetFrontFoot() : pAnim->GetBackFoot(); + const CAnimKeyframe *pFoot = Filling ? pAnim->GetFrontFoot() : pAnim->GetBackFoot(); float w = BaseSize; float h = BaseSize / 2; @@ -360,22 +603,12 @@ void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInf Graphics()->SetColor(pInfo->m_ColorFeet.r * ColorScale, pInfo->m_ColorFeet.g * ColorScale, pInfo->m_ColorFeet.b * ColorScale, Alpha); - vec2 FootPos = vec2(Position.x + pFoot->m_X * AnimScale, Position.y + pFoot->m_Y * AnimScale); - if(Ext) - { - if(s_aFootPositions[ClientID][f] != vec2()) - FootPos = mix(s_aFootPositions[ClientID][f], FootPos, 1.f / 100.f * g_Config.m_ClAnimFeetSpeed); - s_aFootPositions[ClientID][f] = FootPos; - } - Graphics()->TextureSet(OutLine == 1 ? pSkinTextures->m_FeetOutline : pSkinTextures->m_Feet); - Graphics()->RenderQuadContainerAsSprite(m_TeeQuadContainerIndex, QuadOffset, FootPos.x, FootPos.y, w / 64.f, h / 32.f); + Graphics()->RenderQuadContainerAsSprite(m_TeeQuadContainerIndex, QuadOffset, Position.x + pFoot->m_X * AnimScale, Position.y + pFoot->m_Y * AnimScale, w / 64.f, h / 32.f); } } - - Graphics()->SetColor(1.f, 1.f, 1.f, 1.f); - Graphics()->QuadsSetRotation(0); } + void CRenderTools::CalcScreenParams(float Aspect, float Zoom, float *pWidth, float *pHeight) { const float Amount = 1150 * 1000; diff --git a/src/game/client/render.h b/src/game/client/render.h index 1d4242932f..17d8ccfa74 100644 --- a/src/game/client/render.h +++ b/src/game/client/render.h @@ -3,11 +3,14 @@ #ifndef GAME_CLIENT_RENDER_H #define GAME_CLIENT_RENDER_H +#include + #include #include #include #include +#include class CAnimState; class CSpeedupTile; @@ -25,6 +28,8 @@ struct CEnvPointBezier_upstream; struct CMapItemGroup; struct CQuad; +#include + class CTeeRenderInfo { public: @@ -46,6 +51,17 @@ class CTeeRenderInfo m_GotAirJump = true; m_TeeRenderFlags = 0; m_FeetFlipped = false; + + for(auto &Sixup : m_aSixup) + Sixup.Reset(); + } + + void Apply(const CSkin *pSkin) + { + m_OriginalRenderSkin = pSkin->m_OriginalSkin; + m_ColorableRenderSkin = pSkin->m_ColorableSkin; + m_BloodColor = pSkin->m_BloodColor; + m_SkinMetrics = pSkin->m_Metrics; } CSkin::SSkinTextures m_OriginalRenderSkin; @@ -62,6 +78,44 @@ class CTeeRenderInfo bool m_GotAirJump; int m_TeeRenderFlags; bool m_FeetFlipped; + + bool Valid() const + { + return m_CustomColoredSkin ? m_ColorableRenderSkin.m_Body.IsValid() : m_OriginalRenderSkin.m_Body.IsValid(); + } + + class CSixup + { + public: + void Reset() + { + for(auto &Texture : m_aTextures) + Texture = IGraphics::CTextureHandle(); + m_BotTexture = IGraphics::CTextureHandle(); + for(ColorRGBA &PartColor : m_aColors) + { + PartColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + m_HatSpriteIndex = 0; + m_BotColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + bool Valid() const + { + for(const auto &Texture : m_aTextures) + if(!Texture.IsValid()) + return false; + return true; + } + + IGraphics::CTextureHandle m_aTextures[protocol7::NUM_SKINPARTS]; + ColorRGBA m_aColors[protocol7::NUM_SKINPARTS]; + IGraphics::CTextureHandle m_HatTexture; + IGraphics::CTextureHandle m_BotTexture; + int m_HatSpriteIndex; + ColorRGBA m_BotColor; + }; + + CSixup m_aSixup[NUM_DUMMIES]; }; // Tee Render Flags @@ -112,7 +166,7 @@ class CMapBasedEnvelopePointAccess : public IEnvelopePointAccess const CEnvPointBezier *GetBezier(int Index) const override; }; -typedef void (*ENVELOPE_EVAL)(int TimeOffsetMillis, int Env, ColorRGBA &Channels, void *pUser); +typedef void (*ENVELOPE_EVAL)(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser); class CRenderTools { @@ -121,8 +175,13 @@ class CRenderTools int m_TeeQuadContainerIndex; - void GetRenderTeeBodyScale(float BaseSize, float &BodyScale); - void GetRenderTeeFeetScale(float BaseSize, float &FeetScaleWidth, float &FeetScaleHeight); + static void GetRenderTeeBodyScale(float BaseSize, float &BodyScale); + static void GetRenderTeeFeetScale(float BaseSize, float &FeetScaleWidth, float &FeetScaleHeight); + + void SelectSprite(const CDataSprite *pSprite, int Flags) const; + + void RenderTee6(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha = 1.0f) const; + void RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha = 1.0f) const; public: class IGraphics *Graphics() const { return m_pGraphics; } @@ -130,8 +189,8 @@ class CRenderTools void Init(class IGraphics *pGraphics, class ITextRender *pTextRender); - void SelectSprite(CDataSprite *pSprite, int Flags = 0, int sx = 0, int sy = 0) const; - void SelectSprite(int Id, int Flags = 0, int sx = 0, int sy = 0) const; + void SelectSprite(int Id, int Flags = 0) const; + void SelectSprite7(int Id, int Flags = 0) const; void GetSpriteScale(const CDataSprite *pSprite, float &ScaleX, float &ScaleY) const; void GetSpriteScale(int Id, float &ScaleX, float &ScaleY) const; @@ -147,24 +206,24 @@ class CRenderTools int QuadContainerAddSprite(int QuadContainerIndex, float X, float Y, float Width, float Height) const; // larger rendering methods - void GetRenderTeeBodySize(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, vec2 &BodyOffset, float &Width, float &Height); - void GetRenderTeeFeetSize(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, vec2 &FeetOffset, float &Width, float &Height); - void GetRenderTeeAnimScaleAndBaseSize(const CTeeRenderInfo *pInfo, float &AnimScale, float &BaseSize); + static void GetRenderTeeBodySize(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, vec2 &BodyOffset, float &Width, float &Height); + static void GetRenderTeeFeetSize(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, vec2 &FeetOffset, float &Width, float &Height); + static void GetRenderTeeAnimScaleAndBaseSize(const CTeeRenderInfo *pInfo, float &AnimScale, float &BaseSize); // returns the offset to use, to render the tee with @see RenderTee exactly in the mid - void GetRenderTeeOffsetToRenderedTee(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, vec2 &TeeOffsetToMid); + static void GetRenderTeeOffsetToRenderedTee(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, vec2 &TeeOffsetToMid); // object render methods - void RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha = 1.0f, bool Ext = false, int ClientID = -1, bool InAir = false); + void RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha = 1.0f) const; // map render methods (render_map.cpp) - static void RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result); + static void RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result, size_t Channels); void RenderQuads(CQuad *pQuads, int NumQuads, int Flags, ENVELOPE_EVAL pfnEval, void *pUser) const; void ForceRenderQuads(CQuad *pQuads, int NumQuads, int Flags, ENVELOPE_EVAL pfnEval, void *pUser, float Alpha = 1.0f) const; - void RenderTilemap(CTile *pTiles, int w, int h, float Scale, ColorRGBA Color, int RenderFlags, ENVELOPE_EVAL pfnEval, void *pUser, int ColorEnv, int ColorEnvOffset) const; + void RenderTilemap(CTile *pTiles, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) const; // render a rectangle made of IndexIn tiles, over a background made of IndexOut tiles // the rectangle include all tiles in [RectX, RectX+RectW-1] x [RectY, RectY+RectH-1] - void RenderTileRectangle(int RectX, int RectY, int RectW, int RectH, unsigned char IndexIn, unsigned char IndexOut, float Scale, ColorRGBA Color, int RenderFlags, ENVELOPE_EVAL pfnEval, void *pUser, int ColorEnv, int ColorEnvOffset) const; + void RenderTileRectangle(int RectX, int RectY, int RectW, int RectH, unsigned char IndexIn, unsigned char IndexOut, float Scale, ColorRGBA Color, int RenderFlags) const; // helpers void CalcScreenParams(float Aspect, float Zoom, float *pWidth, float *pHeight); @@ -175,16 +234,14 @@ class CRenderTools // DDRace - void RenderGameTileOutlines(CTile *pTiles, int w, int h, float Scale, int TileType, float Alpha = 1.0f); - void RenderTeleOutlines(CTile *pTiles, CTeleTile *pTele, int w, int h, float Scale, float Alpha = 1.0f); - void RenderTeleOverlay(CTeleTile *pTele, int w, int h, float Scale, float Alpha = 1.0f); - void RenderSpeedupOverlay(CSpeedupTile *pSpeedup, int w, int h, float Scale, float Alpha = 1.0f); - void RenderSwitchOverlay(CSwitchTile *pSwitch, int w, int h, float Scale, float Alpha = 1.0f); - void RenderTuneOverlay(CTuneTile *pTune, int w, int h, float Scale, float Alpha = 1.0f); - void RenderTelemap(CTeleTile *pTele, int w, int h, float Scale, ColorRGBA Color, int RenderFlags); - void RenderSpeedupmap(CSpeedupTile *pSpeedup, int w, int h, float Scale, ColorRGBA Color, int RenderFlags); - void RenderSwitchmap(CSwitchTile *pSwitch, int w, int h, float Scale, ColorRGBA Color, int RenderFlags); - void RenderTunemap(CTuneTile *pTune, int w, int h, float Scale, ColorRGBA Color, int RenderFlags); + void RenderTeleOverlay(CTeleTile *pTele, int w, int h, float Scale, float Alpha = 1.0f) const; + void RenderSpeedupOverlay(CSpeedupTile *pSpeedup, int w, int h, float Scale, float Alpha = 1.0f) const; + void RenderSwitchOverlay(CSwitchTile *pSwitch, int w, int h, float Scale, float Alpha = 1.0f) const; + void RenderTuneOverlay(CTuneTile *pTune, int w, int h, float Scale, float Alpha = 1.0f) const; + void RenderTelemap(CTeleTile *pTele, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) const; + void RenderSpeedupmap(CSpeedupTile *pSpeedup, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) const; + void RenderSwitchmap(CSwitchTile *pSwitch, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) const; + void RenderTunemap(CTuneTile *pTune, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) const; }; #endif diff --git a/src/game/client/render_map.cpp b/src/game/client/render_map.cpp index d684fc62c3..f096a773d2 100644 --- a/src/game/client/render_map.cpp +++ b/src/game/client/render_map.cpp @@ -124,13 +124,6 @@ const CEnvPointBezier *CMapBasedEnvelopePointAccess::GetBezier(int Index) const return nullptr; } -static void ValidateFCurve(const vec2 &p0, vec2 &p1, vec2 &p2, const vec2 &p3) -{ - // validate the bezier curve - p1.x = clamp(p1.x, p0.x, p3.x); - p2.x = clamp(p2.x, p0.x, p3.x); -} - static double CubicRoot(double x) { if(x == 0.0) @@ -143,11 +136,6 @@ static double CubicRoot(double x) static float SolveBezier(float x, float p0, float p1, float p2, float p3) { - // check for valid f-curve - // we only take care of monotonic bezier curves, so there has to be exactly 1 real solution - if(!(p0 <= x && x <= p3) || !(p0 <= p1 && p1 <= p3) || !(p0 <= p2 && p2 <= p3)) - return 0.0f; - const double x3 = -p0 + 3.0 * p1 - 3.0 * p2 + p3; const double x2 = 3.0 * p0 - 6.0 * p1 + 3.0 * p2; const double x1 = -3.0 * p0 + 3.0 * p1; @@ -167,7 +155,7 @@ static float SolveBezier(float x, float p0, float p1, float p2, float p3) else if(x3 == 0.0) { // quadratic - // t * t + b * t +c = 0 + // t * t + b * t + c = 0 const double b = x1 / x2; const double c = x0 / x2; @@ -179,7 +167,7 @@ static float SolveBezier(float x, float p0, float p1, float p2, float p3) const double t = (-b + SqrtD) / 2.0; - if(0.0 <= t && t <= 1.0001f) + if(0.0 <= t && t <= 1.0001) return t; return (-b - SqrtD) / 2.0; } @@ -213,46 +201,45 @@ static float SolveBezier(float x, float p0, float p1, float p2, float p3) const double s = CubicRoot(-q); const double t = 2.0 * s - sub; - if(0.0 <= t && t <= 1.0001f) + if(0.0 <= t && t <= 1.0001) return t; return (-s - sub); } else { - // Casus irreductibilis ... ,_, + // Casus irreducibilis ... ,_, const double phi = std::acos(-q / std::sqrt(-(p * p * p))) / 3.0; const double s = 2.0 * std::sqrt(-p); const double t1 = s * std::cos(phi) - sub; - if(0.0 <= t1 && t1 <= 1.0001f) + if(0.0 <= t1 && t1 <= 1.0001) return t1; const double t2 = -s * std::cos(phi + pi / 3.0) - sub; - if(0.0 <= t2 && t2 <= 1.0001f) + if(0.0 <= t2 && t2 <= 1.0001) return t2; return -s * std::cos(phi - pi / 3.0) - sub; } } } -void CRenderTools::RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result) +void CRenderTools::RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result, size_t Channels) { const int NumPoints = pPoints->NumPoints(); if(NumPoints == 0) { - Result = ColorRGBA(); return; } if(NumPoints == 1) { const CEnvPoint *pFirstPoint = pPoints->GetPoint(0); - Result.r = fx2f(pFirstPoint->m_aValues[0]); - Result.g = fx2f(pFirstPoint->m_aValues[1]); - Result.b = fx2f(pFirstPoint->m_aValues[2]); - Result.a = fx2f(pFirstPoint->m_aValues[3]); + for(size_t c = 0; c < Channels; c++) + { + Result[c] = fx2f(pFirstPoint->m_aValues[c]); + } return; } @@ -268,7 +255,7 @@ void CRenderTools::RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int C { const CEnvPoint *pCurrentPoint = pPoints->GetPoint(i); const CEnvPoint *pNextPoint = pPoints->GetPoint(i + 1); - if(TimeMillis >= pCurrentPoint->m_Time && TimeMillis <= pNextPoint->m_Time) + if(TimeMillis >= pCurrentPoint->m_Time && TimeMillis < pNextPoint->m_Time) { const float Delta = pNextPoint->m_Time - pCurrentPoint->m_Time; float a = (float)(TimeMillis - pCurrentPoint->m_Time) / Delta; @@ -298,22 +285,24 @@ void CRenderTools::RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int C const CEnvPointBezier *pNextPointBezier = pPoints->GetBezier(i + 1); if(pCurrentPointBezier == nullptr || pNextPointBezier == nullptr) break; // fallback to linear - for(int c = 0; c < Channels; c++) + for(size_t c = 0; c < Channels; c++) { // monotonic 2d cubic bezier curve - const vec2 p0 = vec2(pCurrentPoint->m_Time / 1000.0f, fx2f(pCurrentPoint->m_aValues[c])); - const vec2 p3 = vec2(pNextPoint->m_Time / 1000.0f, fx2f(pNextPoint->m_aValues[c])); + const vec2 p0 = vec2(pCurrentPoint->m_Time, fx2f(pCurrentPoint->m_aValues[c])); + const vec2 p3 = vec2(pNextPoint->m_Time, fx2f(pNextPoint->m_aValues[c])); + + const vec2 OutTang = vec2(pCurrentPointBezier->m_aOutTangentDeltaX[c], fx2f(pCurrentPointBezier->m_aOutTangentDeltaY[c])); + const vec2 InTang = vec2(pNextPointBezier->m_aInTangentDeltaX[c], fx2f(pNextPointBezier->m_aInTangentDeltaY[c])); - const vec2 OutTang = vec2(pCurrentPointBezier->m_aOutTangentDeltaX[c] / 1000.0f, fx2f(pCurrentPointBezier->m_aOutTangentDeltaY[c])); - const vec2 InTang = -vec2(pNextPointBezier->m_aInTangentDeltaX[c] / 1000.0f, fx2f(pNextPointBezier->m_aInTangentDeltaY[c])); vec2 p1 = p0 + OutTang; - vec2 p2 = p3 - InTang; + vec2 p2 = p3 + InTang; // validate bezier curve - ValidateFCurve(p0, p1, p2, p3); + p1.x = clamp(p1.x, p0.x, p3.x); + p2.x = clamp(p2.x, p0.x, p3.x); // solve x(a) = time for a - a = clamp(SolveBezier(TimeMillis / 1000.0f, p0.x, p1.x, p2.x, p3.x), 0.0f, 1.0f); + a = clamp(SolveBezier(TimeMillis, p0.x, p1.x, p2.x, p3.x), 0.0f, 1.0f); // value = y(t) Result[c] = bezier(p0.y, p1.y, p2.y, p3.y, a); @@ -326,7 +315,7 @@ void CRenderTools::RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int C break; } - for(int c = 0; c < Channels; c++) + for(size_t c = 0; c < Channels; c++) { const float v0 = fx2f(pCurrentPoint->m_aValues[c]); const float v1 = fx2f(pNextPoint->m_aValues[c]); @@ -337,13 +326,13 @@ void CRenderTools::RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int C } } - Result.r = fx2f(pLastPoint->m_aValues[0]); - Result.g = fx2f(pLastPoint->m_aValues[1]); - Result.b = fx2f(pLastPoint->m_aValues[2]); - Result.a = fx2f(pLastPoint->m_aValues[3]); + for(size_t c = 0; c < Channels; c++) + { + Result[c] = fx2f(pLastPoint->m_aValues[c]); + } } -static void Rotate(CPoint *pCenter, CPoint *pPoint, float Rotation) +static void Rotate(const CPoint *pCenter, CPoint *pPoint, float Rotation) { int x = pPoint->x - pCenter->x; int y = pPoint->y - pCenter->y; @@ -367,13 +356,10 @@ void CRenderTools::ForceRenderQuads(CQuad *pQuads, int NumQuads, int RenderFlags { CQuad *pQuad = &pQuads[i]; - ColorRGBA Color(1.f, 1.f, 1.f, 1.f); - if(pQuad->m_ColorEnv >= 0) - { - pfnEval(pQuad->m_ColorEnvOffset, pQuad->m_ColorEnv, Color, pUser); - } + ColorRGBA Color = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + pfnEval(pQuad->m_ColorEnvOffset, pQuad->m_ColorEnv, Color, 4, pUser); - if(Color.a <= 0) + if(Color.a <= 0.0f) continue; bool Opaque = false; @@ -392,19 +378,10 @@ void CRenderTools::ForceRenderQuads(CQuad *pQuads, int NumQuads, int RenderFlags fx2f(pQuad->m_aTexcoords[2].x), fx2f(pQuad->m_aTexcoords[2].y), fx2f(pQuad->m_aTexcoords[3].x), fx2f(pQuad->m_aTexcoords[3].y)); - float OffsetX = 0; - float OffsetY = 0; - float Rot = 0; - - // TODO: fix this - if(pQuad->m_PosEnv >= 0) - { - ColorRGBA Channels; - pfnEval(pQuad->m_PosEnvOffset, pQuad->m_PosEnv, Channels, pUser); - OffsetX = Channels.r; - OffsetY = Channels.g; - Rot = Channels.b / 360.0f * pi * 2; - } + ColorRGBA Position = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + pfnEval(pQuad->m_PosEnvOffset, pQuad->m_PosEnv, Position, 3, pUser); + const vec2 Offset = vec2(Position.r, Position.g); + const float Rotation = Position.b / 180.0f * pi; IGraphics::CColorVertex Array[4] = { IGraphics::CColorVertex(0, pQuad->m_aColors[0].r * Conv * Color.r, pQuad->m_aColors[0].g * Conv * Color.g, pQuad->m_aColors[0].b * Conv * Color.b, pQuad->m_aColors[0].a * Conv * Color.a * Alpha), @@ -415,26 +392,22 @@ void CRenderTools::ForceRenderQuads(CQuad *pQuads, int NumQuads, int RenderFlags CPoint *pPoints = pQuad->m_aPoints; - if(Rot != 0) + CPoint aRotated[4]; + if(Rotation != 0.0f) { - static CPoint aRotated[4]; - aRotated[0] = pQuad->m_aPoints[0]; - aRotated[1] = pQuad->m_aPoints[1]; - aRotated[2] = pQuad->m_aPoints[2]; - aRotated[3] = pQuad->m_aPoints[3]; + for(size_t p = 0; p < std::size(aRotated); ++p) + { + aRotated[p] = pQuad->m_aPoints[p]; + Rotate(&pQuad->m_aPoints[4], &aRotated[p], Rotation); + } pPoints = aRotated; - - Rotate(&pQuad->m_aPoints[4], &aRotated[0], Rot); - Rotate(&pQuad->m_aPoints[4], &aRotated[1], Rot); - Rotate(&pQuad->m_aPoints[4], &aRotated[2], Rot); - Rotate(&pQuad->m_aPoints[4], &aRotated[3], Rot); } IGraphics::CFreeformItem Freeform( - fx2f(pPoints[0].x) + OffsetX, fx2f(pPoints[0].y) + OffsetY, - fx2f(pPoints[1].x) + OffsetX, fx2f(pPoints[1].y) + OffsetY, - fx2f(pPoints[2].x) + OffsetX, fx2f(pPoints[2].y) + OffsetY, - fx2f(pPoints[3].x) + OffsetX, fx2f(pPoints[3].y) + OffsetY); + fx2f(pPoints[0].x) + Offset.x, fx2f(pPoints[0].y) + Offset.y, + fx2f(pPoints[1].x) + Offset.x, fx2f(pPoints[1].y) + Offset.y, + fx2f(pPoints[2].x) + Offset.x, fx2f(pPoints[2].y) + Offset.y, + fx2f(pPoints[3].x) + Offset.x, fx2f(pPoints[3].y) + Offset.y); Graphics()->QuadsDrawFreeform(&Freeform, 1); } Graphics()->TrianglesEnd(); @@ -442,8 +415,7 @@ void CRenderTools::ForceRenderQuads(CQuad *pQuads, int NumQuads, int RenderFlags void CRenderTools::RenderTileRectangle(int RectX, int RectY, int RectW, int RectH, unsigned char IndexIn, unsigned char IndexOut, - float Scale, ColorRGBA Color, int RenderFlags, - ENVELOPE_EVAL pfnEval, void *pUser, int ColorEnv, int ColorEnvOffset) const + float Scale, ColorRGBA Color, int RenderFlags) const { float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); @@ -453,17 +425,11 @@ void CRenderTools::RenderTileRectangle(int RectX, int RectY, int RectW, int Rect float FinalTileSize = Scale / (ScreenX1 - ScreenX0) * Graphics()->ScreenWidth(); float FinalTilesetScale = FinalTileSize / TilePixelSize; - ColorRGBA Channels(1.f, 1.f, 1.f, 1.f); - if(ColorEnv >= 0) - { - pfnEval(ColorEnvOffset, ColorEnv, Channels, pUser); - } - if(Graphics()->HasTextureArraysSupport()) Graphics()->QuadsTex3DBegin(); else Graphics()->QuadsBegin(); - Graphics()->SetColor(Color.r * Channels.r, Color.g * Channels.g, Color.b * Channels.b, Color.a * Channels.a); + Graphics()->SetColor(Color); int StartY = (int)(ScreenY0 / Scale) - 1; int StartX = (int)(ScreenX0 / Scale) - 1; @@ -540,8 +506,7 @@ void CRenderTools::RenderTileRectangle(int RectX, int RectY, int RectW, int Rect Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } -void CRenderTools::RenderTilemap(CTile *pTiles, int w, int h, float Scale, ColorRGBA Color, int RenderFlags, - ENVELOPE_EVAL pfnEval, void *pUser, int ColorEnv, int ColorEnvOffset) const +void CRenderTools::RenderTilemap(CTile *pTiles, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) const { float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); @@ -551,17 +516,12 @@ void CRenderTools::RenderTilemap(CTile *pTiles, int w, int h, float Scale, Color float FinalTileSize = Scale / (ScreenX1 - ScreenX0) * Graphics()->ScreenWidth(); float FinalTilesetScale = FinalTileSize / TilePixelSize; - ColorRGBA Channels(1.f, 1.f, 1.f, 1.f); - if(ColorEnv >= 0) - { - pfnEval(ColorEnvOffset, ColorEnv, Channels, pUser); - } - if(Graphics()->HasTextureArraysSupport()) Graphics()->QuadsTex3DBegin(); else Graphics()->QuadsBegin(); - Graphics()->SetColor(Color.r * Channels.r, Color.g * Channels.g, Color.b * Channels.b, Color.a * Channels.a); + Graphics()->SetColor(Color); + const bool ColorOpaque = Color.a > 254.0f / 255.0f; int StartY = (int)(ScreenY0 / Scale) - 1; int StartX = (int)(ScreenX0 / Scale) - 1; @@ -611,7 +571,7 @@ void CRenderTools::RenderTilemap(CTile *pTiles, int w, int h, float Scale, Color unsigned char Flags = pTiles[c].m_Flags; bool Render = false; - if(Flags & TILEFLAG_OPAQUE && Color.a * Channels.a > 254.0f / 255.0f) + if(ColorOpaque && Flags & TILEFLAG_OPAQUE) { if(RenderFlags & LAYERRENDERFLAG_OPAQUE) Render = true; @@ -707,333 +667,7 @@ void CRenderTools::RenderTilemap(CTile *pTiles, int w, int h, float Scale, Color Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } -void CRenderTools::RenderGameTileOutlines(CTile *pTiles, int w, int h, float Scale, int TileType, float Alpha) -{ - float ScreenX0, ScreenY0, ScreenX1, ScreenY1; - Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); - - int StartY = (int)(ScreenY0 / Scale) - 1; - int StartX = (int)(ScreenX0 / Scale) - 1; - int EndY = (int)(ScreenY1 / Scale) + 1; - int EndX = (int)(ScreenX1 / Scale) + 1; - int MaxScale = 12; - if(EndX - StartX > Graphics()->ScreenWidth() / MaxScale || EndY - StartY > Graphics()->ScreenHeight() / MaxScale) - { - int EdgeX = (EndX - StartX) - (Graphics()->ScreenWidth() / MaxScale); - StartX += EdgeX / 2; - EndX -= EdgeX / 2; - int EdgeY = (EndY - StartY) - (Graphics()->ScreenHeight() / MaxScale); - StartY += EdgeY / 2; - EndY -= EdgeY / 2; - } - Graphics()->TextureClear(); - Graphics()->QuadsBegin(); - ColorRGBA col = ColorRGBA(0.0f, 0.0f, 0.0f, 1.0f); - if(TileType == TILE_FREEZE) - { - col = color_cast(ColorHSLA(g_Config.m_ClOutlineColorFreeze)); - } - else if(TileType == TILE_SOLID) - { - col = color_cast(ColorHSLA(g_Config.m_ClOutlineColorSolid)); - } - else if(TileType == TILE_UNFREEZE) - { - col = color_cast(ColorHSLA(g_Config.m_ClOutlineColorUnfreeze)); - } - Graphics()->SetColor(col.r, col.g, col.b, Alpha); - - for(int y = StartY; y < EndY; y++) - for(int x = StartX; x < EndX; x++) - { - int mx = x; - int my = y; - - if(mx < 1) - continue; // mx = 0; - if(mx >= w - 1) - continue; // mx = w-1; - if(my < 1) - continue; // my = 0; - if(my >= h - 1) - continue; // my = h-1; - - int c = mx + my * w; - - unsigned char Index = pTiles[c].m_Index; - bool IsFreeze = Index == TILE_FREEZE || Index == TILE_DFREEZE; - bool IsUnFreeze = Index == TILE_UNFREEZE || Index == TILE_DUNFREEZE; - bool IsSolid = Index == TILE_SOLID || Index == TILE_NOHOOK; - - if(!(IsSolid || IsFreeze || IsUnFreeze)) //Not an tile we care about - continue; - if(IsSolid && !(TileType == TILE_SOLID)) - continue; - if(IsFreeze && !(TileType == TILE_FREEZE)) - continue; - if(IsUnFreeze && !(TileType == TILE_UNFREEZE)) - continue; - - IGraphics::CQuadItem Array[8]; - bool Neighbors[8]; - if(IsFreeze && TileType == TILE_FREEZE) - { - int IndexN; - IndexN = pTiles[(mx - 1) + (my - 1) * w].m_Index; - Neighbors[0] = IndexN == TILE_AIR || IndexN == TILE_UNFREEZE || IndexN == TILE_DUNFREEZE; - IndexN = pTiles[(mx + 0) + (my - 1) * w].m_Index; - Neighbors[1] = IndexN == TILE_AIR || IndexN == TILE_UNFREEZE || IndexN == TILE_DUNFREEZE; - IndexN = pTiles[(mx + 1) + (my - 1) * w].m_Index; - Neighbors[2] = IndexN == TILE_AIR || IndexN == TILE_UNFREEZE || IndexN == TILE_DUNFREEZE; - IndexN = pTiles[(mx - 1) + (my + 0) * w].m_Index; - Neighbors[3] = IndexN == TILE_AIR || IndexN == TILE_UNFREEZE || IndexN == TILE_DUNFREEZE; - IndexN = pTiles[(mx + 1) + (my + 0) * w].m_Index; - Neighbors[4] = IndexN == TILE_AIR || IndexN == TILE_UNFREEZE || IndexN == TILE_DUNFREEZE; - IndexN = pTiles[(mx - 1) + (my + 1) * w].m_Index; - Neighbors[5] = IndexN == TILE_AIR || IndexN == TILE_UNFREEZE || IndexN == TILE_DUNFREEZE; - IndexN = pTiles[(mx + 0) + (my + 1) * w].m_Index; - Neighbors[6] = IndexN == TILE_AIR || IndexN == TILE_UNFREEZE || IndexN == TILE_DUNFREEZE; - IndexN = pTiles[(mx + 1) + (my + 1) * w].m_Index; - Neighbors[7] = IndexN == TILE_AIR || IndexN == TILE_UNFREEZE || IndexN == TILE_DUNFREEZE; - } - else if(IsSolid && TileType == TILE_SOLID) - { - int IndexN; - IndexN = pTiles[(mx - 1) + (my - 1) * w].m_Index; - Neighbors[0] = IndexN != TILE_NOHOOK && IndexN != Index; - IndexN = pTiles[(mx + 0) + (my - 1) * w].m_Index; - Neighbors[1] = IndexN != TILE_NOHOOK && IndexN != Index; - IndexN = pTiles[(mx + 1) + (my - 1) * w].m_Index; - Neighbors[2] = IndexN != TILE_NOHOOK && IndexN != Index; - IndexN = pTiles[(mx - 1) + (my + 0) * w].m_Index; - Neighbors[3] = IndexN != TILE_NOHOOK && IndexN != Index; - IndexN = pTiles[(mx + 1) + (my + 0) * w].m_Index; - Neighbors[4] = IndexN != TILE_NOHOOK && IndexN != Index; - IndexN = pTiles[(mx - 1) + (my + 1) * w].m_Index; - Neighbors[5] = IndexN != TILE_NOHOOK && IndexN != Index; - IndexN = pTiles[(mx + 0) + (my + 1) * w].m_Index; - Neighbors[6] = IndexN != TILE_NOHOOK && IndexN != Index; - IndexN = pTiles[(mx + 1) + (my + 1) * w].m_Index; - Neighbors[7] = IndexN != TILE_NOHOOK && IndexN != Index; - } - else - { - int IndexN; - IndexN = pTiles[(mx - 1) + (my - 1) * w].m_Index; - Neighbors[0] = IndexN != TILE_UNFREEZE && IndexN != TILE_DUNFREEZE; - IndexN = pTiles[(mx + 0) + (my - 1) * w].m_Index; - Neighbors[1] = IndexN != TILE_UNFREEZE && IndexN != TILE_DUNFREEZE; - IndexN = pTiles[(mx + 1) + (my - 1) * w].m_Index; - Neighbors[2] = IndexN != TILE_UNFREEZE && IndexN != TILE_DUNFREEZE; - IndexN = pTiles[(mx - 1) + (my + 0) * w].m_Index; - Neighbors[3] = IndexN != TILE_UNFREEZE && IndexN != TILE_DUNFREEZE; - IndexN = pTiles[(mx + 1) + (my + 0) * w].m_Index; - Neighbors[4] = IndexN != TILE_UNFREEZE && IndexN != TILE_DUNFREEZE; - IndexN = pTiles[(mx - 1) + (my + 1) * w].m_Index; - Neighbors[5] = IndexN != TILE_UNFREEZE && IndexN != TILE_DUNFREEZE; - IndexN = pTiles[(mx + 0) + (my + 1) * w].m_Index; - Neighbors[6] = IndexN != TILE_UNFREEZE && IndexN != TILE_DUNFREEZE; - IndexN = pTiles[(mx + 1) + (my + 1) * w].m_Index; - Neighbors[7] = IndexN != TILE_UNFREEZE && IndexN != TILE_DUNFREEZE; - } - - float Size = (float)g_Config.m_ClOutlineWidth; - int NumQuads = 0; - - //Do lonely corners first - if(Neighbors[0] && !Neighbors[1] && !Neighbors[3]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale, Size, Size); - NumQuads++; - } - if(Neighbors[2] && !Neighbors[1] && !Neighbors[4]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale, Size, Size); - NumQuads++; - } - if(Neighbors[5] && !Neighbors[3] && !Neighbors[6]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale + Scale - Size, Size, Size); - NumQuads++; - } - if(Neighbors[7] && !Neighbors[6] && !Neighbors[4]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale + Scale - Size, Size, Size); - NumQuads++; - } - //Top - if(Neighbors[1]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale, Scale, Size); - NumQuads++; - } - //Bottom - if(Neighbors[6]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale + Scale - Size, Scale, Size); - NumQuads++; - } - //Left - if(Neighbors[3]) - { - if(!Neighbors[1] && !Neighbors[6]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale, Size, Scale); - else if(!Neighbors[6]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale + Size, Size, Scale - Size); - else if(!Neighbors[1]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale, Size, Scale - Size); - else - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale + Size, Size, Scale - Size * 2.0f); - NumQuads++; - } - //Right - if(Neighbors[4]) - { - if(!Neighbors[1] && !Neighbors[6]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale, Size, Scale); - else if(!Neighbors[6]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale + Size, Size, Scale - Size); - else if(!Neighbors[1]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale, Size, Scale - Size); - else - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale + Size, Size, Scale - Size * 2.0f); - NumQuads++; - } - - Graphics()->QuadsDrawTL(Array, NumQuads); - } - Graphics()->QuadsEnd(); - Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); -} -void CRenderTools::RenderTeleOutlines(CTile *pTiles, CTeleTile *pTele, int w, int h, float Scale, float Alpha) -{ - float ScreenX0, ScreenY0, ScreenX1, ScreenY1; - Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); - - int StartY = (int)(ScreenY0 / Scale) - 1; - int StartX = (int)(ScreenX0 / Scale) - 1; - int EndY = (int)(ScreenY1 / Scale) + 1; - int EndX = (int)(ScreenX1 / Scale) + 1; - - int MaxScale = 12; - if(EndX - StartX > Graphics()->ScreenWidth() / MaxScale || EndY - StartY > Graphics()->ScreenHeight() / MaxScale) - { - int EdgeX = (EndX - StartX) - (Graphics()->ScreenWidth() / MaxScale); - StartX += EdgeX / 2; - EndX -= EdgeX / 2; - int EdgeY = (EndY - StartY) - (Graphics()->ScreenHeight() / MaxScale); - StartY += EdgeY / 2; - EndY -= EdgeY / 2; - } - - Graphics()->TextureClear(); - Graphics()->QuadsBegin(); - ColorRGBA col = color_cast(ColorHSLA(g_Config.m_ClOutlineColorTele)); - Graphics()->SetColor(col.r, col.g, col.b, Alpha); - - for(int y = StartY; y < EndY; y++) - for(int x = StartX; x < EndX; x++) - { - int mx = x; - int my = y; - - if(mx < 1) - continue; // mx = 0; - if(mx >= w - 1) - continue; // mx = w-1; - if(my < 1) - continue; // my = 0; - if(my >= h - 1) - continue; // my = h-1; - - int c = mx + my * w; - - unsigned char Index = pTele[c].m_Type; - if(!Index) - continue; - if(!(Index == TILE_TELECHECKINEVIL || Index == TILE_TELEIN || Index == TILE_TELEINEVIL)) - continue; - - IGraphics::CQuadItem Array[8]; - bool Neighbors[8]; - Neighbors[0] = pTiles[(mx - 1) + (my - 1) * w].m_Index == 0 && !pTele[(mx - 1) + (my - 1) * w].m_Number; - Neighbors[1] = pTiles[(mx + 0) + (my - 1) * w].m_Index == 0 && !pTele[(mx + 0) + (my - 1) * w].m_Number; - Neighbors[2] = pTiles[(mx + 1) + (my - 1) * w].m_Index == 0 && !pTele[(mx + 1) + (my - 1) * w].m_Number; - Neighbors[3] = pTiles[(mx - 1) + (my + 0) * w].m_Index == 0 && !pTele[(mx - 1) + (my + 0) * w].m_Number; - Neighbors[4] = pTiles[(mx + 1) + (my + 0) * w].m_Index == 0 && !pTele[(mx + 1) + (my + 0) * w].m_Number; - Neighbors[5] = pTiles[(mx - 1) + (my + 1) * w].m_Index == 0 && !pTele[(mx - 1) + (my + 1) * w].m_Number; - Neighbors[6] = pTiles[(mx + 0) + (my + 1) * w].m_Index == 0 && !pTele[(mx + 0) + (my + 1) * w].m_Number; - Neighbors[7] = pTiles[(mx + 1) + (my + 1) * w].m_Index == 0 && !pTele[(mx + 1) + (my + 1) * w].m_Number; - - float Size = (float)g_Config.m_ClOutlineWidth; - int NumQuads = 0; - - //Do lonely corners first - if(Neighbors[0] && !Neighbors[1] && !Neighbors[3]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale, Size, Size); - NumQuads++; - } - if(Neighbors[2] && !Neighbors[1] && !Neighbors[4]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale, Size, Size); - NumQuads++; - } - if(Neighbors[5] && !Neighbors[3] && !Neighbors[6]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale + Scale - Size, Size, Size); - NumQuads++; - } - if(Neighbors[7] && !Neighbors[6] && !Neighbors[4]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale + Scale - Size, Size, Size); - NumQuads++; - } - //Top - if(Neighbors[1]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale, Scale, Size); - NumQuads++; - } - //Bottom - if(Neighbors[6]) - { - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale + Scale - Size, Scale, Size); - NumQuads++; - } - //Left - if(Neighbors[3]) - { - if(!Neighbors[1] && !Neighbors[6]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale, Size, Scale); - else if(!Neighbors[6]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale + Size, Size, Scale - Size); - else if(!Neighbors[1]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale, Size, Scale - Size); - else - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale, my * Scale + Size, Size, Scale - Size * 2.0f); - NumQuads++; - } - //Right - if(Neighbors[4]) - { - if(!Neighbors[1] && !Neighbors[6]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale, Size, Scale); - else if(!Neighbors[6]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale + Size, Size, Scale - Size); - else if(!Neighbors[1]) - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale, Size, Scale - Size); - else - Array[NumQuads] = IGraphics::CQuadItem(mx * Scale + Scale - Size, my * Scale + Size, Size, Scale - Size * 2.0f); - NumQuads++; - } - - Graphics()->QuadsDrawTL(Array, NumQuads); - } - Graphics()->QuadsEnd(); - Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); -} -void CRenderTools::RenderTeleOverlay(CTeleTile *pTele, int w, int h, float Scale, float Alpha) +void CRenderTools::RenderTeleOverlay(CTeleTile *pTele, int w, int h, float Scale, float Alpha) const { if(!g_Config.m_ClTextEntities) return; @@ -1050,9 +684,11 @@ void CRenderTools::RenderTeleOverlay(CTeleTile *pTele, int w, int h, float Scale return; // its useless to render text at this distance float Size = g_Config.m_ClTextEntitiesSize / 100.f; - float ToCenterOffset = (1 - Size) / 2.f; + char aBuf[16]; + TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); for(int y = StartY; y < EndY; y++) + { for(int x = StartX; x < EndX; x++) { int mx = x; @@ -1070,20 +706,23 @@ void CRenderTools::RenderTeleOverlay(CTeleTile *pTele, int w, int h, float Scale int c = mx + my * w; unsigned char Index = pTele[c].m_Number; - if(Index && IsTeleTileNumberUsed(pTele[c].m_Type)) + if(Index && IsTeleTileNumberUsedAny(pTele[c].m_Type)) { - char aBuf[16]; - str_from_int(Index, aBuf); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); - TextRender()->Text(mx * Scale - 3.f, (my + ToCenterOffset) * Scale, Size * Scale, aBuf, -1.0f); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + str_format(aBuf, sizeof(aBuf), "%d", Index); + // Auto-resize text to fit inside the tile + float ScaledWidth = TextRender()->TextWidth(Size * Scale, aBuf, -1); + float Factor = clamp(Scale / ScaledWidth, 0.0f, 1.0f); + float LocalSize = Size * Factor; + float ToCenterOffset = (1 - LocalSize) / 2.f; + TextRender()->Text((mx + 0.5f) * Scale - (ScaledWidth * Factor) / 2.0f, (my + ToCenterOffset) * Scale, LocalSize * Scale, aBuf); } } - + } + TextRender()->TextColor(TextRender()->DefaultTextColor()); Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } -void CRenderTools::RenderSpeedupOverlay(CSpeedupTile *pSpeedup, int w, int h, float Scale, float Alpha) +void CRenderTools::RenderSpeedupOverlay(CSpeedupTile *pSpeedup, int w, int h, float Scale, float Alpha) const { float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); @@ -1098,8 +737,11 @@ void CRenderTools::RenderSpeedupOverlay(CSpeedupTile *pSpeedup, int w, int h, fl float Size = g_Config.m_ClTextEntitiesSize / 100.f; float ToCenterOffset = (1 - Size) / 2.f; + char aBuf[16]; + TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); for(int y = StartY; y < EndY; y++) + { for(int x = StartX; x < EndX; x++) { int mx = x; @@ -1123,36 +765,31 @@ void CRenderTools::RenderSpeedupOverlay(CSpeedupTile *pSpeedup, int w, int h, fl // draw arrow Graphics()->TextureSet(g_pData->m_aImages[IMAGE_SPEEDUP_ARROW].m_Id); Graphics()->QuadsBegin(); - Graphics()->SetColor(255.0f, 255.0f, 255.0f, Alpha); - + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); SelectSprite(SPRITE_SPEEDUP_ARROW); Graphics()->QuadsSetRotation(pSpeedup[c].m_Angle * (pi / 180.0f)); DrawSprite(mx * Scale + 16, my * Scale + 16, 35.0f); - Graphics()->QuadsEnd(); + // draw force and max speed if(g_Config.m_ClTextEntities) { - // draw force - char aBuf[16]; - str_from_int(Force, aBuf); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); - TextRender()->Text(mx * Scale, (my + 0.5f + ToCenterOffset / 2) * Scale, Size * Scale / 2.f, aBuf, -1.0f); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + str_format(aBuf, sizeof(aBuf), "%d", Force); + TextRender()->Text(mx * Scale, (my + 0.5f + ToCenterOffset / 2) * Scale, Size * Scale / 2.f, aBuf); if(MaxSpeed) { - str_from_int(MaxSpeed, aBuf); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); - TextRender()->Text(mx * Scale, (my + ToCenterOffset / 2) * Scale, Size * Scale / 2.f, aBuf, -1.0f); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + str_format(aBuf, sizeof(aBuf), "%d", MaxSpeed); + TextRender()->Text(mx * Scale, (my + ToCenterOffset / 2) * Scale, Size * Scale / 2.f, aBuf); } } } } + } + TextRender()->TextColor(TextRender()->DefaultTextColor()); Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } -void CRenderTools::RenderSwitchOverlay(CSwitchTile *pSwitch, int w, int h, float Scale, float Alpha) +void CRenderTools::RenderSwitchOverlay(CSwitchTile *pSwitch, int w, int h, float Scale, float Alpha) const { if(!g_Config.m_ClTextEntities) return; @@ -1170,8 +807,11 @@ void CRenderTools::RenderSwitchOverlay(CSwitchTile *pSwitch, int w, int h, float float Size = g_Config.m_ClTextEntitiesSize / 100.f; float ToCenterOffset = (1 - Size) / 2.f; + char aBuf[16]; + TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); for(int y = StartY; y < EndY; y++) + { for(int x = StartX; x < EndX; x++) { int mx = x; @@ -1191,28 +831,23 @@ void CRenderTools::RenderSwitchOverlay(CSwitchTile *pSwitch, int w, int h, float unsigned char Index = pSwitch[c].m_Number; if(Index && IsSwitchTileNumberUsed(pSwitch[c].m_Type)) { - char aBuf[16]; - str_from_int(Index, aBuf); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); - TextRender()->Text(mx * Scale, (my + ToCenterOffset / 2) * Scale, Size * Scale / 2.f, aBuf, -1.0f); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + str_format(aBuf, sizeof(aBuf), "%d", Index); + TextRender()->Text(mx * Scale, (my + ToCenterOffset / 2) * Scale, Size * Scale / 2.f, aBuf); } unsigned char Delay = pSwitch[c].m_Delay; if(Delay && IsSwitchTileDelayUsed(pSwitch[c].m_Type)) { - char aBuf[16]; - str_from_int(Delay, aBuf); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); - TextRender()->Text(mx * Scale, (my + 0.5f + ToCenterOffset / 2) * Scale, Size * Scale / 2.f, aBuf, -1.0f); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + str_format(aBuf, sizeof(aBuf), "%d", Delay); + TextRender()->Text(mx * Scale, (my + 0.5f + ToCenterOffset / 2) * Scale, Size * Scale / 2.f, aBuf); } } - + } + TextRender()->TextColor(TextRender()->DefaultTextColor()); Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } -void CRenderTools::RenderTuneOverlay(CTuneTile *pTune, int w, int h, float Scale, float Alpha) +void CRenderTools::RenderTuneOverlay(CTuneTile *pTune, int w, int h, float Scale, float Alpha) const { if(!g_Config.m_ClTextEntities) return; @@ -1229,8 +864,11 @@ void CRenderTools::RenderTuneOverlay(CTuneTile *pTune, int w, int h, float Scale return; // its useless to render text at this distance float Size = g_Config.m_ClTextEntitiesSize / 100.f; + char aBuf[16]; + TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); for(int y = StartY; y < EndY; y++) + { for(int x = StartX; x < EndX; x++) { int mx = x; @@ -1250,18 +888,16 @@ void CRenderTools::RenderTuneOverlay(CTuneTile *pTune, int w, int h, float Scale unsigned char Index = pTune[c].m_Number; if(Index) { - char aBuf[16]; - str_from_int(Index, aBuf); - TextRender()->TextColor(1.0f, 1.0f, 1.0f, Alpha); - TextRender()->Text(mx * Scale + 11.f, my * Scale + 6.f, Size * Scale / 1.5f - 5.f, aBuf, -1.0f); // numbers shouldn't be too big and in the center of the tile - TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + str_format(aBuf, sizeof(aBuf), "%d", Index); + TextRender()->Text(mx * Scale + 11.f, my * Scale + 6.f, Size * Scale / 1.5f - 5.f, aBuf); // numbers shouldn't be too big and in the center of the tile } } - + } + TextRender()->TextColor(TextRender()->DefaultTextColor()); Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } -void CRenderTools::RenderTelemap(CTeleTile *pTele, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) +void CRenderTools::RenderTelemap(CTeleTile *pTele, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) const { float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); @@ -1378,12 +1014,10 @@ void CRenderTools::RenderTelemap(CTeleTile *pTele, int w, int h, float Scale, Co Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } -void CRenderTools::RenderSpeedupmap(CSpeedupTile *pSpeedupTile, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) +void CRenderTools::RenderSpeedupmap(CSpeedupTile *pSpeedupTile, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) const { - //Graphics()->TextureSet(img_get(tmap->image)); float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); - //Graphics()->MapScreen(screen_x0-50, screen_y0-50, screen_x1+50, screen_y1+50); // calculate the final pixelsize for the tiles float TilePixelSize = 1024 / 32.0f; @@ -1497,12 +1131,10 @@ void CRenderTools::RenderSpeedupmap(CSpeedupTile *pSpeedupTile, int w, int h, fl Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } -void CRenderTools::RenderSwitchmap(CSwitchTile *pSwitchTile, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) +void CRenderTools::RenderSwitchmap(CSwitchTile *pSwitchTile, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) const { - //Graphics()->TextureSet(img_get(tmap->image)); float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); - //Graphics()->MapScreen(screen_x0-50, screen_y0-50, screen_x1+50, screen_y1+50); // calculate the final pixelsize for the tiles float TilePixelSize = 1024 / 32.0f; @@ -1659,7 +1291,7 @@ void CRenderTools::RenderSwitchmap(CSwitchTile *pSwitchTile, int w, int h, float Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } -void CRenderTools::RenderTunemap(CTuneTile *pTune, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) +void CRenderTools::RenderTunemap(CTuneTile *pTune, int w, int h, float Scale, ColorRGBA Color, int RenderFlags) const { float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); diff --git a/src/game/client/sixup_translate_game.cpp b/src/game/client/sixup_translate_game.cpp new file mode 100644 index 0000000000..ae6f8ed5b0 --- /dev/null +++ b/src/game/client/sixup_translate_game.cpp @@ -0,0 +1,727 @@ +#include +#include +#include +#include +#include + +#include + +enum +{ + STR_TEAM_GAME, + STR_TEAM_RED, + STR_TEAM_BLUE, + STR_TEAM_SPECTATORS, +}; + +static int GetStrTeam7(int Team, bool Teamplay) +{ + if(Teamplay) + { + if(Team == TEAM_RED) + return STR_TEAM_RED; + else if(Team == TEAM_BLUE) + return STR_TEAM_BLUE; + } + else if(Team == 0) + return STR_TEAM_GAME; + + return STR_TEAM_SPECTATORS; +} + +enum +{ + DO_CHAT = 0, + DO_BROADCAST, + DO_SPECIAL, + + PARA_NONE = 0, + PARA_I, + PARA_II, + PARA_III, +}; + +struct CGameMsg7 +{ + int m_Action; + int m_ParaType; + const char *m_pText; +}; + +static CGameMsg7 gs_GameMsgList7[protocol7::NUM_GAMEMSGS] = { + {/*GAMEMSG_TEAM_SWAP*/ DO_CHAT, PARA_NONE, "Teams were swapped"}, + {/*GAMEMSG_SPEC_INVALIDID*/ DO_CHAT, PARA_NONE, "Invalid spectator id used"}, //! + {/*GAMEMSG_TEAM_SHUFFLE*/ DO_CHAT, PARA_NONE, "Teams were shuffled"}, + {/*GAMEMSG_TEAM_BALANCE*/ DO_CHAT, PARA_NONE, "Teams have been balanced"}, + {/*GAMEMSG_CTF_DROP*/ DO_SPECIAL, PARA_NONE, ""}, // special - play ctf drop sound + {/*GAMEMSG_CTF_RETURN*/ DO_SPECIAL, PARA_NONE, ""}, // special - play ctf return sound + + {/*GAMEMSG_TEAM_ALL*/ DO_SPECIAL, PARA_I, ""}, // special - add team name + {/*GAMEMSG_TEAM_BALANCE_VICTIM*/ DO_SPECIAL, PARA_I, ""}, // special - add team name + {/*GAMEMSG_CTF_GRAB*/ DO_SPECIAL, PARA_I, ""}, // special - play ctf grab sound based on team + + {/*GAMEMSG_CTF_CAPTURE*/ DO_SPECIAL, PARA_III, ""}, // special - play ctf capture sound + capture chat message + + {/*GAMEMSG_GAME_PAUSED*/ DO_SPECIAL, PARA_I, ""}, // special - add player name +}; + +void CGameClient::DoTeamChangeMessage7(const char *pName, int ClientId, int Team, const char *pPrefix) +{ + char aBuf[128]; + switch(GetStrTeam7(Team, m_pClient->m_TranslationContext.m_GameFlags & protocol7::GAMEFLAG_TEAMS)) + { + case STR_TEAM_GAME: str_format(aBuf, sizeof(aBuf), "'%s' %sjoined the game", pName, pPrefix); break; + case STR_TEAM_RED: str_format(aBuf, sizeof(aBuf), "'%s' %sjoined the red team", pName, pPrefix); break; + case STR_TEAM_BLUE: str_format(aBuf, sizeof(aBuf), "'%s' %sjoined the blue team", pName, pPrefix); break; + case STR_TEAM_SPECTATORS: str_format(aBuf, sizeof(aBuf), "'%s' %sjoined the spectators", pName, pPrefix); break; + } + m_Chat.AddLine(-1, 0, aBuf); +} + +template +void CGameClient::ApplySkin7InfoFromGameMsg(const T *pMsg, int ClientId, int Conn) +{ + CClientData *pClient = &m_aClients[ClientId]; + char *apSkinPartsPtr[protocol7::NUM_SKINPARTS]; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + str_utf8_copy_num(pClient->m_aSixup[Conn].m_aaSkinPartNames[Part], pMsg->m_apSkinPartNames[Part], sizeof(pClient->m_aSixup[Conn].m_aaSkinPartNames[Part]), protocol7::MAX_SKIN_LENGTH); + apSkinPartsPtr[Part] = pClient->m_aSixup[Conn].m_aaSkinPartNames[Part]; + pClient->m_aSixup[Conn].m_aUseCustomColors[Part] = pMsg->m_aUseCustomColors[Part]; + pClient->m_aSixup[Conn].m_aSkinPartColors[Part] = pMsg->m_aSkinPartColors[Part]; + } + m_Skins7.ValidateSkinParts(apSkinPartsPtr, pClient->m_aSixup[Conn].m_aUseCustomColors, pClient->m_aSixup[Conn].m_aSkinPartColors, m_pClient->m_TranslationContext.m_GameFlags); + + if(time_season() == SEASON_XMAS) + { + pClient->m_SkinInfo.m_aSixup[Conn].m_HatTexture = m_Skins7.m_XmasHatTexture; + pClient->m_SkinInfo.m_aSixup[Conn].m_HatSpriteIndex = ClientId % CSkins7::HAT_NUM; + } + else + pClient->m_SkinInfo.m_aSixup[Conn].m_HatTexture.Invalidate(); + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + int Id = m_Skins7.FindSkinPart(Part, pClient->m_aSixup[Conn].m_aaSkinPartNames[Part], false); + const CSkins7::CSkinPart *pSkinPart = m_Skins7.GetSkinPart(Part, Id); + if(pClient->m_aSixup[Conn].m_aUseCustomColors[Part]) + { + pClient->m_SkinInfo.m_aSixup[Conn].m_aTextures[Part] = pSkinPart->m_ColorTexture; + pClient->m_SkinInfo.m_aSixup[Conn].m_aColors[Part] = m_Skins7.GetColor(pMsg->m_aSkinPartColors[Part], Part == protocol7::SKINPART_MARKING); + } + else + { + pClient->m_SkinInfo.m_aSixup[Conn].m_aTextures[Part] = pSkinPart->m_OrgTexture; + pClient->m_SkinInfo.m_aSixup[Conn].m_aColors[Part] = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + if(pClient->m_SkinInfo.m_aSixup[Conn].m_HatTexture.IsValid()) + { + if(Part == protocol7::SKINPART_BODY && str_comp(pClient->m_aSixup[Conn].m_aaSkinPartNames[Part], "standard")) + pClient->m_SkinInfo.m_aSixup[Conn].m_HatSpriteIndex = CSkins7::HAT_OFFSET_SIDE + (ClientId % CSkins7::HAT_NUM); + if(Part == protocol7::SKINPART_DECORATION && !str_comp(pClient->m_aSixup[Conn].m_aaSkinPartNames[Part], "twinbopp")) + pClient->m_SkinInfo.m_aSixup[Conn].m_HatSpriteIndex = CSkins7::HAT_OFFSET_SIDE + (ClientId % CSkins7::HAT_NUM); + } + } +} + +void CGameClient::ApplySkin7InfoFromSnapObj(const protocol7::CNetObj_De_ClientInfo *pObj, int ClientId) +{ + char aSkinPartNames[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_ARRAY_SIZE]; + protocol7::CNetMsg_Sv_SkinChange Msg; + Msg.m_ClientId = ClientId; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + IntsToStr(pObj->m_aaSkinPartNames[Part], 6, aSkinPartNames[Part], std::size(aSkinPartNames[Part])); + Msg.m_apSkinPartNames[Part] = aSkinPartNames[Part]; + Msg.m_aUseCustomColors[Part] = pObj->m_aUseCustomColors[Part]; + Msg.m_aSkinPartColors[Part] = pObj->m_aSkinPartColors[Part]; + } + ApplySkin7InfoFromGameMsg(&Msg, ClientId, 0); +} + +void *CGameClient::TranslateGameMsg(int *pMsgId, CUnpacker *pUnpacker, int Conn) +{ + if(!m_pClient->IsSixup()) + { + return m_NetObjHandler.SecureUnpackMsg(*pMsgId, pUnpacker); + } + + void *pRawMsg = m_NetObjHandler7.SecureUnpackMsg(*pMsgId, pUnpacker); + if(!pRawMsg) + { + if(*pMsgId > __NETMSGTYPE_UUID_HELPER && *pMsgId < OFFSET_MAPITEMTYPE_UUID) + { + void *pDDNetExMsg = m_NetObjHandler.SecureUnpackMsg(*pMsgId, pUnpacker); + if(pDDNetExMsg) + return pDDNetExMsg; + } + + dbg_msg("sixup", "dropped weird message '%s' (%d), failed on '%s'", m_NetObjHandler7.GetMsgName(*pMsgId), *pMsgId, m_NetObjHandler7.FailedMsgOn()); + return nullptr; + } + static char s_aRawMsg[1024]; + + if(*pMsgId == protocol7::NETMSGTYPE_SV_MOTD) + { + protocol7::CNetMsg_Sv_Motd *pMsg7 = (protocol7::CNetMsg_Sv_Motd *)pRawMsg; + ::CNetMsg_Sv_Motd *pMsg = (::CNetMsg_Sv_Motd *)s_aRawMsg; + + pMsg->m_pMessage = pMsg7->m_pMessage; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_BROADCAST) + { + protocol7::CNetMsg_Sv_Broadcast *pMsg7 = (protocol7::CNetMsg_Sv_Broadcast *)pRawMsg; + ::CNetMsg_Sv_Broadcast *pMsg = (::CNetMsg_Sv_Broadcast *)s_aRawMsg; + + pMsg->m_pMessage = pMsg7->m_pMessage; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETTEAM) + { + protocol7::CNetMsg_Cl_SetTeam *pMsg7 = (protocol7::CNetMsg_Cl_SetTeam *)pRawMsg; + ::CNetMsg_Cl_SetTeam *pMsg = (::CNetMsg_Cl_SetTeam *)s_aRawMsg; + + pMsg->m_Team = pMsg7->m_Team; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_TEAM) + { + protocol7::CNetMsg_Sv_Team *pMsg7 = (protocol7::CNetMsg_Sv_Team *)pRawMsg; + + if(Client()->State() != IClient::STATE_DEMOPLAYBACK) + { + m_aClients[pMsg7->m_ClientId].m_Team = pMsg7->m_Team; + m_pClient->m_TranslationContext.m_aClients[pMsg7->m_ClientId].m_Team = pMsg7->m_Team; + m_aClients[pMsg7->m_ClientId].UpdateRenderInfo(IsTeamPlay()); + + // if(pMsg7->m_ClientId == m_LocalClientId) + // { + // m_TeamCooldownTick = pMsg7->m_CooldownTick; + // m_TeamChangeTime = Client()->LocalTime(); + // } + } + + if(Conn != g_Config.m_ClDummy) + return nullptr; + + if(pMsg7->m_Silent == 0) + { + DoTeamChangeMessage7(m_aClients[pMsg7->m_ClientId].m_aName, pMsg7->m_ClientId, pMsg7->m_Team); + } + + // we drop the message and add the new team + // info to the playerinfo snap item + // using translation context + return nullptr; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_WEAPONPICKUP) + { + protocol7::CNetMsg_Sv_WeaponPickup *pMsg7 = (protocol7::CNetMsg_Sv_WeaponPickup *)pRawMsg; + ::CNetMsg_Sv_WeaponPickup *pMsg = (::CNetMsg_Sv_WeaponPickup *)s_aRawMsg; + + pMsg->m_Weapon = pMsg7->m_Weapon; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_SERVERSETTINGS) + { + // 0.7 only message for ui enrichment like locked teams + protocol7::CNetMsg_Sv_ServerSettings *pMsg = (protocol7::CNetMsg_Sv_ServerSettings *)pRawMsg; + + if(!m_pClient->m_TranslationContext.m_ServerSettings.m_TeamLock && pMsg->m_TeamLock) + m_Chat.AddLine(-1, 0, "Teams were locked"); + else if(m_pClient->m_TranslationContext.m_ServerSettings.m_TeamLock && !pMsg->m_TeamLock) + m_Chat.AddLine(-1, 0, "Teams were unlocked"); + + m_pClient->m_TranslationContext.m_ServerSettings.m_KickVote = pMsg->m_KickVote; + m_pClient->m_TranslationContext.m_ServerSettings.m_KickMin = pMsg->m_KickMin; + m_pClient->m_TranslationContext.m_ServerSettings.m_SpecVote = pMsg->m_SpecVote; + m_pClient->m_TranslationContext.m_ServerSettings.m_TeamLock = pMsg->m_TeamLock; + m_pClient->m_TranslationContext.m_ServerSettings.m_TeamBalance = pMsg->m_TeamBalance; + m_pClient->m_TranslationContext.m_ServerSettings.m_PlayerSlots = pMsg->m_PlayerSlots; + return nullptr; // There is no 0.6 equivalent + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_RACEFINISH) + { + *pMsgId = NETMSGTYPE_SV_RACEFINISH; + protocol7::CNetMsg_Sv_RaceFinish *pMsg7 = (protocol7::CNetMsg_Sv_RaceFinish *)pRawMsg; + ::CNetMsg_Sv_RaceFinish *pMsg = (::CNetMsg_Sv_RaceFinish *)s_aRawMsg; + + pMsg->m_ClientId = pMsg7->m_ClientId; + pMsg->m_Time = pMsg7->m_Time; + pMsg->m_Diff = pMsg7->m_Diff; + pMsg->m_RecordPersonal = pMsg7->m_RecordPersonal; + pMsg->m_RecordServer = pMsg7->m_RecordServer; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_COMMANDINFOREMOVE) + { + *pMsgId = NETMSGTYPE_SV_COMMANDINFOREMOVE; + protocol7::CNetMsg_Sv_CommandInfoRemove *pMsg7 = (protocol7::CNetMsg_Sv_CommandInfoRemove *)pRawMsg; + ::CNetMsg_Sv_CommandInfoRemove *pMsg = (::CNetMsg_Sv_CommandInfoRemove *)s_aRawMsg; + + pMsg->m_pName = pMsg7->m_pName; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_COMMANDINFO) + { + *pMsgId = NETMSGTYPE_SV_COMMANDINFO; + protocol7::CNetMsg_Sv_CommandInfo *pMsg7 = (protocol7::CNetMsg_Sv_CommandInfo *)pRawMsg; + ::CNetMsg_Sv_CommandInfo *pMsg = (::CNetMsg_Sv_CommandInfo *)s_aRawMsg; + + pMsg->m_pName = pMsg7->m_pName; + pMsg->m_pArgsFormat = pMsg7->m_pArgsFormat; + pMsg->m_pHelpText = pMsg7->m_pHelpText; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_SKINCHANGE) + { + protocol7::CNetMsg_Sv_SkinChange *pMsg7 = (protocol7::CNetMsg_Sv_SkinChange *)pRawMsg; + + if(pMsg7->m_ClientId < 0 || pMsg7->m_ClientId >= MAX_CLIENTS) + { + dbg_msg("sixup", "Sv_SkinChange got invalid ClientId: %d", pMsg7->m_ClientId); + return nullptr; + } + + CTranslationContext::CClientData &Client = m_pClient->m_TranslationContext.m_aClients[pMsg7->m_ClientId]; + Client.m_Active = true; + ApplySkin7InfoFromGameMsg(pMsg7, pMsg7->m_ClientId, Conn); + // skin will be moved to the 0.6 snap by the translation context + // and we drop the game message + return nullptr; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTECLEAROPTIONS) + { + *pMsgId = NETMSGTYPE_SV_VOTECLEAROPTIONS; + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTEOPTIONADD) + { + *pMsgId = NETMSGTYPE_SV_VOTEOPTIONADD; + protocol7::CNetMsg_Sv_VoteOptionAdd *pMsg7 = (protocol7::CNetMsg_Sv_VoteOptionAdd *)pRawMsg; + ::CNetMsg_Sv_VoteOptionAdd *pMsg = (::CNetMsg_Sv_VoteOptionAdd *)s_aRawMsg; + pMsg->m_pDescription = pMsg7->m_pDescription; + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTEOPTIONREMOVE) + { + *pMsgId = NETMSGTYPE_SV_VOTEOPTIONREMOVE; + protocol7::CNetMsg_Sv_VoteOptionRemove *pMsg7 = (protocol7::CNetMsg_Sv_VoteOptionRemove *)pRawMsg; + ::CNetMsg_Sv_VoteOptionRemove *pMsg = (::CNetMsg_Sv_VoteOptionRemove *)s_aRawMsg; + pMsg->m_pDescription = pMsg7->m_pDescription; + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTEOPTIONLISTADD) + { + ::CNetMsg_Sv_VoteOptionListAdd *pMsg = (::CNetMsg_Sv_VoteOptionListAdd *)s_aRawMsg; + int NumOptions = pUnpacker->GetInt(); + if(NumOptions > 14) + { + for(int i = 0; i < NumOptions; i++) + { + const char *pDescription = pUnpacker->GetString(CUnpacker::SANITIZE_CC); + if(pUnpacker->Error()) + continue; + + m_Voting.AddOption(pDescription); + } + // 0.7 can send more vote options than + // the 0.6 protocol fit + // in that case we do not translate it but just + // reimplement what 0.6 would do + return nullptr; + } + pMsg->m_NumOptions = 0; + for(int i = 0; i < NumOptions; i++) + { + const char *pDescription = pUnpacker->GetString(CUnpacker::SANITIZE_CC); + if(pUnpacker->Error()) + continue; + + pMsg->m_NumOptions++; + switch(i) + { + case 0: (pMsg->m_pDescription0 = pDescription); break; + case 1: (pMsg->m_pDescription1 = pDescription); break; + case 2: (pMsg->m_pDescription2 = pDescription); break; + case 3: (pMsg->m_pDescription3 = pDescription); break; + case 4: (pMsg->m_pDescription4 = pDescription); break; + case 5: (pMsg->m_pDescription5 = pDescription); break; + case 6: (pMsg->m_pDescription6 = pDescription); break; + case 7: (pMsg->m_pDescription7 = pDescription); break; + case 8: (pMsg->m_pDescription8 = pDescription); break; + case 9: (pMsg->m_pDescription9 = pDescription); break; + case 10: (pMsg->m_pDescription10 = pDescription); break; + case 11: (pMsg->m_pDescription11 = pDescription); break; + case 12: (pMsg->m_pDescription12 = pDescription); break; + case 13: (pMsg->m_pDescription13 = pDescription); break; + case 14: (pMsg->m_pDescription14 = pDescription); + } + } + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTESET) + { + *pMsgId = NETMSGTYPE_SV_VOTESET; + protocol7::CNetMsg_Sv_VoteSet *pMsg7 = (protocol7::CNetMsg_Sv_VoteSet *)pRawMsg; + ::CNetMsg_Sv_VoteSet *pMsg = (::CNetMsg_Sv_VoteSet *)s_aRawMsg; + + pMsg->m_Timeout = pMsg7->m_Timeout; + pMsg->m_pDescription = pMsg7->m_pDescription; + pMsg->m_pReason = pMsg7->m_pReason; + + char aBuf[128]; + if(pMsg7->m_Timeout) + { + if(pMsg7->m_ClientId != -1) + { + const char *pName = m_aClients[pMsg7->m_ClientId].m_aName; + switch(pMsg7->m_Type) + { + case protocol7::VOTE_START_OP: + str_format(aBuf, sizeof(aBuf), "'%s' called vote to change server option '%s' (%s)", pName, pMsg7->m_pDescription, pMsg7->m_pReason); + m_Chat.AddLine(-1, 0, aBuf); + break; + case protocol7::VOTE_START_KICK: + { + str_format(aBuf, sizeof(aBuf), "'%s' called for vote to kick '%s' (%s)", pName, pMsg7->m_pDescription, pMsg7->m_pReason); + m_Chat.AddLine(-1, 0, aBuf); + break; + } + case protocol7::VOTE_START_SPEC: + { + str_format(aBuf, sizeof(aBuf), "'%s' called for vote to move '%s' to spectators (%s)", pName, pMsg7->m_pDescription, pMsg7->m_pReason); + m_Chat.AddLine(-1, 0, aBuf); + } + } + } + } + else + { + switch(pMsg7->m_Type) + { + case protocol7::VOTE_START_OP: + str_format(aBuf, sizeof(aBuf), "Admin forced server option '%s' (%s)", pMsg7->m_pDescription, pMsg7->m_pReason); + m_Chat.AddLine(-1, 0, aBuf); + break; + case protocol7::VOTE_START_SPEC: + str_format(aBuf, sizeof(aBuf), "Admin moved '%s' to spectator (%s)", pMsg7->m_pDescription, pMsg7->m_pReason); + m_Chat.AddLine(-1, 0, aBuf); + break; + case protocol7::VOTE_END_ABORT: + m_Voting.OnReset(); + m_Chat.AddLine(-1, 0, "Vote aborted"); + break; + case protocol7::VOTE_END_PASS: + m_Voting.OnReset(); + m_Chat.AddLine(-1, 0, pMsg7->m_ClientId == -1 ? "Admin forced vote yes" : "Vote passed"); + break; + case protocol7::VOTE_END_FAIL: + m_Voting.OnReset(); + m_Chat.AddLine(-1, 0, pMsg7->m_ClientId == -1 ? "Admin forced vote no" : "Vote failed"); + } + } + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTESTATUS) + { + *pMsgId = NETMSGTYPE_SV_VOTESTATUS; + protocol7::CNetMsg_Sv_VoteStatus *pMsg7 = (protocol7::CNetMsg_Sv_VoteStatus *)pRawMsg; + ::CNetMsg_Sv_VoteStatus *pMsg = (::CNetMsg_Sv_VoteStatus *)s_aRawMsg; + + pMsg->m_Yes = pMsg7->m_Yes; + pMsg->m_No = pMsg7->m_No; + pMsg->m_Pass = pMsg7->m_Pass; + pMsg->m_Total = pMsg7->m_Total; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_READYTOENTER) + { + *pMsgId = NETMSGTYPE_SV_READYTOENTER; + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_CLIENTDROP) + { + protocol7::CNetMsg_Sv_ClientDrop *pMsg7 = (protocol7::CNetMsg_Sv_ClientDrop *)pRawMsg; + if(pMsg7->m_ClientId < 0 || pMsg7->m_ClientId >= MAX_CLIENTS) + { + dbg_msg("sixup", "Sv_ClientDrop got invalid ClientId: %d", pMsg7->m_ClientId); + return nullptr; + } + CTranslationContext::CClientData &Client = m_pClient->m_TranslationContext.m_aClients[pMsg7->m_ClientId]; + Client.Reset(); + + if(pMsg7->m_Silent) + return nullptr; + + if(Conn != g_Config.m_ClDummy) + return nullptr; + + static char s_aBuf[128]; + if(pMsg7->m_pReason[0]) + str_format(s_aBuf, sizeof(s_aBuf), "'%s' has left the game (%s)", m_aClients[pMsg7->m_ClientId].m_aName, pMsg7->m_pReason); + else + str_format(s_aBuf, sizeof(s_aBuf), "'%s' has left the game", m_aClients[pMsg7->m_ClientId].m_aName); + m_Chat.AddLine(-1, 0, s_aBuf); + + return nullptr; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_CLIENTINFO) + { + protocol7::CNetMsg_Sv_ClientInfo *pMsg7 = (protocol7::CNetMsg_Sv_ClientInfo *)pRawMsg; + if(pMsg7->m_ClientId < 0 || pMsg7->m_ClientId >= MAX_CLIENTS) + { + dbg_msg("sixup", "Sv_ClientInfo got invalid ClientId: %d", pMsg7->m_ClientId); + return nullptr; + } + + if(pMsg7->m_Local) + { + m_pClient->m_TranslationContext.m_aLocalClientId[Conn] = pMsg7->m_ClientId; + } + CTranslationContext::CClientData &Client = m_pClient->m_TranslationContext.m_aClients[pMsg7->m_ClientId]; + Client.m_Active = true; + Client.m_Team = pMsg7->m_Team; + str_copy(Client.m_aName, pMsg7->m_pName); + str_copy(Client.m_aClan, pMsg7->m_pClan); + Client.m_Country = pMsg7->m_Country; + ApplySkin7InfoFromGameMsg(pMsg7, pMsg7->m_ClientId, Conn); + if(m_pClient->m_TranslationContext.m_aLocalClientId[Conn] == -1) + return nullptr; + if(pMsg7->m_Silent || pMsg7->m_Local) + return nullptr; + + if(Conn != g_Config.m_ClDummy) + return nullptr; + + DoTeamChangeMessage7( + pMsg7->m_pName, + pMsg7->m_ClientId, + pMsg7->m_Team, + "entered and "); + + return nullptr; // we only do side effects and add stuff to the snap here + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_GAMEINFO) + { + protocol7::CNetMsg_Sv_GameInfo *pMsg7 = (protocol7::CNetMsg_Sv_GameInfo *)pRawMsg; + m_pClient->m_TranslationContext.m_GameFlags = pMsg7->m_GameFlags; + m_pClient->m_TranslationContext.m_ScoreLimit = pMsg7->m_ScoreLimit; + m_pClient->m_TranslationContext.m_TimeLimit = pMsg7->m_TimeLimit; + m_pClient->m_TranslationContext.m_MatchNum = pMsg7->m_MatchNum; + m_pClient->m_TranslationContext.m_MatchCurrent = pMsg7->m_MatchCurrent; + m_pClient->m_TranslationContext.m_ShouldSendGameInfo = true; + return nullptr; // Added to snap by translation context + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_EMOTICON) + { + *pMsgId = NETMSGTYPE_SV_EMOTICON; + protocol7::CNetMsg_Sv_Emoticon *pMsg7 = (protocol7::CNetMsg_Sv_Emoticon *)pRawMsg; + ::CNetMsg_Sv_Emoticon *pMsg = (::CNetMsg_Sv_Emoticon *)s_aRawMsg; + + pMsg->m_ClientId = pMsg7->m_ClientId; + pMsg->m_Emoticon = pMsg7->m_Emoticon; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_KILLMSG) + { + *pMsgId = NETMSGTYPE_SV_KILLMSG; + + protocol7::CNetMsg_Sv_KillMsg *pMsg7 = (protocol7::CNetMsg_Sv_KillMsg *)pRawMsg; + ::CNetMsg_Sv_KillMsg *pMsg = (::CNetMsg_Sv_KillMsg *)s_aRawMsg; + + pMsg->m_Killer = pMsg7->m_Killer; + pMsg->m_Victim = pMsg7->m_Victim; + pMsg->m_Weapon = pMsg7->m_Weapon; + pMsg->m_ModeSpecial = pMsg7->m_ModeSpecial; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_CHAT) + { + *pMsgId = NETMSGTYPE_SV_CHAT; + + protocol7::CNetMsg_Sv_Chat *pMsg7 = (protocol7::CNetMsg_Sv_Chat *)pRawMsg; + ::CNetMsg_Sv_Chat *pMsg = (::CNetMsg_Sv_Chat *)s_aRawMsg; + + if(pMsg7->m_Mode == protocol7::CHAT_WHISPER) + { + bool Receive = pMsg7->m_TargetId == m_pClient->m_TranslationContext.m_aLocalClientId[Conn]; + + pMsg->m_Team = Receive ? 3 : 2; + pMsg->m_ClientId = Receive ? pMsg7->m_ClientId : pMsg7->m_TargetId; + } + else + { + pMsg->m_Team = pMsg7->m_Mode == protocol7::CHAT_TEAM ? 1 : 0; + pMsg->m_ClientId = pMsg7->m_ClientId; + } + + pMsg->m_pMessage = pMsg7->m_pMessage; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_GAMEMSG) + { + int GameMsgId = pUnpacker->GetInt(); + + /** + * Prints chat message only once + * even if it is being sent to main tee and dummy + */ + auto SendChat = [Conn, GameMsgId, this](const char *pText) -> void { + if(GameMsgId != protocol7::GAMEMSG_TEAM_BALANCE_VICTIM && GameMsgId != protocol7::GAMEMSG_SPEC_INVALIDID) + { + if(Conn != g_Config.m_ClDummy) + return; + } + m_Chat.AddLine(-1, 0, pText); + }; + + // check for valid gamemsgid + if(GameMsgId < 0 || GameMsgId >= protocol7::NUM_GAMEMSGS) + return nullptr; + + int aParaI[3] = {0}; + int NumParaI = 0; + + // get paras + switch(gs_GameMsgList7[GameMsgId].m_ParaType) + { + case PARA_I: NumParaI = 1; break; + case PARA_II: NumParaI = 2; break; + case PARA_III: NumParaI = 3; break; + } + for(int i = 0; i < NumParaI; i++) + { + aParaI[i] = pUnpacker->GetInt(); + } + + // check for unpacking errors + if(pUnpacker->Error()) + return nullptr; + + // handle special messages + char aBuf[256]; + bool TeamPlay = m_pClient->m_TranslationContext.m_GameFlags & protocol7::GAMEFLAG_TEAMS; + if(gs_GameMsgList7[GameMsgId].m_Action == DO_SPECIAL) + { + switch(GameMsgId) + { + case protocol7::GAMEMSG_CTF_DROP: + if(Conn == g_Config.m_ClDummy) + m_Sounds.Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_DROP); + break; + case protocol7::GAMEMSG_CTF_RETURN: + if(Conn == g_Config.m_ClDummy) + m_Sounds.Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_RETURN); + break; + case protocol7::GAMEMSG_TEAM_ALL: + { + const char *pMsg = ""; + switch(GetStrTeam7(aParaI[0], TeamPlay)) + { + case STR_TEAM_GAME: pMsg = "All players were moved to the game"; break; + case STR_TEAM_RED: pMsg = "All players were moved to the red team"; break; + case STR_TEAM_BLUE: pMsg = "All players were moved to the blue team"; break; + case STR_TEAM_SPECTATORS: pMsg = "All players were moved to the spectators"; break; + } + m_Broadcast.DoBroadcast(pMsg); // client side broadcast + } + break; + case protocol7::GAMEMSG_TEAM_BALANCE_VICTIM: + { + const char *pMsg = ""; + switch(GetStrTeam7(aParaI[0], TeamPlay)) + { + case STR_TEAM_RED: pMsg = "You were moved to the red team due to team balancing"; break; + case STR_TEAM_BLUE: pMsg = "You were moved to the blue team due to team balancing"; break; + } + m_Broadcast.DoBroadcast(pMsg); // client side broadcast + } + break; + case protocol7::GAMEMSG_CTF_GRAB: + if(Conn == g_Config.m_ClDummy) + m_Sounds.Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_GRAB_EN); + break; + case protocol7::GAMEMSG_GAME_PAUSED: + { + int ClientId = clamp(aParaI[0], 0, MAX_CLIENTS - 1); + str_format(aBuf, sizeof(aBuf), "'%s' initiated a pause", m_aClients[ClientId].m_aName); + SendChat(aBuf); + } + break; + case protocol7::GAMEMSG_CTF_CAPTURE: + if(Conn == g_Config.m_ClDummy) + m_Sounds.Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_CAPTURE); + int ClientId = clamp(aParaI[1], 0, MAX_CLIENTS - 1); + m_aStats[ClientId].m_FlagCaptures++; + + float Time = aParaI[2] / (float)Client()->GameTickSpeed(); + if(Time <= 60) + { + if(aParaI[0]) + { + str_format(aBuf, sizeof(aBuf), "The blue flag was captured by '%s' (%.2f seconds)", m_aClients[ClientId].m_aName, Time); + } + else + { + str_format(aBuf, sizeof(aBuf), "The red flag was captured by '%s' (%.2f seconds)", m_aClients[ClientId].m_aName, Time); + } + } + else + { + if(aParaI[0]) + { + str_format(aBuf, sizeof(aBuf), "The blue flag was captured by '%s'", m_aClients[ClientId].m_aName); + } + else + { + str_format(aBuf, sizeof(aBuf), "The red flag was captured by '%s'", m_aClients[ClientId].m_aName); + } + } + SendChat(aBuf); + } + return nullptr; + } + + // build message + const char *pText = ""; + if(NumParaI == 0) + { + pText = gs_GameMsgList7[GameMsgId].m_pText; + } + + // handle message + switch(gs_GameMsgList7[GameMsgId].m_Action) + { + case DO_CHAT: + SendChat(pText); + break; + case DO_BROADCAST: + m_Broadcast.DoBroadcast(pText); // client side broadcast + break; + } + + // no need to handle it in 0.6 since we printed + // the message already + return nullptr; + } + + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "dropped weird message '%s' (%d), failed on '%s'", m_NetObjHandler7.GetMsgName(*pMsgId), *pMsgId, m_NetObjHandler7.FailedMsgOn()); + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf); + + return nullptr; +} diff --git a/src/game/client/sixup_translate_snapshot.cpp b/src/game/client/sixup_translate_snapshot.cpp new file mode 100644 index 0000000000..2c33f4e1db --- /dev/null +++ b/src/game/client/sixup_translate_snapshot.cpp @@ -0,0 +1,564 @@ +#include +#include +#include +#include +#include + +int CGameClient::TranslateSnap(CSnapshot *pSnapDstSix, CSnapshot *pSnapSrcSeven, int Conn, bool Dummy) +{ + CSnapshotBuilder Builder; + Builder.Init(); + + float LocalTime = Client()->LocalTime(); + int GameTick = Client()->GameTick(g_Config.m_ClDummy); + CTranslationContext &TranslationContext = Client()->m_TranslationContext; + + for(auto &PlayerInfosRace : TranslationContext.m_apPlayerInfosRace) + PlayerInfosRace = nullptr; + + int SpectatorId = -3; + + for(int i = 0; i < pSnapSrcSeven->NumItems(); i++) + { + const CSnapshotItem *pItem7 = (CSnapshotItem *)pSnapSrcSeven->GetItem(i); + const int Size = pSnapSrcSeven->GetItemSize(i); + const int ItemType = pSnapSrcSeven->GetItemType(i); + + // ddnet ex snap items + if((ItemType > __NETOBJTYPE_UUID_HELPER && ItemType < OFFSET_NETMSGTYPE_UUID) || pItem7->Type() == NETOBJTYPE_EX) + { + CUnpacker Unpacker; + Unpacker.Reset(pItem7->Data(), Size); + + void *pRawObj = GetNetObjHandler()->SecureUnpackObj(ItemType, &Unpacker); + if(!pRawObj) + { + if(ItemType != UUID_UNKNOWN) + dbg_msg("sixup", "dropped weird ddnet ex object '%s' (%d), failed on '%s'", GetNetObjHandler()->GetObjName(ItemType), ItemType, GetNetObjHandler()->FailedObjOn()); + continue; + } + const int ItemSize = GetNetObjHandler()->GetUnpackedObjSize(ItemType); + + void *pObj = Builder.NewItem(pItem7->Type(), pItem7->Id(), ItemSize); + if(!pObj) + return -17; + + mem_copy(pObj, pRawObj, ItemSize); + continue; + } + + if(GetNetObjHandler7()->ValidateObj(pItem7->Type(), pItem7->Data(), Size) != 0) + { + if(pItem7->Type() > 0 && pItem7->Type() < CSnapshot::OFFSET_UUID_TYPE) + { + dbg_msg( + "sixup", + "invalidated index=%d type=%d (%s) size=%d id=%d", + i, + pItem7->Type(), + GetNetObjHandler7()->GetObjName(pItem7->Type()), + Size, + pItem7->Id()); + } + pSnapSrcSeven->InvalidateItem(i); + } + + if(pItem7->Type() == protocol7::NETOBJTYPE_PLAYERINFORACE) + { + const protocol7::CNetObj_PlayerInfoRace *pInfo = (const protocol7::CNetObj_PlayerInfoRace *)pItem7->Data(); + int ClientId = pItem7->Id(); + if(ClientId < MAX_CLIENTS && TranslationContext.m_aClients[ClientId].m_Active) + { + TranslationContext.m_apPlayerInfosRace[ClientId] = pInfo; + } + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_SPECTATORINFO) + { + const protocol7::CNetObj_SpectatorInfo *pSpec7 = (const protocol7::CNetObj_SpectatorInfo *)pItem7->Data(); + SpectatorId = pSpec7->m_SpectatorId; + if(pSpec7->m_SpecMode == protocol7::SPEC_FREEVIEW) + SpectatorId = SPEC_FREEVIEW; + } + } + + // hack to put game info in the snap + // even though in 0.7 we get it as a game message + // this message has to be on the top + // the client looks at the items in order and needs the gameinfo at the top + // otherwise it will not render skins with team colors + if(TranslationContext.m_ShouldSendGameInfo) + { + void *pObj = Builder.NewItem(NETOBJTYPE_GAMEINFO, 0, sizeof(CNetObj_GameInfo)); + if(!pObj) + return -1; + + int GameStateFlagsSix = 0; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_GAMEOVER) + GameStateFlagsSix |= GAMESTATEFLAG_GAMEOVER; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_SUDDENDEATH) + GameStateFlagsSix |= GAMESTATEFLAG_SUDDENDEATH; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_PAUSED) + GameStateFlagsSix |= GAMESTATEFLAG_PAUSED; + + /* + This is a 0.7 only flag that we just ignore for now + + GAMESTATEFLAG_ROUNDOVER + */ + + CNetObj_GameInfo Info6 = {}; + Info6.m_GameFlags = TranslationContext.m_GameFlags; + Info6.m_GameStateFlags = GameStateFlagsSix; + Info6.m_RoundStartTick = TranslationContext.m_GameStartTick7; + Info6.m_WarmupTimer = 0; // removed in 0.7 + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_WARMUP) + Info6.m_WarmupTimer = TranslationContext.m_GameStateEndTick7 - GameTick; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_STARTCOUNTDOWN) + Info6.m_WarmupTimer = TranslationContext.m_GameStateEndTick7 - GameTick; + + // hack to port 0.7 race timer to ddnet warmup gametimer hack + int TimerClientId = clamp(TranslationContext.m_aLocalClientId[Conn], 0, (int)MAX_CLIENTS); + if(SpectatorId >= 0) + TimerClientId = SpectatorId; + const protocol7::CNetObj_PlayerInfoRace *pRaceInfo = TranslationContext.m_apPlayerInfosRace[TimerClientId]; + if(pRaceInfo) + { + Info6.m_WarmupTimer = -pRaceInfo->m_RaceStartTick; + Info6.m_GameStateFlags |= GAMESTATEFLAG_RACETIME; + } + + Info6.m_ScoreLimit = TranslationContext.m_ScoreLimit; + Info6.m_TimeLimit = TranslationContext.m_TimeLimit; + Info6.m_RoundNum = TranslationContext.m_MatchNum; + Info6.m_RoundCurrent = TranslationContext.m_MatchCurrent; + + mem_copy(pObj, &Info6, sizeof(CNetObj_GameInfo)); + } + + for(int i = 0; i < MAX_CLIENTS; i++) + { + const CTranslationContext::CClientData &Client = TranslationContext.m_aClients[i]; + if(!Client.m_Active) + continue; + + void *pObj = Builder.NewItem(NETOBJTYPE_CLIENTINFO, i, sizeof(CNetObj_ClientInfo)); + if(!pObj) + return -2; + + CNetObj_ClientInfo Info6 = {}; + StrToInts(&Info6.m_Name0, 4, Client.m_aName); + StrToInts(&Info6.m_Clan0, 3, Client.m_aClan); + Info6.m_Country = Client.m_Country; + StrToInts(&Info6.m_Skin0, 6, Client.m_aSkinName); + Info6.m_UseCustomColor = Client.m_UseCustomColor; + Info6.m_ColorBody = Client.m_ColorBody; + Info6.m_ColorFeet = Client.m_ColorFeet; + mem_copy(pObj, &Info6, sizeof(CNetObj_ClientInfo)); + } + + bool NewGameData = false; + + for(int i = 0; i < pSnapSrcSeven->NumItems(); i++) + { + const CSnapshotItem *pItem7 = pSnapSrcSeven->GetItem(i); + const int Size = pSnapSrcSeven->GetItemSize(i); + // the first few items are a full match + // no translation needed + if(pItem7->Type() == protocol7::NETOBJTYPE_PROJECTILE || + pItem7->Type() == protocol7::NETOBJTYPE_LASER || + pItem7->Type() == protocol7::NETOBJTYPE_FLAG) + { + void *pObj = Builder.NewItem(pItem7->Type(), pItem7->Id(), Size); + if(!pObj) + return -4; + + mem_copy(pObj, pItem7->Data(), Size); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_PICKUP) + { + void *pObj = Builder.NewItem(NETOBJTYPE_PICKUP, pItem7->Id(), sizeof(CNetObj_Pickup)); + if(!pObj) + return -5; + + const protocol7::CNetObj_Pickup *pPickup7 = (const protocol7::CNetObj_Pickup *)pItem7->Data(); + CNetObj_Pickup Pickup6 = {}; + Pickup6.m_X = pPickup7->m_X; + Pickup6.m_Y = pPickup7->m_Y; + PickupType_SevenToSix(pPickup7->m_Type, Pickup6.m_Type, Pickup6.m_Subtype); + + mem_copy(pObj, &Pickup6, sizeof(CNetObj_Pickup)); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATA) + { + const protocol7::CNetObj_GameData *pGameData = (const protocol7::CNetObj_GameData *)pItem7->Data(); + TranslationContext.m_GameStateFlags7 = pGameData->m_GameStateFlags; + TranslationContext.m_GameStartTick7 = pGameData->m_GameStartTick; + TranslationContext.m_GameStateEndTick7 = pGameData->m_GameStateEndTick; + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATATEAM) + { + // 0.7 added GameDataTeam and GameDataFlag + // both items merged together have all fields of the 0.6 GameData + // so if we get either one of them we store the details in the translation context + // and build one GameData snap item after this loop + const protocol7::CNetObj_GameDataTeam *pTeam7 = (const protocol7::CNetObj_GameDataTeam *)pItem7->Data(); + + TranslationContext.m_TeamscoreRed = pTeam7->m_TeamscoreRed; + TranslationContext.m_TeamscoreBlue = pTeam7->m_TeamscoreBlue; + NewGameData = true; + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATAFLAG) + { + const protocol7::CNetObj_GameDataFlag *pFlag7 = (const protocol7::CNetObj_GameDataFlag *)pItem7->Data(); + + TranslationContext.m_FlagCarrierRed = pFlag7->m_FlagCarrierRed; + TranslationContext.m_FlagCarrierBlue = pFlag7->m_FlagCarrierBlue; + NewGameData = true; + + // used for blinking the flags in the hud + // but that already works the 0.6 way + // and is covered by the NETOBJTYPE_GAMEDATA translation + // pFlag7->m_FlagDropTickRed; + // pFlag7->m_FlagDropTickBlue; + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_CHARACTER) + { + void *pObj = Builder.NewItem(NETOBJTYPE_CHARACTER, pItem7->Id(), sizeof(CNetObj_Character)); + if(!pObj) + return -6; + + const protocol7::CNetObj_Character *pChar7 = (const protocol7::CNetObj_Character *)pItem7->Data(); + + CNetObj_Character Char6 = {}; + // character core is unchanged + mem_copy(&Char6, pItem7->Data(), sizeof(CNetObj_CharacterCore)); + + Char6.m_PlayerFlags = 0; + if(pItem7->Id() >= 0 && pItem7->Id() < MAX_CLIENTS) + Char6.m_PlayerFlags = PlayerFlags_SevenToSix(TranslationContext.m_aClients[pItem7->Id()].m_PlayerFlags7); + Char6.m_Health = pChar7->m_Health; + Char6.m_Armor = pChar7->m_Armor; + Char6.m_AmmoCount = pChar7->m_AmmoCount; + Char6.m_Weapon = pChar7->m_Weapon; + Char6.m_Emote = pChar7->m_Emote; + Char6.m_AttackTick = pChar7->m_AttackTick; + + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_ATTACH_PLAYER) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_HOOK_ATTACH_PLAYER; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + + if(TranslationContext.m_aLocalClientId[Conn] != pItem7->Id()) + { + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_GROUND_JUMP) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_PLAYER_JUMP; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_ATTACH_GROUND) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_HOOK_ATTACH_GROUND; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_HIT_NOHOOK) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_HOOK_NOATTACH; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + } + + mem_copy(pObj, &Char6, sizeof(CNetObj_Character)); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_PLAYERINFO) + { + void *pObj = Builder.NewItem(NETOBJTYPE_PLAYERINFO, pItem7->Id(), sizeof(CNetObj_PlayerInfo)); + if(!pObj) + return -8; + + const protocol7::CNetObj_PlayerInfo *pInfo7 = (const protocol7::CNetObj_PlayerInfo *)pItem7->Data(); + CNetObj_PlayerInfo Info6 = {}; + Info6.m_Local = TranslationContext.m_aLocalClientId[Conn] == pItem7->Id(); + Info6.m_ClientId = pItem7->Id(); + Info6.m_Team = 0; + if(pItem7->Id() >= 0 && pItem7->Id() < MAX_CLIENTS) + { + Info6.m_Team = TranslationContext.m_aClients[pItem7->Id()].m_Team; + TranslationContext.m_aClients[pItem7->Id()].m_PlayerFlags7 = pInfo7->m_PlayerFlags; + } + Info6.m_Score = pInfo7->m_Score; + Info6.m_Latency = pInfo7->m_Latency; + mem_copy(pObj, &Info6, sizeof(CNetObj_PlayerInfo)); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_SPECTATORINFO) + { + void *pObj = Builder.NewItem(NETOBJTYPE_SPECTATORINFO, pItem7->Id(), sizeof(CNetObj_SpectatorInfo)); + if(!pObj) + return -9; + + const protocol7::CNetObj_SpectatorInfo *pSpec7 = (const protocol7::CNetObj_SpectatorInfo *)pItem7->Data(); + CNetObj_SpectatorInfo Spec6 = {}; + Spec6.m_SpectatorId = pSpec7->m_SpectatorId; + if(pSpec7->m_SpecMode == protocol7::SPEC_FREEVIEW) + Spec6.m_SpectatorId = SPEC_FREEVIEW; + Spec6.m_X = pSpec7->m_X; + Spec6.m_Y = pSpec7->m_Y; + mem_copy(pObj, &Spec6, sizeof(CNetObj_SpectatorInfo)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_EXPLOSION) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_EXPLOSION, pItem7->Id(), sizeof(CNetEvent_Explosion)); + if(!pEvent) + return -10; + + const protocol7::CNetEvent_Explosion *pExplosion7 = (const protocol7::CNetEvent_Explosion *)pItem7->Data(); + CNetEvent_Explosion Explosion6 = {}; + Explosion6.m_X = pExplosion7->m_X; + Explosion6.m_Y = pExplosion7->m_Y; + mem_copy(pEvent, &Explosion6, sizeof(CNetEvent_Explosion)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_SPAWN) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SPAWN, pItem7->Id(), sizeof(CNetEvent_Spawn)); + if(!pEvent) + return -11; + + const protocol7::CNetEvent_Spawn *pSpawn7 = (const protocol7::CNetEvent_Spawn *)pItem7->Data(); + CNetEvent_Spawn Spawn6 = {}; + Spawn6.m_X = pSpawn7->m_X; + Spawn6.m_Y = pSpawn7->m_Y; + mem_copy(pEvent, &Spawn6, sizeof(CNetEvent_Spawn)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_HAMMERHIT) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_HAMMERHIT, pItem7->Id(), sizeof(CNetEvent_HammerHit)); + if(!pEvent) + return -12; + + const protocol7::CNetEvent_HammerHit *pHammerHit7 = (const protocol7::CNetEvent_HammerHit *)pItem7->Data(); + CNetEvent_HammerHit HammerHit6 = {}; + HammerHit6.m_X = pHammerHit7->m_X; + HammerHit6.m_Y = pHammerHit7->m_Y; + mem_copy(pEvent, &HammerHit6, sizeof(CNetEvent_HammerHit)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_DEATH) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_DEATH, pItem7->Id(), sizeof(CNetEvent_Death)); + if(!pEvent) + return -13; + + const protocol7::CNetEvent_Death *pDeath7 = (const protocol7::CNetEvent_Death *)pItem7->Data(); + CNetEvent_Death Death6 = {}; + Death6.m_X = pDeath7->m_X; + Death6.m_Y = pDeath7->m_Y; + Death6.m_ClientId = pDeath7->m_ClientId; + mem_copy(pEvent, &Death6, sizeof(CNetEvent_Death)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_SOUNDWORLD) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -14; + + const protocol7::CNetEvent_SoundWorld *pSoundWorld7 = (const protocol7::CNetEvent_SoundWorld *)pItem7->Data(); + CNetEvent_SoundWorld SoundWorld6 = {}; + SoundWorld6.m_X = pSoundWorld7->m_X; + SoundWorld6.m_Y = pSoundWorld7->m_Y; + SoundWorld6.m_SoundId = pSoundWorld7->m_SoundId; + mem_copy(pEvent, &SoundWorld6, sizeof(CNetEvent_SoundWorld)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_DAMAGE) + { + // 0.7 introduced amount for damage indicators + // so for one 0.7 item we might create multiple 0.6 ones + const protocol7::CNetEvent_Damage *pDmg7 = (const protocol7::CNetEvent_Damage *)pItem7->Data(); + + int Amount = pDmg7->m_HealthAmount + pDmg7->m_ArmorAmount; + if(Amount < 1) + continue; + + int ClientId = pDmg7->m_ClientId; + TranslationContext.m_aDamageTaken[ClientId]++; + + float Angle; + // create healthmod indicator + if(LocalTime < TranslationContext.m_aDamageTakenTick[ClientId] + 0.5f) + { + // make sure that the damage indicators don't group together + Angle = TranslationContext.m_aDamageTaken[ClientId] * 0.25f; + } + else + { + TranslationContext.m_aDamageTaken[ClientId] = 0; + Angle = 0; + } + + TranslationContext.m_aDamageTakenTick[ClientId] = LocalTime; + + float a = 3 * pi / 2 + Angle; + float s = a - pi / 3; + float e = a + pi / 3; + for(int k = 0; k < Amount; k++) + { + // pItem7->Id() is reused that is technically wrong + // but the client implementation does not look at the ids + // and renders the damage indicators just fine + void *pEvent = Builder.NewItem(NETEVENTTYPE_DAMAGEIND, pItem7->Id(), sizeof(CNetEvent_DamageInd)); + if(!pEvent) + return -16; + + CNetEvent_DamageInd Dmg6 = {}; + Dmg6.m_X = pDmg7->m_X; + Dmg6.m_Y = pDmg7->m_Y; + float f = mix(s, e, float(k + 1) / float(Amount + 2)); + Dmg6.m_Angle = (int)(f * 256.0f); + mem_copy(pEvent, &Dmg6, sizeof(CNetEvent_DamageInd)); + } + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_DE_CLIENTINFO) + { + const protocol7::CNetObj_De_ClientInfo *pInfo = (const protocol7::CNetObj_De_ClientInfo *)pItem7->Data(); + + const int ClientId = pItem7->Id(); + + if(ClientId < 0 || ClientId >= MAX_CLIENTS) + { + dbg_msg("sixup", "De_ClientInfo got invalid ClientId: %d", ClientId); + return -17; + } + + if(pInfo->m_Local) + { + TranslationContext.m_aLocalClientId[Conn] = ClientId; + } + CTranslationContext::CClientData &Client = TranslationContext.m_aClients[ClientId]; + Client.m_Active = true; + Client.m_Team = pInfo->m_Team; + IntsToStr(pInfo->m_aName, 4, Client.m_aName, std::size(Client.m_aName)); + IntsToStr(pInfo->m_aClan, 3, Client.m_aClan, std::size(Client.m_aClan)); + Client.m_Country = pInfo->m_Country; + + ApplySkin7InfoFromSnapObj(pInfo, ClientId); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_DE_GAMEINFO) + { + const protocol7::CNetObj_De_GameInfo *pInfo = (const protocol7::CNetObj_De_GameInfo *)pItem7->Data(); + + TranslationContext.m_GameFlags = pInfo->m_GameFlags; + TranslationContext.m_ScoreLimit = pInfo->m_ScoreLimit; + TranslationContext.m_TimeLimit = pInfo->m_TimeLimit; + TranslationContext.m_MatchNum = pInfo->m_MatchNum; + TranslationContext.m_MatchCurrent = pInfo->m_MatchCurrent; + TranslationContext.m_ShouldSendGameInfo = true; + } + } + + if(NewGameData) + { + void *pObj = Builder.NewItem(NETOBJTYPE_GAMEDATA, 0, sizeof(CNetObj_GameData)); + if(!pObj) + return -17; + + CNetObj_GameData GameData = {}; + GameData.m_TeamscoreRed = TranslationContext.m_TeamscoreRed; + GameData.m_TeamscoreBlue = TranslationContext.m_TeamscoreBlue; + GameData.m_FlagCarrierRed = TranslationContext.m_FlagCarrierRed; + GameData.m_FlagCarrierBlue = TranslationContext.m_FlagCarrierBlue; + mem_copy(pObj, &GameData, sizeof(CNetObj_GameData)); + } + + return Builder.Finish(pSnapDstSix); +} + +int CGameClient::OnDemoRecSnap7(CSnapshot *pFrom, CSnapshot *pTo, int Conn) +{ + CSnapshotBuilder Builder; + Builder.Init7(pFrom); + + // add client info + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(!m_aClients[i].m_Active) + continue; + + void *pItem = Builder.NewItem(protocol7::NETOBJTYPE_DE_CLIENTINFO, i, sizeof(protocol7::CNetObj_De_ClientInfo)); + if(!pItem) + return -1; + + CTranslationContext::CClientData &ClientData = Client()->m_TranslationContext.m_aClients[i]; + + protocol7::CNetObj_De_ClientInfo ClientInfoObj; + ClientInfoObj.m_Local = i == Client()->m_TranslationContext.m_aLocalClientId[Conn]; + ClientInfoObj.m_Team = ClientData.m_Team; + StrToInts(ClientInfoObj.m_aName, 4, m_aClients[i].m_aName); + StrToInts(ClientInfoObj.m_aClan, 3, m_aClients[i].m_aClan); + ClientInfoObj.m_Country = ClientData.m_Country; + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + StrToInts(ClientInfoObj.m_aaSkinPartNames[Part], 6, m_aClients[i].m_aSixup[Conn].m_aaSkinPartNames[Part]); + ClientInfoObj.m_aUseCustomColors[Part] = m_aClients[i].m_aSixup[Conn].m_aUseCustomColors[Part]; + ClientInfoObj.m_aSkinPartColors[Part] = m_aClients[i].m_aSixup[Conn].m_aSkinPartColors[Part]; + } + + mem_copy(pItem, &ClientInfoObj, sizeof(protocol7::CNetObj_De_ClientInfo)); + } + + // add tuning + CTuningParams StandardTuning; + if(mem_comp(&StandardTuning, &m_aTuning[Conn], sizeof(CTuningParams)) != 0) + { + void *pItem = Builder.NewItem(protocol7::NETOBJTYPE_DE_TUNEPARAMS, 0, sizeof(protocol7::CNetObj_De_TuneParams)); + if(!pItem) + return -2; + + protocol7::CNetObj_De_TuneParams TuneParams; + mem_copy(&TuneParams.m_aTuneParams, &m_aTuning[Conn], sizeof(TuneParams)); + mem_copy(pItem, &TuneParams, sizeof(protocol7::CNetObj_De_TuneParams)); + } + + // add game info + void *pItem = Builder.NewItem(protocol7::NETOBJTYPE_DE_GAMEINFO, 0, sizeof(protocol7::CNetObj_De_GameInfo)); + if(!pItem) + return -3; + + protocol7::CNetObj_De_GameInfo GameInfo; + + GameInfo.m_GameFlags = Client()->m_TranslationContext.m_GameFlags; + GameInfo.m_ScoreLimit = Client()->m_TranslationContext.m_ScoreLimit; + GameInfo.m_TimeLimit = Client()->m_TranslationContext.m_TimeLimit; + GameInfo.m_MatchNum = Client()->m_TranslationContext.m_MatchNum; + GameInfo.m_MatchCurrent = Client()->m_TranslationContext.m_MatchCurrent; + + mem_copy(pItem, &GameInfo, sizeof(protocol7::CNetObj_De_GameInfo)); + + return Builder.Finish(pTo); +} diff --git a/src/game/client/skin.h b/src/game/client/skin.h index aed057e3c1..b679186708 100644 --- a/src/game/client/skin.h +++ b/src/game/client/skin.h @@ -37,6 +37,18 @@ struct CSkin for(auto &Eye : m_aEyes) Eye = IGraphics::CTextureHandle(); } + + void Unload(IGraphics *pGraphics) + { + pGraphics->UnloadTexture(&m_Body); + pGraphics->UnloadTexture(&m_BodyOutline); + pGraphics->UnloadTexture(&m_Feet); + pGraphics->UnloadTexture(&m_FeetOutline); + pGraphics->UnloadTexture(&m_Hands); + pGraphics->UnloadTexture(&m_HandsOutline); + for(auto &Eye : m_aEyes) + pGraphics->UnloadTexture(&Eye); + } }; SSkinTextures m_OriginalSkin; @@ -142,6 +154,25 @@ struct CSkin CSkin &operator=(CSkin &&) = default; const char *GetName() const { return m_aName; } + + // has to be kept in sync with m_aSkinNameRestrictions + static bool IsValidName(const char *pName) + { + if(pName[0] == '\0' || str_length(pName) >= (int)sizeof(CSkin("").m_aName)) + { + return false; + } + + for(int i = 0; pName[i] != '\0'; ++i) + { + if(pName[i] == '"' || pName[i] == '/' || pName[i] == '\\') + { + return false; + } + } + return true; + } + static constexpr char m_aSkinNameRestrictions[] = "Skin names must be valid filenames shorter than 24 characters."; }; #endif diff --git a/src/game/client/ui.cpp b/src/game/client/ui.cpp index d1ee0242b6..fada88295b 100644 --- a/src/game/client/ui.cpp +++ b/src/game/client/ui.cpp @@ -18,7 +18,7 @@ using namespace FontIcons; -void CUIElement::Init(CUI *pUI, int RequestedRectCount) +void CUIElement::Init(CUi *pUI, int RequestedRectCount) { m_pUI = pUI; pUI->AddUIElement(this); @@ -57,9 +57,9 @@ void CUIElement::SUIElementRect::Reset() void CUIElement::SUIElementRect::Draw(const CUIRect *pRect, ColorRGBA Color, int Corners, float Rounding) { bool NeedsRecreate = false; - if(m_UIRectQuadContainer == -1 || m_Width != pRect->w || m_Height != pRect->h || mem_comp(&m_QuadColor, &Color, sizeof(Color)) != 0) + if(m_UIRectQuadContainer == -1 || m_Width != pRect->w || m_Height != pRect->h || m_QuadColor != Color) { - m_pParent->UI()->Graphics()->DeleteQuadContainer(m_UIRectQuadContainer); + m_pParent->Ui()->Graphics()->DeleteQuadContainer(m_UIRectQuadContainer); NeedsRecreate = true; } m_X = pRect->x; @@ -70,13 +70,13 @@ void CUIElement::SUIElementRect::Draw(const CUIRect *pRect, ColorRGBA Color, int m_Height = pRect->h; m_QuadColor = Color; - m_pParent->UI()->Graphics()->SetColor(Color); - m_UIRectQuadContainer = m_pParent->UI()->Graphics()->CreateRectQuadContainer(0, 0, pRect->w, pRect->h, Rounding, Corners); - m_pParent->UI()->Graphics()->SetColor(1, 1, 1, 1); + m_pParent->Ui()->Graphics()->SetColor(Color); + m_UIRectQuadContainer = m_pParent->Ui()->Graphics()->CreateRectQuadContainer(0, 0, pRect->w, pRect->h, Rounding, Corners); + m_pParent->Ui()->Graphics()->SetColor(1, 1, 1, 1); } - m_pParent->UI()->Graphics()->TextureClear(); - m_pParent->UI()->Graphics()->RenderQuadContainerEx(m_UIRectQuadContainer, + m_pParent->Ui()->Graphics()->TextureClear(); + m_pParent->Ui()->Graphics()->RenderQuadContainerEx(m_UIRectQuadContainer, 0, -1, m_X, m_Y, 1, 1); } @@ -84,21 +84,21 @@ void CUIElement::SUIElementRect::Draw(const CUIRect *pRect, ColorRGBA Color, int UI *********************************************************/ -const CLinearScrollbarScale CUI::ms_LinearScrollbarScale; -const CLogarithmicScrollbarScale CUI::ms_LogarithmicScrollbarScale(25); -const CDarkButtonColorFunction CUI::ms_DarkButtonColorFunction; -const CLightButtonColorFunction CUI::ms_LightButtonColorFunction; -const CScrollBarColorFunction CUI::ms_ScrollBarColorFunction; -const float CUI::ms_FontmodHeight = 0.8f; +const CLinearScrollbarScale CUi::ms_LinearScrollbarScale; +const CLogarithmicScrollbarScale CUi::ms_LogarithmicScrollbarScale(25); +const CDarkButtonColorFunction CUi::ms_DarkButtonColorFunction; +const CLightButtonColorFunction CUi::ms_LightButtonColorFunction; +const CScrollBarColorFunction CUi::ms_ScrollBarColorFunction; +const float CUi::ms_FontmodHeight = 0.8f; -CUI *CUIElementBase::s_pUI = nullptr; +CUi *CUIElementBase::s_pUI = nullptr; IClient *CUIElementBase::Client() const { return s_pUI->Client(); } IGraphics *CUIElementBase::Graphics() const { return s_pUI->Graphics(); } IInput *CUIElementBase::Input() const { return s_pUI->Input(); } ITextRender *CUIElementBase::TextRender() const { return s_pUI->TextRender(); } -void CUI::Init(IKernel *pKernel) +void CUi::Init(IKernel *pKernel) { m_pClient = pKernel->RequestInterface(); m_pGraphics = pKernel->RequestInterface(); @@ -109,27 +109,15 @@ void CUI::Init(IKernel *pKernel) CUIElementBase::Init(this); } -CUI::CUI() +CUi::CUi() { m_Enabled = true; - m_pHotItem = nullptr; - m_pActiveItem = nullptr; - m_pLastActiveItem = nullptr; - m_pBecomingHotItem = nullptr; - - m_MouseX = 0; - m_MouseY = 0; - m_MouseWorldX = 0; - m_MouseWorldY = 0; - m_MouseButtons = 0; - m_LastMouseButtons = 0; - m_Screen.x = 0.0f; m_Screen.y = 0.0f; } -CUI::~CUI() +CUi::~CUi() { for(CUIElement *&pEl : m_vpOwnUIElements) { @@ -138,7 +126,7 @@ CUI::~CUI() m_vpOwnUIElements.clear(); } -CUIElement *CUI::GetNewUIElement(int RequestedRectCount) +CUIElement *CUi::GetNewUIElement(int RequestedRectCount) { CUIElement *pNewEl = new CUIElement(this, RequestedRectCount); @@ -147,12 +135,12 @@ CUIElement *CUI::GetNewUIElement(int RequestedRectCount) return pNewEl; } -void CUI::AddUIElement(CUIElement *pElement) +void CUi::AddUIElement(CUIElement *pElement) { m_vpUIElements.push_back(pElement); } -void CUI::ResetUIElement(CUIElement &UIElement) const +void CUi::ResetUIElement(CUIElement &UIElement) const { for(CUIElement::SUIElementRect &Rect : UIElement.m_vUIRects) { @@ -162,7 +150,7 @@ void CUI::ResetUIElement(CUIElement &UIElement) const } } -void CUI::OnElementsReset() +void CUi::OnElementsReset() { for(CUIElement *pEl : m_vpUIElements) { @@ -170,56 +158,88 @@ void CUI::OnElementsReset() } } -void CUI::OnWindowResize() +void CUi::OnWindowResize() { OnElementsReset(); } -void CUI::OnCursorMove(float X, float Y) +void CUi::OnCursorMove(float X, float Y) { if(!CheckMouseLock()) { - m_UpdatedMousePos.x = clamp(m_UpdatedMousePos.x + X, 0.0f, (float)Graphics()->WindowWidth()); - m_UpdatedMousePos.y = clamp(m_UpdatedMousePos.y + Y, 0.0f, (float)Graphics()->WindowHeight()); + m_UpdatedMousePos.x = clamp(m_UpdatedMousePos.x + X, 0.0f, Graphics()->WindowWidth() - 1.0f); + m_UpdatedMousePos.y = clamp(m_UpdatedMousePos.y + Y, 0.0f, Graphics()->WindowHeight() - 1.0f); } m_UpdatedMouseDelta += vec2(X, Y); } -void CUI::Update() +void CUi::Update(vec2 MouseWorldPos) { + const vec2 WindowSize = vec2(Graphics()->WindowWidth(), Graphics()->WindowHeight()); const CUIRect *pScreen = Screen(); - const float MouseX = (m_UpdatedMousePos.x / (float)Graphics()->WindowWidth()) * pScreen->w; - const float MouseY = (m_UpdatedMousePos.y / (float)Graphics()->WindowHeight()) * pScreen->h; - Update(MouseX, MouseY, m_UpdatedMouseDelta.x, m_UpdatedMouseDelta.y, MouseX * 3.0f, MouseY * 3.0f); - m_UpdatedMouseDelta = vec2(0.0f, 0.0f); -} -void CUI::Update(float MouseX, float MouseY, float MouseDeltaX, float MouseDeltaY, float MouseWorldX, float MouseWorldY) -{ - unsigned MouseButtons = 0; + unsigned UpdatedMouseButtonsNext = 0; if(Enabled()) { - if(Input()->KeyIsPressed(KEY_MOUSE_1)) - MouseButtons |= 1; - if(Input()->KeyIsPressed(KEY_MOUSE_2)) - MouseButtons |= 2; - if(Input()->KeyIsPressed(KEY_MOUSE_3)) - MouseButtons |= 4; + // Update mouse buttons based on mouse keys + for(int MouseKey = KEY_MOUSE_1; MouseKey <= KEY_MOUSE_3; ++MouseKey) + { + if(Input()->KeyIsPressed(MouseKey)) + { + m_UpdatedMouseButtons |= 1 << (MouseKey - KEY_MOUSE_1); + } + } + + // Update mouse position and buttons based on touch finger state + UpdateTouchState(m_TouchState); + if(m_TouchState.m_AnyPressed) + { + if(!CheckMouseLock()) + { + m_UpdatedMousePos = m_TouchState.m_PrimaryPosition * WindowSize; + m_UpdatedMousePos.x = clamp(m_UpdatedMousePos.x, 0.0f, WindowSize.x - 1.0f); + m_UpdatedMousePos.y = clamp(m_UpdatedMousePos.y, 0.0f, WindowSize.y - 1.0f); + } + m_UpdatedMouseDelta += m_TouchState.m_PrimaryDelta * WindowSize; + + // Scroll currently hovered scroll region with touch scroll gesture. + if(m_TouchState.m_ScrollAmount != vec2(0.0f, 0.0f)) + { + if(m_pHotScrollRegion != nullptr) + { + m_pHotScrollRegion->ScrollRelativeDirect(-m_TouchState.m_ScrollAmount.y * pScreen->h); + } + m_TouchState.m_ScrollAmount = vec2(0.0f, 0.0f); + } + + // We need to delay the click until the next update or it's not possible to use UI + // elements because click and hover would happen at the same time for touch events. + if(m_TouchState.m_PrimaryPressed) + { + UpdatedMouseButtonsNext |= 1; + } + if(m_TouchState.m_SecondaryPressed) + { + UpdatedMouseButtonsNext |= 2; + } + } } - m_MouseDeltaX = MouseDeltaX; - m_MouseDeltaY = MouseDeltaY; - m_MouseX = MouseX; - m_MouseY = MouseY; - m_MouseWorldX = MouseWorldX; - m_MouseWorldY = MouseWorldY; + m_MousePos = m_UpdatedMousePos * vec2(pScreen->w, pScreen->h) / WindowSize; + m_MouseDelta = m_UpdatedMouseDelta; + m_UpdatedMouseDelta = vec2(0.0f, 0.0f); + m_MouseWorldPos = MouseWorldPos; m_LastMouseButtons = m_MouseButtons; - m_MouseButtons = MouseButtons; + m_MouseButtons = m_UpdatedMouseButtons; + m_UpdatedMouseButtons = UpdatedMouseButtonsNext; + m_pHotItem = m_pBecomingHotItem; if(m_pActiveItem) m_pHotItem = m_pActiveItem; m_pBecomingHotItem = nullptr; + m_pHotScrollRegion = m_pBecomingHotScrollRegion; + m_pBecomingHotScrollRegion = nullptr; if(Enabled()) { @@ -231,24 +251,28 @@ void CUI::Update(float MouseX, float MouseY, float MouseDeltaX, float MouseDelta { m_pHotItem = nullptr; m_pActiveItem = nullptr; + m_pHotScrollRegion = nullptr; } + + m_ProgressSpinnerOffset += Client()->RenderFrameTime() * 1.5f; + m_ProgressSpinnerOffset = std::fmod(m_ProgressSpinnerOffset, 1.0f); } -void CUI::DebugRender() +void CUi::DebugRender(float X, float Y) { MapScreen(); char aBuf[128]; str_format(aBuf, sizeof(aBuf), "hot=%p nexthot=%p active=%p lastactive=%p", HotItem(), NextHotItem(), ActiveItem(), m_pLastActiveItem); - TextRender()->Text(2.0f, Screen()->h - 12.0f, 10.0f, aBuf); + TextRender()->Text(X, Y, 10.0f, aBuf); } -bool CUI::MouseInside(const CUIRect *pRect) const +bool CUi::MouseInside(const CUIRect *pRect) const { - return pRect->Inside(m_MouseX, m_MouseY); + return pRect->Inside(MousePos()); } -void CUI::ConvertMouseMove(float *pX, float *pY, IInput::ECursorType CursorType) const +void CUi::ConvertMouseMove(float *pX, float *pY, IInput::ECursorType CursorType) const { float Factor = 1.0f; switch(CursorType) @@ -260,7 +284,7 @@ void CUI::ConvertMouseMove(float *pX, float *pY, IInput::ECursorType CursorType) Factor = g_Config.m_UiControllerSens / 100.0f; break; default: - dbg_msg("assert", "CUI::ConvertMouseMove CursorType %d", (int)CursorType); + dbg_msg("assert", "CUi::ConvertMouseMove CursorType %d", (int)CursorType); dbg_break(); break; } @@ -272,14 +296,95 @@ void CUI::ConvertMouseMove(float *pX, float *pY, IInput::ECursorType CursorType) *pY *= Factor; } -bool CUI::ConsumeHotkey(EHotkey Hotkey) +void CUi::UpdateTouchState(CTouchState &State) const +{ + const std::vector &vTouchFingerStates = Input()->TouchFingerStates(); + + // Updated touch position as long as any finger is beinged pressed. + const bool WasAnyPressed = State.m_AnyPressed; + State.m_AnyPressed = !vTouchFingerStates.empty(); + if(State.m_AnyPressed) + { + // We always use the position of first finger being pressed down. Multi-touch UI is + // not possible and always choosing the last finger would cause the cursor to briefly + // warp without having any effect if multiple fingers are used. + const IInput::CTouchFingerState &PrimaryTouchFingerState = vTouchFingerStates.front(); + State.m_PrimaryPosition = PrimaryTouchFingerState.m_Position; + State.m_PrimaryDelta = PrimaryTouchFingerState.m_Delta; + } + + // Update primary (left click) and secondary (right click) action. + if(State.m_SecondaryPressedNext) + { + // The secondary action is delayed by one frame until the primary has been released, + // otherwise most UI elements cannot be activated by the secondary action because they + // never become the hot-item unless all mouse buttons are released for one frame. + State.m_SecondaryPressedNext = false; + State.m_SecondaryPressed = true; + } + else if(vTouchFingerStates.size() != 1) + { + // Consider primary and secondary to be pressed only when exactly one finger is pressed, + // to avoid UI elements and console text selection being activated while scrolling. + State.m_PrimaryPressed = false; + State.m_SecondaryPressed = false; + } + else if(!WasAnyPressed) + { + State.m_PrimaryPressed = true; + State.m_SecondaryActivationTime = Client()->GlobalTime(); + State.m_SecondaryActivationDelta = vec2(0.0f, 0.0f); + } + else if(State.m_PrimaryPressed) + { + // Activate secondary by pressing and holding roughly on the same position for some time. + const float SecondaryActivationDelay = 0.5f; + const float SecondaryActivationMaxDistance = 0.001f; + State.m_SecondaryActivationDelta += State.m_PrimaryDelta; + if(Client()->GlobalTime() - State.m_SecondaryActivationTime >= SecondaryActivationDelay && + length(State.m_SecondaryActivationDelta) <= SecondaryActivationMaxDistance) + { + State.m_PrimaryPressed = false; + State.m_SecondaryPressedNext = true; + } + } + + // Handle two fingers being moved roughly in same direction as a scrolling gesture. + if(vTouchFingerStates.size() == 2) + { + const vec2 Delta0 = vTouchFingerStates[0].m_Delta; + const vec2 Delta1 = vTouchFingerStates[1].m_Delta; + const float Similarity = dot(normalize(Delta0), normalize(Delta1)); + const float SimilarityThreshold = 0.8f; // How parallel the deltas have to be (1.0f being completely parallel) + if(Similarity > SimilarityThreshold) + { + const float DirectionThreshold = 3.0f; // How much longer the delta of one axis has to be compared to other axis + + // Vertical scrolling (y-delta must be larger than x-delta) + if(absolute(Delta0.y) > DirectionThreshold * absolute(Delta0.x) && + absolute(Delta1.y) > DirectionThreshold * absolute(Delta1.x) && + Delta0.y * Delta1.y > 0.0f) // Same y direction required + { + // Accumulate average delta of the two fingers + State.m_ScrollAmount.y += (Delta0.y + Delta1.y) / 2.0f; + } + } + } + else + { + // Scrolling gesture should start from zero again if released. + State.m_ScrollAmount = vec2(0.0f, 0.0f); + } +} + +bool CUi::ConsumeHotkey(EHotkey Hotkey) { const bool Pressed = m_HotkeysPressed & Hotkey; m_HotkeysPressed &= ~Hotkey; return Pressed; } -bool CUI::OnInput(const IInput::CEvent &Event) +bool CUi::OnInput(const IInput::CEvent &Event) { if(!Enabled()) return false; @@ -303,6 +408,10 @@ bool CUI::OnInput(const IInput::CEvent &Event) m_HotkeysPressed |= HOTKEY_UP; else if(Event.m_Key == KEY_DOWN) m_HotkeysPressed |= HOTKEY_DOWN; + else if(Event.m_Key == KEY_LEFT) + m_HotkeysPressed |= HOTKEY_LEFT; + else if(Event.m_Key == KEY_RIGHT) + m_HotkeysPressed |= HOTKEY_RIGHT; else if(Event.m_Key == KEY_MOUSE_WHEEL_UP) m_HotkeysPressed |= HOTKEY_SCROLL_UP; else if(Event.m_Key == KEY_MOUSE_WHEEL_DOWN) @@ -320,34 +429,34 @@ bool CUI::OnInput(const IInput::CEvent &Event) return false; } -float CUI::ButtonColorMul(const void *pID) +float CUi::ButtonColorMul(const void *pId) { - if(CheckActiveItem(pID)) + if(CheckActiveItem(pId)) return ButtonColorMulActive(); - else if(HotItem() == pID) + else if(HotItem() == pId) return ButtonColorMulHot(); return ButtonColorMulDefault(); } -const CUIRect *CUI::Screen() +const CUIRect *CUi::Screen() { m_Screen.h = 600.0f; m_Screen.w = Graphics()->ScreenAspect() * m_Screen.h; return &m_Screen; } -void CUI::MapScreen() +void CUi::MapScreen() { const CUIRect *pScreen = Screen(); Graphics()->MapScreen(pScreen->x, pScreen->y, pScreen->w, pScreen->h); } -float CUI::PixelSize() +float CUi::PixelSize() { return Screen()->w / Graphics()->ScreenWidth(); } -void CUI::ClipEnable(const CUIRect *pRect) +void CUi::ClipEnable(const CUIRect *pRect) { if(IsClipped()) { @@ -366,20 +475,20 @@ void CUI::ClipEnable(const CUIRect *pRect) UpdateClipping(); } -void CUI::ClipDisable() +void CUi::ClipDisable() { dbg_assert(IsClipped(), "no clip region"); m_vClips.pop_back(); UpdateClipping(); } -const CUIRect *CUI::ClipArea() const +const CUIRect *CUi::ClipArea() const { dbg_assert(IsClipped(), "no clip region"); return &m_vClips.back(); } -void CUI::UpdateClipping() +void CUi::UpdateClipping() { if(IsClipped()) { @@ -394,146 +503,159 @@ void CUI::UpdateClipping() } } -int CUI::DoButtonLogic(const void *pID, int Checked, const CUIRect *pRect) +int CUi::DoButtonLogic(const void *pId, int Checked, const CUIRect *pRect) { // logic int ReturnValue = 0; const bool Inside = MouseHovered(pRect); - static int s_ButtonUsed = -1; - if(CheckActiveItem(pID)) + if(CheckActiveItem(pId)) { - if(s_ButtonUsed >= 0 && !MouseButton(s_ButtonUsed)) + dbg_assert(m_ActiveButtonLogicButton >= 0, "m_ActiveButtonLogicButton invalid"); + if(!MouseButton(m_ActiveButtonLogicButton)) { if(Inside && Checked >= 0) - ReturnValue = 1 + s_ButtonUsed; + ReturnValue = 1 + m_ActiveButtonLogicButton; SetActiveItem(nullptr); - s_ButtonUsed = -1; + m_ActiveButtonLogicButton = -1; } } - else if(HotItem() == pID) + else if(HotItem() == pId) { for(int i = 0; i < 3; ++i) { if(MouseButton(i)) { - SetActiveItem(pID); - s_ButtonUsed = i; + SetActiveItem(pId); + m_ActiveButtonLogicButton = i; } } } if(Inside && !MouseButton(0) && !MouseButton(1) && !MouseButton(2)) - SetHotItem(pID); + SetHotItem(pId); return ReturnValue; } -int CUI::DoDraggableButtonLogic(const void *pID, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted) +int CUi::DoDraggableButtonLogic(const void *pId, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted) { // logic int ReturnValue = 0; const bool Inside = MouseHovered(pRect); - static int s_ButtonUsed = -1; if(pClicked != nullptr) *pClicked = false; if(pAbrupted != nullptr) *pAbrupted = false; - if(CheckActiveItem(pID)) + if(CheckActiveItem(pId)) { - if(s_ButtonUsed == 0) + dbg_assert(m_ActiveDraggableButtonLogicButton >= 0, "m_ActiveDraggableButtonLogicButton invalid"); + if(m_ActiveDraggableButtonLogicButton == 0) { if(Checked >= 0) - ReturnValue = 1 + s_ButtonUsed; - if(!MouseButton(s_ButtonUsed)) + ReturnValue = 1 + m_ActiveDraggableButtonLogicButton; + if(!MouseButton(m_ActiveDraggableButtonLogicButton)) { if(pClicked != nullptr) *pClicked = true; SetActiveItem(nullptr); - s_ButtonUsed = -1; + m_ActiveDraggableButtonLogicButton = -1; } if(MouseButton(1)) { if(pAbrupted != nullptr) *pAbrupted = true; SetActiveItem(nullptr); - s_ButtonUsed = -1; + m_ActiveDraggableButtonLogicButton = -1; } } - else if(s_ButtonUsed > 0 && !MouseButton(s_ButtonUsed)) + else if(!MouseButton(m_ActiveDraggableButtonLogicButton)) { if(Inside && Checked >= 0) - ReturnValue = 1 + s_ButtonUsed; + ReturnValue = 1 + m_ActiveDraggableButtonLogicButton; if(pClicked != nullptr) *pClicked = true; SetActiveItem(nullptr); - s_ButtonUsed = -1; + m_ActiveDraggableButtonLogicButton = -1; } } - else if(HotItem() == pID) + else if(HotItem() == pId) { for(int i = 0; i < 3; ++i) { if(MouseButton(i)) { - SetActiveItem(pID); - s_ButtonUsed = i; + SetActiveItem(pId); + m_ActiveDraggableButtonLogicButton = i; } } } if(Inside && !MouseButton(0) && !MouseButton(1) && !MouseButton(2)) - SetHotItem(pID); + SetHotItem(pId); return ReturnValue; } -EEditState CUI::DoPickerLogic(const void *pID, const CUIRect *pRect, float *pX, float *pY) +bool CUi::DoDoubleClickLogic(const void *pId) { - static const void *s_pEditing = nullptr; + if(m_DoubleClickState.m_pLastClickedId == pId && + Client()->GlobalTime() - m_DoubleClickState.m_LastClickTime < 0.5f && + distance(m_DoubleClickState.m_LastClickPos, MousePos()) <= 32.0f * Screen()->h / Graphics()->ScreenHeight()) + { + m_DoubleClickState.m_pLastClickedId = nullptr; + return true; + } + m_DoubleClickState.m_pLastClickedId = pId; + m_DoubleClickState.m_LastClickTime = Client()->GlobalTime(); + m_DoubleClickState.m_LastClickPos = MousePos(); + return false; +} +EEditState CUi::DoPickerLogic(const void *pId, const CUIRect *pRect, float *pX, float *pY) +{ if(MouseHovered(pRect)) - SetHotItem(pID); + SetHotItem(pId); EEditState Res = EEditState::EDITING; - if(HotItem() == pID && MouseButtonClicked(0)) + if(HotItem() == pId && MouseButtonClicked(0)) { - SetActiveItem(pID); - if(!s_pEditing) + SetActiveItem(pId); + if(!m_pLastEditingItem) { - s_pEditing = pID; + m_pLastEditingItem = pId; Res = EEditState::START; } } - if(CheckActiveItem(pID) && !MouseButton(0)) + if(CheckActiveItem(pId) && !MouseButton(0)) { SetActiveItem(nullptr); - if(s_pEditing == pID) + if(m_pLastEditingItem == pId) { - s_pEditing = nullptr; + m_pLastEditingItem = nullptr; Res = EEditState::END; } } - if(!CheckActiveItem(pID) && Res == EEditState::EDITING) + if(!CheckActiveItem(pId) && Res == EEditState::EDITING) return EEditState::NONE; if(Input()->ShiftIsPressed()) m_MouseSlow = true; if(pX) - *pX = clamp(m_MouseX - pRect->x, 0.0f, pRect->w); + *pX = clamp(MouseX() - pRect->x, 0.0f, pRect->w); if(pY) - *pY = clamp(m_MouseY - pRect->y, 0.0f, pRect->h); + *pY = clamp(MouseY() - pRect->y, 0.0f, pRect->h); return Res; } -void CUI::DoSmoothScrollLogic(float *pScrollOffset, float *pScrollOffsetChange, float ViewPortSize, float TotalSize, bool SmoothClamp, float ScrollSpeed) const +void CUi::DoSmoothScrollLogic(float *pScrollOffset, float *pScrollOffsetChange, float ViewPortSize, float TotalSize, bool SmoothClamp, float ScrollSpeed) const { // reset scrolling if it's not necessary anymore if(TotalSize < ViewPortSize) @@ -639,7 +761,7 @@ static int GetFlagsForLabelProperties(const SLabelProperties &LabelProps, const return Flags; } -vec2 CUI::CalcAlignedCursorPos(const CUIRect *pRect, vec2 TextSize, int Align, const float *pBiggestCharHeight) +vec2 CUi::CalcAlignedCursorPos(const CUIRect *pRect, vec2 TextSize, int Align, const float *pBiggestCharHeight) { vec2 Cursor(pRect->x, pRect->y); @@ -666,7 +788,7 @@ vec2 CUI::CalcAlignedCursorPos(const CUIRect *pRect, vec2 TextSize, int Align, c return Cursor; } -void CUI::DoLabel(const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps) const +void CUi::DoLabel(const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps) const { const int Flags = GetFlagsForLabelProperties(LabelProps, nullptr); const SCursorAndBoundingBox TextBounds = CalcFontSizeCursorHeightAndBoundingBox(TextRender(), pText, Flags, Size, pRect->w, LabelProps); @@ -674,12 +796,12 @@ void CUI::DoLabel(const CUIRect *pRect, const char *pText, float Size, int Align CTextCursor Cursor; TextRender()->SetCursor(&Cursor, CursorPos.x, CursorPos.y, Size, TEXTFLAG_RENDER | Flags); + Cursor.m_vColorSplits = LabelProps.m_vColorSplits; Cursor.m_LineWidth = (float)LabelProps.m_MaxWidth; TextRender()->TextEx(&Cursor, pText, -1); } - -void CUI::DoLabel(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps, int StrLen, const CTextCursor *pReadCursor) const +void CUi::DoLabel(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps, int StrLen, const CTextCursor *pReadCursor) const { const int Flags = GetFlagsForLabelProperties(LabelProps, pReadCursor); const SCursorAndBoundingBox TextBounds = CalcFontSizeCursorHeightAndBoundingBox(TextRender(), pText, Flags, Size, pRect->w, LabelProps); @@ -706,7 +828,7 @@ void CUI::DoLabel(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, cons RectEl.m_Cursor = Cursor; } -void CUI::DoLabelStreamed(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps, int StrLen, const CTextCursor *pReadCursor) const +void CUi::DoLabelStreamed(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps, int StrLen, const CTextCursor *pReadCursor) const { const int ReadCursorGlyphCount = pReadCursor == nullptr ? -1 : pReadCursor->m_GlyphCount; bool NeedsRecreate = false; @@ -762,7 +884,7 @@ void CUI::DoLabelStreamed(CUIElement::SUIElementRect &RectEl, const CUIRect *pRe } } -bool CUI::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners) +bool CUi::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const std::vector &vColorSplits) { const bool Inside = MouseHovered(pRect); const bool Active = m_pLastActiveItem == pLineInput; @@ -800,7 +922,7 @@ bool CUI::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize } } - if(Inside) + if(Inside && !MouseButton(0)) SetHotItem(pLineInput); if(Enabled() && Active && !JustGotActive) @@ -825,7 +947,7 @@ bool CUI::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize if(pMouseSelection->m_Selecting) { pMouseSelection->m_ReleaseMouse = MousePos(); - if(MouseButtonReleased(0)) + if(!MouseButton(0)) { pMouseSelection->m_Selecting = false; } @@ -844,7 +966,7 @@ bool CUI::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize pRect->Draw(ms_LightButtonColorFunction.GetColor(Active, HotItem() == pLineInput), Corners, 3.0f); ClipEnable(pRect); Textbox.x -= ScrollOffset; - const STextBoundingBox BoundingBox = pLineInput->Render(&Textbox, FontSize, TEXTALIGN_ML, Changed || CursorChanged, -1.0f, 0.0f); + const STextBoundingBox BoundingBox = pLineInput->Render(&Textbox, FontSize, TEXTALIGN_ML, Changed || CursorChanged, -1.0f, 0.0f, vColorSplits); ClipDisable(); // Scroll left or right if necessary @@ -865,16 +987,16 @@ bool CUI::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize return Changed; } -bool CUI::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners) +bool CUi::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const std::vector &vColorSplits) { CUIRect EditBox, ClearButton; pRect->VSplitRight(pRect->h, &EditBox, &ClearButton); - bool ReturnValue = DoEditBox(pLineInput, &EditBox, FontSize, Corners & ~IGraphics::CORNER_R); + bool ReturnValue = DoEditBox(pLineInput, &EditBox, FontSize, Corners & ~IGraphics::CORNER_R, vColorSplits); ClearButton.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.33f * ButtonColorMul(pLineInput->GetClearButtonId())), Corners & ~IGraphics::CORNER_L, 3.0f); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - DoLabel(&ClearButton, "×", ClearButton.h * CUI::ms_FontmodHeight * 0.8f, TEXTALIGN_MC); + DoLabel(&ClearButton, "×", ClearButton.h * CUi::ms_FontmodHeight * 0.8f, TEXTALIGN_MC); TextRender()->SetRenderFlags(0); if(DoButtonLogic(pLineInput->GetClearButtonId(), 0, &ClearButton)) { @@ -886,7 +1008,26 @@ bool CUI::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float return ReturnValue; } -int CUI::DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pID, const std::function &GetTextLambda, const CUIRect *pRect, const SMenuButtonProperties &Props) +bool CUi::DoEditBox_Search(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, bool HotkeyEnabled) +{ + CUIRect QuickSearch = *pRect; + TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); + DoLabel(&QuickSearch, FONT_ICON_MAGNIFYING_GLASS, FontSize, TEXTALIGN_ML); + const float SearchWidth = TextRender()->TextWidth(FontSize, FONT_ICON_MAGNIFYING_GLASS); + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); + QuickSearch.VSplitLeft(SearchWidth + 5.0f, nullptr, &QuickSearch); + if(HotkeyEnabled && Input()->ModifierIsPressed() && Input()->KeyPress(KEY_F)) + { + SetActiveItem(pLineInput); + pLineInput->SelectAll(); + } + pLineInput->SetEmptyText(Localize("Search")); + return DoClearableEditBox(pLineInput, &QuickSearch, FontSize); +} + +int CUi::DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pId, const std::function &GetTextLambda, const CUIRect *pRect, const SMenuButtonProperties &Props) { CUIRect Text = *pRect, DropDownIcon; Text.HMargin(pRect->h >= 20.0f ? 2.0f : 1.0f, &Text); @@ -957,7 +1098,7 @@ int CUI::DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pID, const NewRect.m_Text = pText; if(Props.m_UseIconFont) TextRender()->SetFontPreset(EFontPreset::ICON_FONT); - DoLabel(NewRect, &Text, pText, Text.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); + DoLabel(NewRect, &Text, pText, Text.h * CUi::ms_FontmodHeight, TEXTALIGN_MC); if(Props.m_UseIconFont) TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); } @@ -967,9 +1108,9 @@ int CUI::DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pID, const } // render size_t Index = 2; - if(CheckActiveItem(pID)) + if(CheckActiveItem(pId)) Index = 0; - else if(HotItem() == pID) + else if(HotItem() == pId) Index = 1; Graphics()->TextureClear(); Graphics()->RenderQuadContainer(UIElement.Rect(Index)->m_UIRectQuadContainer, -1); @@ -977,7 +1118,7 @@ int CUI::DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pID, const { TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - DoLabel(&DropDownIcon, FONT_ICON_CIRCLE_CHEVRON_DOWN, DropDownIcon.h * CUI::ms_FontmodHeight, TEXTALIGN_MR); + DoLabel(&DropDownIcon, FONT_ICON_CIRCLE_CHEVRON_DOWN, DropDownIcon.h * CUi::ms_FontmodHeight, TEXTALIGN_MR); TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); } @@ -985,10 +1126,10 @@ int CUI::DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pID, const ColorRGBA ColorTextOutline(TextRender()->DefaultTextOutlineColor()); if(UIElement.Rect(0)->m_UITextContainer.Valid()) TextRender()->RenderTextContainer(UIElement.Rect(0)->m_UITextContainer, ColorText, ColorTextOutline); - return DoButtonLogic(pID, Props.m_Checked, pRect); + return DoButtonLogic(pId, Props.m_Checked, pRect); } -int CUI::DoButton_PopupMenu(CButtonContainer *pButtonContainer, const char *pText, const CUIRect *pRect, float Size, int Align, float Padding, bool TransparentInactive, bool Enabled) +int CUi::DoButton_PopupMenu(CButtonContainer *pButtonContainer, const char *pText, const CUIRect *pRect, float Size, int Align, float Padding, bool TransparentInactive, bool Enabled) { if(!TransparentInactive || CheckActiveItem(pButtonContainer) || HotItem() == pButtonContainer) pRect->Draw(Enabled ? ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * ButtonColorMul(pButtonContainer)) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.4f), IGraphics::CORNER_ALL, 3.0f); @@ -1000,98 +1141,93 @@ int CUI::DoButton_PopupMenu(CButtonContainer *pButtonContainer, const char *pTex return Enabled ? DoButtonLogic(pButtonContainer, 0, pRect) : 0; } -int64_t CUI::DoValueSelector(const void *pID, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props) +int64_t CUi::DoValueSelector(const void *pId, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props) { - return DoValueSelectorWithState(pID, pRect, pLabel, Current, Min, Max, Props).m_Value; + return DoValueSelectorWithState(pId, pRect, pLabel, Current, Min, Max, Props).m_Value; } -SEditResult CUI::DoValueSelectorWithState(const void *pID, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props) +SEditResult CUi::DoValueSelectorWithState(const void *pId, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props) { // logic - static float s_Value; - static CLineInputNumber s_NumberInput; - static const void *s_pLastTextID = pID; const bool Inside = MouseInside(pRect); - static const void *s_pEditing = nullptr; - EEditState State = EEditState::NONE; - - if(Inside) - SetHotItem(pID); - const int Base = Props.m_IsHex ? 16 : 10; - if(MouseButtonReleased(1) && HotItem() == pID) + if(HotItem() == pId && m_ActiveValueSelectorState.m_Button >= 0 && !MouseButton(m_ActiveValueSelectorState.m_Button)) { - s_pLastTextID = pID; - m_ValueSelectorTextMode = true; - s_NumberInput.SetInteger64(Current, Base, Props.m_HexPrefix); - s_NumberInput.SelectAll(); - } - - if(CheckActiveItem(pID)) - { - if(!MouseButton(0)) + DisableMouseLock(); + if(CheckActiveItem(pId)) { - DisableMouseLock(); SetActiveItem(nullptr); - m_ValueSelectorTextMode = false; } + if(Inside && ((m_ActiveValueSelectorState.m_Button == 0 && !m_ActiveValueSelectorState.m_DidScroll && DoDoubleClickLogic(pId)) || m_ActiveValueSelectorState.m_Button == 1)) + { + m_ActiveValueSelectorState.m_pLastTextId = pId; + m_ActiveValueSelectorState.m_NumberInput.SetInteger64(Current, Base, Props.m_HexPrefix); + m_ActiveValueSelectorState.m_NumberInput.SelectAll(); + } + m_ActiveValueSelectorState.m_Button = -1; } - if(m_ValueSelectorTextMode && s_pLastTextID == pID) + if(m_ActiveValueSelectorState.m_pLastTextId == pId) { - DoEditBox(&s_NumberInput, pRect, 10.0f); - SetActiveItem(&s_NumberInput); + SetActiveItem(&m_ActiveValueSelectorState.m_NumberInput); + DoEditBox(&m_ActiveValueSelectorState.m_NumberInput, pRect, 10.0f); if(ConsumeHotkey(HOTKEY_ENTER) || ((MouseButtonClicked(1) || MouseButtonClicked(0)) && !Inside)) { - Current = clamp(s_NumberInput.GetInteger64(Base), Min, Max); + Current = clamp(m_ActiveValueSelectorState.m_NumberInput.GetInteger64(Base), Min, Max); DisableMouseLock(); SetActiveItem(nullptr); - m_ValueSelectorTextMode = false; + m_ActiveValueSelectorState.m_pLastTextId = nullptr; } if(ConsumeHotkey(HOTKEY_ESCAPE)) { DisableMouseLock(); SetActiveItem(nullptr); - m_ValueSelectorTextMode = false; + m_ActiveValueSelectorState.m_pLastTextId = nullptr; } } else { - if(CheckActiveItem(pID)) + if(CheckActiveItem(pId)) { - if(Props.m_UseScroll) + dbg_assert(m_ActiveValueSelectorState.m_Button >= 0, "m_ActiveValueSelectorState.m_Button invalid"); + if(Props.m_UseScroll && m_ActiveValueSelectorState.m_Button == 0 && MouseButton(0)) { - if(MouseButton(0)) + m_ActiveValueSelectorState.m_ScrollValue += MouseDeltaX() * (Input()->ShiftIsPressed() ? 0.05f : 1.0f); + + if(absolute(m_ActiveValueSelectorState.m_ScrollValue) > Props.m_Scale) { - s_Value += MouseDeltaX() * (Input()->ShiftIsPressed() ? 0.05f : 1.0f); - - if(absolute(s_Value) > Props.m_Scale) - { - const int64_t Count = (int64_t)(s_Value / Props.m_Scale); - s_Value = std::fmod(s_Value, Props.m_Scale); - Current += Props.m_Step * Count; - Current = clamp(Current, Min, Max); - - // Constrain to discrete steps - if(Count > 0) - Current = Current / Props.m_Step * Props.m_Step; - else - Current = std::ceil(Current / (float)Props.m_Step) * Props.m_Step; - } + const int64_t Count = (int64_t)(m_ActiveValueSelectorState.m_ScrollValue / Props.m_Scale); + m_ActiveValueSelectorState.m_ScrollValue = std::fmod(m_ActiveValueSelectorState.m_ScrollValue, Props.m_Scale); + Current += Props.m_Step * Count; + Current = clamp(Current, Min, Max); + m_ActiveValueSelectorState.m_DidScroll = true; + + // Constrain to discrete steps + if(Count > 0) + Current = Current / Props.m_Step * Props.m_Step; + else + Current = std::ceil(Current / (float)Props.m_Step) * Props.m_Step; } } } - else if(HotItem() == pID) + else if(HotItem() == pId) { - if(MouseButtonClicked(0)) + if(MouseButton(0)) { - s_Value = 0; - SetActiveItem(pID); + m_ActiveValueSelectorState.m_Button = 0; + m_ActiveValueSelectorState.m_DidScroll = false; + m_ActiveValueSelectorState.m_ScrollValue = 0.0f; + SetActiveItem(pId); if(Props.m_UseScroll) - EnableMouseLock(pID); + EnableMouseLock(pId); + } + else if(MouseButton(1)) + { + m_ActiveValueSelectorState.m_Button = 1; + SetActiveItem(pId); } } @@ -1115,29 +1251,29 @@ SEditResult CUI::DoValueSelectorWithState(const void *pID, const CUIRec DoLabel(pRect, aBuf, 10.0f, TEXTALIGN_MC); } - if(!m_ValueSelectorTextMode) - s_NumberInput.Clear(); + if(Inside && !MouseButton(0) && !MouseButton(1)) + SetHotItem(pId); - if(s_pEditing == pID) + EEditState State = EEditState::NONE; + if(m_pLastEditingItem == pId) + { State = EEditState::EDITING; - - bool MouseLocked = CheckMouseLock(); - if((MouseLocked || m_ValueSelectorTextMode) && !s_pEditing) + } + if(((CheckActiveItem(pId) && CheckMouseLock()) || m_ActiveValueSelectorState.m_pLastTextId == pId) && m_pLastEditingItem != pId) { State = EEditState::START; - s_pEditing = pID; + m_pLastEditingItem = pId; } - - if(!CheckMouseLock() && !m_ValueSelectorTextMode && s_pEditing == pID) + if(!CheckMouseLock() && m_ActiveValueSelectorState.m_pLastTextId != pId && m_pLastEditingItem == pId) { State = EEditState::END; - s_pEditing = nullptr; + m_pLastEditingItem = nullptr; } return SEditResult{State, Current}; } -float CUI::DoScrollbarV(const void *pID, const CUIRect *pRect, float Current) +float CUi::DoScrollbarV(const void *pId, const CUIRect *pRect, float Current) { Current = clamp(Current, 0.0f, 1.0f); @@ -1150,12 +1286,11 @@ float CUI::DoScrollbarV(const void *pID, const CUIRect *pRect, float Current) Handle.y = Rail.y + (Rail.h - Handle.h) * Current; // logic - static float s_OffsetY; const bool InsideRail = MouseHovered(&Rail); const bool InsideHandle = MouseHovered(&Handle); bool Grabbed = false; // whether to apply the offset - if(CheckActiveItem(pID)) + if(CheckActiveItem(pId)) { if(MouseButton(0)) { @@ -1168,25 +1303,28 @@ float CUI::DoScrollbarV(const void *pID, const CUIRect *pRect, float Current) SetActiveItem(nullptr); } } - else if(HotItem() == pID) + else if(HotItem() == pId) { - if(MouseButton(0)) + if(InsideHandle) { - SetActiveItem(pID); - s_OffsetY = MouseY() - Handle.y; + if(MouseButton(0)) + { + SetActiveItem(pId); + m_ActiveScrollbarOffset = MouseY() - Handle.y; + Grabbed = true; + } + } + else if(MouseButtonClicked(0)) + { + SetActiveItem(pId); + m_ActiveScrollbarOffset = Handle.h / 2.0f; Grabbed = true; } } - else if(MouseButtonClicked(0) && !InsideHandle && InsideRail) - { - SetActiveItem(pID); - s_OffsetY = Handle.h / 2.0f; - Grabbed = true; - } - if(InsideHandle) + if(InsideRail && !MouseButton(0)) { - SetHotItem(pID); + SetHotItem(pId); } float ReturnValue = Current; @@ -1194,18 +1332,18 @@ float CUI::DoScrollbarV(const void *pID, const CUIRect *pRect, float Current) { const float Min = Rail.y; const float Max = Rail.h - Handle.h; - const float Cur = MouseY() - s_OffsetY; + const float Cur = MouseY() - m_ActiveScrollbarOffset; ReturnValue = clamp((Cur - Min) / Max, 0.0f, 1.0f); } // render Rail.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, Rail.w / 2.0f); - Handle.Draw(ms_ScrollBarColorFunction.GetColor(CheckActiveItem(pID), HotItem() == pID), IGraphics::CORNER_ALL, Handle.w / 2.0f); + Handle.Draw(ms_ScrollBarColorFunction.GetColor(CheckActiveItem(pId), HotItem() == pId), IGraphics::CORNER_ALL, Handle.w / 2.0f); return ReturnValue; } -float CUI::DoScrollbarH(const void *pID, const CUIRect *pRect, float Current, const ColorRGBA *pColorInner) +float CUi::DoScrollbarH(const void *pId, const CUIRect *pRect, float Current, const ColorRGBA *pColorInner) { Current = clamp(Current, 0.0f, 1.0f); @@ -1220,13 +1358,21 @@ float CUI::DoScrollbarH(const void *pID, const CUIRect *pRect, float Current, co Rail.VSplitLeft(pColorInner ? 8.0f : clamp(33.0f, Rail.h, Rail.w / 3.0f), &Handle, 0); Handle.x += (Rail.w - Handle.w) * Current; + CUIRect HandleArea = Handle; + if(!pColorInner) + { + HandleArea.h = pRect->h * 0.9f; + HandleArea.y = pRect->y + pRect->h * 0.05f; + HandleArea.w += 6.0f; + HandleArea.x -= 3.0f; + } + // logic - static float s_OffsetX; const bool InsideRail = MouseHovered(&Rail); - const bool InsideHandle = MouseHovered(&Handle); + const bool InsideHandle = MouseHovered(&HandleArea); bool Grabbed = false; // whether to apply the offset - if(CheckActiveItem(pID)) + if(CheckActiveItem(pId)) { if(MouseButton(0)) { @@ -1239,25 +1385,34 @@ float CUI::DoScrollbarH(const void *pID, const CUIRect *pRect, float Current, co SetActiveItem(nullptr); } } - else if(HotItem() == pID) + else if(HotItem() == pId) { - if(MouseButton(0)) + if(InsideHandle) + { + if(MouseButton(0)) + { + SetActiveItem(pId); + m_ActiveScrollbarOffset = MouseX() - Handle.x; + Grabbed = true; + } + } + else if(MouseButtonClicked(0)) { - SetActiveItem(pID); - s_OffsetX = MouseX() - Handle.x; + SetActiveItem(pId); + m_ActiveScrollbarOffset = Handle.w / 2.0f; Grabbed = true; } } - else if(MouseButtonClicked(0) && !InsideHandle && InsideRail) + + if(!pColorInner && (InsideHandle || Grabbed) && (CheckActiveItem(pId) || HotItem() == pId)) { - SetActiveItem(pID); - s_OffsetX = Handle.w / 2.0f; - Grabbed = true; + Handle.h += 3.0f; + Handle.y -= 1.5f; } - if(InsideHandle) + if(InsideRail && !MouseButton(0)) { - SetHotItem(pID); + SetHotItem(pId); } float ReturnValue = Current; @@ -1265,34 +1420,35 @@ float CUI::DoScrollbarH(const void *pID, const CUIRect *pRect, float Current, co { const float Min = Rail.x; const float Max = Rail.w - Handle.w; - const float Cur = MouseX() - s_OffsetX; + const float Cur = MouseX() - m_ActiveScrollbarOffset; ReturnValue = clamp((Cur - Min) / Max, 0.0f, 1.0f); } // render + const ColorRGBA HandleColor = ms_ScrollBarColorFunction.GetColor(CheckActiveItem(pId), HotItem() == pId); if(pColorInner) { CUIRect Slider; Handle.VMargin(-2.0f, &Slider); Slider.HMargin(-3.0f, &Slider); - Slider.Draw(ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f), IGraphics::CORNER_ALL, 5.0f); + Slider.Draw(ColorRGBA(0.15f, 0.15f, 0.15f, 1.0f).Multiply(HandleColor), IGraphics::CORNER_ALL, 5.0f); Slider.Margin(2.0f, &Slider); - Slider.Draw(*pColorInner, IGraphics::CORNER_ALL, 3.0f); + Slider.Draw(pColorInner->Multiply(HandleColor), IGraphics::CORNER_ALL, 3.0f); } else { Rail.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, Rail.h / 2.0f); - Handle.Draw(ms_ScrollBarColorFunction.GetColor(CheckActiveItem(pID), HotItem() == pID), IGraphics::CORNER_ALL, Handle.h / 2.0f); + Handle.Draw(HandleColor, IGraphics::CORNER_ALL, Rail.h / 2.0f); } return ReturnValue; } -void CUI::DoScrollbarOption(const void *pID, int *pOption, const CUIRect *pRect, const char *pStr, int Min, int Max, const IScrollbarScale *pScale, unsigned Flags, const char *pSuffix) +bool CUi::DoScrollbarOption(const void *pId, int *pOption, const CUIRect *pRect, const char *pStr, int Min, int Max, const IScrollbarScale *pScale, unsigned Flags, const char *pSuffix) { - const bool Infinite = Flags & CUI::SCROLLBAR_OPTION_INFINITE; - const bool NoClampValue = Flags & CUI::SCROLLBAR_OPTION_NOCLAMPVALUE; - const bool MultiLine = Flags & CUI::SCROLLBAR_OPTION_MULTILINE; + const bool Infinite = Flags & CUi::SCROLLBAR_OPTION_INFINITE; + const bool NoClampValue = Flags & CUi::SCROLLBAR_OPTION_NOCLAMPVALUE; + const bool MultiLine = Flags & CUi::SCROLLBAR_OPTION_MULTILINE; int Value = *pOption; if(Infinite) @@ -1320,10 +1476,10 @@ void CUI::DoScrollbarOption(const void *pID, int *pOption, const CUIRect *pRect, else pRect->VSplitMid(&Label, &ScrollBar, minimum(10.0f, pRect->w * 0.05f)); - const float FontSize = Label.h * CUI::ms_FontmodHeight * 0.8f; + const float FontSize = Label.h * CUi::ms_FontmodHeight * 0.8f; DoLabel(&Label, aBuf, FontSize, TEXTALIGN_ML); - Value = pScale->ToAbsolute(DoScrollbarH(pID, &ScrollBar, pScale->ToRelative(Value, Min, Max)), Min, Max); + Value = pScale->ToAbsolute(DoScrollbarH(pId, &ScrollBar, pScale->ToRelative(Value, Min, Max)), Min, Max); if(NoClampValue && ((Value == Min && *pOption < Min) || (Value == Max && *pOption > Max))) { Value = *pOption; // use previous out of range value instead if the scrollbar is at the edge @@ -1334,16 +1490,24 @@ void CUI::DoScrollbarOption(const void *pID, int *pOption, const CUIRect *pRect, Value = 0; } - *pOption = Value; + if(*pOption != Value) + { + *pOption = Value; + return true; + } + return false; } -void CUI::RenderProgressSpinner(vec2 Center, float OuterRadius, const SProgressSpinnerProperties &Props) const +void CUi::RenderProgressBar(CUIRect ProgressBar, float Progress) { - static float s_SpinnerOffset = 0.0f; - static float s_LastRender = Client()->LocalTime(); - s_SpinnerOffset += (Client()->LocalTime() - s_LastRender) * 1.5f; - s_SpinnerOffset = std::fmod(s_SpinnerOffset, 1.0f); + const float Rounding = minimum(5.0f, ProgressBar.h / 2.0f); + ProgressBar.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, Rounding); + ProgressBar.w = maximum(ProgressBar.w * Progress, 2 * Rounding); + ProgressBar.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), IGraphics::CORNER_ALL, Rounding); +} +void CUi::RenderProgressSpinner(vec2 Center, float OuterRadius, const SProgressSpinnerProperties &Props) const +{ Graphics()->TextureClear(); Graphics()->QuadsBegin(); @@ -1366,7 +1530,7 @@ void CUI::RenderProgressSpinner(vec2 Center, float OuterRadius, const SProgressS } const float FilledRatio = Props.m_Progress < 0.0f ? 0.333f : Props.m_Progress; - const int FilledSegmentOffset = Props.m_Progress < 0.0f ? round_to_int(s_SpinnerOffset * Props.m_Segments) : 0; + const int FilledSegmentOffset = Props.m_Progress < 0.0f ? round_to_int(m_ProgressSpinnerOffset * Props.m_Segments) : 0; const int FilledNumSegments = minimum(Props.m_Segments * FilledRatio + (Props.m_Progress < 0.0f ? 0 : 1), Props.m_Segments); Graphics()->SetColor(Props.m_Color); for(int i = 0; i < FilledNumSegments; ++i) @@ -1382,11 +1546,9 @@ void CUI::RenderProgressSpinner(vec2 Center, float OuterRadius, const SProgressS } Graphics()->QuadsEnd(); - - s_LastRender = Client()->LocalTime(); } -void CUI::DoPopupMenu(const SPopupMenuId *pID, int X, int Y, int Width, int Height, void *pContext, FPopupMenuFunction pfnFunc, const SPopupMenuProperties &Props) +void CUi::DoPopupMenu(const SPopupMenuId *pId, int X, int Y, int Width, int Height, void *pContext, FPopupMenuFunction pfnFunc, const SPopupMenuProperties &Props) { constexpr float Margin = SPopupMenu::POPUP_BORDER + SPopupMenu::POPUP_MARGIN; if(X + Width > Screen()->w - Margin) @@ -1396,7 +1558,7 @@ void CUI::DoPopupMenu(const SPopupMenuId *pID, int X, int Y, int Width, int Heig m_vPopupMenus.emplace_back(); SPopupMenu *pNewMenu = &m_vPopupMenus.back(); - pNewMenu->m_pID = pID; + pNewMenu->m_pId = pId; pNewMenu->m_Props = Props; pNewMenu->m_Rect.x = X; pNewMenu->m_Rect.y = Y; @@ -1406,32 +1568,44 @@ void CUI::DoPopupMenu(const SPopupMenuId *pID, int X, int Y, int Width, int Heig pNewMenu->m_pfnFunc = pfnFunc; } -void CUI::RenderPopupMenus() +void CUi::RenderPopupMenus() { for(size_t i = 0; i < m_vPopupMenus.size(); ++i) { const SPopupMenu &PopupMenu = m_vPopupMenus[i]; + const SPopupMenuId *pId = PopupMenu.m_pId; const bool Inside = MouseInside(&PopupMenu.m_Rect); const bool Active = i == m_vPopupMenus.size() - 1; if(Active) - SetHotItem(PopupMenu.m_pID); + { + // Prevent UI elements below the popup menu from being activated. + SetHotItem(pId); + } - if(CheckActiveItem(PopupMenu.m_pID)) + if(CheckActiveItem(pId)) { if(!MouseButton(0)) { if(!Inside) { - ClosePopupMenu(PopupMenu.m_pID); + ClosePopupMenu(pId); + --i; + continue; } SetActiveItem(nullptr); } } - else if(HotItem() == PopupMenu.m_pID) + else if(HotItem() == pId) { if(MouseButton(0)) - SetActiveItem(PopupMenu.m_pID); + SetActiveItem(pId); + } + + if(Inside) + { + // Prevent scroll regions directly behind popup menus from using the mouse scroll events. + SetHotScrollRegion(nullptr); } CUIRect PopupRect = PopupMenu.m_Rect; @@ -1440,15 +1614,17 @@ void CUI::RenderPopupMenus() PopupRect.Draw(PopupMenu.m_Props.m_BackgroundColor, PopupMenu.m_Props.m_Corners, 3.0f); PopupRect.Margin(SPopupMenu::POPUP_MARGIN, &PopupRect); + // The popup render function can open/close popups, which may resize the vector and thus + // invalidate the variable PopupMenu. We therefore store pId in a separate variable. EPopupMenuFunctionResult Result = PopupMenu.m_pfnFunc(PopupMenu.m_pContext, PopupRect, Active); if(Result != POPUP_KEEP_OPEN || (Active && ConsumeHotkey(HOTKEY_ESCAPE))) - ClosePopupMenu(PopupMenu.m_pID, Result == POPUP_CLOSE_CURRENT_AND_DESCENDANTS); + ClosePopupMenu(pId, Result == POPUP_CLOSE_CURRENT_AND_DESCENDANTS); } } -void CUI::ClosePopupMenu(const SPopupMenuId *pID, bool IncludeDescendants) +void CUi::ClosePopupMenu(const SPopupMenuId *pId, bool IncludeDescendants) { - auto PopupMenuToClose = std::find_if(m_vPopupMenus.begin(), m_vPopupMenus.end(), [pID](const SPopupMenu PopupMenu) { return PopupMenu.m_pID == pID; }); + auto PopupMenuToClose = std::find_if(m_vPopupMenus.begin(), m_vPopupMenus.end(), [pId](const SPopupMenu PopupMenu) { return PopupMenu.m_pId == pId; }); if(PopupMenuToClose != m_vPopupMenus.end()) { if(IncludeDescendants) @@ -1461,7 +1637,7 @@ void CUI::ClosePopupMenu(const SPopupMenuId *pID, bool IncludeDescendants) } } -void CUI::ClosePopupMenus() +void CUi::ClosePopupMenus() { if(m_vPopupMenus.empty()) return; @@ -1472,49 +1648,49 @@ void CUI::ClosePopupMenus() m_pfnPopupMenuClosedCallback(); } -bool CUI::IsPopupOpen() const +bool CUi::IsPopupOpen() const { return !m_vPopupMenus.empty(); } -bool CUI::IsPopupOpen(const SPopupMenuId *pID) const +bool CUi::IsPopupOpen(const SPopupMenuId *pId) const { - return std::any_of(m_vPopupMenus.begin(), m_vPopupMenus.end(), [pID](const SPopupMenu PopupMenu) { return PopupMenu.m_pID == pID; }); + return std::any_of(m_vPopupMenus.begin(), m_vPopupMenus.end(), [pId](const SPopupMenu PopupMenu) { return PopupMenu.m_pId == pId; }); } -bool CUI::IsPopupHovered() const +bool CUi::IsPopupHovered() const { return std::any_of(m_vPopupMenus.begin(), m_vPopupMenus.end(), [this](const SPopupMenu PopupMenu) { return MouseHovered(&PopupMenu.m_Rect); }); } -void CUI::SetPopupMenuClosedCallback(FPopupMenuClosedCallback pfnCallback) +void CUi::SetPopupMenuClosedCallback(FPopupMenuClosedCallback pfnCallback) { m_pfnPopupMenuClosedCallback = std::move(pfnCallback); } -void CUI::SMessagePopupContext::DefaultColor(ITextRender *pTextRender) +void CUi::SMessagePopupContext::DefaultColor(ITextRender *pTextRender) { m_TextColor = pTextRender->DefaultTextColor(); } -void CUI::SMessagePopupContext::ErrorColor() +void CUi::SMessagePopupContext::ErrorColor() { m_TextColor = ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f); } -CUI::EPopupMenuFunctionResult CUI::PopupMessage(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CUi::PopupMessage(void *pContext, CUIRect View, bool Active) { SMessagePopupContext *pMessagePopup = static_cast(pContext); - CUI *pUI = pMessagePopup->m_pUI; + CUi *pUI = pMessagePopup->m_pUI; pUI->TextRender()->TextColor(pMessagePopup->m_TextColor); pUI->TextRender()->Text(View.x, View.y, SMessagePopupContext::POPUP_FONT_SIZE, pMessagePopup->m_aMessage, View.w); pUI->TextRender()->TextColor(pUI->TextRender()->DefaultTextColor()); - return (Active && pUI->ConsumeHotkey(HOTKEY_ENTER)) ? CUI::POPUP_CLOSE_CURRENT : CUI::POPUP_KEEP_OPEN; + return (Active && pUI->ConsumeHotkey(HOTKEY_ENTER)) ? CUi::POPUP_CLOSE_CURRENT : CUi::POPUP_KEEP_OPEN; } -void CUI::ShowPopupMessage(float X, float Y, SMessagePopupContext *pContext) +void CUi::ShowPopupMessage(float X, float Y, SMessagePopupContext *pContext) { const float TextWidth = minimum(std::ceil(TextRender()->TextWidth(SMessagePopupContext::POPUP_FONT_SIZE, pContext->m_aMessage, -1, -1.0f) + 0.5f), SMessagePopupContext::POPUP_MAX_WIDTH); float TextHeight = 0.0f; @@ -1525,23 +1701,23 @@ void CUI::ShowPopupMessage(float X, float Y, SMessagePopupContext *pContext) DoPopupMenu(pContext, X, Y, TextWidth + 10.0f, TextHeight + 10.0f, pContext, PopupMessage); } -CUI::SConfirmPopupContext::SConfirmPopupContext() +CUi::SConfirmPopupContext::SConfirmPopupContext() { Reset(); } -void CUI::SConfirmPopupContext::Reset() +void CUi::SConfirmPopupContext::Reset() { m_Result = SConfirmPopupContext::UNSET; } -void CUI::SConfirmPopupContext::YesNoButtons() +void CUi::SConfirmPopupContext::YesNoButtons() { str_copy(m_aPositiveButtonLabel, Localize("Yes")); str_copy(m_aNegativeButtonLabel, Localize("No")); } -void CUI::ShowPopupConfirm(float X, float Y, SConfirmPopupContext *pContext) +void CUi::ShowPopupConfirm(float X, float Y, SConfirmPopupContext *pContext) { const float TextWidth = minimum(std::ceil(TextRender()->TextWidth(SConfirmPopupContext::POPUP_FONT_SIZE, pContext->m_aMessage, -1, -1.0f) + 0.5f), SConfirmPopupContext::POPUP_MAX_WIDTH); float TextHeight = 0.0f; @@ -1554,10 +1730,10 @@ void CUI::ShowPopupConfirm(float X, float Y, SConfirmPopupContext *pContext) DoPopupMenu(pContext, X, Y, TextWidth + 10.0f, PopupHeight, pContext, PopupConfirm); } -CUI::EPopupMenuFunctionResult CUI::PopupConfirm(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CUi::PopupConfirm(void *pContext, CUIRect View, bool Active) { SConfirmPopupContext *pConfirmPopup = static_cast(pContext); - CUI *pUI = pConfirmPopup->m_pUI; + CUi *pUI = pConfirmPopup->m_pUI; CUIRect Label, ButtonBar, CancelButton, ConfirmButton; View.HSplitBottom(SConfirmPopupContext::POPUP_BUTTON_HEIGHT, &Label, &ButtonBar); @@ -1568,24 +1744,24 @@ CUI::EPopupMenuFunctionResult CUI::PopupConfirm(void *pContext, CUIRect View, bo if(pUI->DoButton_PopupMenu(&pConfirmPopup->m_CancelButton, pConfirmPopup->m_aNegativeButtonLabel, &CancelButton, SConfirmPopupContext::POPUP_FONT_SIZE, TEXTALIGN_MC)) { pConfirmPopup->m_Result = SConfirmPopupContext::CANCELED; - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } if(pUI->DoButton_PopupMenu(&pConfirmPopup->m_ConfirmButton, pConfirmPopup->m_aPositiveButtonLabel, &ConfirmButton, SConfirmPopupContext::POPUP_FONT_SIZE, TEXTALIGN_MC) || (Active && pUI->ConsumeHotkey(HOTKEY_ENTER))) { pConfirmPopup->m_Result = SConfirmPopupContext::CONFIRMED; - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::SSelectionPopupContext::SSelectionPopupContext() +CUi::SSelectionPopupContext::SSelectionPopupContext() { Reset(); } -void CUI::SSelectionPopupContext::Reset() +void CUi::SSelectionPopupContext::Reset() { m_Props = SPopupMenuProperties(); m_aMessage[0] = '\0'; @@ -1602,10 +1778,10 @@ void CUI::SSelectionPopupContext::Reset() m_TransparentButtons = false; } -CUI::EPopupMenuFunctionResult CUI::PopupSelection(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CUi::PopupSelection(void *pContext, CUIRect View, bool Active) { SSelectionPopupContext *pSelectionPopup = static_cast(pContext); - CUI *pUI = pSelectionPopup->m_pUI; + CUi *pUI = pSelectionPopup->m_pUI; CScrollRegion *pScrollRegion = pSelectionPopup->m_pScrollRegion; vec2 ScrollOffset(0.0f, 0.0f); @@ -1649,10 +1825,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupSelection(void *pContext, CUIRect View, pScrollRegion->End(); - return pSelectionPopup->m_pSelection == nullptr ? CUI::POPUP_KEEP_OPEN : CUI::POPUP_CLOSE_CURRENT; + return pSelectionPopup->m_pSelection == nullptr ? CUi::POPUP_KEEP_OPEN : CUi::POPUP_CLOSE_CURRENT; } -void CUI::ShowPopupSelection(float X, float Y, SSelectionPopupContext *pContext) +void CUi::ShowPopupSelection(float X, float Y, SSelectionPopupContext *pContext) { const STextBoundingBox TextBoundingBox = TextRender()->TextBoundingBox(pContext->m_FontSize, pContext->m_aMessage, -1, pContext->m_Width); const float PopupHeight = minimum((pContext->m_aMessage[0] == '\0' ? -pContext->m_EntrySpacing : TextBoundingBox.m_H) + pContext->m_vEntries.size() * (pContext->m_EntryHeight + pContext->m_EntrySpacing) + (SPopupMenu::POPUP_BORDER + SPopupMenu::POPUP_MARGIN) * 2 + CScrollRegion::HEIGHT_MAGIC_FIX, Screen()->h * 0.4f); @@ -1681,7 +1857,7 @@ void CUI::ShowPopupSelection(float X, float Y, SSelectionPopupContext *pContext) DoPopupMenu(pContext, X, Y, pContext->m_Width, PopupHeight, pContext, PopupSelection, pContext->m_Props); } -int CUI::DoDropDown(CUIRect *pRect, int CurSelection, const char **pStrs, int Num, SDropDownState &State) +int CUi::DoDropDown(CUIRect *pRect, int CurSelection, const char **pStrs, int Num, SDropDownState &State) { if(!State.m_Init) { @@ -1708,7 +1884,7 @@ int CUI::DoDropDown(CUIRect *pRect, int CurSelection, const char **pStrs, int Nu State.m_SelectionPopupContext.m_vEntries.emplace_back(pStrs[i]); State.m_SelectionPopupContext.m_EntryHeight = pRect->h; State.m_SelectionPopupContext.m_EntryPadding = pRect->h >= 20.0f ? 2.0f : 1.0f; - State.m_SelectionPopupContext.m_FontSize = (State.m_SelectionPopupContext.m_EntryHeight - 2 * State.m_SelectionPopupContext.m_EntryPadding) * CUI::ms_FontmodHeight; + State.m_SelectionPopupContext.m_FontSize = (State.m_SelectionPopupContext.m_EntryHeight - 2 * State.m_SelectionPopupContext.m_EntryPadding) * CUi::ms_FontmodHeight; State.m_SelectionPopupContext.m_Width = pRect->w; State.m_SelectionPopupContext.m_AlignmentHeight = pRect->h; State.m_SelectionPopupContext.m_TransparentButtons = true; @@ -1725,10 +1901,10 @@ int CUI::DoDropDown(CUIRect *pRect, int CurSelection, const char **pStrs, int Nu return CurSelection; } -CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CUi::PopupColorPicker(void *pContext, CUIRect View, bool Active) { SColorPickerPopupContext *pColorPicker = static_cast(pContext); - CUI *pUI = pColorPicker->m_pUI; + CUi *pUI = pColorPicker->m_pUI; pColorPicker->m_State = EEditState::NONE; CUIRect ColorsArea, HueArea, BottomArea, ModeButtonArea, HueRect, SatRect, ValueRect, HexRect, AlphaRect; @@ -1821,10 +1997,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View // Editboxes Area if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_HSVA) { - const unsigned OldH = (unsigned)(PickerColorHSV.h * 255.0f); - const unsigned OldS = (unsigned)(PickerColorHSV.s * 255.0f); - const unsigned OldV = (unsigned)(PickerColorHSV.v * 255.0f); - const unsigned OldA = (unsigned)(PickerColorHSV.a * 255.0f); + const unsigned OldH = round_to_int(PickerColorHSV.h * 255.0f); + const unsigned OldS = round_to_int(PickerColorHSV.s * 255.0f); + const unsigned OldV = round_to_int(PickerColorHSV.v * 255.0f); + const unsigned OldA = round_to_int(PickerColorHSV.a * 255.0f); const auto [StateH, H] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "H:", OldH, 0, 255); const auto [StateS, S] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "S:", OldS, 0, 255); @@ -1849,10 +2025,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View } else if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_RGBA) { - const unsigned OldR = (unsigned)(PickerColorRGB.r * 255.0f); - const unsigned OldG = (unsigned)(PickerColorRGB.g * 255.0f); - const unsigned OldB = (unsigned)(PickerColorRGB.b * 255.0f); - const unsigned OldA = (unsigned)(PickerColorRGB.a * 255.0f); + const unsigned OldR = round_to_int(PickerColorRGB.r * 255.0f); + const unsigned OldG = round_to_int(PickerColorRGB.g * 255.0f); + const unsigned OldB = round_to_int(PickerColorRGB.b * 255.0f); + const unsigned OldA = round_to_int(PickerColorRGB.a * 255.0f); const auto [StateR, R] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "R:", OldR, 0, 255); const auto [StateG, G] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "G:", OldG, 0, 255); @@ -1877,10 +2053,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View } else if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_HSLA) { - const unsigned OldH = (unsigned)(PickerColorHSL.h * 255.0f); - const unsigned OldS = (unsigned)(PickerColorHSL.s * 255.0f); - const unsigned OldL = (unsigned)(PickerColorHSL.l * 255.0f); - const unsigned OldA = (unsigned)(PickerColorHSL.a * 255.0f); + const unsigned OldH = round_to_int(PickerColorHSL.h * 255.0f); + const unsigned OldS = round_to_int(PickerColorHSL.s * 255.0f); + const unsigned OldL = round_to_int(PickerColorHSL.l * 255.0f); + const unsigned OldA = round_to_int(PickerColorHSL.a * 255.0f); const auto [StateH, H] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "H:", OldH, 0, 255); const auto [StateS, S] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "S:", OldS, 0, 255); @@ -1996,10 +2172,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View } } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -void CUI::ShowPopupColorPicker(float X, float Y, SColorPickerPopupContext *pContext) +void CUi::ShowPopupColorPicker(float X, float Y, SColorPickerPopupContext *pContext) { pContext->m_pUI = this; if(pContext->m_ColorMode == SColorPickerPopupContext::MODE_UNSET) diff --git a/src/game/client/ui.h b/src/game/client/ui.h index 60b336d215..a680cfbf42 100644 --- a/src/game/client/ui.h +++ b/src/game/client/ui.h @@ -14,6 +14,7 @@ #include #include +class CScrollRegion; class IClient; class IGraphics; class IKernel; @@ -142,15 +143,15 @@ class CScrollBarColorFunction : public IButtonColorFunction } }; -class CUI; +class CUi; class CUIElement { - friend class CUI; + friend class CUi; - CUI *m_pUI; + CUi *m_pUI; - CUIElement(CUI *pUI, int RequestedRectCount) { Init(pUI, RequestedRectCount); } + CUIElement(CUi *pUI, int RequestedRectCount) { Init(pUI, RequestedRectCount); } public: struct SUIElementRect @@ -185,13 +186,13 @@ class CUIElement }; protected: - CUI *UI() const { return m_pUI; } + CUi *Ui() const { return m_pUI; } std::vector m_vUIRects; public: CUIElement() = default; - void Init(CUI *pUI, int RequestedRectCount); + void Init(CUi *pUI, int RequestedRectCount); SUIElementRect *Rect(size_t Index) { @@ -212,6 +213,7 @@ struct SLabelProperties bool m_StopAtEnd = false; bool m_EllipsisAtEnd = false; bool m_EnableWidthCheck = true; + std::vector m_vColorSplits = {}; }; struct SMenuButtonProperties @@ -230,16 +232,16 @@ struct SMenuButtonProperties class CUIElementBase { private: - static CUI *s_pUI; + static CUi *s_pUI; public: - static void Init(CUI *pUI) { s_pUI = pUI; } + static void Init(CUi *pUI) { s_pUI = pUI; } IClient *Client() const; IGraphics *Graphics() const; IInput *Input() const; ITextRender *TextRender() const; - CUI *UI() const { return s_pUI; } + CUi *Ui() const { return s_pUI; } }; class CButtonContainer @@ -277,7 +279,7 @@ struct SPopupMenuProperties ColorRGBA m_BackgroundColor = ColorRGBA(0.0f, 0.0f, 0.0f, 0.75f); }; -class CUI +class CUi { public: /** @@ -318,25 +320,73 @@ class CUI */ typedef std::function FPopupMenuClosedCallback; + /** + * Represents the aggregated state of current touch events to control a user interface. + */ + class CTouchState + { + friend class CUi; + + bool m_SecondaryPressedNext = false; + float m_SecondaryActivationTime = 0.0f; + vec2 m_SecondaryActivationDelta = vec2(0.0f, 0.0f); + + public: + bool m_AnyPressed = false; + bool m_PrimaryPressed = false; + bool m_SecondaryPressed = false; + vec2 m_PrimaryPosition = vec2(-1.0f, -1.0f); + vec2 m_PrimaryDelta = vec2(0.0f, 0.0f); + vec2 m_ScrollAmount = vec2(0.0f, 0.0f); + }; + private: bool m_Enabled; - const void *m_pHotItem; - const void *m_pActiveItem; - const void *m_pLastActiveItem; // only used internally to track active CLineInput - const void *m_pBecomingHotItem; + const void *m_pHotItem = nullptr; + const void *m_pActiveItem = nullptr; + const void *m_pLastActiveItem = nullptr; // only used internally to track active CLineInput + const void *m_pBecomingHotItem = nullptr; + CScrollRegion *m_pHotScrollRegion = nullptr; + CScrollRegion *m_pBecomingHotScrollRegion = nullptr; bool m_ActiveItemValid = false; - vec2 m_UpdatedMousePos = vec2(0.0f, 0.0f); - vec2 m_UpdatedMouseDelta = vec2(0.0f, 0.0f); - float m_MouseX, m_MouseY; // in gui space - float m_MouseDeltaX, m_MouseDeltaY; // in gui space - float m_MouseWorldX, m_MouseWorldY; // in world space - unsigned m_MouseButtons; - unsigned m_LastMouseButtons; + int m_ActiveButtonLogicButton = -1; + int m_ActiveDraggableButtonLogicButton = -1; + class CDoubleClickState + { + public: + const void *m_pLastClickedId = nullptr; + float m_LastClickTime = -1.0f; + vec2 m_LastClickPos = vec2(-1.0f, -1.0f); + }; + CDoubleClickState m_DoubleClickState; + const void *m_pLastEditingItem = nullptr; + float m_ActiveScrollbarOffset = 0.0f; + float m_ProgressSpinnerOffset = 0.0f; + class CValueSelectorState + { + public: + int m_Button = -1; + bool m_DidScroll = false; + float m_ScrollValue = 0.0f; + CLineInputNumber m_NumberInput; + const void *m_pLastTextId = nullptr; + }; + CValueSelectorState m_ActiveValueSelectorState; + + vec2 m_UpdatedMousePos = vec2(0.0f, 0.0f); // in window screen space + vec2 m_UpdatedMouseDelta = vec2(0.0f, 0.0f); // in window screen space + vec2 m_MousePos = vec2(0.0f, 0.0f); // in gui space + vec2 m_MouseDelta = vec2(0.0f, 0.0f); // in gui space + vec2 m_MouseWorldPos = vec2(-1.0f, -1.0f); // in world space + unsigned m_UpdatedMouseButtons = 0; + unsigned m_MouseButtons = 0; + unsigned m_LastMouseButtons = 0; + CTouchState m_TouchState; bool m_MouseSlow = false; bool m_MouseLock = false; - const void *m_pMouseLockID = nullptr; + const void *m_pMouseLockId = nullptr; unsigned m_HotkeysPressed = 0; @@ -345,14 +395,12 @@ class CUI std::vector m_vClips; void UpdateClipping(); - bool m_ValueSelectorTextMode = false; - struct SPopupMenu { static constexpr float POPUP_BORDER = 1.0f; static constexpr float POPUP_MARGIN = 4.0f; - const SPopupMenuId *m_pID; + const SPopupMenuId *m_pId; SPopupMenuProperties m_Props; CUIRect m_Rect; void *m_pContext; @@ -361,17 +409,17 @@ class CUI std::vector m_vPopupMenus; FPopupMenuClosedCallback m_pfnPopupMenuClosedCallback = nullptr; - static CUI::EPopupMenuFunctionResult PopupMessage(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupConfirm(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupSelection(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupColorPicker(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupMessage(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupConfirm(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupSelection(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupColorPicker(void *pContext, CUIRect View, bool Active); IClient *m_pClient; IGraphics *m_pGraphics; IInput *m_pInput; ITextRender *m_pTextRender; - std::vector m_vpOwnUIElements; // ui elements maintained by CUI class + std::vector m_vpOwnUIElements; // ui elements maintained by CUi class std::vector m_vpUIElements; public: @@ -389,8 +437,8 @@ class CUI IInput *Input() const { return m_pInput; } ITextRender *TextRender() const { return m_pTextRender; } - CUI(); - ~CUI(); + CUi(); + ~CUi(); enum EHotkey : unsigned { @@ -398,14 +446,16 @@ class CUI HOTKEY_ESCAPE = 1 << 1, HOTKEY_UP = 1 << 2, HOTKEY_DOWN = 1 << 3, - HOTKEY_DELETE = 1 << 4, - HOTKEY_TAB = 1 << 5, - HOTKEY_SCROLL_UP = 1 << 6, - HOTKEY_SCROLL_DOWN = 1 << 7, - HOTKEY_PAGE_UP = 1 << 8, - HOTKEY_PAGE_DOWN = 1 << 9, - HOTKEY_HOME = 1 << 10, - HOTKEY_END = 1 << 11, + HOTKEY_LEFT = 1 << 4, + HOTKEY_RIGHT = 1 << 5, + HOTKEY_DELETE = 1 << 6, + HOTKEY_TAB = 1 << 7, + HOTKEY_SCROLL_UP = 1 << 8, + HOTKEY_SCROLL_DOWN = 1 << 9, + HOTKEY_PAGE_UP = 1 << 10, + HOTKEY_PAGE_DOWN = 1 << 11, + HOTKEY_HOME = 1 << 12, + HOTKEY_END = 1 << 13, }; void ResetUIElement(CUIElement &UIElement) const; @@ -419,53 +469,57 @@ class CUI void SetEnabled(bool Enabled) { m_Enabled = Enabled; } bool Enabled() const { return m_Enabled; } - void Update(); - void Update(float MouseX, float MouseY, float MouseDeltaX, float MouseDeltaY, float MouseWorldX, float MouseWorldY); - void DebugRender(); - - float MouseDeltaX() const { return m_MouseDeltaX; } - float MouseDeltaY() const { return m_MouseDeltaY; } - float MouseX() const { return m_MouseX; } - float MouseY() const { return m_MouseY; } - vec2 MousePos() const { return vec2(m_MouseX, m_MouseY); } - float MouseWorldX() const { return m_MouseWorldX; } - float MouseWorldY() const { return m_MouseWorldY; } + void Update(vec2 MouseWorldPos = vec2(-1.0f, -1.0f)); + void DebugRender(float X, float Y); + + vec2 MousePos() const { return m_MousePos; } + float MouseX() const { return m_MousePos.x; } + float MouseY() const { return m_MousePos.y; } + vec2 MouseDelta() const { return m_MouseDelta; } + float MouseDeltaX() const { return m_MouseDelta.x; } + float MouseDeltaY() const { return m_MouseDelta.y; } + vec2 MouseWorldPos() const { return m_MouseWorldPos; } + float MouseWorldX() const { return m_MouseWorldPos.x; } + float MouseWorldY() const { return m_MouseWorldPos.y; } + vec2 UpdatedMousePos() const { return m_UpdatedMousePos; } + vec2 UpdatedMouseDelta() const { return m_UpdatedMouseDelta; } int MouseButton(int Index) const { return (m_MouseButtons >> Index) & 1; } int MouseButtonClicked(int Index) const { return MouseButton(Index) && !((m_LastMouseButtons >> Index) & 1); } - int MouseButtonReleased(int Index) const { return ((m_LastMouseButtons >> Index) & 1) && !MouseButton(Index); } bool CheckMouseLock() { - if(m_MouseLock && ActiveItem() != m_pMouseLockID) + if(m_MouseLock && ActiveItem() != m_pMouseLockId) DisableMouseLock(); return m_MouseLock; } - void EnableMouseLock(const void *pID) + void EnableMouseLock(const void *pId) { m_MouseLock = true; - m_pMouseLockID = pID; + m_pMouseLockId = pId; } void DisableMouseLock() { m_MouseLock = false; } - void SetHotItem(const void *pID) { m_pBecomingHotItem = pID; } - void SetActiveItem(const void *pID) + void SetHotItem(const void *pId) { m_pBecomingHotItem = pId; } + void SetActiveItem(const void *pId) { m_ActiveItemValid = true; - m_pActiveItem = pID; - if(pID) - m_pLastActiveItem = pID; + m_pActiveItem = pId; + if(pId) + m_pLastActiveItem = pId; } - bool CheckActiveItem(const void *pID) + bool CheckActiveItem(const void *pId) { - if(m_pActiveItem == pID) + if(m_pActiveItem == pId) { m_ActiveItemValid = true; return true; } return false; } + void SetHotScrollRegion(CScrollRegion *pId) { m_pBecomingHotScrollRegion = pId; } const void *HotItem() const { return m_pHotItem; } const void *NextHotItem() const { return m_pBecomingHotItem; } const void *ActiveItem() const { return m_pActiveItem; } + const CScrollRegion *HotScrollRegion() const { return m_pHotScrollRegion; } void StartCheck() { m_ActiveItemValid = false; } void FinishCheck() @@ -482,6 +536,7 @@ class CUI bool MouseInsideClip() const { return !IsClipped() || MouseInside(ClipArea()); } bool MouseHovered(const CUIRect *pRect) const { return MouseInside(pRect) && MouseInsideClip(); } void ConvertMouseMove(float *pX, float *pY, IInput::ECursorType CursorType) const; + void UpdateTouchState(CTouchState &State) const; void ResetMouseSlow() { m_MouseSlow = false; } bool ConsumeHotkey(EHotkey Hotkey); @@ -491,7 +546,7 @@ class CUI constexpr float ButtonColorMulActive() const { return 0.5f; } constexpr float ButtonColorMulHot() const { return 1.5f; } constexpr float ButtonColorMulDefault() const { return 1.0f; } - float ButtonColorMul(const void *pID); + float ButtonColorMul(const void *pId); const CUIRect *Screen(); void MapScreen(); @@ -502,30 +557,79 @@ class CUI const CUIRect *ClipArea() const; inline bool IsClipped() const { return !m_vClips.empty(); } - int DoButtonLogic(const void *pID, int Checked, const CUIRect *pRect); - int DoDraggableButtonLogic(const void *pID, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted); - EEditState DoPickerLogic(const void *pID, const CUIRect *pRect, float *pX, float *pY); + int DoButtonLogic(const void *pId, int Checked, const CUIRect *pRect); + int DoDraggableButtonLogic(const void *pId, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted); + bool DoDoubleClickLogic(const void *pId); + EEditState DoPickerLogic(const void *pId, const CUIRect *pRect, float *pX, float *pY); void DoSmoothScrollLogic(float *pScrollOffset, float *pScrollOffsetChange, float ViewPortSize, float TotalSize, bool SmoothClamp = false, float ScrollSpeed = 10.0f) const; static vec2 CalcAlignedCursorPos(const CUIRect *pRect, vec2 TextSize, int Align, const float *pBiggestCharHeight = nullptr); - void DoLabel(const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps = {}) const; void DoLabel(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps = {}, int StrLen = -1, const CTextCursor *pReadCursor = nullptr) const; void DoLabelStreamed(CUIElement::SUIElementRect &RectEl, const CUIRect *pRect, const char *pText, float Size, int Align, const SLabelProperties &LabelProps = {}, int StrLen = -1, const CTextCursor *pReadCursor = nullptr) const; - bool DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL); - bool DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL); + /** + * Creates an input field. + * + * @see DoClearableEditBox + * + * @param pLineInput This pointer will be stored and written to on next user input. + * So you can not pass in a pointer that goes out of scope such as a local variable. + * Pass in either a member variable of the current class or a static variable. + * For example ```static CLineInputBuffered s_MyInput;``` + * @param pRect the UI rect it will attach to with a 2.0f margin + * @param FontSize Size of the font (`10.0f`, `12.0f` and `14.0f` are commonly used here) + * @param Corners Number of corners (default: `IGraphics::CORNER_ALL`) + * @param vColorSplits Sets color splits of the `CTextCursor` to allow multicolored text + * + * @return true if the value of the input field changed since the last call. + */ + bool DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const std::vector &vColorSplits = {}); + + /** + * Creates an input field with a clear [x] button attached to it. + * + * @see DoEditBox + * + * @param pLineInput This pointer will be stored and written to on next user input. + * So you can not pass in a pointer that goes out of scope such as a local variable. + * Pass in either a member variable of the current class or a static variable. + * For example ```static CLineInputBuffered s_MyInput;``` + * @param pRect the UI rect it will attach to + * @param FontSize Size of the font (`10.0f`, `12.0f` and `14.0f` are commonly used here) + * @param Corners Number of corners (default: `IGraphics::CORNER_ALL`) + * @param vColorSplits Sets color splits of the `CTextCursor` to allow multicolored text + * + * @return true if the value of the input field changed since the last call. + */ + bool DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const std::vector &vColorSplits = {}); + + /** + * Creates an input field with a search icon and a clear [x] button attached to it. + * The input will have default text "Search" and the hotkey Ctrl+F can be used to activate the input. + * + * @see DoEditBox + * + * @param pLineInput This pointer will be stored and written to on next user input. + * So you can not pass in a pointer that goes out of scope such as a local variable. + * Pass in either a member variable of the current class or a static variable. + * For example ```static CLineInputBuffered s_MyInput;``` + * @param pRect the UI rect it will attach to + * @param FontSize Size of the font (`10.0f`, `12.0f` and `14.0f` are commonly used here) + * @param HotkeyEnabled Whether the hotkey to enable this editbox is currently enabled. + * + * @return true if the value of the input field changed since the last call. + */ + bool DoEditBox_Search(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, bool HotkeyEnabled); - int DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pID, const std::function &GetTextLambda, const CUIRect *pRect, const SMenuButtonProperties &Props = {}); + int DoButton_Menu(CUIElement &UIElement, const CButtonContainer *pId, const std::function &GetTextLambda, const CUIRect *pRect, const SMenuButtonProperties &Props = {}); // only used for popup menus int DoButton_PopupMenu(CButtonContainer *pButtonContainer, const char *pText, const CUIRect *pRect, float Size, int Align, float Padding = 0.0f, bool TransparentInactive = false, bool Enabled = true); // value selector - SEditResult DoValueSelectorWithState(const void *pID, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props = {}); - int64_t DoValueSelector(const void *pID, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props = {}); - bool IsValueSelectorTextMode() const { return m_ValueSelectorTextMode; } - void SetValueSelectorTextMode(bool TextMode) { m_ValueSelectorTextMode = TextMode; } + SEditResult DoValueSelectorWithState(const void *pId, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props = {}); + int64_t DoValueSelector(const void *pId, const CUIRect *pRect, const char *pLabel, int64_t Current, int64_t Min, int64_t Max, const SValueSelectorProperties &Props = {}); // scrollbars enum @@ -534,20 +638,23 @@ class CUI SCROLLBAR_OPTION_NOCLAMPVALUE = 1 << 1, SCROLLBAR_OPTION_MULTILINE = 1 << 2, }; - float DoScrollbarV(const void *pID, const CUIRect *pRect, float Current); - float DoScrollbarH(const void *pID, const CUIRect *pRect, float Current, const ColorRGBA *pColorInner = nullptr); - void DoScrollbarOption(const void *pID, int *pOption, const CUIRect *pRect, const char *pStr, int Min, int Max, const IScrollbarScale *pScale = &ms_LinearScrollbarScale, unsigned Flags = 0u, const char *pSuffix = ""); + float DoScrollbarV(const void *pId, const CUIRect *pRect, float Current); + float DoScrollbarH(const void *pId, const CUIRect *pRect, float Current, const ColorRGBA *pColorInner = nullptr); + bool DoScrollbarOption(const void *pId, int *pOption, const CUIRect *pRect, const char *pStr, int Min, int Max, const IScrollbarScale *pScale = &ms_LinearScrollbarScale, unsigned Flags = 0u, const char *pSuffix = ""); + + // progress bar + void RenderProgressBar(CUIRect ProgressBar, float Progress); // progress spinner void RenderProgressSpinner(vec2 Center, float OuterRadius, const SProgressSpinnerProperties &Props = {}) const; // popup menu - void DoPopupMenu(const SPopupMenuId *pID, int X, int Y, int Width, int Height, void *pContext, FPopupMenuFunction pfnFunc, const SPopupMenuProperties &Props = {}); + void DoPopupMenu(const SPopupMenuId *pId, int X, int Y, int Width, int Height, void *pContext, FPopupMenuFunction pfnFunc, const SPopupMenuProperties &Props = {}); void RenderPopupMenus(); - void ClosePopupMenu(const SPopupMenuId *pID, bool IncludeDescendants = false); + void ClosePopupMenu(const SPopupMenuId *pId, bool IncludeDescendants = false); void ClosePopupMenus(); bool IsPopupOpen() const; - bool IsPopupOpen(const SPopupMenuId *pID) const; + bool IsPopupOpen(const SPopupMenuId *pId) const; bool IsPopupHovered() const; void SetPopupMenuClosedCallback(FPopupMenuClosedCallback pfnCallback); @@ -556,7 +663,7 @@ class CUI static constexpr float POPUP_MAX_WIDTH = 200.0f; static constexpr float POPUP_FONT_SIZE = 10.0f; - CUI *m_pUI; // set by CUI when popup is shown + CUi *m_pUI; // set by CUi when popup is shown char m_aMessage[1024]; ColorRGBA m_TextColor; @@ -578,7 +685,7 @@ class CUI static constexpr float POPUP_BUTTON_HEIGHT = 12.0f; static constexpr float POPUP_BUTTON_SPACING = 5.0f; - CUI *m_pUI; // set by CUI when popup is shown + CUi *m_pUI; // set by CUi when popup is shown char m_aPositiveButtonLabel[128]; char m_aNegativeButtonLabel[128]; char m_aMessage[1024]; @@ -595,8 +702,8 @@ class CUI struct SSelectionPopupContext : public SPopupMenuId { - CUI *m_pUI; // set by CUI when popup is shown - class CScrollRegion *m_pScrollRegion; + CUi *m_pUI; // set by CUi when popup is shown + CScrollRegion *m_pScrollRegion; SPopupMenuProperties m_Props; char m_aMessage[256]; std::vector m_vEntries; @@ -626,7 +733,7 @@ class CUI MODE_HSLA, }; - CUI *m_pUI; // set by CUI when popup is shown + CUi *m_pUI; // set by CUi when popup is shown EColorPickerMode m_ColorMode = MODE_UNSET; bool m_Alpha = false; unsigned int *m_pHslaColor = nullptr; // may be nullptr diff --git a/src/game/client/ui_listbox.cpp b/src/game/client/ui_listbox.cpp index 14cd6e65e6..cfbe18ddb0 100644 --- a/src/game/client/ui_listbox.cpp +++ b/src/game/client/ui_listbox.cpp @@ -39,7 +39,7 @@ void CListBox::DoHeader(const CUIRect *pRect, const char *pTitle, float HeaderHe // draw header View.HSplitTop(HeaderHeight, &Header, &View); - UI()->DoLabel(&Header, pTitle, Header.h * CUI::ms_FontmodHeight * 0.8f, TEXTALIGN_MC); + Ui()->DoLabel(&Header, pTitle, Header.h * CUi::ms_FontmodHeight * 0.8f, TEXTALIGN_MC); View.HSplitTop(Spacing, &Header, &View); @@ -80,7 +80,7 @@ void CListBox::DoStart(float RowHeight, int NumItems, int ItemsPerRow, int RowsP CUIRect Footer; View.HSplitBottom(m_FooterHeight, &View, &Footer); Footer.VSplitLeft(10.0f, 0, &Footer); - UI()->DoLabel(&Footer, m_pBottomText, Footer.h * CUI::ms_FontmodHeight * 0.8f, TEXTALIGN_MC); + Ui()->DoLabel(&Footer, m_pBottomText, Footer.h * CUi::ms_FontmodHeight * 0.8f, TEXTALIGN_MC); } // setup the variables @@ -93,31 +93,33 @@ void CListBox::DoStart(float RowHeight, int NumItems, int ItemsPerRow, int RowsP m_ListBoxRowHeight = RowHeight; m_ListBoxNumItems = NumItems; m_ListBoxItemsPerRow = ItemsPerRow; - m_ListBoxDoneEvents = false; m_ListBoxItemActivated = false; m_ListBoxItemSelected = false; // handle input if(m_Active && !Input()->ModifierIsPressed() && !Input()->ShiftIsPressed() && !Input()->AltIsPressed()) { - if(UI()->ConsumeHotkey(CUI::HOTKEY_DOWN)) + if(Ui()->ConsumeHotkey(CUi::HOTKEY_DOWN)) + m_ListBoxNewSelOffset += m_ListBoxItemsPerRow; + else if(Ui()->ConsumeHotkey(CUi::HOTKEY_UP)) + m_ListBoxNewSelOffset -= m_ListBoxItemsPerRow; + else if(Ui()->ConsumeHotkey(CUi::HOTKEY_RIGHT) && m_ListBoxItemsPerRow > 1) m_ListBoxNewSelOffset += 1; - else if(UI()->ConsumeHotkey(CUI::HOTKEY_UP)) + else if(Ui()->ConsumeHotkey(CUi::HOTKEY_LEFT) && m_ListBoxItemsPerRow > 1) m_ListBoxNewSelOffset -= 1; - else if(UI()->ConsumeHotkey(CUI::HOTKEY_PAGE_UP)) + else if(Ui()->ConsumeHotkey(CUi::HOTKEY_PAGE_UP)) m_ListBoxNewSelOffset = -ItemsPerRow * RowsPerScroll * 4; - else if(UI()->ConsumeHotkey(CUI::HOTKEY_PAGE_DOWN)) + else if(Ui()->ConsumeHotkey(CUi::HOTKEY_PAGE_DOWN)) m_ListBoxNewSelOffset = ItemsPerRow * RowsPerScroll * 4; - else if(UI()->ConsumeHotkey(CUI::HOTKEY_HOME)) + else if(Ui()->ConsumeHotkey(CUi::HOTKEY_HOME)) m_ListBoxNewSelOffset = 1 - m_ListBoxNumItems; - else if(UI()->ConsumeHotkey(CUI::HOTKEY_END)) + else if(Ui()->ConsumeHotkey(CUi::HOTKEY_END)) m_ListBoxNewSelOffset = m_ListBoxNumItems - 1; } // setup the scrollbar m_ScrollOffset = vec2(0.0f, 0.0f); CScrollRegionParams ScrollParams; - ScrollParams.m_Active = m_Active; ScrollParams.m_ScrollbarWidth = ScrollbarWidthMax(); ScrollParams.m_ScrollbarMargin = ScrollbarMargin(); ScrollParams.m_ScrollUnit = (m_ListBoxRowHeight + m_AutoSpacing) * RowsPerScroll; @@ -148,7 +150,7 @@ CListboxItem CListBox::DoNextRow() return Item; } -CListboxItem CListBox::DoNextItem(const void *pId, bool Selected) +CListboxItem CListBox::DoNextItem(const void *pId, bool Selected, float CornerRadius) { if(m_AutoSpacing > 0.0f && m_ListBoxItemIndex > 0) DoSpacing(m_AutoSpacing); @@ -162,37 +164,31 @@ CListboxItem CListBox::DoNextItem(const void *pId, bool Selected) } CListboxItem Item = DoNextRow(); - bool ItemClicked = false; - - if(Item.m_Visible && UI()->DoButtonLogic(pId, 0, &Item.m_Rect)) + const int ItemClicked = Item.m_Visible ? Ui()->DoButtonLogic(pId, 0, &Item.m_Rect) : 0; + if(ItemClicked) { - ItemClicked = true; m_ListBoxNewSelected = ThisItemIndex; m_ListBoxItemSelected = true; m_Active = true; } - else - ItemClicked = false; // process input, regard selected index - if(m_ListBoxSelectedIndex == ThisItemIndex) + if(m_ListBoxNewSelected == ThisItemIndex) { - if(m_Active && !m_ListBoxDoneEvents) + if(m_Active) { - m_ListBoxDoneEvents = true; - - if(UI()->ConsumeHotkey(CUI::HOTKEY_ENTER) || (ItemClicked && Input()->MouseDoubleClick())) + if(Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER) || (ItemClicked == 1 && Ui()->DoDoubleClickLogic(pId))) { m_ListBoxItemActivated = true; - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } } - Item.m_Rect.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, m_Active ? 0.5f : 0.33f), IGraphics::CORNER_ALL, 5.0f); + Item.m_Rect.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, m_Active ? 0.5f : 0.33f), IGraphics::CORNER_ALL, CornerRadius); } - if(UI()->HotItem() == pId && !m_ScrollRegion.Animating()) + if(Ui()->HotItem() == pId && !m_ScrollRegion.Animating()) { - Item.m_Rect.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.33f), IGraphics::CORNER_ALL, 5.0f); + Item.m_Rect.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.33f), IGraphics::CORNER_ALL, CornerRadius); } return Item; @@ -208,7 +204,7 @@ CListboxItem CListBox::DoSubheader() int CListBox::DoEnd() { m_ScrollRegion.End(); - m_Active |= m_ScrollRegion.Params().m_Active; + m_Active |= m_ScrollRegion.Active(); m_ScrollbarShown = m_ScrollRegion.ScrollbarShown(); if(m_ListBoxNewSelOffset != 0 && m_ListBoxNumItems > 0 && m_ListBoxSelectedIndex == m_ListBoxNewSelected) diff --git a/src/game/client/ui_listbox.h b/src/game/client/ui_listbox.h index 1e2b9af379..26ecc71710 100644 --- a/src/game/client/ui_listbox.h +++ b/src/game/client/ui_listbox.h @@ -24,7 +24,6 @@ class CListBox : private CUIElementBase int m_ListBoxNewSelected; int m_ListBoxNewSelOffset; bool m_ListBoxUpdateScroll; - bool m_ListBoxDoneEvents; int m_ListBoxNumItems; int m_ListBoxItemsPerRow; bool m_ListBoxItemSelected; @@ -54,7 +53,7 @@ class CListBox : private CUIElementBase void DoFooter(const char *pBottomText, float FooterHeight = 20.0f); // call before DoStart to create a footer void DoStart(float RowHeight, int NumItems, int ItemsPerRow, int RowsPerScroll, int SelectedIndex, const CUIRect *pRect = nullptr, bool Background = true, int BackgroundCorners = IGraphics::CORNER_ALL, bool ForceShowScrollbar = false); void ScrollToSelected() { m_ListBoxUpdateScroll = true; } - CListboxItem DoNextItem(const void *pID, bool Selected = false); + CListboxItem DoNextItem(const void *pId, bool Selected = false, float CornerRadius = 5.0f); CListboxItem DoSubheader(); int DoEnd(); diff --git a/src/game/client/ui_rect.cpp b/src/game/client/ui_rect.cpp index 9f44a89f2a..347477ad2a 100644 --- a/src/game/client/ui_rect.cpp +++ b/src/game/client/ui_rect.cpp @@ -25,7 +25,7 @@ void CUIRect::HSplitMid(CUIRect *pTop, CUIRect *pBottom, float Spacing) const pBottom->x = r.x; pBottom->y = r.y + Cut + HalfSpacing; pBottom->w = r.w; - pBottom->h = r.h - Cut - HalfSpacing; + pBottom->h = Cut - HalfSpacing; } } @@ -89,7 +89,7 @@ void CUIRect::VSplitMid(CUIRect *pLeft, CUIRect *pRight, float Spacing) const { pRight->x = r.x + Cut + HalfSpacing; pRight->y = r.y; - pRight->w = r.w - Cut - HalfSpacing; + pRight->w = Cut - HalfSpacing; pRight->h = r.h; } } @@ -161,9 +161,9 @@ void CUIRect::HMargin(float Cut, CUIRect *pOtherRect) const Margin(vec2(0.0f, Cut), pOtherRect); } -bool CUIRect::Inside(float PointX, float PointY) const +bool CUIRect::Inside(vec2 Point) const { - return PointX >= x && PointX < x + w && PointY >= y && PointY < y + h; + return Point.x >= x && Point.x < x + w && Point.y >= y && Point.y < y + h; } void CUIRect::Draw(ColorRGBA Color, int Corners, float Rounding) const diff --git a/src/game/client/ui_rect.h b/src/game/client/ui_rect.h index c95067b521..f3d38e420e 100644 --- a/src/game/client/ui_rect.h +++ b/src/game/client/ui_rect.h @@ -111,19 +111,49 @@ class CUIRect * @param pOtherRect The CUIRect to place inside *this* CUIRect */ void HMargin(float Cut, CUIRect *pOtherRect) const; + /** * Checks whether a point is inside *this* CUIRect. * - * @param PointX The point's X position. - * @param PointY The point's Y position. + * @param Point The point's position. * @return true iff the given point is inside *this* CUIRect. */ - bool Inside(float PointX, float PointY) const; + bool Inside(vec2 Point) const; + /** + * Fill background of *this* CUIRect. + * + * @note Example of filling a black half transparent background with 5px rounded edges on all sides + * @note ```MyCuiRect.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.5f), IGraphics::CORNER_ALL, 5.0f);``` + * + * @note Example of filling a red background with sharp edges + * @note ```MyCuiRect.Draw(ColorRGBA(1.0f, 0.0f, 0.0f), IGraphics::CORNER_NONE, 0.0f);``` + * + * @param Color + * @param Corners + * @param Rounding + */ void Draw(ColorRGBA Color, int Corners, float Rounding) const; void Draw4(ColorRGBA ColorTopLeft, ColorRGBA ColorTopRight, ColorRGBA ColorBottomLeft, ColorRGBA ColorBottomRight, int Corners, float Rounding) const; - vec2 Center() const { return vec2(x + w / 2.0f, y + h / 2.0f); } + /** + * Returns the top-left position of *this* CUIRect as a vec2. + * + * @return Top-left position as vec2. + */ + vec2 TopLeft() const { return vec2(x, y); } + /** + * Returns the size of *this* CUIRect as a vec2. + * + * @return Size as vec2. + */ + vec2 Size() const { return vec2(w, h); } + /** + * Returns the center position of *this* CUIRect as a vec2. + * + * @return Center position as vec2. + */ + vec2 Center() const { return TopLeft() + Size() / 2.0f; } }; #endif diff --git a/src/game/client/ui_scrollregion.cpp b/src/game/client/ui_scrollregion.cpp index 2d3303d4f0..8c0cdba6c5 100644 --- a/src/game/client/ui_scrollregion.cpp +++ b/src/game/client/ui_scrollregion.cpp @@ -60,7 +60,7 @@ void CScrollRegion::Begin(CUIRect *pClipRect, vec2 *pOutOffset, const CScrollReg if(m_Params.m_ClipBgColor.a > 0.0f) pClipRect->Draw(m_Params.m_ClipBgColor, HasScrollBar ? IGraphics::CORNER_L : IGraphics::CORNER_ALL, 4.0f); - UI()->ClipEnable(pClipRect); + Ui()->ClipEnable(pClipRect); m_ClipRect = *pClipRect; m_ContentH = 0.0f; @@ -69,7 +69,7 @@ void CScrollRegion::Begin(CUIRect *pClipRect, vec2 *pOutOffset, const CScrollReg void CScrollRegion::End() { - UI()->ClipDisable(); + Ui()->ClipDisable(); // only show scrollbar if content overflows if(m_ContentH <= m_ClipRect.h) @@ -79,12 +79,12 @@ void CScrollRegion::End() CUIRect RegionRect = m_ClipRect; RegionRect.w += m_Params.m_ScrollbarWidth; - if(m_ScrollDirection != SCROLLRELATIVE_NONE || (UI()->Enabled() && m_Params.m_Active && UI()->MouseHovered(&RegionRect))) + if(m_ScrollDirection != SCROLLRELATIVE_NONE || Ui()->HotScrollRegion() == this) { bool ProgrammaticScroll = false; - if(UI()->ConsumeHotkey(CUI::HOTKEY_SCROLL_UP)) + if(Ui()->ConsumeHotkey(CUi::HOTKEY_SCROLL_UP)) m_ScrollDirection = SCROLLRELATIVE_UP; - else if(UI()->ConsumeHotkey(CUI::HOTKEY_SCROLL_DOWN)) + else if(Ui()->ConsumeHotkey(CUi::HOTKEY_SCROLL_DOWN)) m_ScrollDirection = SCROLLRELATIVE_DOWN; else ProgrammaticScroll = true; @@ -106,6 +106,11 @@ void CScrollRegion::End() } } + if(Ui()->Enabled() && Ui()->MouseHovered(&RegionRect)) + { + Ui()->SetHotScrollRegion(this); + } + const float SliderHeight = maximum(m_Params.m_SliderMinHeight, m_ClipRect.h / m_ContentH * m_RailRect.h); CUIRect Slider = m_RailRect; @@ -140,13 +145,13 @@ void CScrollRegion::End() Slider.y += m_ScrollY / MaxScroll * MaxSlider; bool Grabbed = false; - const void *pID = &m_ScrollY; - const bool InsideSlider = UI()->MouseHovered(&Slider); - const bool InsideRail = UI()->MouseHovered(&m_RailRect); + const void *pId = &m_ScrollY; + const bool InsideSlider = Ui()->MouseHovered(&Slider); + const bool InsideRail = Ui()->MouseHovered(&m_RailRect); - if(UI()->CheckActiveItem(pID) && UI()->MouseButton(0)) + if(Ui()->CheckActiveItem(pId) && Ui()->MouseButton(0)) { - float MouseY = UI()->MouseY(); + float MouseY = Ui()->MouseY(); m_ScrollY += (MouseY - (Slider.y + m_SliderGrabPos)) / MaxSlider * MaxScroll; m_SliderGrabPos = clamp(m_SliderGrabPos, 0.0f, SliderHeight); m_AnimTargetScrollY = m_ScrollY; @@ -155,36 +160,36 @@ void CScrollRegion::End() } else if(InsideSlider) { - UI()->SetHotItem(pID); + if(!Ui()->MouseButton(0)) + Ui()->SetHotItem(pId); - if(!UI()->CheckActiveItem(pID) && UI()->MouseButtonClicked(0)) + if(!Ui()->CheckActiveItem(pId) && Ui()->MouseButtonClicked(0)) { - UI()->SetActiveItem(pID); - m_SliderGrabPos = UI()->MouseY() - Slider.y; + Ui()->SetActiveItem(pId); + m_SliderGrabPos = Ui()->MouseY() - Slider.y; m_AnimTargetScrollY = m_ScrollY; m_AnimTime = 0.0f; - m_Params.m_Active = true; } } - else if(InsideRail && UI()->MouseButtonClicked(0)) + else if(InsideRail && Ui()->MouseButtonClicked(0)) { - m_ScrollY += (UI()->MouseY() - (Slider.y + Slider.h / 2.0f)) / MaxSlider * MaxScroll; - UI()->SetHotItem(pID); - UI()->SetActiveItem(pID); + m_ScrollY += (Ui()->MouseY() - (Slider.y + Slider.h / 2.0f)) / MaxSlider * MaxScroll; + Ui()->SetHotItem(pId); + Ui()->SetActiveItem(pId); m_SliderGrabPos = Slider.h / 2.0f; m_AnimTargetScrollY = m_ScrollY; m_AnimTime = 0.0f; - m_Params.m_Active = true; } - else if(UI()->CheckActiveItem(pID) && !UI()->MouseButton(0)) + + if(Ui()->CheckActiveItem(pId) && !Ui()->MouseButton(0)) { - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } m_ScrollY = clamp(m_ScrollY, 0.0f, MaxScroll); m_ContentScrollOff.y = -m_ScrollY; - Slider.Draw(m_Params.SliderColor(Grabbed, UI()->HotItem() == pID), IGraphics::CORNER_ALL, Slider.w / 2.0f); + Slider.Draw(m_Params.SliderColor(Grabbed, Ui()->HotItem() == pId), IGraphics::CORNER_ALL, Slider.w / 2.0f); } bool CScrollRegion::AddRect(const CUIRect &Rect, bool ShouldScrollHere) @@ -229,6 +234,11 @@ void CScrollRegion::ScrollRelative(EScrollRelative Direction, float SpeedMultipl m_ScrollSpeedMultiplier = SpeedMultiplier; } +void CScrollRegion::ScrollRelativeDirect(float ScrollAmount) +{ + m_RequestScrollY = clamp(m_ScrollY + ScrollAmount, 0.0f, m_ContentH - m_ClipRect.h); +} + void CScrollRegion::DoEdgeScrolling() { if(!ScrollbarShown()) @@ -239,10 +249,10 @@ void CScrollRegion::DoEdgeScrolling() const float ScrollSpeedFactor = MaxScrollMultiplier / ScrollBorderSize; const float TopScrollPosition = m_ClipRect.y + ScrollBorderSize; const float BottomScrollPosition = m_ClipRect.y + m_ClipRect.h - ScrollBorderSize; - if(UI()->MouseY() < TopScrollPosition) - ScrollRelative(SCROLLRELATIVE_UP, minimum(MaxScrollMultiplier, (TopScrollPosition - UI()->MouseY()) * ScrollSpeedFactor)); - else if(UI()->MouseY() > BottomScrollPosition) - ScrollRelative(SCROLLRELATIVE_DOWN, minimum(MaxScrollMultiplier, (UI()->MouseY() - BottomScrollPosition) * ScrollSpeedFactor)); + if(Ui()->MouseY() < TopScrollPosition) + ScrollRelative(SCROLLRELATIVE_UP, minimum(MaxScrollMultiplier, (TopScrollPosition - Ui()->MouseY()) * ScrollSpeedFactor)); + else if(Ui()->MouseY() > BottomScrollPosition) + ScrollRelative(SCROLLRELATIVE_DOWN, minimum(MaxScrollMultiplier, (Ui()->MouseY() - BottomScrollPosition) * ScrollSpeedFactor)); } bool CScrollRegion::RectClipped(const CUIRect &Rect) const @@ -259,3 +269,8 @@ bool CScrollRegion::Animating() const { return m_AnimTime > 0.0f; } + +bool CScrollRegion::Active() const +{ + return Ui()->ActiveItem() == &m_ScrollY; +} diff --git a/src/game/client/ui_scrollregion.h b/src/game/client/ui_scrollregion.h index 2a601be534..0094b66e27 100644 --- a/src/game/client/ui_scrollregion.h +++ b/src/game/client/ui_scrollregion.h @@ -7,7 +7,6 @@ struct CScrollRegionParams { - bool m_Active; float m_ScrollbarWidth; float m_ScrollbarMargin; bool m_ScrollbarNoMarginRight; @@ -28,7 +27,6 @@ struct CScrollRegionParams CScrollRegionParams() { - m_Active = true; m_ScrollbarWidth = 20.0f; m_ScrollbarMargin = 5.0f; m_ScrollbarNoMarginRight = false; @@ -135,11 +133,13 @@ class CScrollRegion : private CUIElementBase bool AddRect(const CUIRect &Rect, bool ShouldScrollHere = false); // returns true if the added rect is visible (not clipped) void ScrollHere(EScrollOption Option = SCROLLHERE_KEEP_IN_VIEW); void ScrollRelative(EScrollRelative Direction, float SpeedMultiplier = 1.0f); + void ScrollRelativeDirect(float ScrollAmount); const CUIRect *ClipRect() const { return &m_ClipRect; } void DoEdgeScrolling(); bool RectClipped(const CUIRect &Rect) const; bool ScrollbarShown() const; bool Animating() const; + bool Active() const; const CScrollRegionParams &Params() const { return m_Params; } }; diff --git a/src/game/collision.cpp b/src/game/collision.cpp index 8612ad7dc2..acdc51558c 100644 --- a/src/game/collision.cpp +++ b/src/game/collision.cpp @@ -38,28 +38,19 @@ vec2 ClampVel(int MoveRestriction, vec2 Vel) CCollision::CCollision() { - m_pTiles = 0; - m_Width = 0; - m_Height = 0; - m_pLayers = 0; - - m_pTele = 0; - m_pSpeedup = 0; - m_pFront = 0; - m_pSwitch = 0; - m_pDoor = 0; - m_pTune = 0; + m_pDoor = nullptr; + Unload(); } CCollision::~CCollision() { - Dest(); + Unload(); } void CCollision::Init(class CLayers *pLayers) { - Dest(); - m_HighestSwitchNumber = 0; + Unload(); + m_pLayers = pLayers; m_Width = m_pLayers->GameLayer()->m_Width; m_Height = m_pLayers->GameLayer()->m_Height; @@ -88,10 +79,6 @@ void CCollision::Init(class CLayers *pLayers) m_pDoor = new CDoorTile[m_Width * m_Height]; mem_zero(m_pDoor, (size_t)m_Width * m_Height * sizeof(CDoorTile)); } - else - { - m_pDoor = 0; - } if(m_pLayers->TuneLayer()) { @@ -131,9 +118,60 @@ void CCollision::Init(class CLayers *pLayers) } } } + + if(m_pTele) + { + for(int i = 0; i < m_Width * m_Height; i++) + { + int Number = m_pTele[i].m_Number; + int Type = m_pTele[i].m_Type; + if(Number > 0) + { + if(Type == TILE_TELEIN) + { + m_TeleIns[Number - 1].emplace_back(i % m_Width * 32.0f + 16.0f, i / m_Width * 32.0f + 16.0f); + } + else if(Type == TILE_TELEOUT) + { + m_TeleOuts[Number - 1].emplace_back(i % m_Width * 32.0f + 16.0f, i / m_Width * 32.0f + 16.0f); + } + else if(Type == TILE_TELECHECKOUT) + { + m_TeleCheckOuts[Number - 1].emplace_back(i % m_Width * 32.0f + 16.0f, i / m_Width * 32.0f + 16.0f); + } + else if(Type) + { + m_TeleOthers[Number - 1].emplace_back(i % m_Width * 32.0f + 16.0f, i / m_Width * 32.0f + 16.0f); + } + } + } + } } -void CCollision::FillAntibot(CAntibotMapData *pMapData) +void CCollision::Unload() +{ + m_pTiles = nullptr; + m_Width = 0; + m_Height = 0; + m_pLayers = nullptr; + + m_HighestSwitchNumber = 0; + + m_TeleIns.clear(); + m_TeleOuts.clear(); + m_TeleCheckOuts.clear(); + m_TeleOthers.clear(); + + m_pTele = nullptr; + m_pSpeedup = nullptr; + m_pFront = nullptr; + m_pSwitch = nullptr; + m_pTune = nullptr; + delete[] m_pDoor; + m_pDoor = nullptr; +} + +void CCollision::FillAntibot(CAntibotMapData *pMapData) const { pMapData->m_Width = m_Width; pMapData->m_Height = m_Height; @@ -558,49 +596,34 @@ void CCollision::MoveBox(vec2 *pInoutPos, vec2 *pInoutVel, vec2 Size, vec2 Elast // DDRace -void CCollision::Dest() -{ - delete[] m_pDoor; - m_pTiles = 0; - m_Width = 0; - m_Height = 0; - m_pLayers = 0; - m_pTele = 0; - m_pSpeedup = 0; - m_pFront = 0; - m_pSwitch = 0; - m_pTune = 0; - m_pDoor = 0; -} - int CCollision::IsSolid(int x, int y) const { int index = GetTile(x, y); return index == TILE_SOLID || index == TILE_NOHOOK; } -bool CCollision::IsThrough(int x, int y, int xoff, int yoff, vec2 pos0, vec2 pos1) const +bool CCollision::IsThrough(int x, int y, int OffsetX, int OffsetY, vec2 Pos0, vec2 Pos1) const { int pos = GetPureMapIndex(x, y); if(m_pFront && (m_pFront[pos].m_Index == TILE_THROUGH_ALL || m_pFront[pos].m_Index == TILE_THROUGH_CUT)) return true; - if(m_pFront && m_pFront[pos].m_Index == TILE_THROUGH_DIR && ((m_pFront[pos].m_Flags == ROTATION_0 && pos0.y > pos1.y) || (m_pFront[pos].m_Flags == ROTATION_90 && pos0.x < pos1.x) || (m_pFront[pos].m_Flags == ROTATION_180 && pos0.y < pos1.y) || (m_pFront[pos].m_Flags == ROTATION_270 && pos0.x > pos1.x))) + if(m_pFront && m_pFront[pos].m_Index == TILE_THROUGH_DIR && ((m_pFront[pos].m_Flags == ROTATION_0 && Pos0.y > Pos1.y) || (m_pFront[pos].m_Flags == ROTATION_90 && Pos0.x < Pos1.x) || (m_pFront[pos].m_Flags == ROTATION_180 && Pos0.y < Pos1.y) || (m_pFront[pos].m_Flags == ROTATION_270 && Pos0.x > Pos1.x))) return true; - int offpos = GetPureMapIndex(x + xoff, y + yoff); + int offpos = GetPureMapIndex(x + OffsetX, y + OffsetY); return m_pTiles[offpos].m_Index == TILE_THROUGH || (m_pFront && m_pFront[offpos].m_Index == TILE_THROUGH); } -bool CCollision::IsHookBlocker(int x, int y, vec2 pos0, vec2 pos1) const +bool CCollision::IsHookBlocker(int x, int y, vec2 Pos0, vec2 Pos1) const { int pos = GetPureMapIndex(x, y); if(m_pTiles[pos].m_Index == TILE_THROUGH_ALL || (m_pFront && m_pFront[pos].m_Index == TILE_THROUGH_ALL)) return true; - if(m_pTiles[pos].m_Index == TILE_THROUGH_DIR && ((m_pTiles[pos].m_Flags == ROTATION_0 && pos0.y < pos1.y) || - (m_pTiles[pos].m_Flags == ROTATION_90 && pos0.x > pos1.x) || - (m_pTiles[pos].m_Flags == ROTATION_180 && pos0.y > pos1.y) || - (m_pTiles[pos].m_Flags == ROTATION_270 && pos0.x < pos1.x))) + if(m_pTiles[pos].m_Index == TILE_THROUGH_DIR && ((m_pTiles[pos].m_Flags == ROTATION_0 && Pos0.y < Pos1.y) || + (m_pTiles[pos].m_Flags == ROTATION_90 && Pos0.x > Pos1.x) || + (m_pTiles[pos].m_Flags == ROTATION_180 && Pos0.y > Pos1.y) || + (m_pTiles[pos].m_Flags == ROTATION_270 && Pos0.x < Pos1.x))) return true; - if(m_pFront && m_pFront[pos].m_Index == TILE_THROUGH_DIR && ((m_pFront[pos].m_Flags == ROTATION_0 && pos0.y < pos1.y) || (m_pFront[pos].m_Flags == ROTATION_90 && pos0.x > pos1.x) || (m_pFront[pos].m_Flags == ROTATION_180 && pos0.y > pos1.y) || (m_pFront[pos].m_Flags == ROTATION_270 && pos0.x < pos1.x))) + if(m_pFront && m_pFront[pos].m_Index == TILE_THROUGH_DIR && ((m_pFront[pos].m_Flags == ROTATION_0 && Pos0.y < Pos1.y) || (m_pFront[pos].m_Flags == ROTATION_90 && Pos0.x > Pos1.x) || (m_pFront[pos].m_Flags == ROTATION_180 && Pos0.y > Pos1.y) || (m_pFront[pos].m_Flags == ROTATION_270 && Pos0.x < Pos1.x))) return true; return false; } @@ -1032,60 +1055,35 @@ int CCollision::GetFTile(int x, int y) const int CCollision::Entity(int x, int y, int Layer) const { if(x < 0 || x >= m_Width || y < 0 || y >= m_Height) - { - const char *pName; - switch(Layer) - { - case LAYER_GAME: - pName = "Game"; - break; - case LAYER_FRONT: - pName = "Front"; - break; - case LAYER_SWITCH: - pName = "Switch"; - break; - case LAYER_TELE: - pName = "Tele"; - break; - case LAYER_SPEEDUP: - pName = "Speedup"; - break; - case LAYER_TUNE: - pName = "Tune"; - break; - default: - pName = "Unknown"; - } - dbg_msg("collision", "Something is VERY wrong with the %s layer near (%d, %d). Please report this at https://github.com/ddnet/ddnet/issues, you will need to post the map as well and any steps that you think may have led to this.", pName, x, y); return 0; - } + + const int Index = y * m_Width + x; switch(Layer) { case LAYER_GAME: - return m_pTiles[y * m_Width + x].m_Index - ENTITY_OFFSET; + return m_pTiles[Index].m_Index - ENTITY_OFFSET; case LAYER_FRONT: - return m_pFront[y * m_Width + x].m_Index - ENTITY_OFFSET; + return m_pFront[Index].m_Index - ENTITY_OFFSET; case LAYER_SWITCH: - return m_pSwitch[y * m_Width + x].m_Type - ENTITY_OFFSET; + return m_pSwitch[Index].m_Type - ENTITY_OFFSET; case LAYER_TELE: - return m_pTele[y * m_Width + x].m_Type - ENTITY_OFFSET; + return m_pTele[Index].m_Type - ENTITY_OFFSET; case LAYER_SPEEDUP: - return m_pSpeedup[y * m_Width + x].m_Type - ENTITY_OFFSET; + return m_pSpeedup[Index].m_Type - ENTITY_OFFSET; case LAYER_TUNE: - return m_pTune[y * m_Width + x].m_Type - ENTITY_OFFSET; + return m_pTune[Index].m_Type - ENTITY_OFFSET; default: - return 0; - break; + dbg_assert(false, "Layer invalid"); + dbg_break(); } } -void CCollision::SetCollisionAt(float x, float y, int id) +void CCollision::SetCollisionAt(float x, float y, int Index) { int Nx = clamp(round_to_int(x) / 32, 0, m_Width - 1); int Ny = clamp(round_to_int(y) / 32, 0, m_Height - 1); - m_pTiles[Ny * m_Width + Nx].m_Index = id; + m_pTiles[Ny * m_Width + Nx].m_Index = Index; } void CCollision::SetDCollisionAt(float x, float y, int Type, int Flags, int Number) @@ -1162,7 +1160,7 @@ int CCollision::IntersectNoLaser(vec2 Pos0, vec2 Pos1, vec2 *pOutCollision, vec2 for(int i = 0, id = std::ceil(d); i < id; i++) { - float a = (int)i / d; + float a = i / d; vec2 Pos = mix(Pos0, Pos1, a); int Nx = clamp(round_to_int(Pos.x) / 32, 0, m_Width - 1); int Ny = clamp(round_to_int(Pos.y) / 32, 0, m_Height - 1); @@ -1267,3 +1265,48 @@ int CCollision::IsFTimeCheckpoint(int Index) const return z - TILE_TIME_CHECKPOINT_FIRST; return -1; } + +vec2 CCollision::TeleAllGet(int Number, size_t Offset) +{ + if(m_TeleIns.count(Number) > 0) + { + if(m_TeleIns[Number].size() > Offset) + return m_TeleIns[Number][Offset]; + else + Offset -= m_TeleIns[Number].size(); + } + if(m_TeleOuts.count(Number) > 0) + { + if(m_TeleOuts[Number].size() > Offset) + return m_TeleOuts[Number][Offset]; + else + Offset -= m_TeleOuts[Number].size(); + } + if(m_TeleCheckOuts.count(Number) > 0) + { + if(m_TeleCheckOuts[Number].size() > Offset) + return m_TeleCheckOuts[Number][Offset]; + else + Offset -= m_TeleCheckOuts[Number].size(); + } + if(m_TeleOthers.count(Number) > 0) + { + if(m_TeleOthers[Number].size() > Offset) + return m_TeleOthers[Number][Offset]; + } + return vec2(-1, -1); +} + +size_t CCollision::TeleAllSize(int Number) +{ + size_t Total = 0; + if(m_TeleIns.count(Number) > 0) + Total += m_TeleIns[Number].size(); + if(m_TeleOuts.count(Number) > 0) + Total += m_TeleOuts[Number].size(); + if(m_TeleCheckOuts.count(Number) > 0) + Total += m_TeleCheckOuts[Number].size(); + if(m_TeleOthers.count(Number) > 0) + Total += m_TeleOthers[Number].size(); + return Total; +} diff --git a/src/game/collision.h b/src/game/collision.h index 351ac05dd2..a5f987488b 100644 --- a/src/game/collision.h +++ b/src/game/collision.h @@ -6,8 +6,17 @@ #include #include +#include #include +class CTile; +class CLayers; +class CTeleTile; +class CSpeedupTile; +class CSwitchTile; +class CTuneTile; +class CDoorTile; + enum { CANTMOVE_LEFT = 1 << 0, @@ -23,16 +32,14 @@ struct CAntibotMapData; class CCollision { - class CTile *m_pTiles; - int m_Width; - int m_Height; - class CLayers *m_pLayers; - public: CCollision(); ~CCollision(); - void Init(class CLayers *pLayers); - void FillAntibot(CAntibotMapData *pMapData); + + void Init(CLayers *pLayers); + void Unload(); + void FillAntibot(CAntibotMapData *pMapData) const; + bool CheckPoint(float x, float y) const { return IsSolid(round_to_int(x), round_to_int(y)); } bool CheckPoint(vec2 Pos) const { return CheckPoint(Pos.x, Pos.y); } int GetCollisionAt(float x, float y) const { return GetTile(round_to_int(x), round_to_int(y)); } @@ -46,10 +53,7 @@ class CCollision bool TestBox(vec2 Pos, vec2 Size) const; // DDRace - - void Dest(); - void SetCollisionAt(float x, float y, int id); - void SetDTile(float x, float y, bool State); + void SetCollisionAt(float x, float y, int Index); void SetDCollisionAt(float x, float y, int Type, int Flags, int Number); int GetDTileIndex(int Index) const; int GetDTileFlags(int Index) const; @@ -63,7 +67,7 @@ class CCollision int GetFIndex(int x, int y) const; int GetMoveRestrictions(CALLBACK_SWITCHACTIVE pfnSwitchActive, void *pUser, vec2 Pos, float Distance = 18.0f, int OverrideCenterTileIndex = -1) const; - int GetMoveRestrictions(vec2 Pos, float Distance = 18.0f) + int GetMoveRestrictions(vec2 Pos, float Distance = 18.0f) const { return GetMoveRestrictions(nullptr, nullptr, Pos, Distance); } @@ -97,8 +101,8 @@ class CCollision int GetSwitchDelay(int Index) const; int IsSolid(int x, int y) const; - bool IsThrough(int x, int y, int xoff, int yoff, vec2 pos0, vec2 pos1) const; - bool IsHookBlocker(int x, int y, vec2 pos0, vec2 pos1) const; + bool IsThrough(int x, int y, int OffsetX, int OffsetY, vec2 Pos0, vec2 Pos1) const; + bool IsHookBlocker(int x, int y, vec2 Pos0, vec2 Pos1) const; int IsWallJump(int Index) const; int IsNoLaser(int x, int y) const; int IsFNoLaser(int x, int y) const; @@ -110,19 +114,61 @@ class CCollision vec2 CpSpeed(int index, int Flags = 0) const; - class CTeleTile *TeleLayer() { return m_pTele; } - class CSwitchTile *SwitchLayer() { return m_pSwitch; } - class CTuneTile *TuneLayer() { return m_pTune; } - class CLayers *Layers() { return m_pLayers; } + const CLayers *Layers() const { return m_pLayers; } + const CTile *GameLayer() const { return m_pTiles; } + const CTeleTile *TeleLayer() const { return m_pTele; } + const CSpeedupTile *SpeedupLayer() const { return m_pSpeedup; } + const CTile *FrontLayer() const { return m_pFront; } + const CSwitchTile *SwitchLayer() const { return m_pSwitch; } + const CTuneTile *TuneLayer() const { return m_pTune; } + int m_HighestSwitchNumber; + /** + * Index all teleporter types (in, out and checkpoints) + * as one consecutive list. + * + * @param Number is the teleporter number (one less than what is shown in game) + * @param Offset picks the n'th occurence of that teleporter in the map + * + * @return The coordinates of the teleporter in the map + * or (-1, -1) if not found + */ + vec2 TeleAllGet(int Number, size_t Offset); + + /** + * @param Number is the teleporter number (one less than what is shown in game) + * @return The amount of occurences of that teleporter across all types (in, out, checkpoint) + */ + size_t TeleAllSize(int Number); + + const std::vector &TeleIns(int Number) { return m_TeleIns[Number]; } + const std::vector &TeleOuts(int Number) { return m_TeleOuts[Number]; } + const std::vector &TeleCheckOuts(int Number) { return m_TeleCheckOuts[Number]; } + const std::vector &TeleOthers(int Number) { return m_TeleOthers[Number]; } + private: - class CTeleTile *m_pTele; - class CSpeedupTile *m_pSpeedup; - class CTile *m_pFront; - class CSwitchTile *m_pSwitch; - class CTuneTile *m_pTune; - class CDoorTile *m_pDoor; + CLayers *m_pLayers; + + int m_Width; + int m_Height; + + CTile *m_pTiles; + CTeleTile *m_pTele; + CSpeedupTile *m_pSpeedup; + CTile *m_pFront; + CSwitchTile *m_pSwitch; + CTuneTile *m_pTune; + CDoorTile *m_pDoor; + + // TILE_TELEIN + std::map> m_TeleIns; + // TILE_TELEOUT + std::map> m_TeleOuts; + // TILE_TELECHECKOUT + std::map> m_TeleCheckOuts; + // TILE_TELEINEVIL, TILE_TELECHECK, TILE_TELECHECKIN, TILE_TELECHECKINEVIL + std::map> m_TeleOthers; }; void ThroughOffset(vec2 Pos0, vec2 Pos1, int *pOffsetX, int *pOffsetY); diff --git a/src/game/editor/auto_map.cpp b/src/game/editor/auto_map.cpp index 4f58f8e0fd..4edb402384 100644 --- a/src/game/editor/auto_map.cpp +++ b/src/game/editor/auto_map.cpp @@ -5,10 +5,10 @@ #include #include +#include #include #include "auto_map.h" -#include "editor.h" // TODO: only needs CLayerTiles #include "editor_actions.h" // Based on triple32inc from https://github.com/skeeto/hash-prospector/tree/79a6074062a84907df6e45b756134b74e2956760 @@ -42,15 +42,15 @@ static int HashLocation(uint32_t Seed, uint32_t Run, uint32_t Rule, uint32_t X, CAutoMapper::CAutoMapper(CEditor *pEditor) { - Init(pEditor); + OnInit(pEditor); } void CAutoMapper::Load(const char *pTileName) { char aPath[IO_MAX_PATH_LENGTH]; str_format(aPath, sizeof(aPath), "editor/automap/%s.rules", pTileName); - IOHANDLE RulesFile = Storage()->OpenFile(aPath, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL); - if(!RulesFile) + CLineReader LineReader; + if(!LineReader.OpenFile(Storage()->OpenFile(aPath, IOFLAG_READ, IStorage::TYPE_ALL))) { char aBuf[IO_MAX_PATH_LENGTH + 32]; str_format(aBuf, sizeof(aBuf), "failed to load %s", aPath); @@ -58,15 +58,12 @@ void CAutoMapper::Load(const char *pTileName) return; } - CLineReader LineReader; - LineReader.Init(RulesFile); - CConfiguration *pCurrentConf = nullptr; CRun *pCurrentRun = nullptr; CIndexRule *pCurrentIndex = nullptr; // read each line - while(char *pLine = LineReader.Get()) + while(const char *pLine = LineReader.Get()) { // skip blank/empty lines as well as comments if(str_length(pLine) > 0 && pLine[0] != '#' && pLine[0] != '\n' && pLine[0] != '\r' && pLine[0] != '\t' && pLine[0] != '\v' && pLine[0] != ' ') @@ -82,16 +79,16 @@ void CAutoMapper::Load(const char *pTileName) NewConf.m_EndX = 0; NewConf.m_EndY = 0; m_vConfigs.push_back(NewConf); - int ConfigurationID = m_vConfigs.size() - 1; - pCurrentConf = &m_vConfigs[ConfigurationID]; + int ConfigurationId = m_vConfigs.size() - 1; + pCurrentConf = &m_vConfigs[ConfigurationId]; str_copy(pCurrentConf->m_aName, pLine, minimum(sizeof(pCurrentConf->m_aName), str_length(pLine))); // add start run CRun NewRun; NewRun.m_AutomapCopy = true; pCurrentConf->m_vRuns.push_back(NewRun); - int RunID = pCurrentConf->m_vRuns.size() - 1; - pCurrentRun = &pCurrentConf->m_vRuns[RunID]; + int RunId = pCurrentConf->m_vRuns.size() - 1; + pCurrentRun = &pCurrentConf->m_vRuns[RunId]; } else if(str_startswith(pLine, "NewRun") && pCurrentConf) { @@ -99,21 +96,21 @@ void CAutoMapper::Load(const char *pTileName) CRun NewRun; NewRun.m_AutomapCopy = true; pCurrentConf->m_vRuns.push_back(NewRun); - int RunID = pCurrentConf->m_vRuns.size() - 1; - pCurrentRun = &pCurrentConf->m_vRuns[RunID]; + int RunId = pCurrentConf->m_vRuns.size() - 1; + pCurrentRun = &pCurrentConf->m_vRuns[RunId]; } else if(str_startswith(pLine, "Index") && pCurrentRun) { // new index - int ID = 0; + int Id = 0; char aOrientation1[128] = ""; char aOrientation2[128] = ""; char aOrientation3[128] = ""; - sscanf(pLine, "Index %d %127s %127s %127s", &ID, aOrientation1, aOrientation2, aOrientation3); + sscanf(pLine, "Index %d %127s %127s %127s", &Id, aOrientation1, aOrientation2, aOrientation3); CIndexRule NewIndexRule; - NewIndexRule.m_ID = ID; + NewIndexRule.m_Id = Id; NewIndexRule.m_Flag = 0; NewIndexRule.m_RandomProbability = 1.0f; NewIndexRule.m_DefaultRule = true; @@ -152,8 +149,8 @@ void CAutoMapper::Load(const char *pTileName) // add the index rule object and make it current pCurrentRun->m_vIndexRules.push_back(NewIndexRule); - int IndexRuleID = pCurrentRun->m_vIndexRules.size() - 1; - pCurrentIndex = &pCurrentRun->m_vIndexRules[IndexRuleID]; + int IndexRuleId = pCurrentRun->m_vIndexRules.size() - 1; + pCurrentIndex = &pCurrentRun->m_vIndexRules[IndexRuleId]; } else if(str_startswith(pLine, "Pos") && pCurrentIndex) { @@ -188,15 +185,15 @@ void CAutoMapper::Load(const char *pTileName) int pWord = 4; while(true) { - int ID = 0; + int Id = 0; char aOrientation1[128] = ""; char aOrientation2[128] = ""; char aOrientation3[128] = ""; char aOrientation4[128] = ""; - sscanf(str_trim_words(pLine, pWord), "%d %127s %127s %127s %127s", &ID, aOrientation1, aOrientation2, aOrientation3, aOrientation4); + sscanf(str_trim_words(pLine, pWord), "%d %127s %127s %127s %127s", &Id, aOrientation1, aOrientation2, aOrientation3, aOrientation4); CIndexInfo NewIndexInfo; - NewIndexInfo.m_ID = ID; + NewIndexInfo.m_Id = Id; NewIndexInfo.m_Flag = 0; NewIndexInfo.m_TestFlag = false; @@ -296,10 +293,18 @@ void CAutoMapper::Load(const char *pTileName) { for(const auto &Index : vNewIndexList) { - if(Value == CPosRule::INDEX && Index.m_ID == 0) + if(Index.m_Id == 0 && Value == CPosRule::INDEX) + { + // Skip full tiles if we have a rule "POS 0 0 INDEX 0" + // because that forces the tile to be empty pCurrentIndex->m_SkipFull = true; - else + } + else if((Index.m_Id > 0 && Value == CPosRule::INDEX) || (Index.m_Id == 0 && Value == CPosRule::NOTINDEX)) + { + // Skip empty tiles if we have a rule "POS 0 0 INDEX i" where i > 0 + // or if we have a rule "POS 0 0 NOTINDEX 0" pCurrentIndex->m_SkipEmpty = true; + } } } } @@ -337,14 +342,25 @@ void CAutoMapper::Load(const char *pTileName) for(auto &IndexRule : Run.m_vIndexRules) { bool Found = false; + + // Search for the exact rule "POS 0 0 INDEX 0" which corresponds to the default rule for(const auto &Rule : IndexRule.m_vRules) { - if(Rule.m_X == 0 && Rule.m_Y == 0) + if(Rule.m_X == 0 && Rule.m_Y == 0 && Rule.m_Value == CPosRule::INDEX) { - Found = true; + for(const auto &Index : Rule.m_vIndexList) + { + if(Index.m_Id == 0) + Found = true; + } break; } + + if(Found) + break; } + + // If the default rule was not found, and we require it, then add it if(!Found && IndexRule.m_DefaultRule) { std::vector vNewIndexList; @@ -356,6 +372,7 @@ void CAutoMapper::Load(const char *pTileName) IndexRule.m_SkipEmpty = true; IndexRule.m_SkipFull = false; } + if(IndexRule.m_SkipEmpty && IndexRule.m_SkipFull) { IndexRule.m_SkipEmpty = false; @@ -365,8 +382,6 @@ void CAutoMapper::Load(const char *pTileName) } } - io_close(RulesFile); - char aBuf[IO_MAX_PATH_LENGTH + 16]; str_format(aBuf, sizeof(aBuf), "loaded %s", aPath); Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "editor/automap", aBuf); @@ -382,9 +397,9 @@ const char *CAutoMapper::GetConfigName(int Index) return m_vConfigs[Index].m_aName; } -void CAutoMapper::ProceedLocalized(CLayerTiles *pLayer, int ConfigID, int Seed, int X, int Y, int Width, int Height) +void CAutoMapper::ProceedLocalized(CLayerTiles *pLayer, int ConfigId, int Seed, int X, int Y, int Width, int Height) { - if(!m_FileLoaded || pLayer->m_Readonly || ConfigID < 0 || ConfigID >= (int)m_vConfigs.size()) + if(!m_FileLoaded || pLayer->m_Readonly || ConfigId < 0 || ConfigId >= (int)m_vConfigs.size()) return; if(Width < 0) @@ -393,7 +408,7 @@ void CAutoMapper::ProceedLocalized(CLayerTiles *pLayer, int ConfigID, int Seed, if(Height < 0) Height = pLayer->m_Height; - CConfiguration *pConf = &m_vConfigs[ConfigID]; + CConfiguration *pConf = &m_vConfigs[ConfigId]; int CommitFromX = clamp(X + pConf->m_StartX, 0, pLayer->m_Width); int CommitFromY = clamp(Y + pConf->m_StartY, 0, pLayer->m_Height); @@ -418,7 +433,7 @@ void CAutoMapper::ProceedLocalized(CLayerTiles *pLayer, int ConfigID, int Seed, } } - Proceed(pUpdateLayer, ConfigID, Seed, UpdateFromX, UpdateFromY); + Proceed(pUpdateLayer, ConfigId, Seed, UpdateFromX, UpdateFromY); for(int y = CommitFromY; y < CommitToY; y++) { @@ -436,15 +451,15 @@ void CAutoMapper::ProceedLocalized(CLayerTiles *pLayer, int ConfigID, int Seed, delete pUpdateLayer; } -void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigID, int Seed, int SeedOffsetX, int SeedOffsetY) +void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigId, int Seed, int SeedOffsetX, int SeedOffsetY) { - if(!m_FileLoaded || pLayer->m_Readonly || ConfigID < 0 || ConfigID >= (int)m_vConfigs.size()) + if(!m_FileLoaded || pLayer->m_Readonly || ConfigId < 0 || ConfigId >= (int)m_vConfigs.size()) return; if(Seed == 0) Seed = rand(); - CConfiguration *pConf = &m_vConfigs[ConfigID]; + CConfiguration *pConf = &m_vConfigs[ConfigId]; pLayer->ClearHistory(); // for every run: copy tiles, automap, overwrite tiles @@ -480,14 +495,15 @@ void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigID, int Seed, int SeedO for(int x = 0; x < pLayer->m_Width; x++) { CTile *pTile = &(pLayer->m_pTiles[y * pLayer->m_Width + x]); + const CTile *pReadTile = &(pReadLayer->m_pTiles[y * pLayer->m_Width + x]); Editor()->m_Map.OnModify(); for(size_t i = 0; i < pRun->m_vIndexRules.size(); ++i) { CIndexRule *pIndexRule = &pRun->m_vIndexRules[i]; - if(pIndexRule->m_SkipEmpty && pTile->m_Index == 0) // skip empty tiles + if(pIndexRule->m_SkipEmpty && pReadTile->m_Index == 0) // skip empty tiles continue; - if(pIndexRule->m_SkipFull && pTile->m_Index != 0) // skip full tiles + if(pIndexRule->m_SkipFull && pReadTile->m_Index != 0) // skip full tiles continue; bool RespectRules = true; @@ -515,7 +531,7 @@ void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigID, int Seed, int SeedO RespectRules = false; for(const auto &Index : pRule->m_vIndexList) { - if(CheckIndex == Index.m_ID && (!Index.m_TestFlag || CheckFlags == Index.m_Flag)) + if(CheckIndex == Index.m_Id && (!Index.m_TestFlag || CheckFlags == Index.m_Flag)) { RespectRules = true; break; @@ -526,7 +542,7 @@ void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigID, int Seed, int SeedO { for(const auto &Index : pRule->m_vIndexList) { - if(CheckIndex == Index.m_ID && (!Index.m_TestFlag || CheckFlags == Index.m_Flag)) + if(CheckIndex == Index.m_Id && (!Index.m_TestFlag || CheckFlags == Index.m_Flag)) { RespectRules = false; break; @@ -539,7 +555,7 @@ void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigID, int Seed, int SeedO (pIndexRule->m_RandomProbability >= 1.0f || HashLocation(Seed, h, i, x + SeedOffsetX, y + SeedOffsetY) < HASH_MAX * pIndexRule->m_RandomProbability)) { CTile Previous = *pTile; - pTile->m_Index = pIndexRule->m_ID; + pTile->m_Index = pIndexRule->m_Id; pTile->m_Flags = pIndexRule->m_Flag; pLayer->RecordStateChange(x, y, Previous, *pTile); } diff --git a/src/game/editor/auto_map.h b/src/game/editor/auto_map.h index 4e998af724..02ddd9b892 100644 --- a/src/game/editor/auto_map.h +++ b/src/game/editor/auto_map.h @@ -9,7 +9,7 @@ class CAutoMapper : public CEditorComponent { struct CIndexInfo { - int m_ID; + int m_Id; int m_Flag; bool m_TestFlag; }; @@ -31,7 +31,7 @@ class CAutoMapper : public CEditorComponent struct CIndexRule { - int m_ID; + int m_Id; std::vector m_vRules; int m_Flag; float m_RandomProbability; @@ -60,8 +60,8 @@ class CAutoMapper : public CEditorComponent explicit CAutoMapper(CEditor *pEditor); void Load(const char *pTileName); - void ProceedLocalized(class CLayerTiles *pLayer, int ConfigID, int Seed = 0, int X = 0, int Y = 0, int Width = -1, int Height = -1); - void Proceed(class CLayerTiles *pLayer, int ConfigID, int Seed = 0, int SeedOffsetX = 0, int SeedOffsetY = 0); + void ProceedLocalized(class CLayerTiles *pLayer, int ConfigId, int Seed = 0, int X = 0, int Y = 0, int Width = -1, int Height = -1); + void Proceed(class CLayerTiles *pLayer, int ConfigId, int Seed = 0, int SeedOffsetX = 0, int SeedOffsetY = 0); int ConfigNamesNum() const { return m_vConfigs.size(); } const char *GetConfigName(int Index); diff --git a/src/game/editor/component.cpp b/src/game/editor/component.cpp index 05e49ad29c..f008ab4e45 100644 --- a/src/game/editor/component.cpp +++ b/src/game/editor/component.cpp @@ -14,7 +14,7 @@ void CEditorComponent::InitSubComponents() { for(CEditorComponent &Component : m_vSubComponents) { - Component.Init(Editor()); + Component.OnInit(Editor()); } } diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 2caf68a624..ca238a279b 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -41,6 +41,7 @@ #include "editor_actions.h" #include +#include #include #include @@ -93,39 +94,36 @@ enum BUTTON_CONTEXT = 1, }; -void CEditor::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels, void *pUser) +void CEditor::EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser) { CEditor *pThis = (CEditor *)pUser; if(Env < 0 || Env >= (int)pThis->m_Map.m_vpEnvelopes.size()) - { - Channels = ColorRGBA(); return; - } std::shared_ptr pEnv = pThis->m_Map.m_vpEnvelopes[Env]; - float t = pThis->m_AnimateTime; - t *= pThis->m_AnimateSpeed; - t += (TimeOffsetMillis / 1000.0f); - pEnv->Eval(t, Channels); + float Time = pThis->m_AnimateTime; + Time *= pThis->m_AnimateSpeed; + Time += (TimeOffsetMillis / 1000.0f); + pEnv->Eval(Time, Result, Channels); } /******************************************************** OTHER *********************************************************/ -bool CEditor::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip) +bool CEditor::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip, const std::vector &vColorSplits) { UpdateTooltip(pLineInput, pRect, pToolTip); - return UI()->DoEditBox(pLineInput, pRect, FontSize, Corners); + return Ui()->DoEditBox(pLineInput, pRect, FontSize, Corners, vColorSplits); } -bool CEditor::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip) +bool CEditor::DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners, const char *pToolTip, const std::vector &vColorSplits) { UpdateTooltip(pLineInput, pRect, pToolTip); - return UI()->DoClearableEditBox(pLineInput, pRect, FontSize, Corners); + return Ui()->DoClearableEditBox(pLineInput, pRect, FontSize, Corners, vColorSplits); } -ColorRGBA CEditor::GetButtonColor(const void *pID, int Checked) +ColorRGBA CEditor::GetButtonColor(const void *pId, int Checked) { if(Checked < 0) return ColorRGBA(0, 0, 0, 0.5f); @@ -135,113 +133,92 @@ ColorRGBA CEditor::GetButtonColor(const void *pID, int Checked) case 8: // invisible return ColorRGBA(0, 0, 0, 0); case 7: // selected + game layers - if(UI()->HotItem() == pID) + if(Ui()->HotItem() == pId) return ColorRGBA(1, 0, 0, 0.4f); return ColorRGBA(1, 0, 0, 0.2f); case 6: // game layers - if(UI()->HotItem() == pID) + if(Ui()->HotItem() == pId) return ColorRGBA(1, 1, 1, 0.4f); return ColorRGBA(1, 1, 1, 0.2f); case 5: // selected + image/sound should be embedded - if(UI()->HotItem() == pID) + if(Ui()->HotItem() == pId) return ColorRGBA(1, 0, 0, 0.75f); return ColorRGBA(1, 0, 0, 0.5f); case 4: // image/sound should be embedded - if(UI()->HotItem() == pID) + if(Ui()->HotItem() == pId) return ColorRGBA(1, 0, 0, 1.0f); return ColorRGBA(1, 0, 0, 0.875f); case 3: // selected + unused image/sound - if(UI()->HotItem() == pID) + if(Ui()->HotItem() == pId) return ColorRGBA(1, 0, 1, 0.75f); return ColorRGBA(1, 0, 1, 0.5f); case 2: // unused image/sound - if(UI()->HotItem() == pID) + if(Ui()->HotItem() == pId) return ColorRGBA(0, 0, 1, 0.75f); return ColorRGBA(0, 0, 1, 0.5f); case 1: // selected - if(UI()->HotItem() == pID) + if(Ui()->HotItem() == pId) return ColorRGBA(1, 0, 0, 0.75f); return ColorRGBA(1, 0, 0, 0.5f); default: // regular - if(UI()->HotItem() == pID) + if(Ui()->HotItem() == pId) return ColorRGBA(1, 1, 1, 0.75f); return ColorRGBA(1, 1, 1, 0.5f); } } -void CEditor::UpdateTooltip(const void *pID, const CUIRect *pRect, const char *pToolTip) +void CEditor::UpdateTooltip(const void *pId, const CUIRect *pRect, const char *pToolTip) { - if(UI()->MouseInside(pRect) && !pToolTip) + if(Ui()->MouseInside(pRect) && !pToolTip) str_copy(m_aTooltip, ""); - else if(UI()->HotItem() == pID && pToolTip) + else if(Ui()->HotItem() == pId && pToolTip) str_copy(m_aTooltip, pToolTip); } -int CEditor::DoButton_Editor_Common(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip) +int CEditor::DoButton_Editor_Common(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip) { - if(UI()->MouseInside(pRect)) + if(Ui()->MouseInside(pRect)) { if(Flags & BUTTON_CONTEXT) - ms_pUiGotContext = pID; + ms_pUiGotContext = pId; } - UpdateTooltip(pID, pRect, pToolTip); - return UI()->DoButtonLogic(pID, Checked, pRect); + UpdateTooltip(pId, pRect, pToolTip); + return Ui()->DoButtonLogic(pId, Checked, pRect); } -int CEditor::DoButton_Editor(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip) +int CEditor::DoButton_Editor(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip) { - pRect->Draw(GetButtonColor(pID, Checked), IGraphics::CORNER_ALL, 3.0f); + pRect->Draw(GetButtonColor(pId, Checked), IGraphics::CORNER_ALL, 3.0f); CUIRect NewRect = *pRect; - UI()->DoLabel(&NewRect, pText, 10.0f, TEXTALIGN_MC); + Ui()->DoLabel(&NewRect, pText, 10.0f, TEXTALIGN_MC); Checked %= 2; - return DoButton_Editor_Common(pID, pText, Checked, pRect, Flags, pToolTip); + return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip); } -int CEditor::DoButton_Env(const void *pID, const char *pText, int Checked, const CUIRect *pRect, const char *pToolTip, ColorRGBA BaseColor, int Corners) +int CEditor::DoButton_Env(const void *pId, const char *pText, int Checked, const CUIRect *pRect, const char *pToolTip, ColorRGBA BaseColor, int Corners) { float Bright = Checked ? 1.0f : 0.5f; - float Alpha = UI()->HotItem() == pID ? 1.0f : 0.75f; + float Alpha = Ui()->HotItem() == pId ? 1.0f : 0.75f; ColorRGBA Color = ColorRGBA(BaseColor.r * Bright, BaseColor.g * Bright, BaseColor.b * Bright, Alpha); pRect->Draw(Color, Corners, 3.0f); - UI()->DoLabel(pRect, pText, 10.0f, TEXTALIGN_MC); + Ui()->DoLabel(pRect, pText, 10.0f, TEXTALIGN_MC); Checked %= 2; - return DoButton_Editor_Common(pID, pText, Checked, pRect, 0, pToolTip); -} - -int CEditor::DoButton_File(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip) -{ - if(Checked) - pRect->Draw(GetButtonColor(pID, Checked), IGraphics::CORNER_ALL, 3.0f); - - CUIRect Rect; - pRect->VMargin(5.0f, &Rect); - UI()->DoLabel(&Rect, pText, 10.0f, TEXTALIGN_ML); - return DoButton_Editor_Common(pID, pText, Checked, pRect, Flags, pToolTip); + return DoButton_Editor_Common(pId, pText, Checked, pRect, 0, pToolTip); } -int CEditor::DoButton_Menu(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip) +int CEditor::DoButton_MenuItem(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip) { - pRect->Draw(ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f), IGraphics::CORNER_T, 3.0f); - - CUIRect Rect; - pRect->VMargin(5.0f, &Rect); - UI()->DoLabel(&Rect, pText, 10.0f, TEXTALIGN_ML); - return DoButton_Editor_Common(pID, pText, Checked, pRect, Flags, pToolTip); -} - -int CEditor::DoButton_MenuItem(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip) -{ - if(UI()->HotItem() == pID || Checked) - pRect->Draw(GetButtonColor(pID, Checked), IGraphics::CORNER_ALL, 3.0f); + if(Ui()->HotItem() == pId || Checked) + pRect->Draw(GetButtonColor(pId, Checked), IGraphics::CORNER_ALL, 3.0f); CUIRect Rect; pRect->VMargin(5.0f, &Rect); @@ -249,56 +226,42 @@ int CEditor::DoButton_MenuItem(const void *pID, const char *pText, int Checked, SLabelProperties Props; Props.m_MaxWidth = Rect.w; Props.m_EllipsisAtEnd = true; - UI()->DoLabel(&Rect, pText, 10.0f, TEXTALIGN_ML, Props); + Ui()->DoLabel(&Rect, pText, 10.0f, TEXTALIGN_ML, Props); - return DoButton_Editor_Common(pID, pText, Checked, pRect, Flags, pToolTip); + return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip); } -int CEditor::DoButton_Ex(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize) +int CEditor::DoButton_Ex(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize, int Align) { - pRect->Draw(GetButtonColor(pID, Checked), Corners, 3.0f); + pRect->Draw(GetButtonColor(pId, Checked), Corners, 3.0f); CUIRect Rect; - pRect->VMargin(pRect->w > 20.0f ? 5.0f : 0.0f, &Rect); + pRect->VMargin(((Align & TEXTALIGN_MASK_HORIZONTAL) == TEXTALIGN_CENTER) ? 1.0f : 5.0f, &Rect); SLabelProperties Props; Props.m_MaxWidth = Rect.w; Props.m_EllipsisAtEnd = true; - UI()->DoLabel(&Rect, pText, FontSize, TEXTALIGN_MC, Props); + Ui()->DoLabel(&Rect, pText, FontSize, Align, Props); - return DoButton_Editor_Common(pID, pText, Checked, pRect, Flags, pToolTip); + return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip); } -int CEditor::DoButton_FontIcon(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize) +int CEditor::DoButton_FontIcon(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize) { - pRect->Draw(GetButtonColor(pID, Checked), Corners, 3.0f); + pRect->Draw(GetButtonColor(pId, Checked), Corners, 3.0f); TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING); - UI()->DoLabel(pRect, pText, FontSize, TEXTALIGN_MC); + Ui()->DoLabel(pRect, pText, FontSize, TEXTALIGN_MC); TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); - return DoButton_Editor_Common(pID, pText, Checked, pRect, Flags, pToolTip); -} - -int CEditor::DoButton_ButtonInc(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip) -{ - pRect->Draw(GetButtonColor(pID, Checked), IGraphics::CORNER_R, 3.0f); - UI()->DoLabel(pRect, pText ? pText : "+", 10.0f, TEXTALIGN_MC); - return DoButton_Editor_Common(pID, pText, Checked, pRect, Flags, pToolTip); -} - -int CEditor::DoButton_ButtonDec(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip) -{ - pRect->Draw(GetButtonColor(pID, Checked), IGraphics::CORNER_L, 3.0f); - UI()->DoLabel(pRect, pText ? pText : "-", 10.0f, TEXTALIGN_MC); - return DoButton_Editor_Common(pID, pText, Checked, pRect, Flags, pToolTip); + return DoButton_Editor_Common(pId, pText, Checked, pRect, Flags, pToolTip); } -int CEditor::DoButton_DraggableEx(const void *pID, const char *pText, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted, int Flags, const char *pToolTip, int Corners, float FontSize) +int CEditor::DoButton_DraggableEx(const void *pId, const char *pText, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted, int Flags, const char *pToolTip, int Corners, float FontSize) { - pRect->Draw(GetButtonColor(pID, Checked), Corners, 3.0f); + pRect->Draw(GetButtonColor(pId, Checked), Corners, 3.0f); CUIRect Rect; pRect->VMargin(pRect->w > 20.0f ? 5.0f : 0.0f, &Rect); @@ -306,19 +269,19 @@ int CEditor::DoButton_DraggableEx(const void *pID, const char *pText, int Checke SLabelProperties Props; Props.m_MaxWidth = Rect.w; Props.m_EllipsisAtEnd = true; - UI()->DoLabel(&Rect, pText, FontSize, TEXTALIGN_MC, Props); + Ui()->DoLabel(&Rect, pText, FontSize, TEXTALIGN_MC, Props); - if(UI()->MouseInside(pRect)) + if(Ui()->MouseInside(pRect)) { if(Flags & BUTTON_CONTEXT) - ms_pUiGotContext = pID; + ms_pUiGotContext = pId; } - UpdateTooltip(pID, pRect, pToolTip); - return UI()->DoDraggableButtonLogic(pID, Checked, pRect, pClicked, pAbrupted); + UpdateTooltip(pId, pRect, pToolTip); + return Ui()->DoDraggableButtonLogic(pId, Checked, pRect, pClicked, pAbrupted); } -void CEditor::RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, float Size, float Brightness) +void CEditor::RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, float Size, float Brightness) const { Graphics()->TextureSet(Texture); Graphics()->BlendNormal(); @@ -330,77 +293,70 @@ void CEditor::RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, Graphics()->QuadsEnd(); } -SEditResult CEditor::UiDoValueSelector(void *pID, CUIRect *pRect, const char *pLabel, int Current, int Min, int Max, int Step, float Scale, const char *pToolTip, bool IsDegree, bool IsHex, int Corners, ColorRGBA *pColor, bool ShowValue) +SEditResult CEditor::UiDoValueSelector(void *pId, CUIRect *pRect, const char *pLabel, int Current, int Min, int Max, int Step, float Scale, const char *pToolTip, bool IsDegree, bool IsHex, int Corners, const ColorRGBA *pColor, bool ShowValue) { // logic - static float s_Value; + static bool s_DidScroll = false; + static float s_ScrollValue = 0.0f; static CLineInputNumber s_NumberInput; - static bool s_TextMode = false; - static void *s_pLastTextID = pID; - const bool Inside = UI()->MouseInside(pRect); - const int Base = IsHex ? 16 : 10; - static bool s_Editing = false; - EEditState State = EEditState::EDITING; + static int s_ButtonUsed = -1; + static void *s_pLastTextId = nullptr; - if(UI()->MouseButton(1) && UI()->HotItem() == pID) - { - s_pLastTextID = pID; - s_TextMode = true; - UI()->DisableMouseLock(); - s_NumberInput.SetInteger(Current, Base); - } + const bool Inside = Ui()->MouseInside(pRect); + const int Base = IsHex ? 16 : 10; - if(UI()->CheckActiveItem(pID)) + if(Ui()->HotItem() == pId && s_ButtonUsed >= 0 && !Ui()->MouseButton(s_ButtonUsed)) { - if(!UI()->MouseButton(0)) + Ui()->DisableMouseLock(); + if(Ui()->CheckActiveItem(pId)) + { + Ui()->SetActiveItem(nullptr); + } + if(Inside && ((s_ButtonUsed == 0 && !s_DidScroll && Ui()->DoDoubleClickLogic(pId)) || s_ButtonUsed == 1)) { - UI()->DisableMouseLock(); - UI()->SetActiveItem(nullptr); - s_TextMode = false; + s_pLastTextId = pId; + s_NumberInput.SetInteger(Current, Base); + s_NumberInput.SelectAll(); } + s_ButtonUsed = -1; } - if(s_TextMode && s_pLastTextID == pID) + if(s_pLastTextId == pId) { str_copy(m_aTooltip, "Type your number"); + Ui()->SetActiveItem(&s_NumberInput); + DoEditBox(&s_NumberInput, pRect, 10.0f, Corners); - DoEditBox(&s_NumberInput, pRect, 10.0f); - - UI()->SetActiveItem(&s_NumberInput); - - if(Input()->KeyIsPressed(KEY_RETURN) || Input()->KeyIsPressed(KEY_KP_ENTER) || - ((UI()->MouseButton(1) || UI()->MouseButton(0)) && !Inside)) + if(Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER) || ((Ui()->MouseButtonClicked(1) || Ui()->MouseButtonClicked(0)) && !Inside)) { Current = clamp(s_NumberInput.GetInteger(Base), Min, Max); - UI()->DisableMouseLock(); - UI()->SetActiveItem(nullptr); - s_TextMode = false; + Ui()->DisableMouseLock(); + Ui()->SetActiveItem(nullptr); + s_pLastTextId = nullptr; } - if(UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) + if(Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) { - UI()->DisableMouseLock(); - UI()->SetActiveItem(nullptr); - s_TextMode = false; + Ui()->DisableMouseLock(); + Ui()->SetActiveItem(nullptr); + s_pLastTextId = nullptr; } } else { - if(UI()->CheckActiveItem(pID)) + if(Ui()->CheckActiveItem(pId)) { - if(UI()->MouseButton(0)) + if(s_ButtonUsed == 0 && Ui()->MouseButton(0)) { - if(Input()->ShiftIsPressed()) - s_Value += m_MouseDeltaX * 0.05f; - else - s_Value += m_MouseDeltaX; + s_ScrollValue += Ui()->MouseDeltaX() * (Input()->ShiftIsPressed() ? 0.05f : 1.0f); - if(absolute(s_Value) >= Scale) + if(absolute(s_ScrollValue) >= Scale) { - int Count = (int)(s_Value / Scale); - s_Value = std::fmod(s_Value, Scale); + int Count = (int)(s_ScrollValue / Scale); + s_ScrollValue = std::fmod(s_ScrollValue, Scale); Current += Step * Count; Current = clamp(Current, Min, Max); + s_DidScroll = true; // Constrain to discrete steps if(Count > 0) @@ -409,24 +365,30 @@ SEditResult CEditor::UiDoValueSelector(void *pID, CUIRect *pRect, const cha Current = std::ceil(Current / (float)Step) * Step; } } - if(pToolTip && !s_TextMode) + + if(pToolTip && s_pLastTextId != pId) str_copy(m_aTooltip, pToolTip); } - else if(UI()->HotItem() == pID) + else if(Ui()->HotItem() == pId) { - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { - UI()->SetActiveItem(pID); - UI()->EnableMouseLock(pID); - s_Value = 0; + s_ButtonUsed = 0; + s_DidScroll = false; + s_ScrollValue = 0.0f; + Ui()->SetActiveItem(pId); + Ui()->EnableMouseLock(pId); } - if(pToolTip && !s_TextMode) + else if(Ui()->MouseButton(1)) + { + s_ButtonUsed = 1; + Ui()->SetActiveItem(pId); + } + + if(pToolTip && s_pLastTextId != pId) str_copy(m_aTooltip, pToolTip); } - if(Inside) - UI()->SetHotItem(pID); - // render char aBuf[128]; if(pLabel[0] != '\0') @@ -441,25 +403,29 @@ SEditResult CEditor::UiDoValueSelector(void *pID, CUIRect *pRect, const cha else if(IsHex) str_format(aBuf, sizeof(aBuf), "#%06X", Current); else - str_from_int(Current, aBuf); - pRect->Draw(pColor ? *pColor : GetButtonColor(pID, 0), Corners, 3.0f); - UI()->DoLabel(pRect, aBuf, 10, TEXTALIGN_MC); + str_format(aBuf, sizeof(aBuf), "%d", Current); + pRect->Draw(pColor ? *pColor : GetButtonColor(pId, 0), Corners, 3.0f); + Ui()->DoLabel(pRect, aBuf, 10, TEXTALIGN_MC); } - if(!s_TextMode) - s_NumberInput.Clear(); + if(Inside && !Ui()->MouseButton(0) && !Ui()->MouseButton(1)) + Ui()->SetHotItem(pId); - bool MouseLocked = UI()->CheckMouseLock(); - if((MouseLocked || s_TextMode) && !s_Editing) + static const void *s_pEditing = nullptr; + EEditState State = EEditState::NONE; + if(s_pEditing == pId) + { + State = EEditState::EDITING; + } + if(((Ui()->CheckActiveItem(pId) && Ui()->CheckMouseLock()) || s_pLastTextId == pId) && s_pEditing != pId) { State = EEditState::START; - s_Editing = true; + s_pEditing = pId; } - - if(!MouseLocked && !s_TextMode && s_Editing) + if(!Ui()->CheckMouseLock() && s_pLastTextId != pId && s_pEditing == pId) { State = EEditState::END; - s_Editing = false; + s_pEditing = nullptr; } return SEditResult{State, Current}; @@ -772,13 +738,62 @@ std::pair CEditor::EnvGetSelectedTimeAndValue() const return std::pair{CurrentTime, CurrentValue}; } +void CEditor::SelectNextLayer() +{ + int CurrentLayer = 0; + for(const auto &Selected : m_vSelectedLayers) + CurrentLayer = maximum(Selected, CurrentLayer); + SelectLayer(CurrentLayer); + + if(m_vSelectedLayers[0] < (int)m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1) + { + SelectLayer(m_vSelectedLayers[0] + 1); + } + else + { + for(size_t Group = m_SelectedGroup + 1; Group < m_Map.m_vpGroups.size(); Group++) + { + if(!m_Map.m_vpGroups[Group]->m_vpLayers.empty()) + { + SelectLayer(0, Group); + break; + } + } + } +} + +void CEditor::SelectPreviousLayer() +{ + int CurrentLayer = std::numeric_limits::max(); + for(const auto &Selected : m_vSelectedLayers) + CurrentLayer = minimum(Selected, CurrentLayer); + SelectLayer(CurrentLayer); + + if(m_vSelectedLayers[0] > 0) + { + SelectLayer(m_vSelectedLayers[0] - 1); + } + else + { + for(int Group = m_SelectedGroup - 1; Group >= 0; Group--) + { + if(!m_Map.m_vpGroups[Group]->m_vpLayers.empty()) + { + SelectLayer(m_Map.m_vpGroups[Group]->m_vpLayers.size() - 1, Group); + break; + } + } + } +} + bool CEditor::CallbackOpenMap(const char *pFileName, int StorageType, void *pUser) { CEditor *pEditor = (CEditor *)pUser; if(pEditor->Load(pFileName, StorageType)) { pEditor->m_ValidSaveFilename = StorageType == IStorage::TYPE_SAVE && (pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentFolder || (pEditor->m_pFileDialogPath == pEditor->m_aFileDialogCurrentLink && str_comp(pEditor->m_aFileDialogCurrentLink, "themes") == 0)); - pEditor->m_Dialog = DIALOG_NONE; + if(pEditor->m_Dialog == DIALOG_FILE) + pEditor->m_Dialog = DIALOG_NONE; return true; } else @@ -883,41 +898,14 @@ bool CEditor::CallbackSaveImage(const char *pFileName, int StorageType, void *pU std::shared_ptr pImg = pEditor->m_Map.m_vpImages[pEditor->m_SelectedImage]; - EImageFormat OutputFormat; - switch(pImg->m_Format) - { - case CImageInfo::FORMAT_RGB: - OutputFormat = IMAGE_FORMAT_RGB; - break; - case CImageInfo::FORMAT_RGBA: - OutputFormat = IMAGE_FORMAT_RGBA; - break; - case CImageInfo::FORMAT_SINGLE_COMPONENT: - OutputFormat = IMAGE_FORMAT_R; - break; - default: - dbg_assert(false, "Image has invalid format."); - return false; - }; - - TImageByteBuffer ByteBuffer; - SImageByteBuffer ImageByteBuffer(&ByteBuffer); - if(SavePNG(OutputFormat, static_cast(pImg->m_pData), ImageByteBuffer, pImg->m_Width, pImg->m_Height)) + if(CImageLoader::SavePng(pEditor->Storage()->OpenFile(pFileName, IOFLAG_WRITE, StorageType), pFileName, *pImg)) { - IOHANDLE File = pEditor->Storage()->OpenFile(pFileName, IOFLAG_WRITE, StorageType); - if(File) - { - io_write(File, &ByteBuffer.front(), ByteBuffer.size()); - io_close(File); - pEditor->m_Dialog = DIALOG_NONE; - return true; - } - pEditor->ShowFileDialogError("Failed to open file '%s'.", pFileName); - return false; + pEditor->m_Dialog = DIALOG_NONE; + return true; } else { - pEditor->ShowFileDialogError("Failed to write image to file."); + pEditor->ShowFileDialogError("Failed to write image to file '%s'.", pFileName); return false; } } @@ -950,25 +938,56 @@ bool CEditor::CallbackSaveSound(const char *pFileName, int StorageType, void *pU return false; } -void CEditor::DoAudioPreview(CUIRect View, const void *pPlayPauseButtonID, const void *pStopButtonID, const void *pSeekBarID, const int SampleID) +bool CEditor::CallbackCustomEntities(const char *pFileName, int StorageType, void *pUser) +{ + CEditor *pEditor = (CEditor *)pUser; + + char aBuf[IO_MAX_PATH_LENGTH]; + IStorage::StripPathAndExtension(pFileName, aBuf, sizeof(aBuf)); + + if(std::find(pEditor->m_vSelectEntitiesFiles.begin(), pEditor->m_vSelectEntitiesFiles.end(), std::string(aBuf)) != pEditor->m_vSelectEntitiesFiles.end()) + { + pEditor->ShowFileDialogError("Custom entities cannot have the same name as default entities."); + return false; + } + + CImageInfo ImgInfo; + if(!pEditor->Graphics()->LoadPng(ImgInfo, pFileName, StorageType)) + { + pEditor->ShowFileDialogError("Failed to load image from file '%s'.", pFileName); + return false; + } + + pEditor->m_SelectEntitiesImage = aBuf; + pEditor->m_AllowPlaceUnusedTiles = -1; + pEditor->m_PreventUnusedTilesWasWarned = false; + + pEditor->Graphics()->UnloadTexture(&pEditor->m_EntitiesTexture); + pEditor->m_EntitiesTexture = pEditor->Graphics()->LoadTextureRawMove(ImgInfo, pEditor->GetTextureUsageFlag()); + + pEditor->m_Dialog = DIALOG_NONE; + return true; +} + +void CEditor::DoAudioPreview(CUIRect View, const void *pPlayPauseButtonId, const void *pStopButtonId, const void *pSeekBarId, int SampleId) { CUIRect Button, SeekBar; // play/pause button { View.VSplitLeft(View.h, &Button, &View); - if(DoButton_FontIcon(pPlayPauseButtonID, Sound()->IsPlaying(SampleID) ? FONT_ICON_PAUSE : FONT_ICON_PLAY, 0, &Button, 0, "Play/pause audio preview", IGraphics::CORNER_ALL) || + if(DoButton_FontIcon(pPlayPauseButtonId, Sound()->IsPlaying(SampleId) ? FONT_ICON_PAUSE : FONT_ICON_PLAY, 0, &Button, 0, "Play/pause audio preview", IGraphics::CORNER_ALL) || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_SPACE))) { - if(Sound()->IsPlaying(SampleID)) + if(Sound()->IsPlaying(SampleId)) { - Sound()->Pause(SampleID); + Sound()->Pause(SampleId); } else { - if(SampleID != m_ToolbarPreviewSound && m_ToolbarPreviewSound && Sound()->IsPlaying(m_ToolbarPreviewSound)) + if(SampleId != m_ToolbarPreviewSound && m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(m_ToolbarPreviewSound)) Sound()->Pause(m_ToolbarPreviewSound); - Sound()->Play(CSounds::CHN_GUI, SampleID, ISound::FLAG_PREVIEW); + Sound()->Play(CSounds::CHN_GUI, SampleId, ISound::FLAG_PREVIEW, 1.0f); } } } @@ -976,9 +995,9 @@ void CEditor::DoAudioPreview(CUIRect View, const void *pPlayPauseButtonID, const { View.VSplitLeft(2.0f, nullptr, &View); View.VSplitLeft(View.h, &Button, &View); - if(DoButton_FontIcon(pStopButtonID, FONT_ICON_STOP, 0, &Button, 0, "Stop audio preview", IGraphics::CORNER_ALL)) + if(DoButton_FontIcon(pStopButtonId, FONT_ICON_STOP, 0, &Button, 0, "Stop audio preview", IGraphics::CORNER_ALL)) { - Sound()->Stop(SampleID); + Sound()->Stop(SampleId); } } // do seekbar @@ -991,8 +1010,8 @@ void CEditor::DoAudioPreview(CUIRect View, const void *pPlayPauseButtonID, const const float Rounding = 5.0f; char aBuffer[64]; - const float CurrentTime = Sound()->GetSampleCurrentTime(SampleID); - const float TotalTime = Sound()->GetSampleTotalTime(SampleID); + const float CurrentTime = Sound()->GetSampleCurrentTime(SampleId); + const float TotalTime = Sound()->GetSampleTotalTime(SampleId); // draw seek bar SeekBar.Draw(ColorRGBA(0, 0, 0, 0.5f), IGraphics::CORNER_ALL, Rounding); @@ -1009,31 +1028,31 @@ void CEditor::DoAudioPreview(CUIRect View, const void *pPlayPauseButtonID, const char aTotalTime[32]; str_time_float(TotalTime, TIME_HOURS, aTotalTime, sizeof(aTotalTime)); str_format(aBuffer, sizeof(aBuffer), "%s / %s", aCurrentTime, aTotalTime); - UI()->DoLabel(&SeekBar, aBuffer, SeekBar.h * 0.70f, TEXTALIGN_MC); + Ui()->DoLabel(&SeekBar, aBuffer, SeekBar.h * 0.70f, TEXTALIGN_MC); // do the logic - const bool Inside = UI()->MouseInside(&SeekBar); + const bool Inside = Ui()->MouseInside(&SeekBar); - if(UI()->CheckActiveItem(pSeekBarID)) + if(Ui()->CheckActiveItem(pSeekBarId)) { - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } else { - const float AmountSeek = clamp((UI()->MouseX() - SeekBar.x - Rounding) / (float)(SeekBar.w - 2 * Rounding), 0.0f, 1.0f); - Sound()->SetSampleCurrentTime(SampleID, AmountSeek); + const float AmountSeek = clamp((Ui()->MouseX() - SeekBar.x - Rounding) / (SeekBar.w - 2 * Rounding), 0.0f, 1.0f); + Sound()->SetSampleCurrentTime(SampleId, AmountSeek); } } - else if(UI()->HotItem() == pSeekBarID) + else if(Ui()->HotItem() == pSeekBarId) { - if(UI()->MouseButton(0)) - UI()->SetActiveItem(pSeekBarID); + if(Ui()->MouseButton(0)) + Ui()->SetActiveItem(pSeekBarId); } - if(Inside) - UI()->SetHotItem(pSeekBarID); + if(Inside && !Ui()->MouseButton(0)) + Ui()->SetHotItem(pSeekBarId); } } @@ -1084,24 +1103,32 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) TB_Top.VSplitLeft(5.0f, nullptr, &TB_Top); // animation button - TB_Top.VSplitLeft(40.0f, &Button, &TB_Top); - static int s_AnimateButton = 0; - if(DoButton_Editor(&s_AnimateButton, "Anim", m_Animate, &Button, 0, "[ctrl+m] Toggle animation") || + TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); + static char s_AnimateButton; + if(DoButton_FontIcon(&s_AnimateButton, FONT_ICON_CIRCLE_PLAY, m_Animate, &Button, 0, "[ctrl+m] Toggle animation", IGraphics::CORNER_L) || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_M) && ModPressed)) { m_AnimateStart = time_get(); m_Animate = !m_Animate; } + // animation settings button + TB_Top.VSplitLeft(14.0f, &Button, &TB_Top); + static char s_AnimateSettingsButton; + if(DoButton_FontIcon(&s_AnimateSettingsButton, FONT_ICON_CIRCLE_CHEVRON_DOWN, 0, &Button, 0, "Change animation settings.", IGraphics::CORNER_R, 8.0f)) + { + m_AnimateUpdatePopup = true; + static SPopupMenuId s_PopupAnimateSettingsId; + Ui()->DoPopupMenu(&s_PopupAnimateSettingsId, Button.x, Button.y + Button.h, 150.0f, 37.0f, this, PopupAnimateSettings); + } + TB_Top.VSplitLeft(5.0f, nullptr, &TB_Top); // proof button TB_Top.VSplitLeft(40.0f, &Button, &TB_Top); - static int s_ProofButton = 0; - if(DoButton_Ex(&s_ProofButton, "Proof", MapView()->ProofMode()->IsEnabled(), &Button, 0, "[ctrl+p] Toggles proof borders. These borders represent what a player maximum can see.", IGraphics::CORNER_L) || - (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_P) && ModPressed)) + if(DoButton_Ex(&m_QuickActionProof, m_QuickActionProof.Label(), m_QuickActionProof.Active(), &Button, 0, m_QuickActionProof.Description(), IGraphics::CORNER_L)) { - MapView()->ProofMode()->Toggle(); + m_QuickActionProof.Call(); } TB_Top.VSplitLeft(14.0f, &Button, &TB_Top); @@ -1109,7 +1136,7 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) if(DoButton_FontIcon(&s_ProofModeButton, FONT_ICON_CIRCLE_CHEVRON_DOWN, 0, &Button, 0, "Select proof mode.", IGraphics::CORNER_R, 8.0f)) { static SPopupMenuId s_PopupProofModeId; - UI()->DoPopupMenu(&s_PopupProofModeId, Button.x, Button.y + Button.h, 60.0f, 36.0f, this, PopupProofMode); + Ui()->DoPopupMenu(&s_PopupProofModeId, Button.x, Button.y + Button.h, 60.0f, 36.0f, this, PopupProofMode); } TB_Top.VSplitLeft(5.0f, nullptr, &TB_Top); @@ -1125,12 +1152,20 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) TB_Top.VSplitLeft(5.0f, nullptr, &TB_Top); // grid button - TB_Top.VSplitLeft(40.0f, &Button, &TB_Top); + TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); static int s_GridButton = 0; - if(DoButton_Editor(&s_GridButton, "Grid", MapView()->MapGrid()->IsEnabled(), &Button, 0, "[ctrl+g] Toggle Grid") || + if(DoButton_FontIcon(&s_GridButton, FONT_ICON_BORDER_ALL, m_QuickActionToggleGrid.Active(), &Button, 0, m_QuickActionToggleGrid.Description(), IGraphics::CORNER_L) || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_G) && ModPressed && !ShiftPressed)) { - MapView()->MapGrid()->Toggle(); + m_QuickActionToggleGrid.Call(); + } + + // grid settings button + TB_Top.VSplitLeft(14.0f, &Button, &TB_Top); + static char s_GridSettingsButton; + if(DoButton_FontIcon(&s_GridSettingsButton, FONT_ICON_CIRCLE_CHEVRON_DOWN, 0, &Button, 0, "Change grid settings.", IGraphics::CORNER_R, 8.0f)) + { + MapView()->MapGrid()->DoSettingsPopup(vec2(Button.x, Button.y + Button.h)); } TB_Top.VSplitLeft(5.0f, nullptr, &TB_Top); @@ -1138,23 +1173,23 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) // zoom group TB_Top.VSplitLeft(20.0f, &Button, &TB_Top); static int s_ZoomOutButton = 0; - if(DoButton_FontIcon(&s_ZoomOutButton, "-", 0, &Button, 0, "[NumPad-] Zoom out", IGraphics::CORNER_L)) + if(DoButton_FontIcon(&s_ZoomOutButton, FONT_ICON_MINUS, 0, &Button, 0, m_QuickActionZoomOut.Description(), IGraphics::CORNER_L)) { - MapView()->Zoom()->ChangeValue(50.0f); + m_QuickActionZoomOut.Call(); } TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); static int s_ZoomNormalButton = 0; - if(DoButton_FontIcon(&s_ZoomNormalButton, FONT_ICON_MAGNIFYING_GLASS, 0, &Button, 0, "[NumPad*] Zoom to normal and remove editor offset", IGraphics::CORNER_NONE)) + if(DoButton_FontIcon(&s_ZoomNormalButton, FONT_ICON_MAGNIFYING_GLASS, 0, &Button, 0, m_QuickActionResetZoom.Description(), IGraphics::CORNER_NONE)) { - MapView()->ResetZoom(); + m_QuickActionResetZoom.Call(); } TB_Top.VSplitLeft(20.0f, &Button, &TB_Top); static int s_ZoomInButton = 0; - if(DoButton_FontIcon(&s_ZoomInButton, "+", 0, &Button, 0, "[NumPad+] Zoom in", IGraphics::CORNER_R)) + if(DoButton_FontIcon(&s_ZoomInButton, FONT_ICON_PLUS, 0, &Button, 0, m_QuickActionZoomIn.Description(), IGraphics::CORNER_R)) { - MapView()->Zoom()->ChangeValue(-50.0f); + m_QuickActionZoomIn.Call(); } TB_Top.VSplitLeft(5.0f, nullptr, &TB_Top); @@ -1229,56 +1264,38 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) for(auto &pLayer : m_pBrush->m_vpLayers) pLayer->BrushRotate(s_RotationAmount / 360.0f * pi * 2); } - - TB_Top.VSplitLeft(5.0f, &Button, &TB_Top); } - // animation speed - if(m_Animate) + // Color pipette and palette { - TB_Top.VSplitLeft(20.0f, &Button, &TB_Top); - static int s_AnimSlowerButton = 0; - if(DoButton_FontIcon(&s_AnimSlowerButton, "-", 0, &Button, 0, "Decrease animation speed", IGraphics::CORNER_L)) - { - if(m_AnimateSpeed > 0.5f) - m_AnimateSpeed -= 0.5f; - } - - TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); - static int s_AnimNormalButton = 0; - if(DoButton_FontIcon(&s_AnimNormalButton, FONT_ICON_CIRCLE_PLAY, 0, &Button, 0, "Normal animation speed", 0)) - m_AnimateSpeed = 1.0f; + const float PipetteButtonWidth = 30.0f; + const float ColorPickerButtonWidth = 20.0f; + const float Spacing = 2.0f; + const size_t NumColorsShown = clamp(round_to_int((TB_Top.w - PipetteButtonWidth - 40.0f) / (ColorPickerButtonWidth + Spacing)), 1, std::size(m_aSavedColors)); - TB_Top.VSplitLeft(20.0f, &Button, &TB_Top); - static int s_AnimFasterButton = 0; - if(DoButton_FontIcon(&s_AnimFasterButton, "+", 0, &Button, 0, "Increase animation speed", IGraphics::CORNER_R)) - m_AnimateSpeed += 0.5f; + CUIRect ColorPalette; + TB_Top.VSplitRight(NumColorsShown * (ColorPickerButtonWidth + Spacing) + PipetteButtonWidth, &TB_Top, &ColorPalette); - TB_Top.VSplitLeft(5.0f, &Button, &TB_Top); - } - - // grid zoom - if(MapView()->MapGrid()->IsEnabled()) - { - TB_Top.VSplitLeft(20.0f, &Button, &TB_Top); - static int s_GridIncreaseButton = 0; - if(DoButton_FontIcon(&s_GridIncreaseButton, "-", 0, &Button, 0, "Decrease grid", IGraphics::CORNER_L)) + // Pipette button + static char s_PipetteButton; + ColorPalette.VSplitLeft(PipetteButtonWidth, &Button, &ColorPalette); + ColorPalette.VSplitLeft(Spacing, nullptr, &ColorPalette); + if(DoButton_FontIcon(&s_PipetteButton, FONT_ICON_EYE_DROPPER, m_QuickActionPipette.Active(), &Button, 0, m_QuickActionPipette.Description(), IGraphics::CORNER_ALL) || + (CLineInput::GetActiveInput() == nullptr && ModPressed && ShiftPressed && Input()->KeyPress(KEY_C))) { - MapView()->MapGrid()->DecreaseFactor(); + m_QuickActionPipette.Call(); } - TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); - static int s_GridNormalButton = 0; - if(DoButton_FontIcon(&s_GridNormalButton, FONT_ICON_BORDER_ALL, 0, &Button, 0, "Normal grid", IGraphics::CORNER_NONE)) - MapView()->MapGrid()->ResetFactor(); - - TB_Top.VSplitLeft(20.0f, &Button, &TB_Top); - static int s_GridDecreaseButton = 0; - if(DoButton_FontIcon(&s_GridDecreaseButton, "+", 0, &Button, 0, "Increase grid", IGraphics::CORNER_R)) + // Palette color pickers + for(size_t i = 0; i < NumColorsShown; ++i) { - MapView()->MapGrid()->IncreaseFactor(); + ColorPalette.VSplitLeft(ColorPickerButtonWidth, &Button, &ColorPalette); + ColorPalette.VSplitLeft(Spacing, nullptr, &ColorPalette); + const auto &&SetColor = [&](ColorRGBA NewColor) { + m_aSavedColors[i] = NewColor; + }; + DoColorPickerButton(&m_aSavedColors[i], &Button, m_aSavedColors[i], SetColor); } - TB_Top.VSplitLeft(5.0f, &Button, &TB_Top); } } @@ -1286,11 +1303,10 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) { // refocus button { - TB_Bottom.VSplitLeft(45.0f, &Button, &TB_Bottom); - static int s_RefocusButton = 0; + TB_Bottom.VSplitLeft(50.0f, &Button, &TB_Bottom); int FocusButtonChecked = MapView()->IsFocused() ? -1 : 1; - if(DoButton_Editor(&s_RefocusButton, "Refocus", FocusButtonChecked, &Button, 0, "[HOME] Restore map focus") || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_HOME))) - MapView()->Focus(); + if(DoButton_Editor(&m_QuickActionRefocus, m_QuickActionRefocus.Label(), FocusButtonChecked, &Button, 0, m_QuickActionRefocus.Description()) || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_HOME))) + m_QuickActionRefocus.Call(); TB_Bottom.VSplitLeft(5.0f, nullptr, &TB_Bottom); } @@ -1302,13 +1318,13 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) if(pS) { const char *pButtonName = nullptr; - CUI::FPopupMenuFunction pfnPopupFunc = nullptr; + CUi::FPopupMenuFunction pfnPopupFunc = nullptr; int Rows = 0; if(pS == m_Map.m_pSwitchLayer) { pButtonName = "Switch"; pfnPopupFunc = PopupSwitch; - Rows = 2; + Rows = 3; } else if(pS == m_Map.m_pSpeedupLayer) { @@ -1326,7 +1342,7 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) { pButtonName = "Tele"; pfnPopupFunc = PopupTele; - Rows = 1; + Rows = 3; } if(pButtonName != nullptr) @@ -1339,9 +1355,9 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) if(DoButton_Ex(&s_ModifierButton, pButtonName, 0, &Button, 0, s_aButtonTooltip, IGraphics::CORNER_ALL) || (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && ModPressed && Input()->KeyPress(KEY_T))) { static SPopupMenuId s_PopupModifierId; - if(!UI()->IsPopupOpen(&s_PopupModifierId)) + if(!Ui()->IsPopupOpen(&s_PopupModifierId)) { - UI()->DoPopupMenu(&s_PopupModifierId, Button.x, Button.y + Button.h, 120, 10.0f + Rows * 13.0f, this, pfnPopupFunc); + Ui()->DoPopupMenu(&s_PopupModifierId, Button.x, Button.y + Button.h, 120, 10.0f + Rows * 13.0f, this, pfnPopupFunc); } } TB_Bottom.VSplitLeft(5.0f, nullptr, &TB_Bottom); @@ -1356,39 +1372,23 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) { TB_Bottom.VSplitLeft(60.0f, &Button, &TB_Bottom); - bool Invoked = false; - static int s_AddItemButton = 0; - if(pLayer->m_Type == LAYERTYPE_QUADS) { - Invoked = DoButton_Editor(&s_AddItemButton, "Add Quad", 0, &Button, 0, "[ctrl+q] Add a new quad") || - (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_Q) && ModPressed); + if(DoButton_Editor(&m_QuickActionAddQuad, m_QuickActionAddQuad.Label(), 0, &Button, 0, m_QuickActionAddQuad.Description()) || + (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_Q) && ModPressed)) + { + m_QuickActionAddQuad.Call(); + } } else if(pLayer->m_Type == LAYERTYPE_SOUNDS) { - Invoked = DoButton_Editor(&s_AddItemButton, "Add Sound", 0, &Button, 0, "[ctrl+q] Add a new sound source") || - (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_Q) && ModPressed); - } - - if(Invoked) - { - std::shared_ptr pGroup = GetSelectedGroup(); - - float aMapping[4]; - pGroup->Mapping(aMapping); - int x = aMapping[0] + (aMapping[2] - aMapping[0]) / 2; - int y = aMapping[1] + (aMapping[3] - aMapping[1]) / 2; - if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_Q) && ModPressed) + if(DoButton_Editor(&m_QuickActionAddSound, m_QuickActionAddSound.Label(), 0, &Button, 0, m_QuickActionAddSound.Description()) || + (m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_Q) && ModPressed)) { - x += UI()->MouseWorldX() - (MapView()->GetWorldOffset().x * pGroup->m_ParallaxX / 100) - pGroup->m_OffsetX; - y += UI()->MouseWorldY() - (MapView()->GetWorldOffset().y * pGroup->m_ParallaxY / 100) - pGroup->m_OffsetY; + m_QuickActionAddSound.Call(); } - - if(pLayer->m_Type == LAYERTYPE_QUADS) - m_EditorHistory.Execute(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0], x, y)); - else if(pLayer->m_Type == LAYERTYPE_SOUNDS) - m_EditorHistory.Execute(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0], x, y)); } + TB_Bottom.VSplitLeft(5.0f, &Button, &TB_Bottom); } @@ -1412,15 +1412,20 @@ void CEditor::DoToolbarSounds(CUIRect ToolBar) if(m_SelectedSound >= 0 && (size_t)m_SelectedSound < m_Map.m_vpSounds.size()) { const std::shared_ptr pSelectedSound = m_Map.m_vpSounds[m_SelectedSound]; - if(pSelectedSound->m_SoundID != m_ToolbarPreviewSound && m_ToolbarPreviewSound && Sound()->IsPlaying(m_ToolbarPreviewSound)) + if(pSelectedSound->m_SoundId != m_ToolbarPreviewSound && m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(m_ToolbarPreviewSound)) Sound()->Stop(m_ToolbarPreviewSound); - m_ToolbarPreviewSound = pSelectedSound->m_SoundID; + m_ToolbarPreviewSound = pSelectedSound->m_SoundId; + } + else + { + m_ToolbarPreviewSound = -1; + } + if(m_ToolbarPreviewSound >= 0) + { static int s_PlayPauseButton, s_StopButton, s_SeekBar = 0; DoAudioPreview(ToolBarBottom, &s_PlayPauseButton, &s_StopButton, &s_SeekBar, m_ToolbarPreviewSound); } - else - m_ToolbarPreviewSound = -1; } static void Rotate(const CPoint *pCenter, CPoint *pPoint, float Rotation) @@ -1431,37 +1436,42 @@ static void Rotate(const CPoint *pCenter, CPoint *pPoint, float Rotation) pPoint->y = (int)(x * std::sin(Rotation) + y * std::cos(Rotation) + pCenter->y); } -void CEditor::DoSoundSource(CSoundSource *pSource, int Index) +void CEditor::DoSoundSource(int LayerIndex, CSoundSource *pSource, int Index) { - enum - { - OP_NONE = 0, - OP_MOVE, - OP_CONTEXT_MENU, - }; + void *pId = &pSource->m_Position; - void *pID = &pSource->m_Position; + static ESoundSourceOp s_Operation = ESoundSourceOp::OP_NONE; - static int s_Operation = OP_NONE; - - float wx = UI()->MouseWorldX(); - float wy = UI()->MouseWorldY(); + float wx = Ui()->MouseWorldX(); + float wy = Ui()->MouseWorldY(); float CenterX = fx2f(pSource->m_Position.x); float CenterY = fx2f(pSource->m_Position.y); - float dx = (CenterX - wx) / m_MouseWScale; - float dy = (CenterY - wy) / m_MouseWScale; + float dx = (CenterX - wx) / m_MouseWorldScale; + float dy = (CenterY - wy) / m_MouseWorldScale; if(dx * dx + dy * dy < 50) - UI()->SetHotItem(pID); + Ui()->SetHotItem(pId); const bool IgnoreGrid = Input()->AltIsPressed(); + static CSoundSourceOperationTracker s_Tracker(this); + + if(s_Operation == ESoundSourceOp::OP_NONE) + { + if(!Ui()->MouseButton(0)) + s_Tracker.End(); + } - if(UI()->CheckActiveItem(pID)) + if(Ui()->CheckActiveItem(pId)) { - if(m_MouseDeltaWx * m_MouseDeltaWx + m_MouseDeltaWy * m_MouseDeltaWy > 0.0f) + if(s_Operation != ESoundSourceOp::OP_NONE) + { + s_Tracker.Begin(pSource, s_Operation, LayerIndex); + } + + if(m_MouseDeltaWorld != vec2(0.0f, 0.0f)) { - if(s_Operation == OP_MOVE) + if(s_Operation == ESoundSourceOp::OP_MOVE) { float x = wx; float y = wy; @@ -1472,52 +1482,52 @@ void CEditor::DoSoundSource(CSoundSource *pSource, int Index) } } - if(s_Operation == OP_CONTEXT_MENU) + if(s_Operation == ESoundSourceOp::OP_CONTEXT_MENU) { - if(!UI()->MouseButton(1)) + if(!Ui()->MouseButton(1)) { if(m_vSelectedLayers.size() == 1) { static SPopupMenuId s_PopupSourceId; - UI()->DoPopupMenu(&s_PopupSourceId, UI()->MouseX(), UI()->MouseY(), 120, 200, this, PopupSource); - UI()->DisableMouseLock(); + Ui()->DoPopupMenu(&s_PopupSourceId, Ui()->MouseX(), Ui()->MouseY(), 120, 200, this, PopupSource); + Ui()->DisableMouseLock(); } - s_Operation = OP_NONE; - UI()->SetActiveItem(nullptr); + s_Operation = ESoundSourceOp::OP_NONE; + Ui()->SetActiveItem(nullptr); } } else { - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { - UI()->DisableMouseLock(); - s_Operation = OP_NONE; - UI()->SetActiveItem(nullptr); + Ui()->DisableMouseLock(); + s_Operation = ESoundSourceOp::OP_NONE; + Ui()->SetActiveItem(nullptr); } } Graphics()->SetColor(1, 1, 1, 1); } - else if(UI()->HotItem() == pID) + else if(Ui()->HotItem() == pId) { - ms_pUiGotContext = pID; + ms_pUiGotContext = pId; Graphics()->SetColor(1, 1, 1, 1); str_copy(m_aTooltip, "Left mouse button to move. Hold alt to ignore grid."); - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { - s_Operation = OP_MOVE; + s_Operation = ESoundSourceOp::OP_MOVE; - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); m_SelectedSource = Index; } - if(UI()->MouseButton(1)) + if(Ui()->MouseButton(1)) { m_SelectedSource = Index; - s_Operation = OP_CONTEXT_MENU; - UI()->SetActiveItem(pID); + s_Operation = ESoundSourceOp::OP_CONTEXT_MENU; + Ui()->SetActiveItem(pId); } } else @@ -1525,7 +1535,7 @@ void CEditor::DoSoundSource(CSoundSource *pSource, int Index) Graphics()->SetColor(0, 1, 0, 1); } - IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale); + IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale); Graphics()->QuadsDraw(&QuadItem, 1); } @@ -1540,7 +1550,7 @@ void CEditor::DoPointDrag(const std::shared_ptr &pLayer, CQuad *pQu pQuad->m_aPoints[PointIndex].y = m_QuadDragOriginalPoints[QuadIndex][PointIndex].y + OffsetY; } -CEditor::EAxis CEditor::GetDragAxis(int OffsetX, int OffsetY) +CEditor::EAxis CEditor::GetDragAxis(int OffsetX, int OffsetY) const { if(Input()->ShiftIsPressed()) if(absolute(OffsetX) < absolute(OffsetY)) @@ -1551,7 +1561,7 @@ CEditor::EAxis CEditor::GetDragAxis(int OffsetX, int OffsetY) return EAxis::AXIS_NONE; } -void CEditor::DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point) +void CEditor::DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point) const { if(Axis == EAxis::AXIS_NONE) return; @@ -1559,17 +1569,17 @@ void CEditor::DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point) Graphics()->SetColor(1, 0, 0.1f, 1); if(Axis == EAxis::AXIS_X) { - IGraphics::CQuadItem Line(fx2f(OriginalPoint.x + Point.x) / 2.0f, fx2f(OriginalPoint.y), fx2f(Point.x - OriginalPoint.x), 1.0f * m_MouseWScale); + IGraphics::CQuadItem Line(fx2f(OriginalPoint.x + Point.x) / 2.0f, fx2f(OriginalPoint.y), fx2f(Point.x - OriginalPoint.x), 1.0f * m_MouseWorldScale); Graphics()->QuadsDraw(&Line, 1); } else if(Axis == EAxis::AXIS_Y) { - IGraphics::CQuadItem Line(fx2f(OriginalPoint.x), fx2f(OriginalPoint.y + Point.y) / 2.0f, 1.0f * m_MouseWScale, fx2f(Point.y - OriginalPoint.y)); + IGraphics::CQuadItem Line(fx2f(OriginalPoint.x), fx2f(OriginalPoint.y + Point.y) / 2.0f, 1.0f * m_MouseWorldScale, fx2f(Point.y - OriginalPoint.y)); Graphics()->QuadsDraw(&Line, 1); } // Draw ghost of original point - IGraphics::CQuadItem QuadItem(fx2f(OriginalPoint.x), fx2f(OriginalPoint.y), 5.0f * m_MouseWScale, 5.0f * m_MouseWScale); + IGraphics::CQuadItem QuadItem(fx2f(OriginalPoint.x), fx2f(OriginalPoint.y), 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale); Graphics()->QuadsDraw(&QuadItem, 1); } @@ -1580,8 +1590,10 @@ void CEditor::ComputePointAlignments(const std::shared_ptr &pLayer, if(!g_Config.m_EdAlignQuads) return; + bool GridEnabled = MapView()->MapGrid()->IsEnabled() && !Input()->AltIsPressed(); + // Perform computation from the original position of this point - int Threshold = f2fx(maximum(10.0f, 10.0f * m_MouseWScale)); + int Threshold = f2fx(maximum(5.0f, 10.0f * m_MouseWorldScale)); CPoint OrigPoint = m_QuadDragOriginalPoints.at(QuadIndex)[PointIndex]; // Get the "current" point by applying the offset CPoint Point = OrigPoint + ivec2(OffsetX, OffsetY); @@ -1598,8 +1610,7 @@ void CEditor::ComputePointAlignments(const std::shared_ptr &pLayer, int DiffX = absolute(DX); int DiffY = absolute(DY); - // Check the X axis - if(DiffX <= Threshold) + if(DiffX <= Threshold && (!GridEnabled || DiffX == 0)) { // Only store alignments that have the smallest difference if(DiffX < SmallestDiffX) @@ -1620,7 +1631,8 @@ void CEditor::ComputePointAlignments(const std::shared_ptr &pLayer, }); } } - if(DiffY <= Threshold) + + if(DiffY <= Threshold && (!GridEnabled || DiffY == 0)) { // Only store alignments that have the smallest difference if(DiffY < SmallestDiffY) @@ -1680,8 +1692,12 @@ void CEditor::ComputePointAlignments(const std::shared_ptr &pLayer, CheckAlignment(pQuadPoint); } - CPoint Center = (Min + Max) / 2.0f; - CheckAlignment(&Center); + // Don't check alignment with center of selected quads + if(!IsQuadSelected(i)) + { + CPoint Center = (Min + Max) / 2.0f; + CheckAlignment(&Center); + } } // Finally concatenate both alignment vectors into the output @@ -1762,10 +1778,12 @@ void CEditor::ComputeAABBAlignments(const std::shared_ptr &pLayer, // This method is a bit different than the point alignment in the way where instead of trying to aling 1 point to all quads, // we try to align 5 points to all quads, these 5 points being 5 points of an AABB. // Otherwise, the concept is the same, we use the original position of the AABB to make the computations. - int Threshold = f2fx(maximum(10.0f, 10.0f * m_MouseWScale)); + int Threshold = f2fx(maximum(5.0f, 10.0f * m_MouseWorldScale)); int SmallestDiffX = Threshold + 1, SmallestDiffY = Threshold + 1; std::vector vAlignmentsX, vAlignmentsY; + bool GridEnabled = MapView()->MapGrid()->IsEnabled() && !Input()->AltIsPressed(); + auto &&CheckAlignment = [&](CPoint &Aligned, int Point) { CPoint ToCheck = AABB.m_aPoints[Point] + ivec2(OffsetX, OffsetY); int DX = Aligned.x - ToCheck.x; @@ -1773,7 +1791,7 @@ void CEditor::ComputeAABBAlignments(const std::shared_ptr &pLayer, int DiffX = absolute(DX); int DiffY = absolute(DY); - if(DiffX <= Threshold) + if(DiffX <= Threshold && (!GridEnabled || DiffX == 0)) { if(DiffX < SmallestDiffX) { @@ -1792,7 +1810,8 @@ void CEditor::ComputeAABBAlignments(const std::shared_ptr &pLayer, }); } } - if(DiffY <= Threshold) + + if(DiffY <= Threshold && (!GridEnabled || DiffY == 0)) { if(DiffY < SmallestDiffY) { @@ -1862,7 +1881,7 @@ void CEditor::ComputeAABBAlignments(const std::shared_ptr &pLayer, vAlignments.insert(vAlignments.end(), vAlignmentsY.begin(), vAlignmentsY.end()); } -void CEditor::DrawPointAlignments(const std::vector &vAlignments, int OffsetX, int OffsetY) +void CEditor::DrawPointAlignments(const std::vector &vAlignments, int OffsetX, int OffsetY) const { if(!g_Config.m_EdAlignQuads) return; @@ -1875,18 +1894,18 @@ void CEditor::DrawPointAlignments(const std::vector &vAlignments // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine. if(Alignment.m_Axis == EAxis::AXIS_X) { // Alignment on X axis is same Y values but different X values - IGraphics::CQuadItem Line(fx2f(Alignment.m_AlignedPoint.x), fx2f(Alignment.m_AlignedPoint.y), fx2f(Alignment.m_X + OffsetX - Alignment.m_AlignedPoint.x), 1.0f * m_MouseWScale); + IGraphics::CQuadItem Line(fx2f(Alignment.m_AlignedPoint.x), fx2f(Alignment.m_AlignedPoint.y), fx2f(Alignment.m_X + OffsetX - Alignment.m_AlignedPoint.x), 1.0f * m_MouseWorldScale); Graphics()->QuadsDrawTL(&Line, 1); } else if(Alignment.m_Axis == EAxis::AXIS_Y) { // Alignment on Y axis is same X values but different Y values - IGraphics::CQuadItem Line(fx2f(Alignment.m_AlignedPoint.x), fx2f(Alignment.m_AlignedPoint.y), 1.0f * m_MouseWScale, fx2f(Alignment.m_Y + OffsetY - Alignment.m_AlignedPoint.y)); + IGraphics::CQuadItem Line(fx2f(Alignment.m_AlignedPoint.x), fx2f(Alignment.m_AlignedPoint.y), 1.0f * m_MouseWorldScale, fx2f(Alignment.m_Y + OffsetY - Alignment.m_AlignedPoint.y)); Graphics()->QuadsDrawTL(&Line, 1); } } } -void CEditor::DrawAABB(const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY) +void CEditor::DrawAABB(const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY) const { // Drawing an AABB is simply converting the points from fixed to float // Then making lines out of quads and drawing them @@ -1898,15 +1917,15 @@ void CEditor::DrawAABB(const SAxisAlignedBoundingBox &AABB, int OffsetX, int Off // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine. IGraphics::CQuadItem Lines[4] = { - {TL.x, TL.y, TR.x - TL.x, 1.0f * m_MouseWScale}, - {TL.x, TL.y, 1.0f * m_MouseWScale, BL.y - TL.y}, - {TR.x, TR.y, 1.0f * m_MouseWScale, BR.y - TR.y}, - {BL.x, BL.y, BR.x - BL.x, 1.0f * m_MouseWScale}, + {TL.x, TL.y, TR.x - TL.x, 1.0f * m_MouseWorldScale}, + {TL.x, TL.y, 1.0f * m_MouseWorldScale, BL.y - TL.y}, + {TR.x, TR.y, 1.0f * m_MouseWorldScale, BR.y - TR.y}, + {BL.x, BL.y, BR.x - BL.x, 1.0f * m_MouseWorldScale}, }; Graphics()->SetColor(1, 0, 1, 1); Graphics()->QuadsDrawTL(Lines, 4); - IGraphics::CQuadItem CenterQuad(Center.x, Center.y, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale); + IGraphics::CQuadItem CenterQuad(Center.x, Center.y, 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale); Graphics()->QuadsDraw(&CenterQuad, 1); } @@ -1924,12 +1943,13 @@ void CEditor::QuadSelectionAABB(const std::shared_ptr &pLayer, SAxi for(int Selected : m_vSelectedQuads) { CQuad *pQuad = &pLayer->m_vQuads[Selected]; - for(auto &Point : pQuad->m_aPoints) + for(int i = 0; i < 4; i++) { - Min.x = minimum(Min.x, Point.x); - Min.y = minimum(Min.y, Point.y); - Max.x = maximum(Max.x, Point.x); - Max.y = maximum(Max.y, Point.y); + auto *pPoint = &pQuad->m_aPoints[i]; + Min.x = minimum(Min.x, pPoint->x); + Min.y = minimum(Min.y, pPoint->y); + Max.x = maximum(Max.x, pPoint->x); + Max.y = maximum(Max.y, pPoint->y); } } CPoint Center = (Min + Max) / 2.0f; @@ -1975,7 +1995,7 @@ void CEditor::ApplyAlignments(const std::vector &vAlignments, in OffsetY += AdjustY; } -void CEditor::ApplyAxisAlignment(int &OffsetX, int &OffsetY) +void CEditor::ApplyAxisAlignment(int &OffsetX, int &OffsetY) const { // This is used to preserve axis alignment when pressing `Shift` // Should be called before any other computation @@ -1984,7 +2004,7 @@ void CEditor::ApplyAxisAlignment(int &OffsetX, int &OffsetY) OffsetY = ((Axis == EAxis::AXIS_NONE || Axis == EAxis::AXIS_Y) ? OffsetY : 0); } -void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, int Index) +void CEditor::DoQuad(int LayerIndex, const std::shared_ptr &pLayer, CQuad *pQuad, int Index) { enum { @@ -1998,14 +2018,14 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i }; // some basic values - void *pID = &pQuad->m_aPoints[4]; // use pivot addr as id + void *pId = &pQuad->m_aPoints[4]; // use pivot addr as id static std::vector> s_vvRotatePoints; static int s_Operation = OP_NONE; static float s_MouseXStart = 0.0f; static float s_MouseYStart = 0.0f; static float s_RotateAngle = 0; - float wx = UI()->MouseWorldX(); - float wy = UI()->MouseWorldY(); + float wx = Ui()->MouseWorldX(); + float wy = Ui()->MouseWorldY(); static CPoint s_OriginalPosition; static std::vector s_PivotAlignments; // Alignments per pivot per quad static std::vector s_vAABBAlignments; // Alignments for one AABB (single quad or selection of multiple quads) @@ -2034,18 +2054,18 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i if(IsQuadSelected(Index)) { Graphics()->SetColor(0, 0, 0, 1); - IGraphics::CQuadItem QuadItem(CenterX, CenterY, 7.0f * m_MouseWScale, 7.0f * m_MouseWScale); + IGraphics::CQuadItem QuadItem(CenterX, CenterY, 7.0f * m_MouseWorldScale, 7.0f * m_MouseWorldScale); Graphics()->QuadsDraw(&QuadItem, 1); } - if(UI()->CheckActiveItem(pID)) + if(Ui()->CheckActiveItem(pId)) { - if(m_MouseDeltaWx * m_MouseDeltaWx + m_MouseDeltaWy * m_MouseDeltaWy > 0.0f) + if(m_MouseDeltaWorld != vec2(0.0f, 0.0f)) { if(s_Operation == OP_SELECT) { - float x = s_MouseXStart - UI()->MouseX(); - float y = s_MouseYStart - UI()->MouseY(); + float x = s_MouseXStart - Ui()->MouseX(); + float y = s_MouseYStart - Ui()->MouseY(); if(x * x + y * y > 20.0f) { @@ -2084,7 +2104,7 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i // check if we only should move pivot if(s_Operation == OP_MOVE_PIVOT) { - m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads, -1, LayerIndex); s_LastOffset = GetDragOffset(); // Update offset ApplyAxisAlignment(s_LastOffset.x, s_LastOffset.y); // Apply axis alignment to the offset @@ -2100,7 +2120,7 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i } else if(s_Operation == OP_MOVE_ALL) { - m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads, -1, LayerIndex); // Compute drag offset s_LastOffset = GetDragOffset(); @@ -2120,7 +2140,7 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i } else if(s_Operation == OP_ROTATE) { - m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads, -1, LayerIndex); for(size_t i = 0; i < m_vSelectedQuads.size(); ++i) { @@ -2132,10 +2152,7 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i } } - if(Input()->ShiftIsPressed()) - s_RotateAngle += (m_MouseDeltaX)*0.0001f; - else - s_RotateAngle += (m_MouseDeltaX)*0.002f; + s_RotateAngle += Ui()->MouseDeltaX() * (Input()->ShiftIsPressed() ? 0.0001f : 0.002f); } } @@ -2144,6 +2161,8 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i { EAxis Axis = GetDragAxis(s_LastOffset.x, s_LastOffset.y); DrawAxis(Axis, s_OriginalPosition, pQuad->m_aPoints[4]); + + str_copy(m_aTooltip, "Hold shift to keep alignment on one axis."); } if(s_Operation == OP_MOVE_PIVOT) @@ -2159,48 +2178,48 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i if(s_Operation == OP_CONTEXT_MENU) { - if(!UI()->MouseButton(1)) + if(!Ui()->MouseButton(1)) { if(m_vSelectedLayers.size() == 1) { m_SelectedQuadIndex = FindSelectedQuadIndex(Index); static SPopupMenuId s_PopupQuadId; - UI()->DoPopupMenu(&s_PopupQuadId, UI()->MouseX(), UI()->MouseY(), 120, 198, this, PopupQuad); - UI()->DisableMouseLock(); + Ui()->DoPopupMenu(&s_PopupQuadId, Ui()->MouseX(), Ui()->MouseY(), 120, 222, this, PopupQuad); + Ui()->DisableMouseLock(); } s_Operation = OP_NONE; - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } } else if(s_Operation == OP_DELETE) { - if(!UI()->MouseButton(1)) + if(!Ui()->MouseButton(1)) { if(m_vSelectedLayers.size() == 1) { - UI()->DisableMouseLock(); + Ui()->DisableMouseLock(); m_Map.OnModify(); DeleteSelectedQuads(); } s_Operation = OP_NONE; - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } } else if(s_Operation == OP_ROTATE) { - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { - UI()->DisableMouseLock(); + Ui()->DisableMouseLock(); s_Operation = OP_NONE; - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); m_QuadTracker.EndQuadTrack(); } - else if(UI()->MouseButton(1)) + else if(Ui()->MouseButton(1)) { - UI()->DisableMouseLock(); + Ui()->DisableMouseLock(); s_Operation = OP_NONE; - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); // Reset points to old position for(size_t i = 0; i < m_vSelectedQuads.size(); ++i) @@ -2213,7 +2232,7 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i } else { - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { if(s_Operation == OP_SELECT) { @@ -2227,9 +2246,9 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i m_QuadTracker.EndQuadTrack(); } - UI()->DisableMouseLock(); + Ui()->DisableMouseLock(); s_Operation = OP_NONE; - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); s_LastOffset = ivec2(); s_OriginalPosition = ivec2(); @@ -2242,8 +2261,8 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i } else if(Input()->KeyPress(KEY_R) && !m_vSelectedQuads.empty() && m_Dialog == DIALOG_NONE) { - UI()->EnableMouseLock(pID); - UI()->SetActiveItem(pID); + Ui()->EnableMouseLock(pId); + Ui()->SetActiveItem(pId); s_Operation = OP_ROTATE; s_RotateAngle = 0; @@ -2260,23 +2279,23 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i s_vvRotatePoints[i][3] = pCurrentQuad->m_aPoints[3]; } } - else if(UI()->HotItem() == pID) + else if(Ui()->HotItem() == pId) { - ms_pUiGotContext = pID; + ms_pUiGotContext = pId; Graphics()->SetColor(1, 1, 1, 1); str_copy(m_aTooltip, "Left mouse button to move. Hold shift to move pivot. Hold alt to ignore grid. Hold shift and right click to delete."); - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); - s_MouseXStart = UI()->MouseX(); - s_MouseYStart = UI()->MouseY(); + s_MouseXStart = Ui()->MouseX(); + s_MouseYStart = Ui()->MouseY(); s_Operation = OP_SELECT; } - else if(UI()->MouseButton(1)) + else if(Ui()->MouseButtonClicked(1)) { if(Input()->ShiftIsPressed()) { @@ -2285,7 +2304,7 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i if(!IsQuadSelected(Index)) SelectQuad(Index); - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); } else { @@ -2294,23 +2313,23 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i if(!IsQuadSelected(Index)) SelectQuad(Index); - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); } } } else Graphics()->SetColor(0, 1, 0, 1); - IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale); + IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale); Graphics()->QuadsDraw(&QuadItem, 1); } -void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int V) +void CEditor::DoQuadPoint(int LayerIndex, const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int V) { - void *pID = &pQuad->m_aPoints[V]; + void *pId = &pQuad->m_aPoints[V]; - float wx = UI()->MouseWorldX(); - float wy = UI()->MouseWorldY(); + float wx = Ui()->MouseWorldX(); + float wy = Ui()->MouseWorldY(); float px = fx2f(pQuad->m_aPoints[V].x); float py = fx2f(pQuad->m_aPoints[V].y); @@ -2319,7 +2338,7 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu if(IsQuadPointSelected(QuadIndex, V)) { Graphics()->SetColor(0, 0, 0, 1); - IGraphics::CQuadItem QuadItem(px, py, 7.0f * m_MouseWScale, 7.0f * m_MouseWScale); + IGraphics::CQuadItem QuadItem(px, py, 7.0f * m_MouseWorldScale, 7.0f * m_MouseWorldScale); Graphics()->QuadsDraw(&QuadItem, 1); } @@ -2353,14 +2372,14 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu return {OffsetX, OffsetY}; }; - if(UI()->CheckActiveItem(pID)) + if(Ui()->CheckActiveItem(pId)) { - if(m_MouseDeltaWx * m_MouseDeltaWx + m_MouseDeltaWy * m_MouseDeltaWy > 0.0f) + if(m_MouseDeltaWorld != vec2(0.0f, 0.0f)) { if(s_Operation == OP_SELECT) { - float x = s_MouseXStart - UI()->MouseX(); - float y = s_MouseYStart - UI()->MouseY(); + float x = s_MouseXStart - Ui()->MouseX(); + float y = s_MouseYStart - Ui()->MouseY(); if(x * x + y * y > 20.0f) { @@ -2370,23 +2389,26 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu if(Input()->ShiftIsPressed()) { s_Operation = OP_MOVEUV; - UI()->EnableMouseLock(pID); + Ui()->EnableMouseLock(pId); } else { s_Operation = OP_MOVEPOINT; // Save original positions before moving s_OriginalPoint = pQuad->m_aPoints[V]; - for(int m = 0; m < 4; m++) - if(IsQuadPointSelected(QuadIndex, m)) - PreparePointDrag(pLayer, pQuad, QuadIndex, m); + for(int Selected : m_vSelectedQuads) + { + for(int m = 0; m < 4; m++) + if(IsQuadPointSelected(Selected, m)) + PreparePointDrag(pLayer, &pLayer->m_vQuads[Selected], Selected, m); + } } } } if(s_Operation == OP_MOVEPOINT) { - m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads, -1, LayerIndex); s_LastOffset = GetDragOffset(); // Update offset ApplyAxisAlignment(s_LastOffset.x, s_LastOffset.y); // Apply axis alignment to offset @@ -2394,11 +2416,14 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu ComputePointsAlignments(pLayer, false, s_LastOffset.x, s_LastOffset.y, s_Alignments); ApplyAlignments(s_Alignments, s_LastOffset.x, s_LastOffset.y); - for(int m = 0; m < 4; m++) + for(int Selected : m_vSelectedQuads) { - if(IsQuadPointSelected(QuadIndex, m)) + for(int m = 0; m < 4; m++) { - DoPointDrag(pLayer, pQuad, QuadIndex, m, s_LastOffset.x, s_LastOffset.y); + if(IsQuadPointSelected(Selected, m)) + { + DoPointDrag(pLayer, &pLayer->m_vQuads[Selected], Selected, m, s_LastOffset.x, s_LastOffset.y); + } } } } @@ -2406,22 +2431,26 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu { int SelectedPoints = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3); - m_QuadTracker.BeginQuadPointPropTrack(pLayer, m_vSelectedQuads, SelectedPoints); + m_QuadTracker.BeginQuadPointPropTrack(pLayer, m_vSelectedQuads, SelectedPoints, -1, LayerIndex); m_QuadTracker.AddQuadPointPropTrack(EQuadPointProp::PROP_TEX_U); m_QuadTracker.AddQuadPointPropTrack(EQuadPointProp::PROP_TEX_V); - for(int m = 0; m < 4; m++) + for(int Selected : m_vSelectedQuads) { - if(IsQuadPointSelected(QuadIndex, m)) + CQuad *pSelectedQuad = &pLayer->m_vQuads[Selected]; + for(int m = 0; m < 4; m++) { - // 0,2;1,3 - line x - // 0,1;2,3 - line y + if(IsQuadPointSelected(Selected, m)) + { + // 0,2;1,3 - line x + // 0,1;2,3 - line y - pQuad->m_aTexcoords[m].x += f2fx(m_MouseDeltaWx * 0.001f); - pQuad->m_aTexcoords[(m + 2) % 4].x += f2fx(m_MouseDeltaWx * 0.001f); + pSelectedQuad->m_aTexcoords[m].x += f2fx(m_MouseDeltaWorld.x * 0.001f); + pSelectedQuad->m_aTexcoords[(m + 2) % 4].x += f2fx(m_MouseDeltaWorld.x * 0.001f); - pQuad->m_aTexcoords[m].y += f2fx(m_MouseDeltaWy * 0.001f); - pQuad->m_aTexcoords[m ^ 1].y += f2fx(m_MouseDeltaWy * 0.001f); + pSelectedQuad->m_aTexcoords[m].y += f2fx(m_MouseDeltaWorld.y * 0.001f); + pSelectedQuad->m_aTexcoords[m ^ 1].y += f2fx(m_MouseDeltaWorld.y * 0.001f); + } } } } @@ -2438,11 +2467,13 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu // Alignments DrawPointAlignments(s_Alignments, s_LastOffset.x, s_LastOffset.y); + + str_copy(m_aTooltip, "Hold shift to keep alignment on one axis."); } if(s_Operation == OP_CONTEXT_MENU) { - if(!UI()->MouseButton(1)) + if(!Ui()->MouseButton(1)) { if(m_vSelectedLayers.size() == 1) { @@ -2453,14 +2484,14 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu m_SelectedQuadIndex = FindSelectedQuadIndex(QuadIndex); static SPopupMenuId s_PopupPointId; - UI()->DoPopupMenu(&s_PopupPointId, UI()->MouseX(), UI()->MouseY(), 120, 75, this, PopupPoint); + Ui()->DoPopupMenu(&s_PopupPointId, Ui()->MouseX(), Ui()->MouseY(), 120, 75, this, PopupPoint); } - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } } else { - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { if(s_Operation == OP_SELECT) { @@ -2479,34 +2510,34 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu m_QuadTracker.EndQuadPointPropTrackAll(); } - UI()->DisableMouseLock(); - UI()->SetActiveItem(nullptr); + Ui()->DisableMouseLock(); + Ui()->SetActiveItem(nullptr); } } Graphics()->SetColor(1, 1, 1, 1); } - else if(UI()->HotItem() == pID) + else if(Ui()->HotItem() == pId) { - ms_pUiGotContext = pID; + ms_pUiGotContext = pId; Graphics()->SetColor(1, 1, 1, 1); str_copy(m_aTooltip, "Left mouse button to move. Hold shift to move the texture. Hold alt to ignore grid."); - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); - s_MouseXStart = UI()->MouseX(); - s_MouseYStart = UI()->MouseY(); + s_MouseXStart = Ui()->MouseX(); + s_MouseYStart = Ui()->MouseY(); s_Operation = OP_SELECT; } - else if(UI()->MouseButton(1)) + else if(Ui()->MouseButton(1)) { s_Operation = OP_CONTEXT_MENU; - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); if(!IsQuadPointSelected(QuadIndex, V)) SelectQuadPoint(QuadIndex, V); @@ -2515,7 +2546,7 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu else Graphics()->SetColor(1, 0, 0, 1); - IGraphics::CQuadItem QuadItem(px, py, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale); + IGraphics::CQuadItem QuadItem(px, py, 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale); Graphics()->QuadsDraw(&QuadItem, 1); } @@ -2551,9 +2582,9 @@ void CEditor::DoQuadKnife(int QuadIndex) CQuad *pQuad = &pLayer->m_vQuads[QuadIndex]; const bool IgnoreGrid = Input()->AltIsPressed(); - float SnapRadius = 4.f * m_MouseWScale; + float SnapRadius = 4.f * m_MouseWorldScale; - vec2 Mouse = vec2(UI()->MouseWorldX(), UI()->MouseWorldY()); + vec2 Mouse = vec2(Ui()->MouseWorldX(), Ui()->MouseWorldY()); vec2 Point = Mouse; vec2 v[4] = { @@ -2564,7 +2595,7 @@ void CEditor::DoQuadKnife(int QuadIndex) str_copy(m_aTooltip, "Left click inside the quad to select an area to slice. Hold alt to ignore grid. Right click to leave knife mode"); - if(UI()->MouseButtonClicked(1)) + if(Ui()->MouseButtonClicked(1)) { m_QuadKnifeActive = false; return; @@ -2658,7 +2689,7 @@ void CEditor::DoQuadKnife(int QuadIndex) bool ValidPosition = IsInTriangle(Point, v[0], v[1], v[2]) || IsInTriangle(Point, v[0], v[3], v[2]); - if(UI()->MouseButtonClicked(0) && ValidPosition) + if(Ui()->MouseButtonClicked(0) && ValidPosition) { m_aQuadKnifePoints[m_QuadKnifeCount] = Point; m_QuadKnifeCount++; @@ -2754,14 +2785,14 @@ void CEditor::DoQuadKnife(int QuadIndex) IGraphics::CQuadItem aMarkers[4]; for(int i = 0; i < m_QuadKnifeCount; i++) - aMarkers[i] = IGraphics::CQuadItem(m_aQuadKnifePoints[i].x, m_aQuadKnifePoints[i].y, 5.f * m_MouseWScale, 5.f * m_MouseWScale); + aMarkers[i] = IGraphics::CQuadItem(m_aQuadKnifePoints[i].x, m_aQuadKnifePoints[i].y, 5.f * m_MouseWorldScale, 5.f * m_MouseWorldScale); Graphics()->SetColor(0.f, 0.f, 1.f, 1.f); Graphics()->QuadsDraw(aMarkers, m_QuadKnifeCount); if(ValidPosition) { - IGraphics::CQuadItem MarkerCurrent(Point.x, Point.y, 5.f * m_MouseWScale, 5.f * m_MouseWScale); + IGraphics::CQuadItem MarkerCurrent(Point.x, Point.y, 5.f * m_MouseWorldScale, 5.f * m_MouseWorldScale); Graphics()->QuadsDraw(&MarkerCurrent, 1); } @@ -2795,15 +2826,16 @@ void CEditor::DoQuadEnvelopes(const std::vector &vQuads, IGraphics::CText const CPoint *pPivotPoint = &vQuads[j].m_aPoints[4]; for(size_t i = 0; i < apEnvelope[j]->m_vPoints.size() - 1; i++) { - ColorRGBA Result; - apEnvelope[j]->Eval(apEnvelope[j]->m_vPoints[i].m_Time / 1000.0f + 0.000001f, Result); + ColorRGBA Result = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + apEnvelope[j]->Eval(apEnvelope[j]->m_vPoints[i].m_Time / 1000.0f + 0.000001f, Result, 2); vec2 Pos0 = vec2(fx2f(pPivotPoint->x) + Result.r, fx2f(pPivotPoint->y) + Result.g); const int Steps = 15; for(int n = 1; n <= Steps; n++) { const float Time = mix(apEnvelope[j]->m_vPoints[i].m_Time, apEnvelope[j]->m_vPoints[i + 1].m_Time, (float)n / Steps); - apEnvelope[j]->Eval(Time / 1000.0f - 0.000001f, Result); + Result = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + apEnvelope[j]->Eval(Time / 1000.0f - 0.000001f, Result, 2); vec2 Pos1 = vec2(fx2f(pPivotPoint->x) + Result.r, fx2f(pPivotPoint->y) + Result.g); @@ -2846,9 +2878,9 @@ void CEditor::DoQuadEnvelopes(const std::vector &vQuads, IGraphics::CText Graphics()->SetColorVertex(aArray, 4); // Rotation + CPoint aRotated[4]; if(Rot != 0) { - static CPoint aRotated[4]; aRotated[0] = vQuads[j].m_aPoints[0]; aRotated[1] = vQuads[j].m_aPoints[1]; aRotated[2] = vQuads[j].m_aPoints[2]; @@ -2907,10 +2939,10 @@ void CEditor::DoQuadEnvPoint(const CQuad *pQuad, int QIndex, int PIndex) static float s_LastWx = 0; static float s_LastWy = 0; static int s_Operation = OP_NONE; - float wx = UI()->MouseWorldX(); - float wy = UI()->MouseWorldY(); + float wx = Ui()->MouseWorldX(); + float wy = Ui()->MouseWorldY(); std::shared_ptr pEnvelope = m_Map.m_vpEnvelopes[pQuad->m_PosEnv]; - void *pID = &pEnvelope->m_vPoints[PIndex]; + void *pId = &pEnvelope->m_vPoints[PIndex]; // get pivot float CenterX = fx2f(pQuad->m_aPoints[4].x) + fx2f(pEnvelope->m_vPoints[PIndex].m_aValues[0]); @@ -2918,7 +2950,7 @@ void CEditor::DoQuadEnvPoint(const CQuad *pQuad, int QIndex, int PIndex) const bool IgnoreGrid = Input()->AltIsPressed(); - if(UI()->CheckActiveItem(pID) && m_CurrentQuadIndex == QIndex) + if(Ui()->CheckActiveItem(pId) && m_CurrentQuadIndex == QIndex) { if(s_Operation == OP_MOVE) { @@ -2937,32 +2969,32 @@ void CEditor::DoQuadEnvPoint(const CQuad *pQuad, int QIndex, int PIndex) } } else if(s_Operation == OP_ROTATE) - pEnvelope->m_vPoints[PIndex].m_aValues[2] += 10 * m_MouseDeltaX; + pEnvelope->m_vPoints[PIndex].m_aValues[2] += 10 * Ui()->MouseDeltaX(); s_LastWx = wx; s_LastWy = wy; - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { - UI()->DisableMouseLock(); + Ui()->DisableMouseLock(); s_Operation = OP_NONE; - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); } - else if(UI()->HotItem() == pID && m_CurrentQuadIndex == QIndex) + else if(Ui()->HotItem() == pId && m_CurrentQuadIndex == QIndex) { - ms_pUiGotContext = pID; + ms_pUiGotContext = pId; Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); str_copy(m_aTooltip, "Left mouse button to move. Hold ctrl to rotate. Hold alt to ignore grid."); - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { if(Input()->ModifierIsPressed()) { - UI()->EnableMouseLock(pID); + Ui()->EnableMouseLock(pId); s_Operation = OP_ROTATE; SelectQuad(QIndex); @@ -2977,7 +3009,7 @@ void CEditor::DoQuadEnvPoint(const CQuad *pQuad, int QIndex, int PIndex) SelectEnvPoint(PIndex); m_SelectedQuadEnvelope = pQuad->m_PosEnv; - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); s_LastWx = wx; s_LastWy = wy; @@ -2991,7 +3023,7 @@ void CEditor::DoQuadEnvPoint(const CQuad *pQuad, int QIndex, int PIndex) else Graphics()->SetColor(0.0f, 1.0f, 0.0f, 1.0f); - IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale); + IGraphics::CQuadItem QuadItem(CenterX, CenterY, 5.0f * m_MouseWorldScale, 5.0f * m_MouseWorldScale); Graphics()->QuadsDraw(&QuadItem, 1); } @@ -3009,14 +3041,13 @@ void CEditor::DoMapEditor(CUIRect View) View.w = View.h = Max; } - static void *s_pEditorID = (void *)&s_pEditorID; - const bool Inside = !m_GuiActive || UI()->MouseInside(&View); + const bool Inside = Ui()->MouseInside(&View); // fetch mouse position - float wx = UI()->MouseWorldX(); - float wy = UI()->MouseWorldY(); - float mx = UI()->MouseX(); - float my = UI()->MouseY(); + float wx = Ui()->MouseWorldX(); + float wy = Ui()->MouseWorldY(); + float mx = Ui()->MouseX(); + float my = Ui()->MouseY(); static float s_StartWx = 0; static float s_StartWy = 0; @@ -3034,7 +3065,7 @@ void CEditor::DoMapEditor(CUIRect View) // remap the screen so it can display the whole tileset if(m_ShowPicker) { - CUIRect Screen = *UI()->Screen(); + CUIRect Screen = *Ui()->Screen(); float Size = 32.0f * 16.0f; float w = Size * (Screen.w / View.w); float h = Size * (Screen.h / View.h); @@ -3093,17 +3124,17 @@ void CEditor::DoMapEditor(CUIRect View) static int s_Operation = OP_NONE; // draw layer borders - std::shared_ptr apEditLayers[128]; + std::pair> apEditLayers[128]; size_t NumEditLayers = 0; if(m_ShowPicker && GetSelectedLayer(0) && GetSelectedLayer(0)->m_Type == LAYERTYPE_TILES) { - apEditLayers[0] = m_pTilesetPicker; + apEditLayers[0] = {0, m_pTilesetPicker}; NumEditLayers++; } else if(m_ShowPicker) { - apEditLayers[0] = m_pQuadsetPicker; + apEditLayers[0] = {0, m_pQuadsetPicker}; NumEditLayers++; } else @@ -3122,8 +3153,8 @@ void CEditor::DoMapEditor(CUIRect View) } for(size_t i = 0; i < m_vSelectedLayers.size() && NumEditLayers < 128; i++) { - apEditLayers[NumEditLayers] = GetSelectedLayerType(i, EditingType); - if(apEditLayers[NumEditLayers]) + apEditLayers[NumEditLayers] = {m_vSelectedLayers[i], GetSelectedLayerType(i, EditingType)}; + if(apEditLayers[NumEditLayers].second) { NumEditLayers++; } @@ -3133,30 +3164,46 @@ void CEditor::DoMapEditor(CUIRect View) MapView()->MapGrid()->OnRender(View); } + const bool ShouldPan = Ui()->HotItem() == &m_MapEditorId && ((Input()->ModifierIsPressed() && Ui()->MouseButton(0)) || Ui()->MouseButton(2)); + if(m_pContainerPanned == &m_MapEditorId) + { + // do panning + if(ShouldPan) + { + if(Input()->ShiftIsPressed()) + s_Operation = OP_PAN_EDITOR; + else + s_Operation = OP_PAN_WORLD; + Ui()->SetActiveItem(&m_MapEditorId); + } + else + s_Operation = OP_NONE; + + if(s_Operation == OP_PAN_WORLD) + MapView()->OffsetWorld(-Ui()->MouseDelta() * m_MouseWorldScale); + else if(s_Operation == OP_PAN_EDITOR) + MapView()->OffsetEditor(-Ui()->MouseDelta() * m_MouseWorldScale); + + if(s_Operation == OP_NONE) + m_pContainerPanned = nullptr; + } + if(Inside) { - UI()->SetHotItem(s_pEditorID); + Ui()->SetHotItem(&m_MapEditorId); // do global operations like pan and zoom - if(UI()->CheckActiveItem(nullptr) && (UI()->MouseButton(0) || UI()->MouseButton(2))) + if(Ui()->CheckActiveItem(nullptr) && (Ui()->MouseButton(0) || Ui()->MouseButton(2))) { s_StartWx = wx; s_StartWy = wy; - if(Input()->ModifierIsPressed() || UI()->MouseButton(2)) - { - if(Input()->ShiftIsPressed()) - s_Operation = OP_PAN_EDITOR; - else - s_Operation = OP_PAN_WORLD; - UI()->SetActiveItem(s_pEditorID); - } - else - s_Operation = OP_NONE; + if(ShouldPan && m_pContainerPanned == nullptr) + m_pContainerPanned = &m_MapEditorId; } // brush editing - if(UI()->HotItem() == s_pEditorID) + if(Ui()->HotItem() == &m_MapEditorId) { if(m_ShowPicker) { @@ -3201,11 +3248,16 @@ void CEditor::DoMapEditor(CUIRect View) else if(m_pBrush->IsEmpty() && GetSelectedLayerType(0, LAYERTYPE_QUADS) != nullptr) str_copy(m_aTooltip, "Use left mouse button to drag and create a brush. Hold shift to select multiple quads. Press R to rotate selected quads. Use ctrl+right mouse to select layer."); else if(m_pBrush->IsEmpty()) - str_copy(m_aTooltip, "Use left mouse button to drag and create a brush. Use ctrl+right mouse to select layer."); + { + if(g_Config.m_EdLayerSelector) + str_copy(m_aTooltip, "Use left mouse button to drag and create a brush. Use ctrl+right click to select layer for hovered tile."); + else + str_copy(m_aTooltip, "Use left mouse button to drag and create a brush."); + } else str_copy(m_aTooltip, "Use left mouse button to paint with the brush. Right button clears the brush."); - if(UI()->CheckActiveItem(s_pEditorID)) + if(Ui()->CheckActiveItem(&m_MapEditorId)) { CUIRect r; r.x = s_StartWx; @@ -3232,19 +3284,19 @@ void CEditor::DoMapEditor(CUIRect View) for(size_t k = 0; k < NumEditLayers; k++) { size_t BrushIndex = k % m_pBrush->m_vpLayers.size(); - if(apEditLayers[k]->m_Type == m_pBrush->m_vpLayers[BrushIndex]->m_Type) + if(apEditLayers[k].second->m_Type == m_pBrush->m_vpLayers[BrushIndex]->m_Type) { - if(apEditLayers[k]->m_Type == LAYERTYPE_TILES) + if(apEditLayers[k].second->m_Type == LAYERTYPE_TILES) { - std::shared_ptr pLayer = std::static_pointer_cast(apEditLayers[k]); + std::shared_ptr pLayer = std::static_pointer_cast(apEditLayers[k].second); std::shared_ptr pBrushLayer = std::static_pointer_cast(m_pBrush->m_vpLayers[BrushIndex]); if(pLayer->m_Tele <= pBrushLayer->m_Tele && pLayer->m_Speedup <= pBrushLayer->m_Speedup && pLayer->m_Front <= pBrushLayer->m_Front && pLayer->m_Game <= pBrushLayer->m_Game && pLayer->m_Switch <= pBrushLayer->m_Switch && pLayer->m_Tune <= pBrushLayer->m_Tune) - pLayer->BrushDraw(pBrushLayer, wx, wy); + pLayer->BrushDraw(pBrushLayer, vec2(wx, wy)); } else { - apEditLayers[k]->BrushDraw(m_pBrush->m_vpLayers[BrushIndex], wx, wy); + apEditLayers[k].second->BrushDraw(m_pBrush->m_vpLayers[BrushIndex], vec2(wx, wy)); } } } @@ -3252,7 +3304,7 @@ void CEditor::DoMapEditor(CUIRect View) } else if(s_Operation == OP_BRUSH_GRAB) { - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { std::shared_ptr pQuadLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); if(Input()->ShiftIsPressed() && pQuadLayer) @@ -3261,10 +3313,8 @@ void CEditor::DoMapEditor(CUIRect View) for(size_t i = 0; i < pQuadLayer->m_vQuads.size(); i++) { const CQuad &Quad = pQuadLayer->m_vQuads[i]; - float px = fx2f(Quad.m_aPoints[4].x); - float py = fx2f(Quad.m_aPoints[4].y); - - if(r.Inside(px, py) && !IsQuadSelected(i)) + vec2 Position = vec2(fx2f(Quad.m_aPoints[4].x), fx2f(Quad.m_aPoints[4].y)); + if(r.Inside(Position) && !IsQuadSelected(i)) ToggleSelectQuad(i); } } @@ -3278,7 +3328,7 @@ void CEditor::DoMapEditor(CUIRect View) // TODO: do all layers int Grabs = 0; for(size_t k = 0; k < NumEditLayers; k++) - Grabs += apEditLayers[k]->BrushGrab(m_pBrush, r); + Grabs += apEditLayers[k].second->BrushGrab(m_pBrush, r); if(Grabs == 0) m_pBrush->Clear(); @@ -3289,13 +3339,13 @@ void CEditor::DoMapEditor(CUIRect View) else { for(size_t k = 0; k < NumEditLayers; k++) - apEditLayers[k]->BrushSelecting(r); - UI()->MapScreen(); + apEditLayers[k].second->BrushSelecting(r); + Ui()->MapScreen(); } } else if(s_Operation == OP_BRUSH_PAINT) { - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { for(size_t k = 0; k < NumEditLayers; k++) { @@ -3303,7 +3353,7 @@ void CEditor::DoMapEditor(CUIRect View) if(m_pBrush->m_vpLayers.size() != NumEditLayers) BrushIndex = 0; std::shared_ptr pBrush = m_pBrush->IsEmpty() ? nullptr : m_pBrush->m_vpLayers[BrushIndex]; - apEditLayers[k]->FillSelection(m_pBrush->IsEmpty(), pBrush, r); + apEditLayers[k].second->FillSelection(m_pBrush->IsEmpty(), pBrush, r); } std::shared_ptr Action = std::make_shared(this, m_SelectedGroup); m_EditorHistory.RecordAction(Action); @@ -3311,21 +3361,21 @@ void CEditor::DoMapEditor(CUIRect View) else { for(size_t k = 0; k < NumEditLayers; k++) - apEditLayers[k]->BrushSelecting(r); - UI()->MapScreen(); + apEditLayers[k].second->BrushSelecting(r); + Ui()->MapScreen(); } } } else { - if(UI()->MouseButton(1)) + if(Ui()->MouseButton(1)) { m_pBrush->Clear(); } - if(UI()->MouseButton(0) && s_Operation == OP_NONE && !m_QuadKnifeActive) + if(!Input()->ModifierIsPressed() && Ui()->MouseButton(0) && s_Operation == OP_NONE && !m_QuadKnifeActive) { - UI()->SetActiveItem(s_pEditorID); + Ui()->SetActiveItem(&m_MapEditorId); if(m_pBrush->IsEmpty()) s_Operation = OP_BRUSH_GRAB; @@ -3338,8 +3388,8 @@ void CEditor::DoMapEditor(CUIRect View) if(m_pBrush->m_vpLayers.size() != NumEditLayers) BrushIndex = 0; - if(apEditLayers[k]->m_Type == m_pBrush->m_vpLayers[BrushIndex]->m_Type) - apEditLayers[k]->BrushPlace(m_pBrush->m_vpLayers[BrushIndex], wx, wy); + if(apEditLayers[k].second->m_Type == m_pBrush->m_vpLayers[BrushIndex]->m_Type) + apEditLayers[k].second->BrushPlace(m_pBrush->m_vpLayers[BrushIndex], vec2(wx, wy)); } } @@ -3398,9 +3448,11 @@ void CEditor::DoMapEditor(CUIRect View) for(size_t k = 0; k < NumEditLayers; k++) { - if(apEditLayers[k]->m_Type == LAYERTYPE_QUADS) + auto &[LayerIndex, pEditLayer] = apEditLayers[k]; + + if(pEditLayer->m_Type == LAYERTYPE_QUADS) { - std::shared_ptr pLayer = std::static_pointer_cast(apEditLayers[k]); + std::shared_ptr pLayer = std::static_pointer_cast(pEditLayer); if(m_ShowEnvelopePreview == SHOWENV_NONE) m_ShowEnvelopePreview = SHOWENV_ALL; @@ -3416,29 +3468,29 @@ void CEditor::DoMapEditor(CUIRect View) for(size_t i = 0; i < pLayer->m_vQuads.size(); i++) { for(int v = 0; v < 4; v++) - DoQuadPoint(pLayer, &pLayer->m_vQuads[i], i, v); + DoQuadPoint(LayerIndex, pLayer, &pLayer->m_vQuads[i], i, v); - DoQuad(pLayer, &pLayer->m_vQuads[i], i); + DoQuad(LayerIndex, pLayer, &pLayer->m_vQuads[i], i); } Graphics()->QuadsEnd(); } } - if(apEditLayers[k]->m_Type == LAYERTYPE_SOUNDS) + if(pEditLayer->m_Type == LAYERTYPE_SOUNDS) { - std::shared_ptr pLayer = std::static_pointer_cast(apEditLayers[k]); + std::shared_ptr pLayer = std::static_pointer_cast(pEditLayer); Graphics()->TextureClear(); Graphics()->QuadsBegin(); for(size_t i = 0; i < pLayer->m_vSources.size(); i++) { - DoSoundSource(&pLayer->m_vSources[i], i); + DoSoundSource(LayerIndex, &pLayer->m_vSources[i], i); } Graphics()->QuadsEnd(); } } - UI()->MapScreen(); + Ui()->MapScreen(); } } @@ -3452,21 +3504,20 @@ void CEditor::DoMapEditor(CUIRect View) Pos += MapView()->GetWorldOffset() - MapView()->ProofMode()->m_vMenuBackgroundPositions[MapView()->ProofMode()->m_CurrentMenuProofIndex]; Pos.y -= 3.0f; - vec2 MousePos(m_MouseWorldNoParaX, m_MouseWorldNoParaY); - if(distance(Pos, MousePos) <= 20.0f) + if(distance(Pos, m_MouseWorldNoParaPos) <= 20.0f) { - UI()->SetHotItem(&MapView()->ProofMode()->m_vMenuBackgroundPositions[i]); + Ui()->SetHotItem(&MapView()->ProofMode()->m_vMenuBackgroundPositions[i]); - if(i != MapView()->ProofMode()->m_CurrentMenuProofIndex && UI()->CheckActiveItem(&MapView()->ProofMode()->m_vMenuBackgroundPositions[i])) + if(i != MapView()->ProofMode()->m_CurrentMenuProofIndex && Ui()->CheckActiveItem(&MapView()->ProofMode()->m_vMenuBackgroundPositions[i])) { - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { MapView()->ProofMode()->m_CurrentMenuProofIndex = i; MapView()->SetWorldOffset(MapView()->ProofMode()->m_vMenuBackgroundPositions[i]); - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } } - else if(UI()->HotItem() == &MapView()->ProofMode()->m_vMenuBackgroundPositions[i]) + else if(Ui()->HotItem() == &MapView()->ProofMode()->m_vMenuBackgroundPositions[i]) { char aTooltipPrefix[32] = "Switch proof position to"; if(i == MapView()->ProofMode()->m_CurrentMenuProofIndex) @@ -3490,8 +3541,7 @@ void CEditor::DoMapEditor(CUIRect View) Pos += MapView()->GetWorldOffset() - MapView()->ProofMode()->m_vMenuBackgroundPositions[MapView()->ProofMode()->m_CurrentMenuProofIndex]; Pos.y -= 3.0f; - MousePos = vec2(m_MouseWorldNoParaX, m_MouseWorldNoParaY); - if(distance(Pos, MousePos) > 20.0f) + if(distance(Pos, m_MouseWorldNoParaPos) > 20.0f) continue; if(i < (TILE_TIME_CHECKPOINT_LAST - TILE_TIME_CHECKPOINT_FIRST)) @@ -3506,24 +3556,18 @@ void CEditor::DoMapEditor(CUIRect View) str_format(m_aMenuBackgroundTooltip, sizeof(m_aMenuBackgroundTooltip), "%s %s", aTooltipPrefix, aTooltipPositions); str_copy(m_aTooltip, m_aMenuBackgroundTooltip); - if(UI()->MouseButton(0)) - UI()->SetActiveItem(&MapView()->ProofMode()->m_vMenuBackgroundPositions[i]); + if(Ui()->MouseButton(0)) + Ui()->SetActiveItem(&MapView()->ProofMode()->m_vMenuBackgroundPositions[i]); } break; } } } - if(UI()->CheckActiveItem(s_pEditorID)) + if(Ui()->CheckActiveItem(&m_MapEditorId) && m_pContainerPanned == nullptr) { - // do panning - if(s_Operation == OP_PAN_WORLD) - MapView()->OffsetWorld(-vec2(m_MouseDeltaX, m_MouseDeltaY) * m_MouseWScale); - else if(s_Operation == OP_PAN_EDITOR) - MapView()->OffsetEditor(-vec2(m_MouseDeltaX, m_MouseDeltaY) * m_MouseWScale); - // release mouse - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { if(s_Operation == OP_BRUSH_DRAW) { @@ -3534,29 +3578,29 @@ void CEditor::DoMapEditor(CUIRect View) } s_Operation = OP_NONE; - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } } - if(!Input()->ShiftIsPressed() && !Input()->ModifierIsPressed() && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr) + if(!Input()->ModifierIsPressed() && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr) { - float PanSpeed = 64.0f; + float PanSpeed = Input()->ShiftIsPressed() ? 200.0f : 64.0f; if(Input()->KeyPress(KEY_A)) - MapView()->OffsetWorld({-PanSpeed * m_MouseWScale, 0}); + MapView()->OffsetWorld({-PanSpeed * m_MouseWorldScale, 0}); else if(Input()->KeyPress(KEY_D)) - MapView()->OffsetWorld({PanSpeed * m_MouseWScale, 0}); + MapView()->OffsetWorld({PanSpeed * m_MouseWorldScale, 0}); if(Input()->KeyPress(KEY_W)) - MapView()->OffsetWorld({0, -PanSpeed * m_MouseWScale}); + MapView()->OffsetWorld({0, -PanSpeed * m_MouseWorldScale}); else if(Input()->KeyPress(KEY_S)) - MapView()->OffsetWorld({0, PanSpeed * m_MouseWScale}); + MapView()->OffsetWorld({0, PanSpeed * m_MouseWorldScale}); } } - else if(UI()->CheckActiveItem(s_pEditorID)) + else if(Ui()->CheckActiveItem(&m_MapEditorId) && m_pContainerPanned == nullptr) { // release mouse - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { s_Operation = OP_NONE; - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } } @@ -3600,26 +3644,26 @@ void CEditor::DoMapEditor(CUIRect View) m_ShowEnvelopePreview = SHOWENV_NONE; } - UI()->MapScreen(); + Ui()->MapScreen(); } void CEditor::SetHotQuadPoint(const std::shared_ptr &pLayer) { - float wx = UI()->MouseWorldX(); - float wy = UI()->MouseWorldY(); + float wx = Ui()->MouseWorldX(); + float wy = Ui()->MouseWorldY(); float MinDist = 500.0f; void *pMinPoint = nullptr; - auto UpdateMinimum = [&](float px, float py, void *pID) { - float dx = (px - wx) / m_MouseWScale; - float dy = (py - wy) / m_MouseWScale; + auto UpdateMinimum = [&](float px, float py, void *pId) { + float dx = (px - wx) / m_MouseWorldScale; + float dy = (py - wy) / m_MouseWorldScale; float CurrDist = dx * dx + dy * dy; if(CurrDist < MinDist) { MinDist = CurrDist; - pMinPoint = pID; + pMinPoint = pId; return true; } return false; @@ -3645,27 +3689,27 @@ void CEditor::SetHotQuadPoint(const std::shared_ptr &pLayer) } if(pMinPoint != nullptr) - UI()->SetHotItem(pMinPoint); + Ui()->SetHotItem(pMinPoint); } -void CEditor::DoColorPickerButton(const void *pID, const CUIRect *pRect, ColorRGBA Color, const std::function &SetColor) +void CEditor::DoColorPickerButton(const void *pId, const CUIRect *pRect, ColorRGBA Color, const std::function &SetColor) { CUIRect ColorRect; - pRect->Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * UI()->ButtonColorMul(pID)), IGraphics::CORNER_ALL, 3.0f); + pRect->Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f * Ui()->ButtonColorMul(pId)), IGraphics::CORNER_ALL, 3.0f); pRect->Margin(1.0f, &ColorRect); ColorRect.Draw(Color, IGraphics::CORNER_ALL, 3.0f); - const int ButtonResult = DoButton_Editor_Common(pID, nullptr, 0, pRect, 0, "Click to show the color picker. Shift+rightclick to copy color to clipboard. Shift+leftclick to paste color from clipboard."); + const int ButtonResult = DoButton_Editor_Common(pId, nullptr, 0, pRect, 0, "Click to show the color picker. Shift+rightclick to copy color to clipboard. Shift+leftclick to paste color from clipboard."); if(Input()->ShiftIsPressed()) { if(ButtonResult == 1) { - const char *pClipboard = Input()->GetClipboardText(); - if(*pClipboard == '#' || *pClipboard == '$') // ignore leading # (web color format) and $ (console color format) - ++pClipboard; - if(str_isallnum_hex(pClipboard)) + std::string Clipboard = Input()->GetClipboardText(); + if(Clipboard[0] == '#' || Clipboard[0] == '$') // ignore leading # (web color format) and $ (console color format) + Clipboard = Clipboard.substr(1); + if(str_isallnum_hex(Clipboard.c_str())) { - std::optional ParsedColor = color_parse(pClipboard); + std::optional ParsedColor = color_parse(Clipboard.c_str()); if(ParsedColor) { m_ColorPickerPopupContext.m_State = EEditState::ONE_GO; @@ -3682,24 +3726,24 @@ void CEditor::DoColorPickerButton(const void *pID, const CUIRect *pRect, ColorRG } else if(ButtonResult > 0) { - if(m_ColorPickerPopupContext.m_ColorMode == CUI::SColorPickerPopupContext::MODE_UNSET) - m_ColorPickerPopupContext.m_ColorMode = CUI::SColorPickerPopupContext::MODE_RGBA; + if(m_ColorPickerPopupContext.m_ColorMode == CUi::SColorPickerPopupContext::MODE_UNSET) + m_ColorPickerPopupContext.m_ColorMode = CUi::SColorPickerPopupContext::MODE_RGBA; m_ColorPickerPopupContext.m_RgbaColor = Color; m_ColorPickerPopupContext.m_HslaColor = color_cast(Color); m_ColorPickerPopupContext.m_HsvaColor = color_cast(m_ColorPickerPopupContext.m_HslaColor); m_ColorPickerPopupContext.m_Alpha = true; - m_pColorPickerPopupActiveID = pID; - UI()->ShowPopupColorPicker(UI()->MouseX(), UI()->MouseY(), &m_ColorPickerPopupContext); + m_pColorPickerPopupActiveId = pId; + Ui()->ShowPopupColorPicker(Ui()->MouseX(), Ui()->MouseY(), &m_ColorPickerPopupContext); } - if(UI()->IsPopupOpen(&m_ColorPickerPopupContext)) + if(Ui()->IsPopupOpen(&m_ColorPickerPopupContext)) { - if(m_pColorPickerPopupActiveID == pID) + if(m_pColorPickerPopupActiveId == pId) SetColor(m_ColorPickerPopupContext.m_RgbaColor); } else { - m_pColorPickerPopupActiveID = nullptr; + m_pColorPickerPopupActiveId = nullptr; if(m_ColorPickerPopupContext.m_State == EEditState::EDITING) { ColorRGBA c = color_cast(m_ColorPickerPopupContext.m_HsvaColor); @@ -3735,9 +3779,10 @@ void CEditor::RenderLayers(CUIRect LayersBox) }; static int s_Operation = OP_NONE; static int s_PreviousOperation = OP_NONE; - static const void *s_pDraggedButton = 0; + static const void *s_pDraggedButton = nullptr; static float s_InitialMouseY = 0; static float s_InitialCutHeight = 0; + constexpr float MinDragDistance = 5.0f; int GroupAfterDraggedLayer = -1; int LayerAfterDraggedLayer = -1; bool DraggedPositionFound = false; @@ -3745,7 +3790,6 @@ void CEditor::RenderLayers(CUIRect LayersBox) bool MoveGroup = false; bool StartDragLayer = false; bool StartDragGroup = false; - bool AnyButtonActive = false; std::vector vButtonsPerGroup; auto SetOperation = [](int Operation) { @@ -3753,6 +3797,10 @@ void CEditor::RenderLayers(CUIRect LayersBox) { s_PreviousOperation = s_Operation; s_Operation = Operation; + if(Operation == OP_NONE) + { + s_pDraggedButton = nullptr; + } } }; @@ -3762,8 +3810,10 @@ void CEditor::RenderLayers(CUIRect LayersBox) vButtonsPerGroup.push_back(pGroup->m_vpLayers.size() + 1); } - if(!UI()->CheckActiveItem(s_pDraggedButton)) + if(s_pDraggedButton != nullptr && Ui()->ActiveItem() != s_pDraggedButton) + { SetOperation(OP_NONE); + } if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG) { @@ -3786,7 +3836,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) } UnscrolledLayersBox.HSplitTop(s_InitialCutHeight, nullptr, &UnscrolledLayersBox); - UnscrolledLayersBox.y -= s_InitialMouseY - UI()->MouseY(); + UnscrolledLayersBox.y -= s_InitialMouseY - Ui()->MouseY(); UnscrolledLayersBox.y = clamp(UnscrolledLayersBox.y, MinDraggableValue, MaxDraggableValue); @@ -3794,13 +3844,13 @@ void CEditor::RenderLayers(CUIRect LayersBox) } static bool s_ScrollToSelectionNext = false; - const bool ScrollToSelection = SelectLayerByTile() || s_ScrollToSelectionNext; + const bool ScrollToSelection = LayerSelector()->SelectByTile() || s_ScrollToSelectionNext; s_ScrollToSelectionNext = false; // render layers for(int g = 0; g < (int)m_Map.m_vpGroups.size(); g++) { - if(s_Operation == OP_LAYER_DRAG && g > 0 && !DraggedPositionFound && UI()->MouseY() < LayersBox.y + RowHeight / 2) + if(s_Operation == OP_LAYER_DRAG && g > 0 && !DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2) { DraggedPositionFound = true; GroupAfterDraggedLayer = g; @@ -3820,7 +3870,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) UnscrolledLayersBox.HSplitTop(RowHeight, &Slot, &UnscrolledLayersBox); UnscrolledLayersBox.HSplitTop(2.0f, nullptr, &UnscrolledLayersBox); } - else if(!DraggedPositionFound && UI()->MouseY() < LayersBox.y + RowHeight * vButtonsPerGroup[g] / 2 + 3.0f) + else if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight * vButtonsPerGroup[g] / 2 + 3.0f) { DraggedPositionFound = true; GroupAfterDraggedLayer = g; @@ -3845,21 +3895,54 @@ void CEditor::RenderLayers(CUIRect LayersBox) if(s_ScrollRegion.AddRect(Slot)) { Slot.VSplitLeft(15.0f, &VisibleToggle, &Slot); - if(DoButton_FontIcon(&m_Map.m_vpGroups[g]->m_Visible, m_Map.m_vpGroups[g]->m_Visible ? FONT_ICON_EYE : FONT_ICON_EYE_SLASH, m_Map.m_vpGroups[g]->m_Collapse ? 1 : 0, &VisibleToggle, 0, "Toggle group visibility", IGraphics::CORNER_L, 8.0f)) + + const int MouseClick = DoButton_FontIcon(&m_Map.m_vpGroups[g]->m_Visible, m_Map.m_vpGroups[g]->m_Visible ? FONT_ICON_EYE : FONT_ICON_EYE_SLASH, m_Map.m_vpGroups[g]->m_Collapse ? 1 : 0, &VisibleToggle, 0, "Left click to toggle visibility. Right click to show this group only.", IGraphics::CORNER_L, 8.0f); + if(MouseClick == 1) + { m_Map.m_vpGroups[g]->m_Visible = !m_Map.m_vpGroups[g]->m_Visible; + } + else if(MouseClick == 2) + { + if(Input()->ShiftIsPressed()) + { + if(g != m_SelectedGroup) + SelectLayer(0, g); + } + + int NumActive = 0; + for(auto &Group : m_Map.m_vpGroups) + { + if(Group == m_Map.m_vpGroups[g]) + { + Group->m_Visible = true; + continue; + } + + if(Group->m_Visible) + { + Group->m_Visible = false; + NumActive++; + } + } + if(NumActive == 0) + { + for(auto &Group : m_Map.m_vpGroups) + { + Group->m_Visible = true; + } + } + } str_format(aBuf, sizeof(aBuf), "#%d %s", g, m_Map.m_vpGroups[g]->m_aName); bool Clicked; bool Abrupted; - if(int Result = DoButton_DraggableEx(&m_Map.m_vpGroups[g], aBuf, g == m_SelectedGroup, &Slot, &Clicked, &Abrupted, + if(int Result = DoButton_DraggableEx(m_Map.m_vpGroups[g].get(), aBuf, g == m_SelectedGroup, &Slot, &Clicked, &Abrupted, BUTTON_CONTEXT, m_Map.m_vpGroups[g]->m_Collapse ? "Select group. Shift click to select all layers. Double click to expand." : "Select group. Shift click to select all layers. Double click to collapse.", IGraphics::CORNER_R)) { - AnyButtonActive = true; - if(s_Operation == OP_NONE) { - s_InitialMouseY = UI()->MouseY(); + s_InitialMouseY = Ui()->MouseY(); s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y; SetOperation(OP_CLICK); @@ -3868,14 +3951,14 @@ void CEditor::RenderLayers(CUIRect LayersBox) } if(Abrupted) + { SetOperation(OP_NONE); + } - if(s_Operation == OP_CLICK) + if(s_Operation == OP_CLICK && absolute(Ui()->MouseY() - s_InitialMouseY) > MinDragDistance) { - if(absolute(UI()->MouseY() - s_InitialMouseY) > 5) - StartDragGroup = true; - - s_pDraggedButton = &m_Map.m_vpGroups[g]; + StartDragGroup = true; + s_pDraggedButton = m_Map.m_vpGroups[g].get(); } if(s_Operation == OP_CLICK && Clicked) @@ -3895,10 +3978,10 @@ void CEditor::RenderLayers(CUIRect LayersBox) if(Result == 2) { static SPopupMenuId s_PopupGroupId; - UI()->DoPopupMenu(&s_PopupGroupId, UI()->MouseX(), UI()->MouseY(), 145, 256, this, PopupGroup); + Ui()->DoPopupMenu(&s_PopupGroupId, Ui()->MouseX(), Ui()->MouseY(), 145, 256, this, PopupGroup); } - if(!m_Map.m_vpGroups[g]->m_vpLayers.empty() && Input()->MouseDoubleClick()) + if(!m_Map.m_vpGroups[g]->m_vpLayers.empty() && Ui()->DoDoubleClickLogic(m_Map.m_vpGroups[g].get())) m_Map.m_vpGroups[g]->m_Collapse ^= 1; SetOperation(OP_NONE); @@ -3907,7 +3990,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) if(s_Operation == OP_GROUP_DRAG && Clicked) MoveGroup = true; } - else if(s_pDraggedButton == &m_Map.m_vpGroups[g]) + else if(s_pDraggedButton == m_Map.m_vpGroups[g].get()) { SetOperation(OP_NONE); } @@ -3943,7 +4026,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) } else { - if(!DraggedPositionFound && UI()->MouseY() < LayersBox.y + RowHeight / 2) + if(!DraggedPositionFound && Ui()->MouseY() < LayersBox.y + RowHeight / 2) { DraggedPositionFound = true; GroupAfterDraggedLayer = g + 1; @@ -3972,24 +4055,58 @@ void CEditor::RenderLayers(CUIRect LayersBox) Slot.VSplitLeft(12.0f, nullptr, &Slot); Slot.VSplitLeft(15.0f, &VisibleToggle, &Button); - if(DoButton_FontIcon(&m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible, m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible ? FONT_ICON_EYE : FONT_ICON_EYE_SLASH, 0, &VisibleToggle, 0, "Toggle layer visibility", IGraphics::CORNER_L, 8.0f)) + const int MouseClick = DoButton_FontIcon(&m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible, m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible ? FONT_ICON_EYE : FONT_ICON_EYE_SLASH, 0, &VisibleToggle, 0, "Left click to toggle visibility. Right click to show only this layer within its group.", IGraphics::CORNER_L, 8.0f); + if(MouseClick == 1) + { m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible = !m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Visible; - - if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_aName[0]) - str_copy(aBuf, m_Map.m_vpGroups[g]->m_vpLayers[i]->m_aName); - else + } + else if(MouseClick == 2) { - if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_TILES) - { - std::shared_ptr pTiles = std::static_pointer_cast(m_Map.m_vpGroups[g]->m_vpLayers[i]); - str_copy(aBuf, pTiles->m_Image >= 0 ? m_Map.m_vpImages[pTiles->m_Image]->m_aName : "Tiles"); - } - else if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_QUADS) + if(Input()->ShiftIsPressed()) { - std::shared_ptr pQuads = std::static_pointer_cast(m_Map.m_vpGroups[g]->m_vpLayers[i]); - str_copy(aBuf, pQuads->m_Image >= 0 ? m_Map.m_vpImages[pQuads->m_Image]->m_aName : "Quads"); + if(!IsLayerSelected) + SelectLayer(i, g); } - else if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_SOUNDS) + + int NumActive = 0; + for(auto &Layer : m_Map.m_vpGroups[g]->m_vpLayers) + { + if(Layer == m_Map.m_vpGroups[g]->m_vpLayers[i]) + { + Layer->m_Visible = true; + continue; + } + + if(Layer->m_Visible) + { + Layer->m_Visible = false; + NumActive++; + } + } + if(NumActive == 0) + { + for(auto &Layer : m_Map.m_vpGroups[g]->m_vpLayers) + { + Layer->m_Visible = true; + } + } + } + + if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_aName[0]) + str_copy(aBuf, m_Map.m_vpGroups[g]->m_vpLayers[i]->m_aName); + else + { + if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_TILES) + { + std::shared_ptr pTiles = std::static_pointer_cast(m_Map.m_vpGroups[g]->m_vpLayers[i]); + str_copy(aBuf, pTiles->m_Image >= 0 ? m_Map.m_vpImages[pTiles->m_Image]->m_aName : "Tiles"); + } + else if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_QUADS) + { + std::shared_ptr pQuads = std::static_pointer_cast(m_Map.m_vpGroups[g]->m_vpLayers[i]); + str_copy(aBuf, pQuads->m_Image >= 0 ? m_Map.m_vpImages[pQuads->m_Image]->m_aName : "Quads"); + } + else if(m_Map.m_vpGroups[g]->m_vpLayers[i]->m_Type == LAYERTYPE_SOUNDS) { std::shared_ptr pSounds = std::static_pointer_cast(m_Map.m_vpGroups[g]->m_vpLayers[i]); str_copy(aBuf, pSounds->m_Sound >= 0 ? m_Map.m_vpSounds[pSounds->m_Sound]->m_aName : "Sounds"); @@ -4007,12 +4124,11 @@ void CEditor::RenderLayers(CUIRect LayersBox) if(int Result = DoButton_DraggableEx(m_Map.m_vpGroups[g]->m_vpLayers[i].get(), aBuf, Checked, &Button, &Clicked, &Abrupted, BUTTON_CONTEXT, "Select layer. Shift click to select multiple.", IGraphics::CORNER_R)) { - AnyButtonActive = true; - if(s_Operation == OP_NONE) { - s_InitialMouseY = UI()->MouseY(); + s_InitialMouseY = Ui()->MouseY(); s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y; + SetOperation(OP_CLICK); if(!Input()->ShiftIsPressed() && !IsLayerSelected) @@ -4022,23 +4138,22 @@ void CEditor::RenderLayers(CUIRect LayersBox) } if(Abrupted) + { SetOperation(OP_NONE); + } - if(s_Operation == OP_CLICK) + if(s_Operation == OP_CLICK && absolute(Ui()->MouseY() - s_InitialMouseY) > MinDragDistance) { - if(absolute(UI()->MouseY() - s_InitialMouseY) > 5) + bool EntitiesLayerSelected = false; + for(int k : m_vSelectedLayers) { - bool EntitiesLayerSelected = false; - for(int k : m_vSelectedLayers) - { - if(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[k]->IsEntitiesLayer()) - EntitiesLayerSelected = true; - } - - if(!EntitiesLayerSelected) - StartDragLayer = true; + if(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[k]->IsEntitiesLayer()) + EntitiesLayerSelected = true; } + if(!EntitiesLayerSelected) + StartDragLayer = true; + s_pDraggedButton = m_Map.m_vpGroups[g]->m_vpLayers[i].get(); } @@ -4063,6 +4178,9 @@ void CEditor::RenderLayers(CUIRect LayersBox) } else if(Result == 2) { + s_LayerPopupContext.m_vpLayers.clear(); + s_LayerPopupContext.m_vLayerIndices.clear(); + if(!IsLayerSelected) { SelectLayer(i, g); @@ -4090,13 +4208,8 @@ void CEditor::RenderLayers(CUIRect LayersBox) s_LayerPopupContext.m_vLayerIndices.clear(); } } - else - { - s_LayerPopupContext.m_vpLayers.clear(); - s_LayerPopupContext.m_vLayerIndices.clear(); - } - UI()->DoPopupMenu(&s_LayerPopupContext, UI()->MouseX(), UI()->MouseY(), 120, 270, &s_LayerPopupContext, PopupLayer); + Ui()->DoPopupMenu(&s_LayerPopupContext, Ui()->MouseX(), Ui()->MouseY(), 120, 270, &s_LayerPopupContext, PopupLayer); } SetOperation(OP_NONE); @@ -4199,7 +4312,9 @@ void CEditor::RenderLayers(CUIRect LayersBox) static std::vector s_vInitialLayerIndices; if(MoveLayers || MoveGroup) + { SetOperation(OP_NONE); + } if(StartDragLayer) { SetOperation(OP_LAYER_DRAG); @@ -4214,11 +4329,18 @@ void CEditor::RenderLayers(CUIRect LayersBox) if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG) { - s_ScrollRegion.DoEdgeScrolling(); - UI()->SetActiveItem(s_pDraggedButton); + if(s_pDraggedButton == nullptr) + { + SetOperation(OP_NONE); + } + else + { + s_ScrollRegion.DoEdgeScrolling(); + Ui()->SetActiveItem(s_pDraggedButton); + } } - if(Input()->KeyPress(KEY_DOWN) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && s_Operation == OP_NONE) + if(Input()->KeyPress(KEY_DOWN) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && s_Operation == OP_NONE) { if(Input()->ShiftIsPressed()) { @@ -4227,30 +4349,11 @@ void CEditor::RenderLayers(CUIRect LayersBox) } else { - int CurrentLayer = 0; - for(const auto &Selected : m_vSelectedLayers) - CurrentLayer = maximum(Selected, CurrentLayer); - SelectLayer(CurrentLayer); - - if(m_vSelectedLayers[0] < (int)m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1) - { - SelectLayer(m_vSelectedLayers[0] + 1); - } - else - { - for(size_t Group = m_SelectedGroup + 1; Group < m_Map.m_vpGroups.size(); Group++) - { - if(!m_Map.m_vpGroups[Group]->m_vpLayers.empty()) - { - SelectLayer(0, Group); - break; - } - } - } + SelectNextLayer(); } s_ScrollToSelectionNext = true; } - if(Input()->KeyPress(KEY_UP) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && s_Operation == OP_NONE) + if(Input()->KeyPress(KEY_UP) && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && CLineInput::GetActiveInput() == nullptr && s_Operation == OP_NONE) { if(Input()->ShiftIsPressed()) { @@ -4259,48 +4362,53 @@ void CEditor::RenderLayers(CUIRect LayersBox) } else { - int CurrentLayer = std::numeric_limits::max(); - for(const auto &Selected : m_vSelectedLayers) - CurrentLayer = minimum(Selected, CurrentLayer); - SelectLayer(CurrentLayer); - - if(m_vSelectedLayers[0] > 0) - { - SelectLayer(m_vSelectedLayers[0] - 1); - } - else - { - for(int Group = m_SelectedGroup - 1; Group >= 0; Group--) - { - if(!m_Map.m_vpGroups[Group]->m_vpLayers.empty()) - { - SelectLayer(m_Map.m_vpGroups[Group]->m_vpLayers.size() - 1, Group); - break; - } - } - } + SelectPreviousLayer(); } + s_ScrollToSelectionNext = true; } - CUIRect AddGroupButton; + CUIRect AddGroupButton, CollapseAllButton; LayersBox.HSplitTop(RowHeight + 1.0f, &AddGroupButton, &LayersBox); if(s_ScrollRegion.AddRect(AddGroupButton)) { AddGroupButton.HSplitTop(RowHeight, &AddGroupButton, 0); - static int s_AddGroupButton = 0; - if(DoButton_Editor(&s_AddGroupButton, "Add group", 0, &AddGroupButton, IGraphics::CORNER_R, "Adds a new group")) + if(DoButton_Editor(&m_QuickActionAddGroup, m_QuickActionAddGroup.Label(), 0, &AddGroupButton, IGraphics::CORNER_R, m_QuickActionAddGroup.Description())) { - m_Map.NewGroup(); - m_SelectedGroup = m_Map.m_vpGroups.size() - 1; - m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, false)); + m_QuickActionAddGroup.Call(); } } - s_ScrollRegion.End(); + LayersBox.HSplitTop(5.0f, nullptr, &LayersBox); + LayersBox.HSplitTop(RowHeight + 1.0f, &CollapseAllButton, &LayersBox); + if(s_ScrollRegion.AddRect(CollapseAllButton)) + { + unsigned long TotalCollapsed = 0; + for(const auto &pGroup : m_Map.m_vpGroups) + { + if(pGroup->m_Collapse) + { + TotalCollapsed++; + } + } - if(!AnyButtonActive) - SetOperation(OP_NONE); + const char *pActionText = TotalCollapsed == m_Map.m_vpGroups.size() ? "Expand all" : "Collapse all"; + + CollapseAllButton.HSplitTop(RowHeight, &CollapseAllButton, 0); + static int s_CollapseAllButton = 0; + if(DoButton_Editor(&s_CollapseAllButton, pActionText, 0, &CollapseAllButton, IGraphics::CORNER_R, "Expand or collapse all groups")) + { + for(const auto &pGroup : m_Map.m_vpGroups) + { + if(TotalCollapsed == m_Map.m_vpGroups.size()) + pGroup->m_Collapse = false; + else + pGroup->m_Collapse = true; + } + } + } + + s_ScrollRegion.End(); if(s_Operation == OP_NONE) { @@ -4318,79 +4426,21 @@ void CEditor::RenderLayers(CUIRect LayersBox) else { std::vector> vpActions; - for(int k = 0; k < (int)m_vSelectedLayers.size(); k++) + std::vector vLayerIndices = m_vSelectedLayers; + std::sort(vLayerIndices.begin(), vLayerIndices.end()); + std::sort(s_vInitialLayerIndices.begin(), s_vInitialLayerIndices.end()); + for(int k = 0; k < (int)vLayerIndices.size(); k++) { - int LayerIndex = m_vSelectedLayers[k]; + int LayerIndex = vLayerIndices[k]; vpActions.push_back(std::make_shared(this, m_SelectedGroup, LayerIndex, ELayerProp::PROP_ORDER, s_vInitialLayerIndices[k], LayerIndex)); } - m_EditorHistory.RecordAction(std::make_shared(CEditorActionBulk(this, vpActions))); + m_EditorHistory.RecordAction(std::make_shared(this, vpActions, nullptr, true)); } s_PreviousOperation = OP_NONE; } } } -bool CEditor::SelectLayerByTile() -{ - // ctrl+rightclick a map index to select the layer that has a tile there - static bool s_CtrlClick = false; - static int s_Selected = 0; - int MatchedGroup = -1; - int MatchedLayer = -1; - int Matches = 0; - bool IsFound = false; - if(UI()->MouseButton(1) && Input()->ModifierIsPressed()) - { - if(s_CtrlClick) - return false; - s_CtrlClick = true; - for(size_t g = 0; g < m_Map.m_vpGroups.size(); g++) - { - for(size_t l = 0; l < m_Map.m_vpGroups[g]->m_vpLayers.size(); l++) - { - if(IsFound) - continue; - if(m_Map.m_vpGroups[g]->m_vpLayers[l]->m_Type != LAYERTYPE_TILES) - continue; - - std::shared_ptr pTiles = std::static_pointer_cast(m_Map.m_vpGroups[g]->m_vpLayers[l]); - int x = (int)UI()->MouseWorldX() / 32 + m_Map.m_vpGroups[g]->m_OffsetX; - int y = (int)UI()->MouseWorldY() / 32 + m_Map.m_vpGroups[g]->m_OffsetY; - if(x < 0 || x >= pTiles->m_Width) - continue; - if(y < 0 || y >= pTiles->m_Height) - continue; - CTile Tile = pTiles->GetTile(x, y); - if(Tile.m_Index) - { - if(MatchedGroup == -1) - { - MatchedGroup = g; - MatchedLayer = l; - } - if(++Matches > s_Selected) - { - s_Selected++; - MatchedGroup = g; - MatchedLayer = l; - IsFound = true; - } - } - } - } - if(MatchedGroup != -1 && MatchedLayer != -1) - { - if(!IsFound) - s_Selected = 1; - SelectLayer(MatchedLayer, MatchedGroup); - return true; - } - } - else - s_CtrlClick = false; - return false; -} - bool CEditor::ReplaceImage(const char *pFileName, int StorageType, bool CheckDuplicate) { // check if we have that image already @@ -4408,8 +4458,8 @@ bool CEditor::ReplaceImage(const char *pFileName, int StorageType, bool CheckDup } } - CEditorImage ImgInfo(this); - if(!Graphics()->LoadPNG(&ImgInfo, pFileName, StorageType)) + CImageInfo ImgInfo; + if(!Graphics()->LoadPng(ImgInfo, pFileName, StorageType)) { ShowFileDialogError("Failed to load image from file '%s'.", pFileName); return false; @@ -4417,23 +4467,26 @@ bool CEditor::ReplaceImage(const char *pFileName, int StorageType, bool CheckDup std::shared_ptr pImg = m_Map.m_vpImages[m_SelectedImage]; Graphics()->UnloadTexture(&(pImg->m_Texture)); - free(pImg->m_pData); - pImg->m_pData = nullptr; - *pImg = ImgInfo; + pImg->Free(); + pImg->m_Width = ImgInfo.m_Width; + pImg->m_Height = ImgInfo.m_Height; + pImg->m_Format = ImgInfo.m_Format; + pImg->m_pData = ImgInfo.m_pData; str_copy(pImg->m_aName, aBuf); pImg->m_External = IsVanillaImage(pImg->m_aName); - if(!pImg->m_External && g_Config.m_ClEditorDilate == 1 && pImg->m_Format == CImageInfo::FORMAT_RGBA) + ConvertToRgba(*pImg); + if(g_Config.m_ClEditorDilate == 1) { - DilateImage((unsigned char *)ImgInfo.m_pData, ImgInfo.m_Width, ImgInfo.m_Height); + DilateImage(*pImg); } pImg->m_AutoMapper.Load(pImg->m_aName); int TextureLoadFlag = Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; - if(ImgInfo.m_Width % 16 != 0 || ImgInfo.m_Height % 16 != 0) + if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0) TextureLoadFlag = 0; - pImg->m_Texture = Graphics()->LoadTextureRaw(ImgInfo.m_Width, ImgInfo.m_Height, ImgInfo.m_Format, ImgInfo.m_pData, TextureLoadFlag, pFileName); - ImgInfo.m_pData = nullptr; + pImg->m_Texture = Graphics()->LoadTextureRaw(*pImg, TextureLoadFlag, pFileName); + SortImages(); for(size_t i = 0; i < m_Map.m_vpImages.size(); ++i) { @@ -4472,27 +4525,30 @@ bool CEditor::AddImage(const char *pFileName, int StorageType, void *pUser) return false; } - CEditorImage ImgInfo(pEditor); - if(!pEditor->Graphics()->LoadPNG(&ImgInfo, pFileName, StorageType)) + CImageInfo ImgInfo; + if(!pEditor->Graphics()->LoadPng(ImgInfo, pFileName, StorageType)) { pEditor->ShowFileDialogError("Failed to load image from file '%s'.", pFileName); return false; } std::shared_ptr pImg = std::make_shared(pEditor); - *pImg = ImgInfo; + pImg->m_Width = ImgInfo.m_Width; + pImg->m_Height = ImgInfo.m_Height; + pImg->m_Format = ImgInfo.m_Format; + pImg->m_pData = ImgInfo.m_pData; pImg->m_External = IsVanillaImage(aBuf); - if(!pImg->m_External && g_Config.m_ClEditorDilate == 1 && pImg->m_Format == CImageInfo::FORMAT_RGBA) + ConvertToRgba(*pImg); + if(g_Config.m_ClEditorDilate == 1) { - DilateImage((unsigned char *)ImgInfo.m_pData, ImgInfo.m_Width, ImgInfo.m_Height); + DilateImage(*pImg); } int TextureLoadFlag = pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; - if(ImgInfo.m_Width % 16 != 0 || ImgInfo.m_Height % 16 != 0) + if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0) TextureLoadFlag = 0; - pImg->m_Texture = pEditor->Graphics()->LoadTextureRaw(ImgInfo.m_Width, ImgInfo.m_Height, ImgInfo.m_Format, ImgInfo.m_pData, TextureLoadFlag, pFileName); - ImgInfo.m_pData = nullptr; + pImg->m_Texture = pEditor->Graphics()->LoadTextureRaw(*pImg, TextureLoadFlag, pFileName); str_copy(pImg->m_aName, aBuf); pImg->m_AutoMapper.Load(pImg->m_aName); pEditor->m_Map.m_vpImages.push_back(pImg); @@ -4553,7 +4609,7 @@ bool CEditor::AddSound(const char *pFileName, int StorageType, void *pUser) // add sound std::shared_ptr pSound = std::make_shared(pEditor); - pSound->m_SoundID = SoundId; + pSound->m_SoundId = SoundId; pSound->m_DataSize = DataSize; pSound->m_pData = pData; str_copy(pSound->m_aName, aBuf); @@ -4612,12 +4668,12 @@ bool CEditor::ReplaceSound(const char *pFileName, int StorageType, bool CheckDup std::shared_ptr pSound = m_Map.m_vpSounds[m_SelectedSound]; // unload sample - Sound()->UnloadSample(pSound->m_SoundID); + Sound()->UnloadSample(pSound->m_SoundId); free(pSound->m_pData); // replace sound str_copy(pSound->m_aName, aBuf); - pSound->m_SoundID = SoundId; + pSound->m_SoundId = SoundId; pSound->m_pData = pData; pSound->m_DataSize = DataSize; @@ -4753,7 +4809,7 @@ void CEditor::RenderImagesList(CUIRect ToolBox) CUIRect Slot; ToolBox.HSplitTop(RowHeight + 3.0f, &Slot, &ToolBox); if(s_ScrollRegion.AddRect(Slot)) - UI()->DoLabel(&Slot, e == 0 ? "Embedded" : "External", 12.0f, TEXTALIGN_MC); + Ui()->DoLabel(&Slot, e == 0 ? "Embedded" : "External", 12.0f, TEXTALIGN_MC); for(int i = 0; i < (int)m_Map.m_vpImages.size(); i++) { @@ -4798,9 +4854,9 @@ void CEditor::RenderImagesList(CUIRect ToolBox) if(Result == 2) { const std::shared_ptr pImg = m_Map.m_vpImages[m_SelectedImage]; - const int Height = !pImg->m_External && IsVanillaImage(pImg->m_aName) ? 107 : (pImg->m_External ? 73 : 90); + const int Height = pImg->m_External ? 73 : 107; static SPopupMenuId s_PopupImageId; - UI()->DoPopupMenu(&s_PopupImageId, UI()->MouseX(), UI()->MouseY(), 140, Height, this, PopupImage); + Ui()->DoPopupMenu(&s_PopupImageId, Ui()->MouseX(), Ui()->MouseY(), 140, Height, this, PopupImage); } } } @@ -4825,8 +4881,8 @@ void CEditor::RenderImagesList(CUIRect ToolBox) { AddImageButton.HSplitTop(5.0f, nullptr, &AddImageButton); AddImageButton.HSplitTop(RowHeight, &AddImageButton, nullptr); - if(DoButton_Editor(&s_AddImageButton, "Add", 0, &AddImageButton, 0, "Load a new image to use in the map")) - InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_IMG, "Add Image", "Add", "mapres", false, AddImage, this); + if(DoButton_Editor(&s_AddImageButton, m_QuickActionAddImage.Label(), 0, &AddImageButton, 0, m_QuickActionAddImage.Description())) + m_QuickActionAddImage.Call(); } s_ScrollRegion.End(); } @@ -4885,7 +4941,7 @@ void CEditor::RenderSounds(CUIRect ToolBox) CUIRect Slot; ToolBox.HSplitTop(RowHeight + 3.0f, &Slot, &ToolBox); if(s_ScrollRegion.AddRect(Slot)) - UI()->DoLabel(&Slot, "Embedded", 12.0f, TEXTALIGN_MC); + Ui()->DoLabel(&Slot, "Embedded", 12.0f, TEXTALIGN_MC); for(int i = 0; i < (int)m_Map.m_vpSounds.size(); i++) { @@ -4914,7 +4970,7 @@ void CEditor::RenderSounds(CUIRect ToolBox) if(Result == 2) { static SPopupMenuId s_PopupSoundId; - UI()->DoPopupMenu(&s_PopupSoundId, UI()->MouseX(), UI()->MouseY(), 140, 90, this, PopupSound); + Ui()->DoPopupMenu(&s_PopupSoundId, Ui()->MouseX(), Ui()->MouseY(), 140, 90, this, PopupSound); } } } @@ -4996,8 +5052,8 @@ void CEditor::SortFilteredFileList() void CEditor::RenderFileDialog() { // GUI coordsys - UI()->MapScreen(); - CUIRect View = *UI()->Screen(); + Ui()->MapScreen(); + CUIRect View = *Ui()->Screen(); CUIRect Preview = {0.0f, 0.0f, 0.0f, 0.0f}; float Width = View.w, Height = View.h; @@ -5075,7 +5131,7 @@ void CEditor::RenderFileDialog() Title.Draw(ColorRGBA(1, 1, 1, 0.25f), IGraphics::CORNER_ALL, 4.0f); Title.VMargin(10.0f, &Title); - UI()->DoLabel(&Title, m_pFileDialogTitle, 12.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Title, m_pFileDialogTitle, 12.0f, TEXTALIGN_ML); // pathbox if(m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType >= IStorage::TYPE_SAVE) @@ -5083,7 +5139,7 @@ void CEditor::RenderFileDialog() char aPath[IO_MAX_PATH_LENGTH], aBuf[128 + IO_MAX_PATH_LENGTH]; Storage()->GetCompletePath(m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType, m_pFileDialogPath, aPath, sizeof(aPath)); str_format(aBuf, sizeof(aBuf), "Current path: %s", aPath); - UI()->DoLabel(&PathBox, aBuf, 10.0f, TEXTALIGN_ML); + Ui()->DoLabel(&PathBox, aBuf, 10.0f, TEXTALIGN_ML); } const auto &&UpdateFileNameInput = [this]() { @@ -5099,11 +5155,11 @@ void CEditor::RenderFileDialog() // filebox static CListBox s_ListBox; - s_ListBox.SetActive(!UI()->IsPopupOpen()); + s_ListBox.SetActive(!Ui()->IsPopupOpen()); if(m_FileDialogStorageType == IStorage::TYPE_SAVE) { - UI()->DoLabel(&FileBoxLabel, "Filename:", 10.0f, TEXTALIGN_ML); + Ui()->DoLabel(&FileBoxLabel, "Filename:", 10.0f, TEXTALIGN_ML); if(DoEditBox(&m_FileDialogFileNameInput, &FileBox, 10.0f)) { // remove '/' and '\' @@ -5132,18 +5188,18 @@ void CEditor::RenderFileDialog() } if(m_FileDialogOpening) - UI()->SetActiveItem(&m_FileDialogFileNameInput); + Ui()->SetActiveItem(&m_FileDialogFileNameInput); } else { // render search bar - UI()->DoLabel(&FileBoxLabel, "Search:", 10.0f, TEXTALIGN_ML); + Ui()->DoLabel(&FileBoxLabel, "Search:", 10.0f, TEXTALIGN_ML); if(m_FileDialogOpening || (Input()->KeyPress(KEY_F) && Input()->ModifierIsPressed())) { - UI()->SetActiveItem(&m_FileDialogFilterInput); + Ui()->SetActiveItem(&m_FileDialogFilterInput); m_FileDialogFilterInput.SelectAll(); } - if(UI()->DoClearableEditBox(&m_FileDialogFilterInput, &FileBox, 10.0f)) + if(Ui()->DoClearableEditBox(&m_FileDialogFilterInput, &FileBox, 10.0f)) { RefreshFilteredFileList(); if(m_vpFilteredFileList.empty()) @@ -5188,11 +5244,13 @@ void CEditor::RenderFileDialog() { char aBuffer[IO_MAX_PATH_LENGTH]; str_format(aBuffer, sizeof(aBuffer), "%s/%s", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename); - if(Graphics()->LoadPNG(&m_FilePreviewImageInfo, aBuffer, m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType)) + CImageInfo PreviewImageInfo; + if(Graphics()->LoadPng(PreviewImageInfo, aBuffer, m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType)) { Graphics()->UnloadTexture(&m_FilePreviewImage); - m_FilePreviewImage = Graphics()->LoadTextureRaw(m_FilePreviewImageInfo.m_Width, m_FilePreviewImageInfo.m_Height, m_FilePreviewImageInfo.m_Format, m_FilePreviewImageInfo.m_pData, 0); - Graphics()->FreePNG(&m_FilePreviewImageInfo); + m_FilePreviewImageWidth = PreviewImageInfo.m_Width; + m_FilePreviewImageHeight = PreviewImageInfo.m_Height; + m_FilePreviewImage = Graphics()->LoadTextureRawMove(PreviewImageInfo, 0, aBuffer); m_FilePreviewState = PREVIEW_LOADED; } else @@ -5215,11 +5273,11 @@ void CEditor::RenderFileDialog() Preview.Margin(10.0f, &Preview); if(m_FilePreviewState == PREVIEW_LOADED) { - int w = m_FilePreviewImageInfo.m_Width; - int h = m_FilePreviewImageInfo.m_Height; - if(m_FilePreviewImageInfo.m_Width > Preview.w) + int w = m_FilePreviewImageWidth; + int h = m_FilePreviewImageHeight; + if(m_FilePreviewImageWidth > Preview.w) { - h = m_FilePreviewImageInfo.m_Height * Preview.w / m_FilePreviewImageInfo.m_Width; + h = m_FilePreviewImageHeight * Preview.w / m_FilePreviewImageWidth; w = Preview.w; } if(h > Preview.h) @@ -5239,7 +5297,7 @@ void CEditor::RenderFileDialog() { SLabelProperties Props; Props.m_MaxWidth = Preview.w; - UI()->DoLabel(&Preview, "Failed to load the image (check the local console for details).", 12.0f, TEXTALIGN_TL, Props); + Ui()->DoLabel(&Preview, "Failed to load the image (check the local console for details).", 12.0f, TEXTALIGN_TL, Props); } } else if(m_FileDialogFileType == CEditor::FILETYPE_SOUND) @@ -5257,7 +5315,7 @@ void CEditor::RenderFileDialog() { SLabelProperties Props; Props.m_MaxWidth = Preview.w; - UI()->DoLabel(&Preview, "Failed to load the sound (check the local console for details). Make sure you enabled sounds in the settings.", 12.0f, TEXTALIGN_TL, Props); + Ui()->DoLabel(&Preview, "Failed to load the sound (check the local console for details). Make sure you enabled sounds in the settings.", 12.0f, TEXTALIGN_TL, Props); } } } @@ -5304,20 +5362,20 @@ void CEditor::RenderFileDialog() TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING); - UI()->DoLabel(&FileIcon, pIconType, 12.0f, TEXTALIGN_ML); + Ui()->DoLabel(&FileIcon, pIconType, 12.0f, TEXTALIGN_ML); TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); SLabelProperties Props; Props.m_MaxWidth = Button.w; Props.m_EllipsisAtEnd = true; - UI()->DoLabel(&Button, m_vpFilteredFileList[i]->m_aName, 10.0f, TEXTALIGN_ML, Props); + Ui()->DoLabel(&Button, m_vpFilteredFileList[i]->m_aName, 10.0f, TEXTALIGN_ML, Props); if(!m_vpFilteredFileList[i]->m_IsLink && str_comp(m_vpFilteredFileList[i]->m_aFilename, "..") != 0) { char aBufTimeModified[64]; str_timestamp_ex(m_vpFilteredFileList[i]->m_TimeModified, aBufTimeModified, sizeof(aBufTimeModified), "%d.%m.%Y %H:%M"); - UI()->DoLabel(&TimeModified, aBufTimeModified, 10.0f, TEXTALIGN_MR); + Ui()->DoLabel(&TimeModified, aBufTimeModified, 10.0f, TEXTALIGN_MR); } } @@ -5346,12 +5404,12 @@ void CEditor::RenderFileDialog() CUIRect Button; ButtonBar.VSplitRight(50.0f, &ButtonBar, &Button); const bool IsDir = m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsDir; - if(DoButton_Editor(&s_OkButton, IsDir ? "Open" : m_pFileDialogButtonText, 0, &Button, 0, nullptr) || s_ListBox.WasItemActivated() || (s_ListBox.Active() && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + if(DoButton_Editor(&s_OkButton, IsDir ? "Open" : m_pFileDialogButtonText, 0, &Button, 0, nullptr) || s_ListBox.WasItemActivated() || (s_ListBox.Active() && Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) { if(IsDir) // folder { m_FileDialogFilterInput.Clear(); - UI()->SetActiveItem(&m_FileDialogFilterInput); + Ui()->SetActiveItem(&m_FileDialogFilterInput); const bool ParentFolder = str_comp(m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, "..") == 0; if(ParentFolder) // parent folder { @@ -5425,7 +5483,7 @@ void CEditor::RenderFileDialog() ButtonBar.VSplitRight(ButtonSpacing, &ButtonBar, nullptr); ButtonBar.VSplitRight(50.0f, &ButtonBar, &Button); - if(DoButton_Editor(&s_CancelButton, "Cancel", 0, &Button, 0, nullptr) || (s_ListBox.Active() && UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE))) + if(DoButton_Editor(&s_CancelButton, "Cancel", 0, &Button, 0, nullptr) || (s_ListBox.Active() && Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE))) { OnDialogClose(); m_Dialog = DIALOG_NONE; @@ -5444,7 +5502,7 @@ void CEditor::RenderFileDialog() { char aOpenPath[IO_MAX_PATH_LENGTH]; Storage()->GetCompletePath(m_FilesSelectedIndex >= 0 ? m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType : IStorage::TYPE_SAVE, m_pFileDialogPath, aOpenPath, sizeof(aOpenPath)); - if(!open_file(aOpenPath)) + if(!Client()->ViewFile(aOpenPath)) { ShowFileDialogError("Failed to open the directory '%s'.", aOpenPath); } @@ -5453,17 +5511,17 @@ void CEditor::RenderFileDialog() ButtonBar.VSplitRight(ButtonSpacing, &ButtonBar, nullptr); ButtonBar.VSplitRight(50.0f, &ButtonBar, &Button); - static CUI::SConfirmPopupContext s_ConfirmDeletePopupContext; + static CUi::SConfirmPopupContext s_ConfirmDeletePopupContext; if(m_FilesSelectedIndex >= 0 && m_vpFilteredFileList[m_FilesSelectedIndex]->m_StorageType == IStorage::TYPE_SAVE && !m_vpFilteredFileList[m_FilesSelectedIndex]->m_IsLink && str_comp(m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename, "..") != 0) { - if(DoButton_Editor(&s_DeleteButton, "Delete", 0, &Button, 0, nullptr) || (s_ListBox.Active() && UI()->ConsumeHotkey(CUI::HOTKEY_DELETE))) + if(DoButton_Editor(&s_DeleteButton, "Delete", 0, &Button, 0, nullptr) || (s_ListBox.Active() && Ui()->ConsumeHotkey(CUi::HOTKEY_DELETE))) { s_ConfirmDeletePopupContext.Reset(); s_ConfirmDeletePopupContext.YesNoButtons(); str_format(s_ConfirmDeletePopupContext.m_aMessage, sizeof(s_ConfirmDeletePopupContext.m_aMessage), "Are you sure that you want to delete the %s '%s/%s'?", IsDir ? "folder" : "file", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename); - UI()->ShowPopupConfirm(UI()->MouseX(), UI()->MouseY(), &s_ConfirmDeletePopupContext); + Ui()->ShowPopupConfirm(Ui()->MouseX(), Ui()->MouseY(), &s_ConfirmDeletePopupContext); } - if(s_ConfirmDeletePopupContext.m_Result == CUI::SConfirmPopupContext::CONFIRMED) + if(s_ConfirmDeletePopupContext.m_Result == CUi::SConfirmPopupContext::CONFIRMED) { char aDeleteFilePath[IO_MAX_PATH_LENGTH]; str_format(aDeleteFilePath, sizeof(aDeleteFilePath), "%s/%s", m_pFileDialogPath, m_vpFilteredFileList[m_FilesSelectedIndex]->m_aFilename); @@ -5483,7 +5541,7 @@ void CEditor::RenderFileDialog() } UpdateFileNameInput(); } - if(s_ConfirmDeletePopupContext.m_Result != CUI::SConfirmPopupContext::UNSET) + if(s_ConfirmDeletePopupContext.m_Result != CUi::SConfirmPopupContext::UNSET) s_ConfirmDeletePopupContext.Reset(); } else @@ -5498,8 +5556,8 @@ void CEditor::RenderFileDialog() static SPopupMenuId s_PopupNewFolderId; constexpr float PopupWidth = 400.0f; constexpr float PopupHeight = 110.0f; - UI()->DoPopupMenu(&s_PopupNewFolderId, Width / 2.0f - PopupWidth / 2.0f, Height / 2.0f - PopupHeight / 2.0f, PopupWidth, PopupHeight, this, PopupNewFolder); - UI()->SetActiveItem(&m_FileDialogNewFolderNameInput); + Ui()->DoPopupMenu(&s_PopupNewFolderId, Width / 2.0f - PopupWidth / 2.0f, Height / 2.0f - PopupHeight / 2.0f, PopupWidth, PopupHeight, this, PopupNewFolder); + Ui()->SetActiveItem(&m_FileDialogNewFolderNameInput); } } } @@ -5637,7 +5695,7 @@ void CEditor::InvokeFileDialog(int StorageType, int FileType, const char *pTitle m_FileDialogMultipleStorages = false; } - UI()->ClosePopupMenus(); + Ui()->ClosePopupMenus(); m_pFileDialogTitle = pTitle; m_pFileDialogButtonText = pButtonText; m_pfnFileDialogFunc = pfnFunc; @@ -5676,20 +5734,20 @@ void CEditor::ShowFileDialogError(const char *pFormat, ...) va_end(VarArgs); auto ContextIterator = m_PopupMessageContexts.find(aMessage); - CUI::SMessagePopupContext *pContext; + CUi::SMessagePopupContext *pContext; if(ContextIterator != m_PopupMessageContexts.end()) { pContext = ContextIterator->second; - UI()->ClosePopupMenu(pContext); + Ui()->ClosePopupMenu(pContext); } else { - pContext = new CUI::SMessagePopupContext(); + pContext = new CUi::SMessagePopupContext(); pContext->ErrorColor(); str_copy(pContext->m_aMessage, aMessage); m_PopupMessageContexts[pContext->m_aMessage] = pContext; } - UI()->ShowPopupMessage(UI()->MouseX(), UI()->MouseY(), pContext); + Ui()->ShowPopupMessage(Ui()->MouseX(), Ui()->MouseY(), pContext); } void CEditor::RenderModebar(CUIRect View) @@ -5698,7 +5756,9 @@ void CEditor::RenderModebar(CUIRect View) View.HSplitTop(12.0f, &Mentions, &View); View.HSplitTop(12.0f, &IngameMoved, &View); View.HSplitTop(8.0f, nullptr, &ModeButtons); - ModeButtons.VSplitLeft(96.0f, &ModeButtons, nullptr); + const float Width = m_ToolBoxWidth - 5.0f; + ModeButtons.VSplitLeft(Width, &ModeButtons, nullptr); + const float ButtonWidth = Width / 3; // mentions if(m_Mentions) @@ -5712,7 +5772,7 @@ void CEditor::RenderModebar(CUIRect View) str_copy(aBuf, Localize("9+ new mentions")); TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f)); - UI()->DoLabel(&Mentions, aBuf, 10.0f, TEXTALIGN_MC); + Ui()->DoLabel(&Mentions, aBuf, 10.0f, TEXTALIGN_MC); TextRender()->TextColor(TextRender()->DefaultTextColor()); } @@ -5720,27 +5780,27 @@ void CEditor::RenderModebar(CUIRect View) if(m_IngameMoved) { TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f)); - UI()->DoLabel(&IngameMoved, Localize("Moved ingame"), 10.0f, TEXTALIGN_MC); + Ui()->DoLabel(&IngameMoved, Localize("Moved ingame"), 10.0f, TEXTALIGN_MC); TextRender()->TextColor(TextRender()->DefaultTextColor()); } // mode buttons { - ModeButtons.VSplitLeft(32.0f, &ModeButton, &ModeButtons); + ModeButtons.VSplitLeft(ButtonWidth, &ModeButton, &ModeButtons); static int s_LayersButton = 0; if(DoButton_FontIcon(&s_LayersButton, FONT_ICON_LAYER_GROUP, m_Mode == MODE_LAYERS, &ModeButton, 0, "Go to layers management.", IGraphics::CORNER_L)) { m_Mode = MODE_LAYERS; } - ModeButtons.VSplitLeft(32.0f, &ModeButton, &ModeButtons); + ModeButtons.VSplitLeft(ButtonWidth, &ModeButton, &ModeButtons); static int s_ImagesButton = 0; if(DoButton_FontIcon(&s_ImagesButton, FONT_ICON_IMAGE, m_Mode == MODE_IMAGES, &ModeButton, 0, "Go to images management.", IGraphics::CORNER_NONE)) { m_Mode = MODE_IMAGES; } - ModeButtons.VSplitLeft(32.0f, &ModeButton, &ModeButtons); + ModeButtons.VSplitLeft(ButtonWidth, &ModeButton, &ModeButtons); static int s_SoundsButton = 0; if(DoButton_FontIcon(&s_SoundsButton, FONT_ICON_MUSIC, m_Mode == MODE_SOUNDS, &ModeButton, 0, "Go to sounds management.", IGraphics::CORNER_R)) { @@ -5760,30 +5820,25 @@ void CEditor::RenderModebar(CUIRect View) void CEditor::RenderStatusbar(CUIRect View, CUIRect *pTooltipRect) { - const bool ButtonsDisabled = m_ShowPicker; - CUIRect Button; View.VSplitRight(100.0f, &View, &Button); - static int s_EnvelopeButton = 0; - if(DoButton_Editor(&s_EnvelopeButton, "Envelopes", ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES, &Button, 0, "Toggles the envelope editor.") == 1) + if(DoButton_Editor(&m_QuickActionEnvelopes, m_QuickActionEnvelopes.Label(), m_QuickActionEnvelopes.Color(), &Button, 0, m_QuickActionEnvelopes.Description()) == 1) { - m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES ? EXTRAEDITOR_NONE : EXTRAEDITOR_ENVELOPES; + m_QuickActionEnvelopes.Call(); } View.VSplitRight(10.0f, &View, nullptr); View.VSplitRight(100.0f, &View, &Button); - static int s_SettingsButton = 0; - if(DoButton_Editor(&s_SettingsButton, "Server settings", ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS, &Button, 0, "Toggles the server settings editor.") == 1) + if(DoButton_Editor(&m_QuickActionServerSettings, m_QuickActionServerSettings.Label(), m_QuickActionServerSettings.Color(), &Button, 0, m_QuickActionServerSettings.Description()) == 1) { - m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS ? EXTRAEDITOR_NONE : EXTRAEDITOR_SERVER_SETTINGS; + m_QuickActionServerSettings.Call(); } View.VSplitRight(10.0f, &View, nullptr); View.VSplitRight(100.0f, &View, &Button); - static int s_HistoryButton = 0; - if(DoButton_Editor(&s_HistoryButton, "History", ButtonsDisabled ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_HISTORY, &Button, 0, "Toggles the editor history view.") == 1) + if(DoButton_Editor(&m_QuickActionHistory, m_QuickActionHistory.Label(), m_QuickActionHistory.Color(), &Button, 0, m_QuickActionHistory.Description()) == 1) { - m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_HISTORY ? EXTRAEDITOR_NONE : EXTRAEDITOR_HISTORY; + m_QuickActionHistory.Call(); } View.VSplitRight(10.0f, pTooltipRect, nullptr); @@ -5795,7 +5850,7 @@ void CEditor::RenderTooltip(CUIRect TooltipRect) return; char aBuf[256]; - if(ms_pUiGotContext && ms_pUiGotContext == UI()->HotItem()) + if(ms_pUiGotContext && ms_pUiGotContext == Ui()->HotItem()) str_format(aBuf, sizeof(aBuf), "%s Right click for context menu.", m_aTooltip); else str_copy(aBuf, m_aTooltip); @@ -5803,7 +5858,7 @@ void CEditor::RenderTooltip(CUIRect TooltipRect) SLabelProperties Props; Props.m_MaxWidth = TooltipRect.w; Props.m_EllipsisAtEnd = true; - UI()->DoLabel(&TooltipRect, aBuf, 10.0f, TEXTALIGN_ML, Props); + Ui()->DoLabel(&TooltipRect, aBuf, 10.0f, TEXTALIGN_ML, Props); } bool CEditor::IsEnvelopeUsed(int EnvelopeIndex) const @@ -5847,6 +5902,8 @@ bool CEditor::IsEnvelopeUsed(int EnvelopeIndex) const void CEditor::RemoveUnusedEnvelopes() { + m_EnvelopeEditorHistory.BeginBulk(); + int DeletedCount = 0; for(size_t Envelope = 0; Envelope < m_Map.m_vpEnvelopes.size();) { if(IsEnvelopeUsed(Envelope)) @@ -5855,14 +5912,19 @@ void CEditor::RemoveUnusedEnvelopes() } else { + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, Envelope)); m_Map.DeleteEnvelope(Envelope); + DeletedCount++; } } + char aDisplay[256]; + str_format(aDisplay, sizeof(aDisplay), "Tool 'Remove unused envelopes': delete %d envelopes", DeletedCount); + m_EnvelopeEditorHistory.EndBulk(aDisplay); } void CEditor::ZoomAdaptOffsetX(float ZoomFactor, const CUIRect &View) { - float PosX = g_Config.m_EdZoomTarget ? (UI()->MouseX() - View.x) / View.w : 0.5f; + float PosX = g_Config.m_EdZoomTarget ? (Ui()->MouseX() - View.x) / View.w : 0.5f; m_OffsetEnvelopeX = PosX - (PosX - m_OffsetEnvelopeX) * ZoomFactor; } @@ -5875,7 +5937,7 @@ void CEditor::UpdateZoomEnvelopeX(const CUIRect &View) void CEditor::ZoomAdaptOffsetY(float ZoomFactor, const CUIRect &View) { - float PosY = g_Config.m_EdZoomTarget ? 1.0f - (UI()->MouseY() - View.y) / View.h : 0.5f; + float PosY = g_Config.m_EdZoomTarget ? 1.0f - (Ui()->MouseY() - View.y) / View.h : 0.5f; m_OffsetEnvelopeY = PosY - (PosY - m_OffsetEnvelopeY) * ZoomFactor; } @@ -5970,14 +6032,14 @@ float CEditor::EnvelopeToScreenY(const CUIRect &View, float y) const return View.y + View.h - y / m_ZoomEnvelopeY.GetValue() * View.h - m_OffsetEnvelopeY * View.h; } -float CEditor::ScreenToEnvelopeDX(const CUIRect &View, float dx) +float CEditor::ScreenToEnvelopeDX(const CUIRect &View, float DeltaX) { - return dx / Graphics()->ScreenWidth() * UI()->Screen()->w / View.w * m_ZoomEnvelopeX.GetValue(); + return DeltaX / Graphics()->ScreenWidth() * Ui()->Screen()->w / View.w * m_ZoomEnvelopeX.GetValue(); } -float CEditor::ScreenToEnvelopeDY(const CUIRect &View, float dy) +float CEditor::ScreenToEnvelopeDY(const CUIRect &View, float DeltaY) { - return dy / Graphics()->ScreenHeight() * UI()->Screen()->h / View.h * m_ZoomEnvelopeY.GetValue(); + return DeltaY / Graphics()->ScreenHeight() * Ui()->Screen()->h / View.h * m_ZoomEnvelopeY.GetValue(); } void CEditor::RemoveTimeOffsetEnvelope(const std::shared_ptr &pEnvelope) @@ -6075,16 +6137,16 @@ class CTimeStep void CEditor::SetHotEnvelopePoint(const CUIRect &View, const std::shared_ptr &pEnvelope, int ActiveChannels) { - if(!UI()->MouseInside(&View)) + if(!Ui()->MouseInside(&View)) return; - float mx = UI()->MouseX(); - float my = UI()->MouseY(); + float mx = Ui()->MouseX(); + float my = Ui()->MouseY(); float MinDist = 200.0f; int *pMinPoint = nullptr; - auto UpdateMinimum = [&](float px, float py, int *pID) { + auto UpdateMinimum = [&](float px, float py, int *pId) { float dx = px - mx; float dy = py - my; @@ -6092,7 +6154,7 @@ void CEditor::SetHotEnvelopePoint(const CUIRect &View, const std::shared_ptrSetHotItem(pMinPoint); + Ui()->SetHotItem(pMinPoint); } void CEditor::RenderEnvelopeEditor(CUIRect View) @@ -6151,7 +6213,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) DragBar.y -= 2.0f; DragBar.w += 2.0f; DragBar.h += 4.0f; - RenderExtraEditorDragBar(View, DragBar); + DoEditorDragBar(View, &DragBar, EDragSide::SIDE_TOP, &m_aExtraEditorSplits[EXTRAEDITOR_ENVELOPES]); View.HSplitTop(15.0f, &ToolBar, &View); View.HSplitTop(15.0f, &CurveBar, &View); ToolBar.Margin(2.0f, &ToolBar); @@ -6254,7 +6316,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); ToolBar.VSplitRight(20.0f, &ToolBar, &Button); static int s_ZoomOutButton = 0; - if(DoButton_FontIcon(&s_ZoomOutButton, "-", 0, &Button, 0, "[NumPad-] Zoom out horizontally, hold shift to zoom vertically", IGraphics::CORNER_R, 9.0f)) + if(DoButton_FontIcon(&s_ZoomOutButton, FONT_ICON_MINUS, 0, &Button, 0, "[NumPad-] Zoom out horizontally, hold shift to zoom vertically", IGraphics::CORNER_R, 9.0f)) { if(Input()->ShiftIsPressed()) m_ZoomEnvelopeY.ChangeValue(0.1f * m_ZoomEnvelopeY.GetValue()); @@ -6269,7 +6331,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) ToolBar.VSplitRight(20.0f, &ToolBar, &Button); static int s_ZoomInButton = 0; - if(DoButton_FontIcon(&s_ZoomInButton, "+", 0, &Button, 0, "[NumPad+] Zoom in horizontally, hold shift to zoom vertically", IGraphics::CORNER_L, 9.0f)) + if(DoButton_FontIcon(&s_ZoomInButton, FONT_ICON_PLUS, 0, &Button, 0, "[NumPad+] Zoom in horizontally, hold shift to zoom vertically", IGraphics::CORNER_L, 9.0f)) { if(Input()->ShiftIsPressed()) m_ZoomEnvelopeY.ChangeValue(-0.1f * m_ZoomEnvelopeY.GetValue()); @@ -6321,7 +6383,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } static int s_PrevButton = 0; - if(DoButton_ButtonDec(&s_PrevButton, nullptr, 0, &Dec, 0, "Previous Envelope")) + if(DoButton_FontIcon(&s_PrevButton, FONT_ICON_MINUS, 0, &Dec, 0, "Previous Envelope", IGraphics::CORNER_L, 7.0f)) { m_SelectedEnvelope--; if(m_SelectedEnvelope < 0) @@ -6330,7 +6392,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } static int s_NextButton = 0; - if(DoButton_ButtonInc(&s_NextButton, nullptr, 0, &Inc, 0, "Next Envelope")) + if(DoButton_FontIcon(&s_NextButton, FONT_ICON_PLUS, 0, &Inc, 0, "Next Envelope", IGraphics::CORNER_R, 7.0f)) { m_SelectedEnvelope++; if(m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size()) @@ -6342,7 +6404,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { ToolBar.VSplitLeft(15.0f, nullptr, &ToolBar); ToolBar.VSplitLeft(40.0f, &Button, &ToolBar); - UI()->DoLabel(&Button, "Name:", 10.0f, TEXTALIGN_MR); + Ui()->DoLabel(&Button, "Name:", 10.0f, TEXTALIGN_MR); ToolBar.VSplitLeft(3.0f, nullptr, &ToolBar); ToolBar.VSplitLeft(ToolBar.w > ToolBar.h * 40 ? 80.0f : 60.0f, &Button, &ToolBar); @@ -6373,8 +6435,6 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) ResetZoomEnvelope(pEnvelope, s_ActiveChannels); } - static int s_EnvelopeEditorID = 0; - ColorRGBA aColors[] = {ColorRGBA(1, 0.2f, 0.2f), ColorRGBA(0.2f, 1, 0.2f), ColorRGBA(0.2f, 0.2f, 1), ColorRGBA(1, 1, 0.2f)}; CUIRect Button; @@ -6416,30 +6476,39 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } - // sync checkbox + // toggle sync button ToolBar.VSplitLeft(15.0f, nullptr, &ToolBar); - ToolBar.VSplitLeft(12.0f, &Button, &ToolBar); + ToolBar.VSplitLeft(40.0f, &Button, &ToolBar); + static int s_SyncButton; - if(DoButton_Editor(&s_SyncButton, pEnvelope->m_Synchronized ? "X" : "", 0, &Button, 0, "Synchronize envelope animation to game time (restarts when you touch the start line)")) + if(DoButton_Editor(&s_SyncButton, "Sync", pEnvelope->m_Synchronized, &Button, 0, "Synchronize envelope animation to game time (restarts when you touch the start line)")) { m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, m_SelectedEnvelope, CEditorActionEnvelopeEdit::EEditType::SYNC, pEnvelope->m_Synchronized, !pEnvelope->m_Synchronized)); pEnvelope->m_Synchronized = !pEnvelope->m_Synchronized; m_Map.OnModify(); } - ToolBar.VSplitLeft(4.0f, nullptr, &ToolBar); - ToolBar.VSplitLeft(40.0f, &Button, &ToolBar); - UI()->DoLabel(&Button, "Sync.", 10.0f, TEXTALIGN_ML); - - if(UI()->MouseInside(&View) && m_Dialog == DIALOG_NONE) + static int s_EnvelopeEditorId = 0; + static int s_EnvelopeEditorButtonUsed = -1; + const bool ShouldPan = s_Operation == EEnvelopeEditorOp::OP_NONE && (Ui()->MouseButton(2) || (Ui()->MouseButton(0) && Input()->ModifierIsPressed())); + if(m_pContainerPanned == &s_EnvelopeEditorId) { - UI()->SetHotItem(&s_EnvelopeEditorID); - - if(s_Operation == EEnvelopeEditorOp::OP_NONE && (UI()->MouseButton(2) || (UI()->MouseButton(0) && Input()->ModifierIsPressed()))) + if(!ShouldPan) + m_pContainerPanned = nullptr; + else { - m_OffsetEnvelopeX += UI()->MouseDeltaX() / Graphics()->ScreenWidth() * UI()->Screen()->w / View.w; - m_OffsetEnvelopeY -= UI()->MouseDeltaY() / Graphics()->ScreenHeight() * UI()->Screen()->h / View.h; + m_OffsetEnvelopeX += Ui()->MouseDeltaX() / Graphics()->ScreenWidth() * Ui()->Screen()->w / View.w; + m_OffsetEnvelopeY -= Ui()->MouseDeltaY() / Graphics()->ScreenHeight() * Ui()->Screen()->h / View.h; } + } + + if(Ui()->MouseInside(&View) && m_Dialog == DIALOG_NONE) + { + Ui()->SetHotItem(&s_EnvelopeEditorId); + + if(ShouldPan && m_pContainerPanned == nullptr) + m_pContainerPanned = &s_EnvelopeEditorId; + if(Input()->KeyPress(KEY_KP_MULTIPLY)) ResetZoomEnvelope(pEnvelope, s_ActiveChannels); if(Input()->ShiftIsPressed()) @@ -6466,20 +6535,28 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } - if(UI()->HotItem() == &s_EnvelopeEditorID) + if(Ui()->HotItem() == &s_EnvelopeEditorId) { // do stuff - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) + { + s_EnvelopeEditorButtonUsed = 0; + if(s_Operation != EEnvelopeEditorOp::OP_BOX_SELECT && !Input()->ModifierIsPressed()) + { + s_Operation = EEnvelopeEditorOp::OP_BOX_SELECT; + s_MouseXStart = Ui()->MouseX(); + s_MouseYStart = Ui()->MouseY(); + } + } + else if(s_EnvelopeEditorButtonUsed == 0) { - if(Input()->MouseDoubleClick()) + if(Ui()->DoDoubleClickLogic(&s_EnvelopeEditorId)) { // add point - float Time = ScreenToEnvelopeX(View, UI()->MouseX()); - ColorRGBA Channels; + float Time = ScreenToEnvelopeX(View, Ui()->MouseX()); + ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); if(in_range(Time, 0.0f, pEnvelope->EndTime())) - pEnvelope->Eval(Time, Channels); - else - Channels = {0, 0, 0, 0}; + pEnvelope->Eval(Time, Channels, 4); int FixedTime = std::round(Time * 1000.0f); bool TimeFound = false; @@ -6496,14 +6573,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) RemoveTimeOffsetEnvelope(pEnvelope); m_Map.OnModify(); } - else if(s_Operation != EEnvelopeEditorOp::OP_BOX_SELECT && !Input()->ModifierIsPressed()) - { - static int s_BoxSelectID = 0; - UI()->SetActiveItem(&s_BoxSelectID); - s_Operation = EEnvelopeEditorOp::OP_BOX_SELECT; - s_MouseXStart = UI()->MouseX(); - s_MouseYStart = UI()->MouseY(); - } + s_EnvelopeEditorButtonUsed = -1; } m_ShowEnvelopePreview = SHOWENV_SELECTED; @@ -6521,9 +6591,9 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(Value / m_ZoomEnvelopeY.GetValue() * View.h < 40.0f) UnitsPerLineY = Value; } - int NumLinesY = m_ZoomEnvelopeY.GetValue() / static_cast(UnitsPerLineY) + 1; + int NumLinesY = m_ZoomEnvelopeY.GetValue() / UnitsPerLineY + 1; - UI()->ClipEnable(&View); + Ui()->ClipEnable(&View); Graphics()->TextureClear(); Graphics()->LinesBegin(); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.2f); @@ -6538,23 +6608,23 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Graphics()->LinesEnd(); - UI()->TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.4f); + Ui()->TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.4f); for(int i = 0; i <= NumLinesY; i++) { float Value = UnitsPerLineY * i - BaseValue; char aValueBuffer[16]; if(UnitsPerLineY >= 1.0f) { - str_from_int(static_cast(Value), aValueBuffer); + str_format(aValueBuffer, sizeof(aValueBuffer), "%d", static_cast(Value)); } else { str_format(aValueBuffer, sizeof(aValueBuffer), "%.3f", Value); } - UI()->TextRender()->Text(View.x, EnvelopeToScreenY(View, Value) + 4.0f, 8.0f, aValueBuffer); + Ui()->TextRender()->Text(View.x, EnvelopeToScreenY(View, Value) + 4.0f, 8.0f, aValueBuffer); } - UI()->TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - UI()->ClipDisable(); + Ui()->TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + Ui()->ClipDisable(); } { @@ -6566,9 +6636,9 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(Value.AsSeconds() / m_ZoomEnvelopeX.GetValue() * View.w < 160.0f) UnitsPerLineX = Value; } - int NumLinesX = m_ZoomEnvelopeX.GetValue() / static_cast(UnitsPerLineX.AsSeconds()) + 1; + int NumLinesX = m_ZoomEnvelopeX.GetValue() / UnitsPerLineX.AsSeconds() + 1; - UI()->ClipEnable(&View); + Ui()->ClipEnable(&View); Graphics()->TextureClear(); Graphics()->LinesBegin(); Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.2f); @@ -6583,7 +6653,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Graphics()->LinesEnd(); - UI()->TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.4f); + Ui()->TextRender()->TextColor(1.0f, 1.0f, 1.0f, 0.4f); for(int i = 0; i <= NumLinesX; i++) { CTimeStep Value = UnitsPerLineX * i - BaseValue; @@ -6592,16 +6662,16 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) char aValueBuffer[16]; Value.Format(aValueBuffer, sizeof(aValueBuffer)); - UI()->TextRender()->Text(EnvelopeToScreenX(View, Value.AsSeconds()) + 1.0f, View.y + View.h - 8.0f, 8.0f, aValueBuffer); + Ui()->TextRender()->Text(EnvelopeToScreenX(View, Value.AsSeconds()) + 1.0f, View.y + View.h - 8.0f, 8.0f, aValueBuffer); } } - UI()->TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); - UI()->ClipDisable(); + Ui()->TextRender()->TextColor(1.0f, 1.0f, 1.0f, 1.0f); + Ui()->ClipDisable(); } // render tangents for bezier curves { - UI()->ClipEnable(&View); + Ui()->ClipEnable(&View); Graphics()->TextureClear(); Graphics()->LinesBegin(); for(int c = 0; c < pEnvelope->GetChannels(); c++) @@ -6646,7 +6716,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } Graphics()->LinesEnd(); - UI()->ClipDisable(); + Ui()->ClipDisable(); } // render lines @@ -6658,7 +6728,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) float EndTime = ScreenToEnvelopeX(View, EndX); float StartTime = ScreenToEnvelopeX(View, StartX); - UI()->ClipEnable(&View); + Ui()->ClipEnable(&View); Graphics()->TextureClear(); Graphics()->LinesBegin(); for(int c = 0; c < pEnvelope->GetChannels(); c++) @@ -6668,16 +6738,17 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) else Graphics()->SetColor(aColors[c].r * 0.5f, aColors[c].g * 0.5f, aColors[c].b * 0.5f, 1); - int Steps = static_cast(((EndX - StartX) / UI()->Screen()->w) * Graphics()->ScreenWidth()); + int Steps = static_cast(((EndX - StartX) / Ui()->Screen()->w) * Graphics()->ScreenWidth()); float StepTime = (EndTime - StartTime) / static_cast(Steps); float StepSize = (EndX - StartX) / static_cast(Steps); - ColorRGBA Channels; - pEnvelope->Eval(StartTime + StepTime, Channels); + ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + pEnvelope->Eval(StartTime, Channels, c + 1); float PrevY = EnvelopeToScreenY(View, Channels[c]); - for(int i = 2; i < Steps; i++) + for(int i = 1; i < Steps; i++) { - pEnvelope->Eval(StartTime + i * StepTime, Channels); + Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + pEnvelope->Eval(StartTime + i * StepTime, Channels, c + 1); float CurrentY = EnvelopeToScreenY(View, Channels[c]); IGraphics::CLineItem LineItem( @@ -6691,7 +6762,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } Graphics()->LinesEnd(); - UI()->ClipDisable(); + Ui()->ClipDisable(); } // render curve options @@ -6707,7 +6778,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) CurveButton.h = CurveBar.h; CurveButton.w = CurveBar.h; CurveButton.x -= CurveButton.w / 2.0f; - const void *pID = &pEnvelope->m_vPoints[i].m_Curvetype; + const void *pId = &pEnvelope->m_vPoints[i].m_Curvetype; const char *apTypeName[] = {"N", "L", "S", "F", "M", "B"}; const char *pTypeName = "!?"; if(0 <= pEnvelope->m_vPoints[i].m_Curvetype && pEnvelope->m_vPoints[i].m_Curvetype < (int)std::size(apTypeName)) @@ -6715,7 +6786,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(CurveButton.x >= View.x) { - if(DoButton_Editor(pID, pTypeName, 0, &CurveButton, 0, "Switch curve type (N = step, L = linear, S = slow, F = fast, M = smooth, B = bezier)")) + if(DoButton_Editor(pId, pTypeName, 0, &CurveButton, 0, "Switch curve type (N = step, L = linear, S = slow, F = fast, M = smooth, B = bezier)")) { int PrevCurve = pEnvelope->m_vPoints[i].m_Curvetype; pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + 1) % NUM_CURVETYPES; @@ -6731,7 +6802,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) // render colorbar if(ShowColorBar) { - UI()->ClipEnable(&ColorBar); + Ui()->ClipEnable(&ColorBar); float StartX = maximum(EnvelopeToScreenX(View, 0), ColorBar.x); float EndX = EnvelopeToScreenX(View, pEnvelope->EndTime()); @@ -6768,7 +6839,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Graphics()->QuadsDrawTL(&QuadItem, 1); } Graphics()->QuadsEnd(); - UI()->ClipDisable(); + Ui()->ClipDisable(); } // render handles @@ -6781,13 +6852,13 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { static SPopupMenuId s_PopupEnvPointId; const auto &&ShowPopupEnvPoint = [&]() { - UI()->DoPopupMenu(&s_PopupEnvPointId, UI()->MouseX(), UI()->MouseY(), 150, 56 + (pEnvelope->GetChannels() == 4 ? 16.0f : 0.0f), this, PopupEnvPoint); + Ui()->DoPopupMenu(&s_PopupEnvPointId, Ui()->MouseX(), Ui()->MouseY(), 150, 56 + (pEnvelope->GetChannels() == 4 && !IsTangentSelected() ? 16.0f : 0.0f), this, PopupEnvPoint); }; if(s_Operation == EEnvelopeEditorOp::OP_NONE) { SetHotEnvelopePoint(View, pEnvelope, s_ActiveChannels); - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) m_EnvOpTracker.Stop(false); } else @@ -6795,7 +6866,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) m_EnvOpTracker.Begin(s_Operation); } - UI()->ClipEnable(&View); + Ui()->ClipEnable(&View); Graphics()->TextureClear(); Graphics()->QuadsBegin(); for(int c = 0; c < pEnvelope->GetChannels(); c++) @@ -6815,7 +6886,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Final.w = 4.0f; Final.h = 4.0f; - const void *pID = &pEnvelope->m_vPoints[i].m_aValues[c]; + const void *pId = &pEnvelope->m_vPoints[i].m_aValues[c]; if(IsEnvPointSelected(i, c)) { @@ -6829,14 +6900,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Graphics()->QuadsDrawTL(&QuadItem, 1); } - if(UI()->CheckActiveItem(pID)) + if(Ui()->CheckActiveItem(pId)) { m_ShowEnvelopePreview = SHOWENV_SELECTED; if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { - float dx = s_MouseXStart - UI()->MouseX(); - float dy = s_MouseYStart - UI()->MouseY(); + float dx = s_MouseXStart - Ui()->MouseX(); + float dy = s_MouseYStart - Ui()->MouseY(); if(dx * dx + dy * dy > 20.0f) { @@ -6860,7 +6931,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } else { - float DeltaX = ScreenToEnvelopeDX(View, UI()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f); + float DeltaX = ScreenToEnvelopeDX(View, Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f); for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++) { @@ -6911,7 +6982,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } else { - float DeltaY = ScreenToEnvelopeDY(View, UI()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f); + float DeltaY = ScreenToEnvelopeDY(View, Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f); for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++) { auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints[k]; @@ -6930,7 +7001,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU) { - if(!UI()->MouseButton(1)) + if(!Ui()->MouseButton(1)) { if(m_vSelectedEnvelopePoints.size() == 1) { @@ -6940,15 +7011,15 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) else if(m_vSelectedEnvelopePoints.size() > 1) { static SPopupMenuId s_PopupEnvPointMultiId; - UI()->DoPopupMenu(&s_PopupEnvPointMultiId, UI()->MouseX(), UI()->MouseY(), 80, 22, this, PopupEnvPointMulti); + Ui()->DoPopupMenu(&s_PopupEnvPointMultiId, Ui()->MouseX(), Ui()->MouseY(), 80, 22, this, PopupEnvPointMulti); } - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); s_Operation = EEnvelopeEditorOp::OP_NONE; } } - else if(!UI()->MouseButton(0)) + else if(!Ui()->MouseButton(0)) { - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); m_SelectedQuadEnvelope = -1; if(s_Operation == EEnvelopeEditorOp::OP_SELECT) @@ -6965,18 +7036,18 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Graphics()->SetColor(1, 1, 1, 1); } - else if(UI()->HotItem() == pID) + else if(Ui()->HotItem() == pId) { - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); s_Operation = EEnvelopeEditorOp::OP_SELECT; m_SelectedQuadEnvelope = m_SelectedEnvelope; - s_MouseXStart = UI()->MouseX(); - s_MouseYStart = UI()->MouseY(); + s_MouseXStart = Ui()->MouseX(); + s_MouseYStart = Ui()->MouseY(); } - else if(UI()->MouseButtonClicked(1)) + else if(Ui()->MouseButtonClicked(1)) { if(Input()->ShiftIsPressed()) { @@ -6987,14 +7058,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU; if(!IsEnvPointSelected(i, c)) SelectEnvPoint(i, c); - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); } } m_ShowEnvelopePreview = SHOWENV_SELECTED; Graphics()->SetColor(1, 1, 1, 1); str_copy(m_aTooltip, "Envelope point. Left mouse to drag. Hold ctrl to be more precise. Hold shift to alter time. Shift + right-click to delete."); - ms_pUiGotContext = pID; + ms_pUiGotContext = pId; } else Graphics()->SetColor(aColors[c].r, aColors[c].g, aColors[c].b, 1.0f); @@ -7018,7 +7089,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Final.h = 4.0f; // handle logic - const void *pID = &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]; + const void *pId = &pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c]; if(IsTangentOutPointSelected(i, c)) { @@ -7035,14 +7106,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Graphics()->QuadsDrawFreeform(&FreeformItem, 1); } - if(UI()->CheckActiveItem(pID)) + if(Ui()->CheckActiveItem(pId)) { m_ShowEnvelopePreview = SHOWENV_SELECTED; if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { - float dx = s_MouseXStart - UI()->MouseX(); - float dy = s_MouseYStart - UI()->MouseY(); + float dx = s_MouseXStart - Ui()->MouseX(); + float dy = s_MouseYStart - Ui()->MouseY(); if(dx * dx + dy * dy > 20.0f) { @@ -7058,8 +7129,8 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT) { - float DeltaX = ScreenToEnvelopeDX(View, UI()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f); - float DeltaY = ScreenToEnvelopeDY(View, UI()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f); + float DeltaX = ScreenToEnvelopeDX(View, Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f); + float DeltaY = ScreenToEnvelopeDY(View, Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f); s_vAccurateDragValuesX[0] += DeltaX; s_vAccurateDragValuesY[0] -= DeltaY; @@ -7073,19 +7144,20 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU) { - if(!UI()->MouseButton(1)) + if(!Ui()->MouseButton(1)) { if(IsTangentOutPointSelected(i, c)) { m_UpdateEnvPointInfo = true; ShowPopupEnvPoint(); } - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); + s_Operation = EEnvelopeEditorOp::OP_NONE; } } - else if(!UI()->MouseButton(0)) + else if(!Ui()->MouseButton(0)) { - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); m_SelectedQuadEnvelope = -1; if(s_Operation == EEnvelopeEditorOp::OP_SELECT) @@ -7097,18 +7169,18 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Graphics()->SetColor(1, 1, 1, 1); } - else if(UI()->HotItem() == pID) + else if(Ui()->HotItem() == pId) { - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); s_Operation = EEnvelopeEditorOp::OP_SELECT; m_SelectedQuadEnvelope = m_SelectedEnvelope; - s_MouseXStart = UI()->MouseX(); - s_MouseYStart = UI()->MouseY(); + s_MouseXStart = Ui()->MouseX(); + s_MouseYStart = Ui()->MouseY(); } - else if(UI()->MouseButtonClicked(1)) + else if(Ui()->MouseButtonClicked(1)) { if(Input()->ShiftIsPressed()) { @@ -7121,14 +7193,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU; SelectTangentOutPoint(i, c); - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); } } m_ShowEnvelopePreview = SHOWENV_SELECTED; Graphics()->SetColor(1, 1, 1, 1); str_copy(m_aTooltip, "Bezier out-tangent. Left mouse to drag. Hold ctrl to be more precise. Shift + right-click to reset."); - ms_pUiGotContext = pID; + ms_pUiGotContext = pId; } else Graphics()->SetColor(aColors[c].r, aColors[c].g, aColors[c].b, 1.0f); @@ -7150,7 +7222,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Final.h = 4.0f; // handle logic - const void *pID = &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]; + const void *pId = &pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c]; if(IsTangentInPointSelected(i, c)) { @@ -7167,14 +7239,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Graphics()->QuadsDrawFreeform(&FreeformItem, 1); } - if(UI()->CheckActiveItem(pID)) + if(Ui()->CheckActiveItem(pId)) { m_ShowEnvelopePreview = SHOWENV_SELECTED; if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { - float dx = s_MouseXStart - UI()->MouseX(); - float dy = s_MouseYStart - UI()->MouseY(); + float dx = s_MouseXStart - Ui()->MouseX(); + float dy = s_MouseYStart - Ui()->MouseY(); if(dx * dx + dy * dy > 20.0f) { @@ -7190,8 +7262,8 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT) { - float DeltaX = ScreenToEnvelopeDX(View, UI()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f); - float DeltaY = ScreenToEnvelopeDY(View, UI()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f); + float DeltaX = ScreenToEnvelopeDX(View, Ui()->MouseDeltaX()) * (Input()->ModifierIsPressed() ? 50.0f : 1000.0f); + float DeltaY = ScreenToEnvelopeDY(View, Ui()->MouseDeltaY()) * (Input()->ModifierIsPressed() ? 51.2f : 1024.0f); s_vAccurateDragValuesX[0] += DeltaX; s_vAccurateDragValuesY[0] -= DeltaY; @@ -7205,19 +7277,20 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU) { - if(!UI()->MouseButton(1)) + if(!Ui()->MouseButton(1)) { if(IsTangentInPointSelected(i, c)) { m_UpdateEnvPointInfo = true; ShowPopupEnvPoint(); } - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); + s_Operation = EEnvelopeEditorOp::OP_NONE; } } - else if(!UI()->MouseButton(0)) + else if(!Ui()->MouseButton(0)) { - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); m_SelectedQuadEnvelope = -1; if(s_Operation == EEnvelopeEditorOp::OP_SELECT) @@ -7229,18 +7302,18 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) Graphics()->SetColor(1, 1, 1, 1); } - else if(UI()->HotItem() == pID) + else if(Ui()->HotItem() == pId) { - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); s_Operation = EEnvelopeEditorOp::OP_SELECT; m_SelectedQuadEnvelope = m_SelectedEnvelope; - s_MouseXStart = UI()->MouseX(); - s_MouseYStart = UI()->MouseY(); + s_MouseXStart = Ui()->MouseX(); + s_MouseYStart = Ui()->MouseY(); } - else if(UI()->MouseButtonClicked(1)) + else if(Ui()->MouseButtonClicked(1)) { if(Input()->ShiftIsPressed()) { @@ -7253,14 +7326,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU; SelectTangentInPoint(i, c); - UI()->SetActiveItem(pID); + Ui()->SetActiveItem(pId); } } m_ShowEnvelopePreview = SHOWENV_SELECTED; Graphics()->SetColor(1, 1, 1, 1); str_copy(m_aTooltip, "Bezier in-tangent. Left mouse to drag. Hold ctrl to be more precise. Shift + right-click to reset."); - ms_pUiGotContext = pID; + ms_pUiGotContext = pId; } else Graphics()->SetColor(aColors[c].r, aColors[c].g, aColors[c].b, 1.0f); @@ -7273,7 +7346,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } Graphics()->QuadsEnd(); - UI()->ClipDisable(); + Ui()->ClipDisable(); } // handle scaling @@ -7321,7 +7394,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(Input()->ShiftIsPressed()) { - s_ScaleFactorX += UI()->MouseDeltaX() / Graphics()->ScreenWidth() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f); + s_ScaleFactorX += Ui()->MouseDeltaX() / Graphics()->ScreenWidth() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f); float Midpoint = Input()->AltIsPressed() ? s_MidpointX : 0.0f; for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++) { @@ -7374,7 +7447,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } else { - s_ScaleFactorY -= UI()->MouseDeltaY() / Graphics()->ScreenHeight() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f); + s_ScaleFactorY -= Ui()->MouseDeltaY() / Graphics()->ScreenHeight() * (Input()->ModifierIsPressed() ? 0.5f : 10.0f); for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++) { auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints[k]; @@ -7388,12 +7461,12 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } - if(UI()->MouseButton(0)) + if(Ui()->MouseButton(0)) { s_Operation = EEnvelopeEditorOp::OP_NONE; m_EnvOpTracker.Stop(false); } - else if(UI()->MouseButton(1) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) + else if(Ui()->MouseButton(1) || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) { for(size_t k = 0; k < m_vSelectedEnvelopePoints.size(); k++) { @@ -7414,25 +7487,25 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(s_Operation == EEnvelopeEditorOp::OP_BOX_SELECT) { IGraphics::CLineItem aLines[4] = { - {s_MouseXStart, s_MouseYStart, UI()->MouseX(), s_MouseYStart}, - {s_MouseXStart, s_MouseYStart, s_MouseXStart, UI()->MouseY()}, - {s_MouseXStart, UI()->MouseY(), UI()->MouseX(), UI()->MouseY()}, - {UI()->MouseX(), s_MouseYStart, UI()->MouseX(), UI()->MouseY()}}; - UI()->ClipEnable(&View); + {s_MouseXStart, s_MouseYStart, Ui()->MouseX(), s_MouseYStart}, + {s_MouseXStart, s_MouseYStart, s_MouseXStart, Ui()->MouseY()}, + {s_MouseXStart, Ui()->MouseY(), Ui()->MouseX(), Ui()->MouseY()}, + {Ui()->MouseX(), s_MouseYStart, Ui()->MouseX(), Ui()->MouseY()}}; + Ui()->ClipEnable(&View); Graphics()->LinesBegin(); Graphics()->LinesDraw(aLines, std::size(aLines)); Graphics()->LinesEnd(); - UI()->ClipDisable(); + Ui()->ClipDisable(); - if(!UI()->MouseButton(0)) + if(!Ui()->MouseButton(0)) { s_Operation = EEnvelopeEditorOp::OP_NONE; - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); float TimeStart = ScreenToEnvelopeX(View, s_MouseXStart); - float TimeEnd = ScreenToEnvelopeX(View, UI()->MouseX()); + float TimeEnd = ScreenToEnvelopeX(View, Ui()->MouseX()); float ValueStart = ScreenToEnvelopeY(View, s_MouseYStart); - float ValueEnd = ScreenToEnvelopeY(View, UI()->MouseY()); + float ValueEnd = ScreenToEnvelopeY(View, Ui()->MouseY()); float TimeMin = minimum(TimeStart, TimeEnd); float TimeMax = maximum(TimeStart, TimeEnd); @@ -7461,189 +7534,6 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } -void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast) -{ - // TODO: improve validation (https://github.com/ddnet/ddnet/issues/1406) - // Returns true if the argument is a valid server setting - const auto &&ValidateServerSetting = [](const char *pStr) { - return str_find(pStr, " ") != nullptr; - }; - - static int s_CommandSelectedIndex = -1; - static CListBox s_ListBox; - s_ListBox.SetActive(m_Dialog == DIALOG_NONE && !UI()->IsPopupOpen()); - - bool GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); - const bool CurrentInputValid = ValidateServerSetting(m_SettingsCommandInput.GetString()); - - CUIRect ToolBar, Button, Label, List, DragBar; - View.HSplitTop(22.0f, &DragBar, nullptr); - DragBar.y -= 2.0f; - DragBar.w += 2.0f; - DragBar.h += 4.0f; - RenderExtraEditorDragBar(View, DragBar); - View.HSplitTop(20.0f, &ToolBar, &View); - View.HSplitTop(2.0f, nullptr, &List); - ToolBar.HMargin(2.0f, &ToolBar); - - // delete button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); - static int s_DeleteButton = 0; - if(DoButton_FontIcon(&s_DeleteButton, FONT_ICON_TRASH, GotSelection ? 0 : -1, &Button, 0, "[Delete] Delete the selected command from the command list.", IGraphics::CORNER_ALL, 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_DELETE))) - { - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); - - m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + s_CommandSelectedIndex); - if(s_CommandSelectedIndex >= (int)m_Map.m_vSettings.size()) - s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; - if(s_CommandSelectedIndex >= 0) - m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); - m_Map.OnModify(); - s_ListBox.ScrollToSelected(); - } - - // move down button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - const bool CanMoveDown = GotSelection && s_CommandSelectedIndex < (int)m_Map.m_vSettings.size() - 1; - static int s_DownButton = 0; - if(DoButton_FontIcon(&s_DownButton, FONT_ICON_SORT_DOWN, CanMoveDown ? 0 : -1, &Button, 0, "[Alt+Down] Move the selected command down.", IGraphics::CORNER_R, 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_DOWN))) - { - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::MOVE_DOWN, &s_CommandSelectedIndex, s_CommandSelectedIndex)); - - std::swap(m_Map.m_vSettings[s_CommandSelectedIndex], m_Map.m_vSettings[s_CommandSelectedIndex + 1]); - s_CommandSelectedIndex++; - m_Map.OnModify(); - s_ListBox.ScrollToSelected(); - } - - // move up button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); - const bool CanMoveUp = GotSelection && s_CommandSelectedIndex > 0; - static int s_UpButton = 0; - if(DoButton_FontIcon(&s_UpButton, FONT_ICON_SORT_UP, CanMoveUp ? 0 : -1, &Button, 0, "[Alt+Up] Move the selected command up.", IGraphics::CORNER_L, 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_UP))) - { - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::MOVE_UP, &s_CommandSelectedIndex, s_CommandSelectedIndex)); - - std::swap(m_Map.m_vSettings[s_CommandSelectedIndex], m_Map.m_vSettings[s_CommandSelectedIndex - 1]); - s_CommandSelectedIndex--; - m_Map.OnModify(); - s_ListBox.ScrollToSelected(); - } - - // redo button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - static int s_RedoButton = 0; - if(DoButton_FontIcon(&s_RedoButton, FONT_ICON_REDO, m_ServerSettingsHistory.CanRedo() ? 0 : -1, &Button, 0, "[Ctrl+Y] Redo command edit", IGraphics::CORNER_R, 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_DOWN))) - { - m_ServerSettingsHistory.Redo(); - } - - // undo button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); - static int s_UndoButton = 0; - if(DoButton_FontIcon(&s_UndoButton, FONT_ICON_UNDO, m_ServerSettingsHistory.CanUndo() ? 0 : -1, &Button, 0, "[Ctrl+Z] Undo command edit", IGraphics::CORNER_L, 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && UI()->ConsumeHotkey(CUI::HOTKEY_UP))) - { - m_ServerSettingsHistory.Undo(); - } - - GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); - - // update button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - const bool CanUpdate = GotSelection && CurrentInputValid && str_comp(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, m_SettingsCommandInput.GetString()) != 0; - static int s_UpdateButton = 0; - if(DoButton_FontIcon(&s_UpdateButton, FONT_ICON_PENCIL, CanUpdate ? 0 : -1, &Button, 0, "[Alt+Enter] Update the selected command based on the entered value.", IGraphics::CORNER_R, 9.0f) == 1 || (CanUpdate && Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) - { - bool Found = false; - int i; - for(i = 0; i < (int)m_Map.m_vSettings.size(); ++i) - { - if(i != s_CommandSelectedIndex && !str_comp(m_Map.m_vSettings[i].m_aCommand, m_SettingsCommandInput.GetString())) - { - Found = true; - break; - } - } - if(Found) - { - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); - m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + s_CommandSelectedIndex); - s_CommandSelectedIndex = i > s_CommandSelectedIndex ? i - 1 : i; - } - else - { - const char *pStr = m_SettingsCommandInput.GetString(); - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); - str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); - } - m_Map.OnModify(); - s_ListBox.ScrollToSelected(); - UI()->SetActiveItem(&m_SettingsCommandInput); - } - - // add button - ToolBar.VSplitRight(25.0f, &ToolBar, &Button); - ToolBar.VSplitRight(100.0f, &ToolBar, nullptr); - const bool CanAdd = s_ListBox.Active() && CurrentInputValid; - static int s_AddButton = 0; - if(DoButton_FontIcon(&s_AddButton, FONT_ICON_PLUS, CanAdd ? 0 : -1, &Button, 0, "[Enter] Add a command to the command list.", IGraphics::CORNER_L) == 1 || (CanAdd && !Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) - { - bool Found = false; - for(size_t i = 0; i < m_Map.m_vSettings.size(); ++i) - { - if(!str_comp(m_Map.m_vSettings[i].m_aCommand, m_SettingsCommandInput.GetString())) - { - s_CommandSelectedIndex = i; - Found = true; - break; - } - } - - if(!Found) - { - m_Map.m_vSettings.emplace_back(m_SettingsCommandInput.GetString()); - s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; - m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::ADD, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); - m_Map.OnModify(); - } - s_ListBox.ScrollToSelected(); - UI()->SetActiveItem(&m_SettingsCommandInput); - } - - // command input (use remaining toolbar width) - if(!ShowServerSettingsEditorLast) // Just activated - UI()->SetActiveItem(&m_SettingsCommandInput); - m_SettingsCommandInput.SetEmptyText("Command"); - DoClearableEditBox(&m_SettingsCommandInput, &ToolBar, 12.0f, IGraphics::CORNER_ALL, "Enter a server setting."); - - // command list - s_ListBox.DoStart(15.0f, m_Map.m_vSettings.size(), 1, 3, s_CommandSelectedIndex, &List); - - for(size_t i = 0; i < m_Map.m_vSettings.size(); i++) - { - const CListboxItem Item = s_ListBox.DoNextItem(&m_Map.m_vSettings[i], s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex == i); - if(!Item.m_Visible) - continue; - - Item.m_Rect.VMargin(5.0f, &Label); - - SLabelProperties Props; - Props.m_MaxWidth = Label.w; - Props.m_EllipsisAtEnd = true; - UI()->DoLabel(&Label, m_Map.m_vSettings[i].m_aCommand, 10.0f, TEXTALIGN_ML, Props); - } - - const int NewSelected = s_ListBox.DoEnd(); - if(s_CommandSelectedIndex != NewSelected) - { - s_CommandSelectedIndex = NewSelected; - m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); - } -} - void CEditor::RenderEditorHistory(CUIRect View) { enum EHistoryType @@ -7656,7 +7546,7 @@ void CEditor::RenderEditorHistory(CUIRect View) static EHistoryType s_HistoryType = EDITOR_HISTORY; static int s_ActionSelectedIndex = 0; static CListBox s_ListBox; - s_ListBox.SetActive(m_Dialog == DIALOG_NONE && !UI()->IsPopupOpen()); + s_ListBox.SetActive(m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen()); const bool GotSelection = s_ListBox.Active() && s_ActionSelectedIndex >= 0 && (size_t)s_ActionSelectedIndex < m_Map.m_vSettings.size(); @@ -7665,7 +7555,7 @@ void CEditor::RenderEditorHistory(CUIRect View) DragBar.y -= 2.0f; DragBar.w += 2.0f; DragBar.h += 4.0f; - RenderExtraEditorDragBar(View, DragBar); + DoEditorDragBar(View, &DragBar, EDragSide::SIDE_TOP, &m_aExtraEditorSplits[EXTRAEDITOR_HISTORY]); View.HSplitTop(20.0f, &ToolBar, &View); View.HSplitTop(2.0f, nullptr, &List); ToolBar.HMargin(2.0f, &ToolBar); @@ -7702,7 +7592,7 @@ void CEditor::RenderEditorHistory(CUIRect View) InfoProps.m_MaxWidth = ToolBar.w - 60.f; InfoProps.m_EllipsisAtEnd = true; Label.VSplitLeft(8.0f, nullptr, &Label); - UI()->DoLabel(&Label, "Editor history. Click on an action to undo all actions above.", 10.0f, TEXTALIGN_ML, InfoProps); + Ui()->DoLabel(&Label, "Editor history. Click on an action to undo all actions above.", 10.0f, TEXTALIGN_ML, InfoProps); CEditorHistory *pCurrentHistory; if(s_HistoryType == EDITOR_HISTORY) @@ -7718,7 +7608,7 @@ void CEditor::RenderEditorHistory(CUIRect View) ToolBar.VSplitRight(25.0f, &ToolBar, &Button); ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); static int s_DeleteButton = 0; - if(DoButton_FontIcon(&s_DeleteButton, FONT_ICON_TRASH, (!pCurrentHistory->m_vpUndoActions.empty() || !pCurrentHistory->m_vpRedoActions.empty()) ? 0 : -1, &Button, 0, "Clear the history.", IGraphics::CORNER_ALL, 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && UI()->ConsumeHotkey(CUI::HOTKEY_DELETE))) + if(DoButton_FontIcon(&s_DeleteButton, FONT_ICON_TRASH, (!pCurrentHistory->m_vpUndoActions.empty() || !pCurrentHistory->m_vpRedoActions.empty()) ? 0 : -1, &Button, 0, "Clear the history.", IGraphics::CORNER_ALL, 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(CUi::HOTKEY_DELETE))) { pCurrentHistory->Clear(); s_ActionSelectedIndex = 0; @@ -7743,7 +7633,7 @@ void CEditor::RenderEditorHistory(CUIRect View) Props.m_EllipsisAtEnd = true; TextRender()->TextColor({.5f, .5f, .5f}); TextRender()->TextOutlineColor(TextRender()->DefaultTextOutlineColor()); - UI()->DoLabel(&Label, pCurrentHistory->m_vpRedoActions[i]->DisplayText(), 10.0f, TEXTALIGN_ML, Props); + Ui()->DoLabel(&Label, pCurrentHistory->m_vpRedoActions[i]->DisplayText(), 10.0f, TEXTALIGN_ML, Props); TextRender()->TextColor(TextRender()->DefaultTextColor()); } @@ -7758,7 +7648,7 @@ void CEditor::RenderEditorHistory(CUIRect View) SLabelProperties Props; Props.m_MaxWidth = Label.w; Props.m_EllipsisAtEnd = true; - UI()->DoLabel(&Label, pCurrentHistory->m_vpUndoActions[UndoSize - i - 1]->DisplayText(), 10.0f, TEXTALIGN_ML, Props); + Ui()->DoLabel(&Label, pCurrentHistory->m_vpUndoActions[UndoSize - i - 1]->DisplayText(), 10.0f, TEXTALIGN_ML, Props); } { // Base action "Loaded map" that cannot be undone @@ -7768,7 +7658,7 @@ void CEditor::RenderEditorHistory(CUIRect View) { Item.m_Rect.VMargin(5.0f, &Label); - UI()->DoLabel(&Label, "Loaded map", 10.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, "Loaded map", 10.0f, TEXTALIGN_ML); } } @@ -7795,7 +7685,7 @@ void CEditor::RenderEditorHistory(CUIRect View) } } -void CEditor::RenderExtraEditorDragBar(CUIRect View, CUIRect DragBar) +void CEditor::DoEditorDragBar(CUIRect View, CUIRect *pDragBar, EDragSide Side, float *pValue, float MinValue, float MaxValue) { enum EDragOperation { @@ -7806,26 +7696,46 @@ void CEditor::RenderExtraEditorDragBar(CUIRect View, CUIRect DragBar) static EDragOperation s_Operation = OP_NONE; static float s_InitialMouseY = 0.0f; static float s_InitialMouseOffsetY = 0.0f; + static float s_InitialMouseX = 0.0f; + static float s_InitialMouseOffsetX = 0.0f; + + bool IsVertical = Side == EDragSide::SIDE_TOP || Side == EDragSide::SIDE_BOTTOM; + + if(Ui()->MouseInside(pDragBar) && Ui()->HotItem() == pDragBar) + m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H; bool Clicked; bool Abrupted; - if(int Result = DoButton_DraggableEx(&s_Operation, "", 8, &DragBar, &Clicked, &Abrupted, 0, "Change the size of the editor by dragging.")) + if(int Result = DoButton_DraggableEx(pDragBar, "", 8, pDragBar, &Clicked, &Abrupted, 0, "Change the size of the editor by dragging.")) { if(s_Operation == OP_NONE && Result == 1) { - s_InitialMouseY = UI()->MouseY(); - s_InitialMouseOffsetY = UI()->MouseY() - DragBar.y; + s_InitialMouseY = Ui()->MouseY(); + s_InitialMouseOffsetY = Ui()->MouseY() - pDragBar->y; + s_InitialMouseX = Ui()->MouseX(); + s_InitialMouseOffsetX = Ui()->MouseX() - pDragBar->x; s_Operation = OP_CLICKED; } if(Clicked || Abrupted) s_Operation = OP_NONE; - if(s_Operation == OP_CLICKED && absolute(UI()->MouseY() - s_InitialMouseY) > 5.0f) + if(s_Operation == OP_CLICKED && absolute(IsVertical ? Ui()->MouseY() - s_InitialMouseY : Ui()->MouseX() - s_InitialMouseX) > 5.0f) s_Operation = OP_DRAGGING; if(s_Operation == OP_DRAGGING) - m_aExtraEditorSplits[(int)m_ActiveExtraEditor] = clamp(s_InitialMouseOffsetY + View.y + View.h - UI()->MouseY(), 100.0f, 400.0f); + { + if(Side == EDragSide::SIDE_TOP) + *pValue = clamp(s_InitialMouseOffsetY + View.y + View.h - Ui()->MouseY(), MinValue, MaxValue); + else if(Side == EDragSide::SIDE_RIGHT) + *pValue = clamp(Ui()->MouseX() - s_InitialMouseOffsetX - View.x + pDragBar->w, MinValue, MaxValue); + else if(Side == EDragSide::SIDE_BOTTOM) + *pValue = clamp(Ui()->MouseY() - s_InitialMouseOffsetY - View.y + pDragBar->h, MinValue, MaxValue); + else if(Side == EDragSide::SIDE_LEFT) + *pValue = clamp(s_InitialMouseOffsetX + View.x + View.w - Ui()->MouseX(), MinValue, MaxValue); + + m_CursorType = IsVertical ? CURSOR_RESIZE_V : CURSOR_RESIZE_H; + } } } @@ -7837,10 +7747,10 @@ void CEditor::RenderMenubar(CUIRect MenuBar) CUIRect FileButton; static int s_FileButton = 0; MenuBar.VSplitLeft(60.0f, &FileButton, &MenuBar); - if(DoButton_Menu(&s_FileButton, "File", 0, &FileButton, 0, nullptr)) + if(DoButton_Ex(&s_FileButton, "File", 0, &FileButton, 0, nullptr, IGraphics::CORNER_T, EditorFontSizes::MENU, TEXTALIGN_ML)) { static SPopupMenuId s_PopupMenuFileId; - UI()->DoPopupMenu(&s_PopupMenuFileId, FileButton.x, FileButton.y + FileButton.h - 1.0f, 120.0f, 174.0f, this, PopupMenuFile, PopupProperties); + Ui()->DoPopupMenu(&s_PopupMenuFileId, FileButton.x, FileButton.y + FileButton.h - 1.0f, 120.0f, 174.0f, this, PopupMenuFile, PopupProperties); } MenuBar.VSplitLeft(5.0f, nullptr, &MenuBar); @@ -7848,10 +7758,10 @@ void CEditor::RenderMenubar(CUIRect MenuBar) CUIRect ToolsButton; static int s_ToolsButton = 0; MenuBar.VSplitLeft(60.0f, &ToolsButton, &MenuBar); - if(DoButton_Menu(&s_ToolsButton, "Tools", 0, &ToolsButton, 0, nullptr)) + if(DoButton_Ex(&s_ToolsButton, "Tools", 0, &ToolsButton, 0, nullptr, IGraphics::CORNER_T, EditorFontSizes::MENU, TEXTALIGN_ML)) { static SPopupMenuId s_PopupMenuToolsId; - UI()->DoPopupMenu(&s_PopupMenuToolsId, ToolsButton.x, ToolsButton.y + ToolsButton.h - 1.0f, 200.0f, 64.0f, this, PopupMenuTools, PopupProperties); + Ui()->DoPopupMenu(&s_PopupMenuToolsId, ToolsButton.x, ToolsButton.y + ToolsButton.h - 1.0f, 200.0f, 64.0f, this, PopupMenuTools, PopupProperties); } MenuBar.VSplitLeft(5.0f, nullptr, &MenuBar); @@ -7859,17 +7769,19 @@ void CEditor::RenderMenubar(CUIRect MenuBar) CUIRect SettingsButton; static int s_SettingsButton = 0; MenuBar.VSplitLeft(60.0f, &SettingsButton, &MenuBar); - if(DoButton_Menu(&s_SettingsButton, "Settings", 0, &SettingsButton, 0, nullptr)) + if(DoButton_Ex(&s_SettingsButton, "Settings", 0, &SettingsButton, 0, nullptr, IGraphics::CORNER_T, EditorFontSizes::MENU, TEXTALIGN_ML)) { static SPopupMenuId s_PopupMenuEntitiesId; - UI()->DoPopupMenu(&s_PopupMenuEntitiesId, SettingsButton.x, SettingsButton.y + SettingsButton.h - 1.0f, 200.0f, 92.0f, this, PopupMenuSettings, PopupProperties); + Ui()->DoPopupMenu(&s_PopupMenuEntitiesId, SettingsButton.x, SettingsButton.y + SettingsButton.h - 1.0f, 210.0f, 120.0f, this, PopupMenuSettings, PopupProperties); } - CUIRect ChangedIndicator, Info, Close; + CUIRect ChangedIndicator, Info, Help, Close; MenuBar.VSplitLeft(5.0f, nullptr, &MenuBar); MenuBar.VSplitLeft(MenuBar.h, &ChangedIndicator, &MenuBar); - MenuBar.VSplitRight(20.0f, &MenuBar, &Close); - Close.VSplitLeft(5.0f, nullptr, &Close); + MenuBar.VSplitRight(15.0f, &MenuBar, &Close); + MenuBar.VSplitRight(5.0f, &MenuBar, nullptr); + MenuBar.VSplitRight(15.0f, &MenuBar, &Help); + MenuBar.VSplitRight(5.0f, &MenuBar, nullptr); MenuBar.VSplitLeft(MenuBar.w * 0.6f, &MenuBar, &Info); MenuBar.VSplitRight(5.0f, &MenuBar, nullptr); @@ -7877,7 +7789,7 @@ void CEditor::RenderMenubar(CUIRect MenuBar) { TextRender()->SetFontPreset(EFontPreset::ICON_FONT); TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); - UI()->DoLabel(&ChangedIndicator, FONT_ICON_CIRCLE, 8.0f, TEXTALIGN_MC); + Ui()->DoLabel(&ChangedIndicator, FONT_ICON_CIRCLE, 8.0f, TEXTALIGN_MC); TextRender()->SetRenderFlags(0); TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); static int s_ChangedIndicator; @@ -7889,13 +7801,23 @@ void CEditor::RenderMenubar(CUIRect MenuBar) SLabelProperties Props; Props.m_MaxWidth = MenuBar.w; Props.m_EllipsisAtEnd = true; - UI()->DoLabel(&MenuBar, aBuf, 10.0f, TEXTALIGN_ML, Props); + Ui()->DoLabel(&MenuBar, aBuf, 10.0f, TEXTALIGN_ML, Props); char aTimeStr[6]; str_timestamp_format(aTimeStr, sizeof(aTimeStr), "%H:%M"); - str_format(aBuf, sizeof(aBuf), "X: %.1f, Y: %.1f, Z: %.1f, A: %.1f, G: %i %s", UI()->MouseWorldX() / 32.0f, UI()->MouseWorldY() / 32.0f, MapView()->Zoom()->GetValue(), m_AnimateSpeed, MapView()->MapGrid()->Factor(), aTimeStr); - UI()->DoLabel(&Info, aBuf, 10.0f, TEXTALIGN_MR); + str_format(aBuf, sizeof(aBuf), "X: %.1f, Y: %.1f, Z: %.1f, A: %.1f, G: %i %s", Ui()->MouseWorldX() / 32.0f, Ui()->MouseWorldY() / 32.0f, MapView()->Zoom()->GetValue(), m_AnimateSpeed, MapView()->MapGrid()->Factor(), aTimeStr); + Ui()->DoLabel(&Info, aBuf, 10.0f, TEXTALIGN_MR); + + static int s_HelpButton = 0; + if(DoButton_Editor(&s_HelpButton, "?", 0, &Help, 0, "[F1] Open the DDNet Wiki page for the Map Editor in a web browser") || (Input()->KeyPress(KEY_F1) && m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr)) + { + const char *pLink = Localize("https://wiki.ddnet.org/wiki/Mapping"); + if(!Client()->ViewLink(pLink)) + { + ShowFileDialogError("Failed to open the link '%s' in the default web browser.", pLink); + } + } static int s_CloseButton = 0; if(DoButton_Editor(&s_CloseButton, "×", 0, &Close, 0, "Exits from the editor")) @@ -7909,8 +7831,9 @@ void CEditor::Render() { // basic start Graphics()->Clear(0.0f, 0.0f, 0.0f); - CUIRect View = *UI()->Screen(); - UI()->MapScreen(); + CUIRect View = *Ui()->Screen(); + Ui()->MapScreen(); + m_CursorType = CURSOR_NORMAL; float Width = View.w; float Height = View.h; @@ -7928,7 +7851,8 @@ void CEditor::Render() { View.HSplitTop(16.0f, &MenuBar, &View); View.HSplitTop(53.0f, &ToolBar, &View); - View.VSplitLeft(100.0f, &ToolBox, &View); + View.VSplitLeft(m_ToolBoxWidth, &ToolBox, &View); + View.HSplitBottom(16.0f, &View, &StatusBar); if(!m_ShowPicker && m_ActiveExtraEditor != EXTRAEDITOR_NONE) View.HSplitBottom(m_aExtraEditorSplits[(int)m_ActiveExtraEditor], &View, &ExtraEditor); @@ -7949,9 +7873,9 @@ void CEditor::Render() if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr) { // handle undo/redo hotkeys - if(Input()->KeyPress(KEY_Z) && Input()->ModifierIsPressed()) + if(Input()->KeyPress(KEY_Z) && Input()->ModifierIsPressed() && !Input()->ShiftIsPressed()) UndoLastAction(); - if(Input()->KeyPress(KEY_Y) && Input()->ModifierIsPressed()) + if((Input()->KeyPress(KEY_Y) && Input()->ModifierIsPressed()) || (Input()->KeyPress(KEY_Z) && Input()->ModifierIsPressed() && Input()->ShiftIsPressed())) RedoLastAction(); // handle brush save/load hotkeys @@ -7986,7 +7910,7 @@ void CEditor::Render() RenderBackground(ToolBar, m_BackgroundTexture, 128.0f, Brightness); ToolBar.Margin(2.0f, &ToolBar); - ToolBar.VSplitLeft(100.0f, &ModeBar, &ToolBar); + ToolBar.VSplitLeft(m_ToolBoxWidth, &ModeBar, &ToolBar); RenderBackground(StatusBar, m_BackgroundTexture, 128.0f, Brightness); StatusBar.Margin(2.0f, &StatusBar); @@ -7998,11 +7922,21 @@ void CEditor::Render() else if(m_Mode == MODE_SOUNDS) DoToolbarSounds(ToolBar); - if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr) + if(m_Dialog == DIALOG_NONE) { const bool ModPressed = Input()->ModifierIsPressed(); const bool ShiftPressed = Input()->ShiftIsPressed(); const bool AltPressed = Input()->AltIsPressed(); + + if(CLineInput::GetActiveInput() == nullptr) + { + // ctrl+a to append map + if(Input()->KeyPress(KEY_A) && ModPressed) + { + InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_MAP, "Append map", "Append", "maps", false, CallbackAppendMap, this); + } + } + // ctrl+n to create new map if(Input()->KeyPress(KEY_N) && ModPressed) { @@ -8020,11 +7954,6 @@ void CEditor::Render() m_aFileName[0] = 0; } } - // ctrl+a to append map - if(Input()->KeyPress(KEY_A) && ModPressed) - { - InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_MAP, "Append map", "Append", "maps", false, CallbackAppendMap, this); - } // ctrl+o or ctrl+l to open if((Input()->KeyPress(KEY_O) || Input()->KeyPress(KEY_L)) && ModPressed) { @@ -8065,7 +7994,7 @@ void CEditor::Render() InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_MAP, "Save map", "Save", "maps", true, CallbackSaveCopyMap, this); // ctrl+shift+s to save as else if(Input()->KeyPress(KEY_S) && ModPressed && ShiftPressed) - InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_MAP, "Save map", "Save", "maps", true, CallbackSaveMap, this); + m_QuickActionSaveAs.Call(); // ctrl+s to save else if(Input()->KeyPress(KEY_S) && ModPressed) { @@ -8084,6 +8013,12 @@ void CEditor::Render() if(m_GuiActive) { + CUIRect DragBar; + ToolBox.VSplitRight(1.0f, &ToolBox, &DragBar); + DragBar.x -= 2.0f; + DragBar.w += 4.0f; + DoEditorDragBar(ToolBox, &DragBar, EDragSide::SIDE_RIGHT, &m_ToolBoxWidth); + if(m_Mode == MODE_LAYERS) RenderLayers(ToolBox); else if(m_Mode == MODE_IMAGES) @@ -8095,7 +8030,7 @@ void CEditor::Render() RenderSounds(ToolBox); } - UI()->MapScreen(); + Ui()->MapScreen(); CUIRect TooltipRect; if(m_GuiActive) @@ -8135,21 +8070,27 @@ void CEditor::Render() if(m_Dialog == DIALOG_FILE) { static int s_NullUiTarget = 0; - UI()->SetHotItem(&s_NullUiTarget); + Ui()->SetHotItem(&s_NullUiTarget); RenderFileDialog(); } + else if(m_Dialog == DIALOG_MAPSETTINGS_ERROR) + { + static int s_NullUiTarget = 0; + Ui()->SetHotItem(&s_NullUiTarget); + RenderMapSettingsErrorDialog(); + } if(m_PopupEventActivated) { static SPopupMenuId s_PopupEventId; constexpr float PopupWidth = 400.0f; constexpr float PopupHeight = 150.0f; - UI()->DoPopupMenu(&s_PopupEventId, Width / 2.0f - PopupWidth / 2.0f, Height / 2.0f - PopupHeight / 2.0f, PopupWidth, PopupHeight, this, PopupEvent); + Ui()->DoPopupMenu(&s_PopupEventId, Width / 2.0f - PopupWidth / 2.0f, Height / 2.0f - PopupHeight / 2.0f, PopupWidth, PopupHeight, this, PopupEvent); m_PopupEventActivated = false; m_PopupEventWasActivated = true; } - if(m_Dialog == DIALOG_NONE && !UI()->IsPopupHovered() && (!m_GuiActive || UI()->MouseInside(&View))) + if(m_Dialog == DIALOG_NONE && !Ui()->IsPopupHovered() && Ui()->MouseInside(&View)) { // handle zoom hotkeys if(Input()->KeyPress(KEY_KP_MINUS)) @@ -8158,18 +8099,53 @@ void CEditor::Render() MapView()->Zoom()->ChangeValue(-50.0f); if(Input()->KeyPress(KEY_KP_MULTIPLY)) MapView()->ResetZoom(); - if(Input()->KeyPress(KEY_MOUSE_WHEEL_DOWN)) - MapView()->Zoom()->ChangeValue(20.0f); - if(Input()->KeyPress(KEY_MOUSE_WHEEL_UP)) - MapView()->Zoom()->ChangeValue(-20.0f); + + if(m_pBrush->IsEmpty() || !Input()->ShiftIsPressed()) + { + if(Input()->KeyPress(KEY_MOUSE_WHEEL_DOWN)) + MapView()->Zoom()->ChangeValue(20.0f); + if(Input()->KeyPress(KEY_MOUSE_WHEEL_UP)) + MapView()->Zoom()->ChangeValue(-20.0f); + } + if(!m_pBrush->IsEmpty()) + { + const bool HasTeleTiles = std::any_of(m_pBrush->m_vpLayers.begin(), m_pBrush->m_vpLayers.end(), [](auto pLayer) { + return pLayer->m_Type == LAYERTYPE_TILES && std::static_pointer_cast(pLayer)->m_Tele; + }); + if(HasTeleTiles) + str_copy(m_aTooltip, "Use shift+mousewheel up/down to adjust the tele numbers. Use ctrl+f to change all tele numbers to the first unused number."); + + if(Input()->ShiftIsPressed()) + { + if(Input()->KeyPress(KEY_MOUSE_WHEEL_DOWN)) + AdjustBrushSpecialTiles(false, -1); + if(Input()->KeyPress(KEY_MOUSE_WHEEL_UP)) + AdjustBrushSpecialTiles(false, 1); + } + + // Use ctrl+f to replace number in brush with next free + if(Input()->ModifierIsPressed() && Input()->KeyPress(KEY_F)) + AdjustBrushSpecialTiles(true); + } } + for(CEditorComponent &Component : m_vComponents) + Component.OnRender(View); + MapView()->UpdateZoom(); - UI()->RenderPopupMenus(); + // Cancel color pipette with escape before closing popup menus with escape + if(m_ColorPipetteActive && Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + m_ColorPipetteActive = false; + } + + Ui()->RenderPopupMenus(); FreeDynamicPopupMenus(); - if(m_Dialog == DIALOG_NONE && !m_PopupEventActivated && UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) + UpdateColorPipette(); + + if(m_Dialog == DIALOG_NONE && !m_PopupEventActivated && Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) { OnClose(); g_Config.m_ClEditor = 0; @@ -8187,7 +8163,7 @@ void CEditor::RenderPressedKeys(CUIRect View) if(!g_Config.m_EdShowkeys) return; - UI()->MapScreen(); + Ui()->MapScreen(); CTextCursor Cursor; TextRender()->SetCursor(&Cursor, View.x + 10, View.y + View.h - 24 - 10, 24.0f, TEXTFLAG_RENDER); @@ -8212,14 +8188,14 @@ void CEditor::RenderSavingIndicator(CUIRect View) const char *pText = "Saving…"; const float FontSize = 24.0f; - UI()->MapScreen(); + Ui()->MapScreen(); CUIRect Label, Spinner; View.Margin(20.0f, &View); View.HSplitBottom(FontSize, nullptr, &View); View.VSplitRight(TextRender()->TextWidth(FontSize, pText) + 2.0f, &Spinner, &Label); Spinner.VSplitRight(Spinner.h, nullptr, &Spinner); - UI()->DoLabel(&Label, pText, FontSize, TEXTALIGN_MR); - UI()->RenderProgressSpinner(Spinner.Center(), 8.0f); + Ui()->DoLabel(&Label, pText, FontSize, TEXTALIGN_MR); + Ui()->RenderProgressSpinner(Spinner.Center(), 8.0f); } void CEditor::FreeDynamicPopupMenus() @@ -8227,9 +8203,9 @@ void CEditor::FreeDynamicPopupMenus() auto Iterator = m_PopupMessageContexts.begin(); while(Iterator != m_PopupMessageContexts.end()) { - if(!UI()->IsPopupOpen(Iterator->second)) + if(!Ui()->IsPopupOpen(Iterator->second)) { - CUI::SMessagePopupContext *pContext = Iterator->second; + CUi::SMessagePopupContext *pContext = Iterator->second; Iterator = m_PopupMessageContexts.erase(Iterator); delete pContext; } @@ -8238,28 +8214,116 @@ void CEditor::FreeDynamicPopupMenus() } } +void CEditor::UpdateColorPipette() +{ + if(!m_ColorPipetteActive) + return; + + static char s_PipetteScreenButton; + if(Ui()->HotItem() == &s_PipetteScreenButton) + { + // Read color one pixel to the top and left as we would otherwise not read the correct + // color due to the cursor sprite being rendered over the current mouse position. + const int PixelX = clamp(round_to_int((Ui()->MouseX() - 1.0f) / Ui()->Screen()->w * Graphics()->ScreenWidth()), 0, Graphics()->ScreenWidth() - 1); + const int PixelY = clamp(round_to_int((Ui()->MouseY() - 1.0f) / Ui()->Screen()->h * Graphics()->ScreenHeight()), 0, Graphics()->ScreenHeight() - 1); + Graphics()->ReadPixel(ivec2(PixelX, PixelY), &m_PipetteColor); + } + + // Simulate button overlaying the entire screen to intercept all clicks for color pipette. + const int ButtonResult = DoButton_Editor_Common(&s_PipetteScreenButton, "", 0, Ui()->Screen(), 0, "Left click to pick a color from the screen. Right click to cancel pipette mode."); + // Don't handle clicks if we are panning, so the pipette stays active while panning. + // Checking m_pContainerPanned alone is not enough, as this variable is reset when + // panning ends before this function is called. + if(m_pContainerPanned == nullptr && m_pContainerPannedLast == nullptr) + { + if(ButtonResult == 1) + { + char aClipboard[9]; + str_format(aClipboard, sizeof(aClipboard), "%08X", m_PipetteColor.PackAlphaLast()); + Input()->SetClipboardText(aClipboard); + + // Check if any of the saved colors is equal to the picked color and + // bring it to the front of the list instead of adding a duplicate. + int ShiftEnd = (int)std::size(m_aSavedColors) - 1; + for(int i = 0; i < (int)std::size(m_aSavedColors); ++i) + { + if(m_aSavedColors[i].Pack() == m_PipetteColor.Pack()) + { + ShiftEnd = i; + break; + } + } + for(int i = ShiftEnd; i > 0; --i) + { + m_aSavedColors[i] = m_aSavedColors[i - 1]; + } + m_aSavedColors[0] = m_PipetteColor; + } + if(ButtonResult > 0) + { + m_ColorPipetteActive = false; + } + } +} + void CEditor::RenderMousePointer() { if(!m_ShowMousePointer) return; + constexpr float CursorSize = 16.0f; + + // Cursor Graphics()->WrapClamp(); - Graphics()->TextureSet(m_CursorTexture); + Graphics()->TextureSet(m_aCursorTextures[m_CursorType]); Graphics()->QuadsBegin(); - if(ms_pUiGotContext == UI()->HotItem()) - Graphics()->SetColor(1, 0, 0, 1); - IGraphics::CQuadItem QuadItem(UI()->MouseX(), UI()->MouseY(), 16.0f, 16.0f); + if(m_CursorType == CURSOR_RESIZE_V) + { + Graphics()->QuadsSetRotation(pi / 2.0f); + } + if(ms_pUiGotContext == Ui()->HotItem()) + { + Graphics()->SetColor(1.0f, 0.0f, 0.0f, 1.0f); + } + const float CursorOffset = m_CursorType == CURSOR_RESIZE_V || m_CursorType == CURSOR_RESIZE_H ? -CursorSize / 2.0f : 0.0f; + IGraphics::CQuadItem QuadItem(Ui()->MouseX() + CursorOffset, Ui()->MouseY() + CursorOffset, CursorSize, CursorSize); Graphics()->QuadsDrawTL(&QuadItem, 1); Graphics()->QuadsEnd(); Graphics()->WrapNormal(); + + // Pipette color + if(m_ColorPipetteActive) + { + CUIRect PipetteRect = {Ui()->MouseX() + CursorSize, Ui()->MouseY() + CursorSize, 80.0f, 20.0f}; + if(PipetteRect.x + PipetteRect.w + 2.0f > Ui()->Screen()->w) + { + PipetteRect.x = Ui()->MouseX() - PipetteRect.w - CursorSize / 2.0f; + } + if(PipetteRect.y + PipetteRect.h + 2.0f > Ui()->Screen()->h) + { + PipetteRect.y = Ui()->MouseY() - PipetteRect.h - CursorSize / 2.0f; + } + PipetteRect.Draw(ColorRGBA(0.2f, 0.2f, 0.2f, 0.7f), IGraphics::CORNER_ALL, 3.0f); + + CUIRect Pipette, Label; + PipetteRect.VSplitLeft(PipetteRect.h, &Pipette, &Label); + Pipette.Margin(2.0f, &Pipette); + Pipette.Draw(m_PipetteColor, IGraphics::CORNER_ALL, 3.0f); + + char aLabel[8]; + str_format(aLabel, sizeof(aLabel), "#%06X", m_PipetteColor.PackAlphaLast(false)); + Ui()->DoLabel(&Label, aLabel, 10.0f, TEXTALIGN_MC); + } } void CEditor::Reset(bool CreateDefault) { - UI()->ClosePopupMenus(); + Ui()->ClosePopupMenus(); m_Map.Clean(); - m_MapView.OnReset(); + for(CEditorComponent &Component : m_vComponents) + Component.OnReset(); + // create default layers if(CreateDefault) { @@ -8275,10 +8339,8 @@ void CEditor::Reset(bool CreateDefault) m_SelectedSound = 0; m_SelectedSource = -1; - m_MouseDeltaX = 0; - m_MouseDeltaY = 0; - m_MouseDeltaWx = 0; - m_MouseDeltaWy = 0; + m_pContainerPanned = nullptr; + m_pContainerPannedLast = nullptr; m_Map.m_Modified = false; m_Map.m_ModifiedAuto = false; @@ -8300,9 +8362,11 @@ void CEditor::Reset(bool CreateDefault) m_EnvOpTracker.m_pEditor = this; m_EnvOpTracker.Reset(); + + m_MapSettingsCommandContext.Reset(); } -int CEditor::GetTextureUsageFlag() +int CEditor::GetTextureUsageFlag() const { return Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; } @@ -8353,7 +8417,8 @@ void CEditor::Init() { m_pInput = Kernel()->RequestInterface(); m_pClient = Kernel()->RequestInterface(); - m_pConfig = Kernel()->RequestInterface()->Values(); + m_pConfigManager = Kernel()->RequestInterface(); + m_pConfig = m_pConfigManager->Values(); m_pConsole = Kernel()->RequestInterface(); m_pEngine = Kernel()->RequestInterface(); m_pGraphics = Kernel()->RequestInterface(); @@ -8365,17 +8430,22 @@ void CEditor::Init() m_PopupEventWasActivated = false; }); m_RenderTools.Init(m_pGraphics, m_pTextRender); - m_ZoomEnvelopeX.Init(this); - m_ZoomEnvelopeY.Init(this); + m_ZoomEnvelopeX.OnInit(this); + m_ZoomEnvelopeY.OnInit(this); m_Map.m_pEditor = this; m_vComponents.emplace_back(m_MapView); + m_vComponents.emplace_back(m_MapSettingsBackend); + m_vComponents.emplace_back(m_LayerSelector); + m_vComponents.emplace_back(m_Prompt); for(CEditorComponent &Component : m_vComponents) - Component.Init(this); + Component.OnInit(this); m_CheckerTexture = Graphics()->LoadTexture("editor/checker.png", IStorage::TYPE_ALL); m_BackgroundTexture = Graphics()->LoadTexture("editor/background.png", IStorage::TYPE_ALL); - m_CursorTexture = Graphics()->LoadTexture("editor/cursor.png", IStorage::TYPE_ALL); + m_aCursorTextures[CURSOR_NORMAL] = Graphics()->LoadTexture("editor/cursor.png", IStorage::TYPE_ALL); + m_aCursorTextures[CURSOR_RESIZE_H] = Graphics()->LoadTexture("editor/cursor_resize.png", IStorage::TYPE_ALL); + m_aCursorTextures[CURSOR_RESIZE_V] = m_aCursorTextures[CURSOR_RESIZE_H]; m_pTilesetPicker = std::make_shared(this, 16, 16); m_pTilesetPicker->MakePalette(); @@ -8416,29 +8486,11 @@ void CEditor::PlaceBorderTiles() void CEditor::HandleCursorMovement() { - static float s_MouseX = 0.0f; - static float s_MouseY = 0.0f; - - float MouseRelX = 0.0f, MouseRelY = 0.0f; - IInput::ECursorType CursorType = Input()->CursorRelative(&MouseRelX, &MouseRelY); - if(CursorType != IInput::CURSOR_NONE) - UI()->ConvertMouseMove(&MouseRelX, &MouseRelY, CursorType); - - m_MouseDeltaX += MouseRelX; - m_MouseDeltaY += MouseRelY; - - if(!UI()->CheckMouseLock()) - { - s_MouseX = clamp(s_MouseX + MouseRelX, 0.0f, Graphics()->WindowWidth()); - s_MouseY = clamp(s_MouseY + MouseRelY, 0.0f, Graphics()->WindowHeight()); - } - - // update positions for ui, but only update ui when rendering - m_MouseX = UI()->Screen()->w * ((float)s_MouseX / Graphics()->WindowWidth()); - m_MouseY = UI()->Screen()->h * ((float)s_MouseY / Graphics()->WindowHeight()); + const vec2 UpdatedMousePos = Ui()->UpdatedMousePos(); + const vec2 UpdatedMouseDelta = Ui()->UpdatedMouseDelta(); // fix correct world x and y - std::shared_ptr pGroup = GetSelectedGroup(); + const std::shared_ptr pGroup = GetSelectedGroup(); if(pGroup) { float aPoints[4]; @@ -8447,19 +8499,20 @@ void CEditor::HandleCursorMovement() float WorldWidth = aPoints[2] - aPoints[0]; float WorldHeight = aPoints[3] - aPoints[1]; - m_MouseWScale = WorldWidth / Graphics()->WindowWidth(); + m_MouseWorldScale = WorldWidth / Graphics()->WindowWidth(); - m_MouseWorldX = aPoints[0] + WorldWidth * (s_MouseX / Graphics()->WindowWidth()); - m_MouseWorldY = aPoints[1] + WorldHeight * (s_MouseY / Graphics()->WindowHeight()); - m_MouseDeltaWx = m_MouseDeltaX * (WorldWidth / Graphics()->WindowWidth()); - m_MouseDeltaWy = m_MouseDeltaY * (WorldHeight / Graphics()->WindowHeight()); + m_MouseWorldPos.x = aPoints[0] + WorldWidth * (UpdatedMousePos.x / Graphics()->WindowWidth()); + m_MouseWorldPos.y = aPoints[1] + WorldHeight * (UpdatedMousePos.y / Graphics()->WindowHeight()); + m_MouseDeltaWorld.x = UpdatedMouseDelta.x * (WorldWidth / Graphics()->WindowWidth()); + m_MouseDeltaWorld.y = UpdatedMouseDelta.y * (WorldHeight / Graphics()->WindowHeight()); } else { - m_MouseWorldX = 0.0f; - m_MouseWorldY = 0.0f; + m_MouseWorldPos = vec2(-1.0f, -1.0f); + m_MouseDeltaWorld = vec2(0.0f, 0.0f); } + m_MouseWorldNoParaPos = vec2(-1.0f, -1.0f); for(const std::shared_ptr &pGameGroup : m_Map.m_vpGroups) { if(!pGameGroup->m_GameGroup) @@ -8471,20 +8524,59 @@ void CEditor::HandleCursorMovement() float WorldWidth = aPoints[2] - aPoints[0]; float WorldHeight = aPoints[3] - aPoints[1]; - m_MouseWorldNoParaX = aPoints[0] + WorldWidth * (s_MouseX / Graphics()->WindowWidth()); - m_MouseWorldNoParaY = aPoints[1] + WorldHeight * (s_MouseY / Graphics()->WindowHeight()); + m_MouseWorldNoParaPos.x = aPoints[0] + WorldWidth * (UpdatedMousePos.x / Graphics()->WindowWidth()); + m_MouseWorldNoParaPos.y = aPoints[1] + WorldHeight * (UpdatedMousePos.y / Graphics()->WindowHeight()); } + + OnMouseMove(UpdatedMousePos); } -void CEditor::DispatchInputEvents() +void CEditor::OnMouseMove(vec2 MousePos) { - for(size_t i = 0; i < Input()->NumEvents(); i++) + m_vHoverTiles.clear(); + for(size_t g = 0; g < m_Map.m_vpGroups.size(); g++) { - const IInput::CEvent &Event = Input()->GetEvent(i); - if(!Input()->IsEventValid(Event)) - continue; - UI()->OnInput(Event); + const std::shared_ptr pGroup = m_Map.m_vpGroups[g]; + for(size_t l = 0; l < pGroup->m_vpLayers.size(); l++) + { + const std::shared_ptr pLayer = pGroup->m_vpLayers[l]; + int LayerType = pLayer->m_Type; + if(LayerType != LAYERTYPE_TILES && + LayerType != LAYERTYPE_FRONT && + LayerType != LAYERTYPE_TELE && + LayerType != LAYERTYPE_SPEEDUP && + LayerType != LAYERTYPE_SWITCH && + LayerType != LAYERTYPE_TUNE) + continue; + + std::shared_ptr pTiles = std::static_pointer_cast(pLayer); + pGroup->MapScreen(); + float aPoints[4]; + pGroup->Mapping(aPoints); + float WorldWidth = aPoints[2] - aPoints[0]; + float WorldHeight = aPoints[3] - aPoints[1]; + CUIRect Rect; + Rect.x = aPoints[0] + WorldWidth * (MousePos.x / Graphics()->WindowWidth()); + Rect.y = aPoints[1] + WorldHeight * (MousePos.y / Graphics()->WindowHeight()); + Rect.w = 0; + Rect.h = 0; + RECTi r; + pTiles->Convert(Rect, &r); + pTiles->Clamp(&r); + int x = r.x; + int y = r.y; + + if(x < 0 || x >= pTiles->m_Width) + continue; + if(y < 0 || y >= pTiles->m_Height) + continue; + CTile Tile = pTiles->GetTile(x, y); + if(Tile.m_Index) + m_vHoverTiles.emplace_back( + g, l, x, y, Tile); + } } + Ui()->MapScreen(); } void CEditor::HandleAutosave() @@ -8561,7 +8653,7 @@ void CEditor::HandleWriterFinishJobs() return; std::shared_ptr pJob = m_WriterFinishJobs.front(); - if(pJob->Status() != IJob::STATE_DONE) + if(!pJob->Done()) return; m_WriterFinishJobs.pop_front(); @@ -8586,28 +8678,36 @@ void CEditor::HandleWriterFinishJobs() Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "editor/save", aBuf); // send rcon.. if we can - if(Client()->RconAuthed()) + if(Client()->RconAuthed() && g_Config.m_EdAutoMapReload) { CServerInfo CurrentServerInfo; Client()->GetServerInfo(&CurrentServerInfo); - NETADDR ServerAddr = Client()->ServerAddress(); - const unsigned char aIpv4Localhost[16] = {127, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - const unsigned char aIpv6Localhost[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; - // and if we're on localhost - if(!mem_comp(ServerAddr.ip, aIpv4Localhost, sizeof(aIpv4Localhost)) || !mem_comp(ServerAddr.ip, aIpv6Localhost, sizeof(aIpv6Localhost))) + NETADDR pAddr = Client()->ServerAddress(); + char aAddrStr[NETADDR_MAXSTRSIZE]; + net_addr_str(&Client()->ServerAddress(), aAddrStr, sizeof(aAddrStr), true); + + // and if we're on a local address + bool IsLocalAddress = false; + if(pAddr.ip[0] == 127 || pAddr.ip[0] == 10 || (pAddr.ip[0] == 192 && pAddr.ip[1] == 168) || (pAddr.ip[0] == 172 && (pAddr.ip[1] >= 16 && pAddr.ip[1] <= 31))) + IsLocalAddress = true; + + if(str_startswith(aAddrStr, "[fe80:") || str_startswith(aAddrStr, "[::1")) + IsLocalAddress = true; + + if(IsLocalAddress) { char aMapName[128]; IStorage::StripPathAndExtension(pJob->GetRealFileName(), aMapName, sizeof(aMapName)); if(!str_comp(aMapName, CurrentServerInfo.m_aMap)) - Client()->Rcon("reload"); + Client()->Rcon("hot_reload"); } } } void CEditor::OnUpdate() { - CUIElementBase::Init(UI()); // update static pointer because game and editor use separate UI + CUIElementBase::Init(Ui()); // update static pointer because game and editor use separate UI if(!m_EditorWasUsedBefore) { @@ -8615,29 +8715,40 @@ void CEditor::OnUpdate() Reset(); } - // handle key presses - for(size_t i = 0; i < Input()->NumEvents(); i++) + m_pContainerPannedLast = m_pContainerPanned; + + // handle mouse movement + vec2 CursorRel = vec2(0.0f, 0.0f); + IInput::ECursorType CursorType = Input()->CursorRelative(&CursorRel.x, &CursorRel.y); + if(CursorType != IInput::CURSOR_NONE) { - const IInput::CEvent &Event = Input()->GetEvent(i); - if(!Input()->IsEventValid(Event)) - continue; + Ui()->ConvertMouseMove(&CursorRel.x, &CursorRel.y, CursorType); + Ui()->OnCursorMove(CursorRel.x, CursorRel.y); + } + // handle key presses + Input()->ConsumeEvents([&](const IInput::CEvent &Event) { for(CEditorComponent &Component : m_vComponents) { - if(Component.OnInput(Event)) - break; + // Events with flag `FLAG_RELEASE` must always be forwarded to all components so keys being + // released can be handled in all components also after some components have been disabled. + if(Component.OnInput(Event) && (Event.m_Flags & ~IInput::FLAG_RELEASE) != 0) + return; } - } + Ui()->OnInput(Event); + }); HandleCursorMovement(); - DispatchInputEvents(); HandleAutosave(); HandleWriterFinishJobs(); + + for(CEditorComponent &Component : m_vComponents) + Component.OnUpdate(); } void CEditor::OnRender() { - UI()->ResetMouseSlow(); + Ui()->ResetMouseSlow(); // toggle gui if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_TAB)) @@ -8652,16 +8763,13 @@ void CEditor::OnRender() m_AnimateTime = 0; ms_pUiGotContext = nullptr; - UI()->StartCheck(); + Ui()->StartCheck(); - UI()->Update(m_MouseX, m_MouseY, m_MouseDeltaX, m_MouseDeltaY, m_MouseWorldX, m_MouseWorldY); + Ui()->Update(m_MouseWorldPos); Render(); - m_MouseDeltaX = 0.0f; - m_MouseDeltaY = 0.0f; - m_MouseDeltaWx = 0.0f; - m_MouseDeltaWy = 0.0f; + m_MouseDeltaWorld = vec2(0.0f, 0.0f); if(Input()->KeyPress(KEY_F10)) { @@ -8669,8 +8777,11 @@ void CEditor::OnRender() m_ShowMousePointer = true; } - UI()->FinishCheck(); - UI()->ClearHotkeys(); + if(g_Config.m_Debug) + Ui()->DebugRender(2.0f, Ui()->Screen()->h - 27.0f); + + Ui()->FinishCheck(); + Ui()->ClearHotkeys(); Input()->Clear(); CLineInput::RenderCandidates(); @@ -8684,24 +8795,25 @@ void CEditor::OnActivate() void CEditor::OnWindowResize() { - UI()->OnWindowResize(); + Ui()->OnWindowResize(); } void CEditor::OnClose() { - if(m_ToolbarPreviewSound && Sound()->IsPlaying(m_ToolbarPreviewSound)) + m_ColorPipetteActive = false; + + if(m_ToolbarPreviewSound >= 0 && Sound()->IsPlaying(m_ToolbarPreviewSound)) Sound()->Pause(m_ToolbarPreviewSound); - if(m_FilePreviewSound && Sound()->IsPlaying(m_FilePreviewSound)) + if(m_FilePreviewSound >= 0 && Sound()->IsPlaying(m_FilePreviewSound)) Sound()->Pause(m_FilePreviewSound); } void CEditor::OnDialogClose() { - if(m_FilePreviewSound) - { - Sound()->UnloadSample(m_FilePreviewSound); - m_FilePreviewSound = -1; - } + Graphics()->UnloadTexture(&m_FilePreviewImage); + Sound()->UnloadSample(m_FilePreviewSound); + m_FilePreviewSound = -1; + m_FilePreviewState = PREVIEW_UNLOADED; } void CEditor::LoadCurrentMap() @@ -8728,7 +8840,11 @@ bool CEditor::Save(const char *pFilename) if(std::any_of(std::begin(m_WriterFinishJobs), std::end(m_WriterFinishJobs), [pFilename](const std::shared_ptr &Job) { return str_comp(pFilename, Job->GetRealFileName()) == 0; })) return false; - return m_Map.Save(pFilename); + const auto &&ErrorHandler = [this](const char *pErrorMessage) { + ShowFileDialogError("%s", pErrorMessage); + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "editor/save", pErrorMessage); + }; + return m_Map.Save(pFilename, ErrorHandler); } bool CEditor::HandleMapDrop(const char *pFileName, int StorageType) @@ -8760,7 +8876,9 @@ bool CEditor::Load(const char *pFileName, int StorageType) str_copy(m_aFileName, pFileName); SortImages(); SelectGameLayer(); - MapView()->OnMapLoad(); + + for(CEditorComponent &Component : m_vComponents) + Component.OnMapLoad(); } else { @@ -8787,31 +8905,63 @@ bool CEditor::Append(const char *pFileName, int StorageType, bool IgnoreHistory) (int)m_Map.m_vpSounds.size(), (int)m_Map.m_vpEnvelopes.size()}; + // Keep a map to check if specific indices have already been replaced to prevent + // replacing those indices again when transfering images + static std::map s_ReplacedMap; static const auto &&s_ReplaceIndex = [](int ToReplace, int ReplaceWith) { return [ToReplace, ReplaceWith](int *pIndex) { - if(*pIndex == ToReplace) + if(*pIndex == ToReplace && !s_ReplacedMap[pIndex]) + { *pIndex = ReplaceWith; + s_ReplacedMap[pIndex] = true; + } }; }; - //Transfer non-duplicate images + const auto &&Rename = [&](const std::shared_ptr &pImage) { + char aRenamed[IO_MAX_PATH_LENGTH]; + int DuplicateCount = 1; + str_copy(aRenamed, pImage->m_aName); + while(std::find_if(m_Map.m_vpImages.begin(), m_Map.m_vpImages.end(), [aRenamed](const std::shared_ptr &OtherImage) { return str_comp(OtherImage->m_aName, aRenamed) == 0; }) != m_Map.m_vpImages.end()) + str_format(aRenamed, sizeof(aRenamed), "%s (%d)", pImage->m_aName, DuplicateCount++); // Rename to "image_name (%d)" + str_copy(pImage->m_aName, aRenamed); + }; + + // Transfer non-duplicate images + s_ReplacedMap.clear(); for(auto NewMapIt = NewMap.m_vpImages.begin(); NewMapIt != NewMap.m_vpImages.end(); ++NewMapIt) { auto pNewImage = *NewMapIt; auto NameIsTaken = [pNewImage](const std::shared_ptr &OtherImage) { return str_comp(pNewImage->m_aName, OtherImage->m_aName) == 0; }; - auto MatchInCurrentMap = std::find_if(begin(m_Map.m_vpImages), end(m_Map.m_vpImages), NameIsTaken); + auto MatchInCurrentMap = std::find_if(m_Map.m_vpImages.begin(), m_Map.m_vpImages.end(), NameIsTaken); - const bool IsDuplicate = MatchInCurrentMap != std::end(m_Map.m_vpImages); + const bool IsDuplicate = MatchInCurrentMap != m_Map.m_vpImages.end(); const int IndexToReplace = NewMapIt - NewMap.m_vpImages.begin(); if(IsDuplicate) { - const int IndexToReplaceWith = MatchInCurrentMap - m_Map.m_vpImages.begin(); + // Check for image data + const bool ImageDataEquals = (*MatchInCurrentMap)->DataEquals(*pNewImage); + + if(ImageDataEquals) + { + const int IndexToReplaceWith = MatchInCurrentMap - m_Map.m_vpImages.begin(); + + dbg_msg("editor", "map already contains image %s with the same data, removing duplicate", pNewImage->m_aName); - dbg_msg("editor", "map contains image %s already, removing duplicate", pNewImage->m_aName); + // In the new map, replace the index of the duplicate image to the index of the same in the current map. + NewMap.ModifyImageIndex(s_ReplaceIndex(IndexToReplace, IndexToReplaceWith)); + } + else + { + // Rename image and add it + Rename(pNewImage); + + dbg_msg("editor", "map already contains image %s but contents of appended image is different. Renaming to %s", (*MatchInCurrentMap)->m_aName, pNewImage->m_aName); - //In the new map, replace the index of the duplicate image to the index of the same in the current map. - NewMap.ModifyImageIndex(s_ReplaceIndex(IndexToReplace, IndexToReplaceWith)); + NewMap.ModifyImageIndex(s_ReplaceIndex(IndexToReplace, m_Map.m_vpImages.size())); + m_Map.m_vpImages.push_back(pNewImage); + } } else { @@ -8853,6 +9003,22 @@ bool CEditor::Append(const char *pFileName, int StorageType, bool IgnoreHistory) } NewMap.m_vpGroups.clear(); + // transfer server settings + for(const auto &pSetting : NewMap.m_vSettings) + { + // Check if setting already exists + bool AlreadyExists = false; + for(const auto &pExistingSetting : m_Map.m_vSettings) + { + if(!str_comp(pExistingSetting.m_aCommand, pSetting.m_aCommand)) + AlreadyExists = true; + } + if(!AlreadyExists) + m_Map.m_vSettings.push_back(pSetting); + } + + NewMap.m_vSettings.clear(); + auto IndexMap = SortImages(); if(!IgnoreHistory) @@ -8862,8 +9028,6 @@ bool CEditor::Append(const char *pFileName, int StorageType, bool IgnoreHistory) return true; } -IEditor *CreateEditor() { return new CEditor; } - void CEditor::UndoLastAction() { if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS) @@ -8883,3 +9047,118 @@ void CEditor::RedoLastAction() else m_EditorHistory.Redo(); } + +void CEditor::AdjustBrushSpecialTiles(bool UseNextFree, int Adjust) +{ + // Adjust m_Number field of tune, switch and tele tiles by `Adjust` if `UseNextFree` is false + // If true, then use the next free number instead + + auto &&AdjustNumber = [Adjust](unsigned char &Number) { + Number = ((Number + Adjust) - 1 + 255) % 255 + 1; + }; + + for(auto &pLayer : m_pBrush->m_vpLayers) + { + if(pLayer->m_Type != LAYERTYPE_TILES) + continue; + + std::shared_ptr pLayerTiles = std::static_pointer_cast(pLayer); + + // Only handle tele, switch and tune layers + if(pLayerTiles->m_Tele) + { + int NextFreeTeleNumber = FindNextFreeTeleNumber(); + int NextFreeCPNumber = FindNextFreeTeleNumber(true); + + std::shared_ptr pTeleLayer = std::static_pointer_cast(pLayer); + for(int y = 0; y < pTeleLayer->m_Height; y++) + { + for(int x = 0; x < pTeleLayer->m_Width; x++) + { + int i = y * pTeleLayer->m_Width + x; + if(!IsValidTeleTile(pTeleLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pTeleLayer->m_pTeleTile[i].m_Number)) + continue; + + if(UseNextFree) + { + if(IsTeleTileCheckpoint(pTeleLayer->m_pTiles[i].m_Index)) + pTeleLayer->m_pTeleTile[i].m_Number = NextFreeCPNumber; + else + pTeleLayer->m_pTeleTile[i].m_Number = NextFreeTeleNumber; + } + else + AdjustNumber(pTeleLayer->m_pTeleTile[i].m_Number); + } + } + } + else if(pLayerTiles->m_Tune) + { + if(!UseNextFree) + { + std::shared_ptr pTuneLayer = std::static_pointer_cast(pLayer); + for(int y = 0; y < pTuneLayer->m_Height; y++) + { + for(int x = 0; x < pTuneLayer->m_Width; x++) + { + int i = y * pTuneLayer->m_Width + x; + if(!IsValidTuneTile(pTuneLayer->m_pTiles[i].m_Index) || !pTuneLayer->m_pTuneTile[i].m_Number) + continue; + + AdjustNumber(pTuneLayer->m_pTuneTile[i].m_Number); + } + } + } + } + else if(pLayerTiles->m_Switch) + { + int NextFreeNumber = FindNextFreeSwitchNumber(); + + std::shared_ptr pSwitchLayer = std::static_pointer_cast(pLayer); + for(int y = 0; y < pSwitchLayer->m_Height; y++) + { + for(int x = 0; x < pSwitchLayer->m_Width; x++) + { + int i = y * pSwitchLayer->m_Width + x; + if(!IsValidSwitchTile(pSwitchLayer->m_pTiles[i].m_Index) || (!UseNextFree && !pSwitchLayer->m_pSwitchTile[i].m_Number)) + continue; + + if(UseNextFree) + pSwitchLayer->m_pSwitchTile[i].m_Number = NextFreeNumber; + else + AdjustNumber(pSwitchLayer->m_pSwitchTile[i].m_Number); + } + } + } + } +} + +int CEditor::FindNextFreeSwitchNumber() +{ + int Number = -1; + + for(int i = 1; i <= 255; i++) + { + if(!m_Map.m_pSwitchLayer->ContainsElementWithId(i)) + { + Number = i; + break; + } + } + return Number; +} + +int CEditor::FindNextFreeTeleNumber(bool IsCheckpoint) +{ + int Number = -1; + for(int i = 1; i <= 255; i++) + { + if(!m_Map.m_pTeleLayer->ContainsElementWithId(i, IsCheckpoint)) + { + Number = i; + break; + } + } + return Number; +} + +IEditor *CreateEditor() { return new CEditor; } diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 7977cb0904..9e1287c788 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -8,8 +8,10 @@ #include #include +#include #include +#include #include #include #include @@ -23,17 +25,22 @@ #include #include +#include #include #include #include #include #include -#include "auto_map.h" #include "editor_history.h" +#include "editor_server_settings.h" #include "editor_trackers.h" +#include "editor_ui.h" +#include "layer_selector.h" #include "map_view.h" #include "smooth_value.h" +#include +#include #include #include @@ -43,6 +50,8 @@ #include typedef std::function FIndexModifyFunction; +template +using FDropdownRenderCallback = std::function &)>; // CEditor SPECIFIC enum @@ -54,6 +63,8 @@ enum DIALOG_NONE = 0, DIALOG_FILE, + DIALOG_MAPSETTINGS_ERROR, + DIALOG_QUICK_PROMPT, }; class CEditorImage; @@ -116,16 +127,7 @@ class CEditorMap CMapInfo m_MapInfo; CMapInfo m_MapInfoTmp; - struct CSetting - { - char m_aCommand[256]; - - CSetting(const char *pCommand) - { - str_copy(m_aCommand, pCommand); - } - }; - std::vector m_vSettings; + std::vector m_vSettings; std::shared_ptr m_pGameLayer; std::shared_ptr m_pGameGroup; @@ -198,7 +200,8 @@ class CEditorMap void CreateDefault(IGraphics::CTextureHandle EntitiesTexture); // io - bool Save(const char *pFilename); + bool Save(const char *pFilename, const std::function &ErrorHandler); + bool PerformPreSaveSanityChecks(const std::function &ErrorHandler); bool Load(const char *pFilename, int StorageType, const std::function &ErrorHandler); void PerformSanityChecks(const std::function &ErrorHandler); @@ -229,8 +232,7 @@ enum { PROPTYPE_NULL = 0, PROPTYPE_BOOL, - PROPTYPE_INT_STEP, - PROPTYPE_INT_SCROLL, + PROPTYPE_INT, PROPTYPE_ANGLE_SCROLL, PROPTYPE_COLOR, PROPTYPE_IMAGE, @@ -267,6 +269,7 @@ class CEditor : public IEditor { class IInput *m_pInput = nullptr; class IClient *m_pClient = nullptr; + class IConfigManager *m_pConfigManager = nullptr; class CConfig *m_pConfig = nullptr; class IConsole *m_pConsole = nullptr; class IEngine *m_pEngine = nullptr; @@ -275,10 +278,12 @@ class CEditor : public IEditor class ISound *m_pSound = nullptr; class IStorage *m_pStorage = nullptr; CRenderTools m_RenderTools; - CUI m_UI; + CUi m_UI; std::vector> m_vComponents; CMapView m_MapView; + CLayerSelector m_LayerSelector; + CPrompt m_Prompt; bool m_EditorWasUsedBefore = false; @@ -290,7 +295,7 @@ class CEditor : public IEditor IGraphics::CTextureHandle m_SwitchTexture; IGraphics::CTextureHandle m_TuneTexture; - int GetTextureUsageFlag(); + int GetTextureUsageFlag() const; enum EPreviewState { @@ -300,26 +305,56 @@ class CEditor : public IEditor }; std::shared_ptr m_apSavedBrushes[10]; + static const ColorRGBA ms_DefaultPropColor; public: - class IInput *Input() { return m_pInput; } - class IClient *Client() { return m_pClient; } - class CConfig *Config() { return m_pConfig; } - class IConsole *Console() { return m_pConsole; } - class IEngine *Engine() { return m_pEngine; } - class IGraphics *Graphics() { return m_pGraphics; } - class ISound *Sound() { return m_pSound; } - class ITextRender *TextRender() { return m_pTextRender; } - class IStorage *Storage() { return m_pStorage; } - CUI *UI() { return &m_UI; } + class IInput *Input() const { return m_pInput; } + class IClient *Client() const { return m_pClient; } + class IConfigManager *ConfigManager() const { return m_pConfigManager; } + class CConfig *Config() const { return m_pConfig; } + class IConsole *Console() const { return m_pConsole; } + class IEngine *Engine() const { return m_pEngine; } + class IGraphics *Graphics() const { return m_pGraphics; } + class ISound *Sound() const { return m_pSound; } + class ITextRender *TextRender() const { return m_pTextRender; } + class IStorage *Storage() const { return m_pStorage; } + CUi *Ui() { return &m_UI; } CRenderTools *RenderTools() { return &m_RenderTools; } CMapView *MapView() { return &m_MapView; } const CMapView *MapView() const { return &m_MapView; } + CLayerSelector *LayerSelector() { return &m_LayerSelector; } + + void SelectNextLayer(); + void SelectPreviousLayer(); + + void FillGameTiles(EGameTileOp FillTile) const; + bool CanFillGameTiles() const; + void AddQuadOrSound(); + void AddGroup(); + void AddSoundLayer(); + void AddTileLayer(); + void AddQuadsLayer(); + void AddSwitchLayer(); + void AddFrontLayer(); + void AddTuneLayer(); + void AddSpeedupLayer(); + void AddTeleLayer(); + void DeleteSelectedLayer(); + void LayerSelectImage(); + bool IsNonGameTileLayerSelected() const; + void MapDetails(); +#define REGISTER_QUICK_ACTION(name, text, callback, disabled, active, button_color, description) CQuickAction m_QuickAction##name; +#include +#undef REGISTER_QUICK_ACTION CEditor() : +#define REGISTER_QUICK_ACTION(name, text, callback, disabled, active, button_color, description) m_QuickAction##name(text, description, callback, disabled, active, button_color), +#include +#undef REGISTER_QUICK_ACTION m_ZoomEnvelopeX(1.0f, 0.1f, 600.0f), - m_ZoomEnvelopeY(640.0f, 0.1f, 32000.0f) + m_ZoomEnvelopeY(640.0f, 0.1f, 32000.0f), + m_MapSettingsCommandContext(m_MapSettingsBackend.NewContext(&m_SettingsCommandInput)) { m_EntitiesTexture.Invalidate(); m_FrontTexture.Invalidate(); @@ -366,10 +401,6 @@ class CEditor : public IEditor m_OffsetEnvelopeY = 0.5f; m_ShowMousePointer = true; - m_MouseDeltaX = 0; - m_MouseDeltaY = 0; - m_MouseDeltaWx = 0; - m_MouseDeltaWy = 0; m_GuiActive = true; m_PreviewZoom = false; @@ -380,6 +411,7 @@ class CEditor : public IEditor m_AnimateStart = 0; m_AnimateTime = 0; m_AnimateSpeed = 1; + m_AnimateUpdatePopup = false; m_ShowEnvelopePreview = SHOWENV_NONE; m_SelectedQuadEnvelope = -1; @@ -394,15 +426,24 @@ class CEditor : public IEditor m_QuadKnifeCount = 0; mem_zero(m_aQuadKnifePoints, sizeof(m_aQuadKnifePoints)); + for(size_t i = 0; i < std::size(m_aSavedColors); ++i) + { + m_aSavedColors[i] = color_cast(ColorHSLA(i / (float)std::size(m_aSavedColors), 1.0f, 0.5f)); + } + m_CheckerTexture.Invalidate(); m_BackgroundTexture.Invalidate(); - m_CursorTexture.Invalidate(); + for(auto &CursorTexture : m_aCursorTextures) + CursorTexture.Invalidate(); + + m_CursorType = CURSOR_NORMAL; ms_pUiGotContext = nullptr; // DDRace m_TeleNumber = 1; + m_TeleCheckpointNumber = 1; m_SwitchNum = 1; m_TuningNum = 1; m_SwitchDelay = 0; @@ -415,6 +456,27 @@ class CEditor : public IEditor m_BrushDrawDestructive = true; } + class CHoverTile + { + public: + CHoverTile(int Group, int Layer, int x, int y, const CTile Tile) : + m_Group(Group), + m_Layer(Layer), + m_X(x), + m_Y(y), + m_Tile(Tile) + { + } + + int m_Group; + int m_Layer; + int m_X; + int m_Y; + const CTile m_Tile; + }; + std::vector m_vHoverTiles; + const std::vector &HoverTiles() const { return m_vHoverTiles; } + void Init() override; void OnUpdate() override; void OnRender() override; @@ -429,7 +491,7 @@ class CEditor : public IEditor void ResetIngameMoved() override { m_IngameMoved = false; } void HandleCursorMovement(); - void DispatchInputEvents(); + void OnMouseMove(vec2 MousePos); void HandleAutosave(); bool PerformAutosave(); void HandleWriterFinishJobs(); @@ -446,7 +508,7 @@ class CEditor : public IEditor return str_comp(pLhs, pRhs) < 0; } }; - std::map m_PopupMessageContexts; + std::map m_PopupMessageContexts; void ShowFileDialogError(const char *pFormat, ...) GNUC_ATTRIBUTE((format(printf, 2, 3))); @@ -461,6 +523,7 @@ class CEditor : public IEditor void RenderPressedKeys(CUIRect View); void RenderSavingIndicator(CUIRect View); void FreeDynamicPopupMenus(); + void UpdateColorPipette(); void RenderMousePointer(); std::vector GetSelectedQuads(); @@ -499,12 +562,12 @@ class CEditor : public IEditor std::pair EnvGetSelectedTimeAndValue() const; template - SEditResult DoPropertiesWithState(CUIRect *pToolbox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color = ColorRGBA(1, 1, 1, 0.5f)); - int DoProperties(CUIRect *pToolbox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color = ColorRGBA(1, 1, 1, 0.5f)); + SEditResult DoPropertiesWithState(CUIRect *pToolbox, CProperty *pProps, int *pIds, int *pNewVal, const std::vector &vColors = {}); + int DoProperties(CUIRect *pToolbox, CProperty *pProps, int *pIds, int *pNewVal, const std::vector &vColors = {}); - CUI::SColorPickerPopupContext m_ColorPickerPopupContext; - const void *m_pColorPickerPopupActiveID = nullptr; - void DoColorPickerButton(const void *pID, const CUIRect *pRect, ColorRGBA Color, const std::function &SetColor); + CUi::SColorPickerPopupContext m_ColorPickerPopupContext; + const void *m_pColorPickerPopupActiveId = nullptr; + void DoColorPickerButton(const void *pId, const CUIRect *pRect, ColorRGBA Color, const std::function &SetColor); int m_Mode; int m_Dialog; @@ -579,7 +642,8 @@ class CEditor : public IEditor IGraphics::CTextureHandle m_FilePreviewImage; int m_FilePreviewSound; EPreviewState m_FilePreviewState; - CImageInfo m_FilePreviewImageInfo; + int m_FilePreviewImageWidth; + int m_FilePreviewImageHeight; bool m_FileDialogOpening; int m_ToolbarPreviewSound; @@ -669,17 +733,13 @@ class CEditor : public IEditor char m_aMenuBackgroundTooltip[256]; bool m_PreviewZoom; - float m_MouseWScale = 1.0f; // Mouse (i.e. UI) scale relative to the World (selected Group) - float m_MouseX = 0.0f; - float m_MouseY = 0.0f; - float m_MouseWorldX = 0.0f; - float m_MouseWorldY = 0.0f; - float m_MouseWorldNoParaX = 0.0f; - float m_MouseWorldNoParaY = 0.0f; - float m_MouseDeltaX; - float m_MouseDeltaY; - float m_MouseDeltaWx; - float m_MouseDeltaWy; + float m_MouseWorldScale = 1.0f; // Mouse (i.e. UI) scale relative to the World (selected Group) + vec2 m_MouseWorldPos = vec2(0.0f, 0.0f); + vec2 m_MouseWorldNoParaPos = vec2(0.0f, 0.0f); + vec2 m_MouseDeltaWorld = vec2(0.0f, 0.0f); + const void *m_pContainerPanned; + const void *m_pContainerPannedLast; + char m_MapEditorId; // UI element ID for the main map editor enum EShowTile { @@ -689,10 +749,12 @@ class CEditor : public IEditor }; EShowTile m_ShowTileInfo; bool m_ShowDetail; + bool m_Animate; int64_t m_AnimateStart; float m_AnimateTime; float m_AnimateSpeed; + bool m_AnimateUpdatePopup; enum EExtraEditor { @@ -704,6 +766,7 @@ class CEditor : public IEditor }; EExtraEditor m_ActiveExtraEditor = EXTRAEDITOR_NONE; float m_aExtraEditorSplits[NUM_EXTRAEDITORS] = {250.0f, 250.0f, 250.0f}; + float m_ToolBoxWidth = 100.0f; enum EShowEnvelope { @@ -735,9 +798,23 @@ class CEditor : public IEditor int m_QuadKnifeCount; vec2 m_aQuadKnifePoints[4]; + // Color palette and pipette + ColorRGBA m_aSavedColors[8]; + ColorRGBA m_PipetteColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + bool m_ColorPipetteActive = false; + IGraphics::CTextureHandle m_CheckerTexture; IGraphics::CTextureHandle m_BackgroundTexture; - IGraphics::CTextureHandle m_CursorTexture; + + enum ECursorType + { + CURSOR_NORMAL, + CURSOR_RESIZE_V, + CURSOR_RESIZE_H, + NUM_CURSORS + }; + IGraphics::CTextureHandle m_aCursorTextures[ECursorType::NUM_CURSORS]; + ECursorType m_CursorType; IGraphics::CTextureHandle GetEntitiesTexture(); @@ -752,9 +829,11 @@ class CEditor : public IEditor int m_ShiftBy; - static void EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Channels, void *pUser); + static void EnvelopeEval(int TimeOffsetMillis, int Env, ColorRGBA &Result, size_t Channels, void *pUser); CLineInputBuffered<256> m_SettingsCommandInput; + CMapSettingsBackend m_MapSettingsBackend; + CMapSettingsBackend::CContext m_MapSettingsCommandContext; CImageInfo m_TileartImageInfo; char m_aTileartFilename[IO_MAX_PATH_LENGTH]; @@ -763,34 +842,35 @@ class CEditor : public IEditor void PlaceBorderTiles(); - void UpdateTooltip(const void *pID, const CUIRect *pRect, const char *pToolTip); - int DoButton_Editor_Common(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip); - int DoButton_Editor(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip); - int DoButton_Env(const void *pID, const char *pText, int Checked, const CUIRect *pRect, const char *pToolTip, ColorRGBA Color, int Corners); + void UpdateTooltip(const void *pId, const CUIRect *pRect, const char *pToolTip); + int DoButton_Editor_Common(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip); + int DoButton_Editor(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip); + int DoButton_Env(const void *pId, const char *pText, int Checked, const CUIRect *pRect, const char *pToolTip, ColorRGBA Color, int Corners); - int DoButton_Ex(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize = 10.0f); - int DoButton_FontIcon(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize = 10.0f); - int DoButton_ButtonDec(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip); - int DoButton_ButtonInc(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip); + int DoButton_Ex(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize = EditorFontSizes::MENU, int Align = TEXTALIGN_MC); + int DoButton_FontIcon(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip, int Corners, float FontSize = 10.0f); + int DoButton_MenuItem(const void *pId, const char *pText, int Checked, const CUIRect *pRect, int Flags = 0, const char *pToolTip = nullptr); - int DoButton_File(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip); + int DoButton_DraggableEx(const void *pId, const char *pText, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted, int Flags, const char *pToolTip = nullptr, int Corners = IGraphics::CORNER_ALL, float FontSize = 10.0f); - int DoButton_Menu(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags, const char *pToolTip); - int DoButton_MenuItem(const void *pID, const char *pText, int Checked, const CUIRect *pRect, int Flags = 0, const char *pToolTip = nullptr); + bool DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const char *pToolTip = nullptr, const std::vector &vColorSplits = {}); + bool DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const char *pToolTip = nullptr, const std::vector &vColorSplits = {}); - int DoButton_DraggableEx(const void *pID, const char *pText, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted, int Flags, const char *pToolTip = nullptr, int Corners = IGraphics::CORNER_ALL, float FontSize = 10.0f); + void DoMapSettingsEditBox(CMapSettingsBackend::CContext *pContext, const CUIRect *pRect, float FontSize, float DropdownMaxHeight, int Corners = IGraphics::CORNER_ALL, const char *pToolTip = nullptr); - bool DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const char *pToolTip = nullptr); - bool DoClearableEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize, int Corners = IGraphics::CORNER_ALL, const char *pToolTip = nullptr); + template + int DoEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CLineInput *pLineInput, const CUIRect *pEditBoxRect, int x, float MaxHeight, bool AutoWidth, const std::vector &vData, const FDropdownRenderCallback &pfnMatchCallback); + template + int RenderEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CUIRect View, CLineInput *pLineInput, int x, float MaxHeight, bool AutoWidth, const std::vector &vData, const FDropdownRenderCallback &pfnMatchCallback); - void RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, float Size, float Brightness); + void RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, float Size, float Brightness) const; - SEditResult UiDoValueSelector(void *pID, CUIRect *pRect, const char *pLabel, int Current, int Min, int Max, int Step, float Scale, const char *pToolTip, bool IsDegree = false, bool IsHex = false, int corners = IGraphics::CORNER_ALL, ColorRGBA *pColor = nullptr, bool ShowValue = true); + SEditResult UiDoValueSelector(void *pId, CUIRect *pRect, const char *pLabel, int Current, int Min, int Max, int Step, float Scale, const char *pToolTip, bool IsDegree = false, bool IsHex = false, int Corners = IGraphics::CORNER_ALL, const ColorRGBA *pColor = nullptr, bool ShowValue = true); - static CUI::EPopupMenuFunctionResult PopupMenuFile(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupMenuTools(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupMenuSettings(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupGroup(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupMenuFile(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupMenuTools(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupMenuSettings(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupGroup(void *pContext, CUIRect View, bool Active); struct SLayerPopupContext : public SPopupMenuId { CEditor *m_pEditor; @@ -798,29 +878,30 @@ class CEditor : public IEditor std::vector m_vLayerIndices; CLayerTiles::SCommonPropState m_CommonPropState; }; - static CUI::EPopupMenuFunctionResult PopupLayer(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupQuad(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupSource(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupPoint(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupEnvPoint(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupEnvPointMulti(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupEnvPointCurveType(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupImage(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupSound(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupNewFolder(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupMapInfo(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupEvent(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupSelectImage(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupSelectSound(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupSelectGametileOp(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupSelectConfigAutoMap(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupTele(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupSpeedup(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupSwitch(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupTune(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupGoto(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupEntities(void *pContext, CUIRect View, bool Active); - static CUI::EPopupMenuFunctionResult PopupProofMode(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupLayer(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupQuad(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupSource(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupPoint(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupEnvPoint(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupEnvPointMulti(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupEnvPointCurveType(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupImage(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupSound(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupNewFolder(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupMapInfo(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupEvent(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupSelectImage(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupSelectSound(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupSelectGametileOp(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupSelectConfigAutoMap(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupTele(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupSpeedup(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupSwitch(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupTune(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupGoto(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupEntities(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupProofMode(void *pContext, CUIRect View, bool Active); + static CUi::EPopupMenuFunctionResult PopupAnimateSettings(void *pContext, CUIRect View, bool Active); static bool CallbackOpenMap(const char *pFileName, int StorageType, void *pUser); static bool CallbackAppendMap(const char *pFileName, int StorageType, void *pUser); @@ -829,6 +910,7 @@ class CEditor : public IEditor static bool CallbackAddTileart(const char *pFilepath, int StorageType, void *pUser); static bool CallbackSaveImage(const char *pFileName, int StorageType, void *pUser); static bool CallbackSaveSound(const char *pFileName, int StorageType, void *pUser); + static bool CallbackCustomEntities(const char *pFileName, int StorageType, void *pUser); void PopupSelectImageInvoke(int Current, float x, float y); int PopupSelectImageResult(); @@ -844,14 +926,14 @@ class CEditor : public IEditor void DoQuadEnvelopes(const std::vector &vQuads, IGraphics::CTextureHandle Texture = IGraphics::CTextureHandle()); void DoQuadEnvPoint(const CQuad *pQuad, int QIndex, int pIndex); - void DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int v); + void DoQuadPoint(int LayerIndex, const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int v); void SetHotQuadPoint(const std::shared_ptr &pLayer); float TriangleArea(vec2 A, vec2 B, vec2 C); bool IsInTriangle(vec2 Point, vec2 A, vec2 B, vec2 C); void DoQuadKnife(int QuadIndex); - void DoSoundSource(CSoundSource *pSource, int Index); + void DoSoundSource(int LayerIndex, CSoundSource *pSource, int Index); enum class EAxis { @@ -875,13 +957,13 @@ class CEditor : public IEditor void DoMapEditor(CUIRect View); void DoToolbarLayers(CUIRect Toolbar); void DoToolbarSounds(CUIRect Toolbar); - void DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, int Index); + void DoQuad(int LayerIndex, const std::shared_ptr &pLayer, CQuad *pQuad, int Index); void PreparePointDrag(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex); void DoPointDrag(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY); - EAxis GetDragAxis(int OffsetX, int OffsetY); - void DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point); - void DrawAABB(const SAxisAlignedBoundingBox &AABB, int OffsetX = 0, int OffsetY = 0); - ColorRGBA GetButtonColor(const void *pID, int Checked); + EAxis GetDragAxis(int OffsetX, int OffsetY) const; + void DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point) const; + void DrawAABB(const SAxisAlignedBoundingBox &AABB, int OffsetX = 0, int OffsetY = 0) const; + ColorRGBA GetButtonColor(const void *pId, int Checked); // Alignment methods // These methods take `OffsetX` and `OffsetY` because the calculations are made with the original positions @@ -904,10 +986,10 @@ class CEditor : public IEditor void ComputePointAlignments(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY, std::vector &vAlignments, bool Append = false) const; void ComputePointsAlignments(const std::shared_ptr &pLayer, bool Pivot, int OffsetX, int OffsetY, std::vector &vAlignments) const; void ComputeAABBAlignments(const std::shared_ptr &pLayer, const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY, std::vector &vAlignments) const; - void DrawPointAlignments(const std::vector &vAlignments, int OffsetX, int OffsetY); + void DrawPointAlignments(const std::vector &vAlignments, int OffsetX, int OffsetY) const; void QuadSelectionAABB(const std::shared_ptr &pLayer, SAxisAlignedBoundingBox &OutAABB); void ApplyAlignments(const std::vector &vAlignments, int &OffsetX, int &OffsetY); - void ApplyAxisAlignment(int &OffsetX, int &OffsetY); + void ApplyAxisAlignment(int &OffsetX, int &OffsetY) const; bool ReplaceImage(const char *pFilename, int StorageType, bool CheckDuplicate); static bool ReplaceImageCallback(const char *pFilename, int StorageType, void *pUser); @@ -930,10 +1012,21 @@ class CEditor : public IEditor void RenderTooltip(CUIRect TooltipRect); void RenderEnvelopeEditor(CUIRect View); + + void RenderMapSettingsErrorDialog(); void RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast); + static void MapSettingsDropdownRenderCallback(const SPossibleValueMatch &Match, char (&aOutput)[128], std::vector &vColorSplits); + void RenderEditorHistory(CUIRect View); - void RenderExtraEditorDragBar(CUIRect View, CUIRect DragBar); + enum class EDragSide // Which side is the drag bar on + { + SIDE_BOTTOM, + SIDE_LEFT, + SIDE_TOP, + SIDE_RIGHT + }; + void DoEditorDragBar(CUIRect View, CUIRect *pDragBar, EDragSide Side, float *pValue, float MinValue = 100.0f, float MaxValue = 400.0f); void SetHotEnvelopePoint(const CUIRect &View, const std::shared_ptr &pEnvelope, int ActiveChannels); @@ -942,9 +1035,8 @@ class CEditor : public IEditor void SelectGameLayer(); std::vector SortImages(); - bool SelectLayerByTile(); - void DoAudioPreview(CUIRect View, const void *pPlayPauseButtonID, const void *pStopButtonID, const void *pSeekBarID, const int SampleID); + void DoAudioPreview(CUIRect View, const void *pPlayPauseButtonId, const void *pStopButtonId, const void *pSeekBarId, int SampleId); // Tile Numbers For Explanations - TODO: Add/Improve tiles and explanations enum @@ -1039,8 +1131,8 @@ class CEditor : public IEditor float EnvelopeToScreenX(const CUIRect &View, float x) const; float ScreenToEnvelopeY(const CUIRect &View, float y) const; float EnvelopeToScreenY(const CUIRect &View, float y) const; - float ScreenToEnvelopeDX(const CUIRect &View, float dx); - float ScreenToEnvelopeDY(const CUIRect &View, float dy); + float ScreenToEnvelopeDX(const CUIRect &View, float DeltaX); + float ScreenToEnvelopeDY(const CUIRect &View, float DeltaY); // DDRace @@ -1051,6 +1143,8 @@ class CEditor : public IEditor IGraphics::CTextureHandle GetTuneTexture(); unsigned char m_TeleNumber; + unsigned char m_TeleCheckpointNumber; + unsigned char m_ViewTeleNumber; unsigned char m_TuningNum; @@ -1060,8 +1154,12 @@ class CEditor : public IEditor unsigned char m_SwitchNum; unsigned char m_SwitchDelay; + unsigned char m_ViewSwitch; + + void AdjustBrushSpecialTiles(bool UseNextFree, int Adjust = 0); + int FindNextFreeSwitchNumber(); + int FindNextFreeTeleNumber(bool IsCheckpoint = false); -public: // Undo/Redo CEditorHistory m_EditorHistory; CEditorHistory m_ServerSettingsHistory; @@ -1073,7 +1171,6 @@ class CEditor : public IEditor void UndoLastAction(); void RedoLastAction(); -private: std::map m_QuadDragOriginalPoints; }; diff --git a/src/game/editor/editor_actions.cpp b/src/game/editor/editor_actions.cpp index 88ce5d5590..8f4334d04b 100644 --- a/src/game/editor/editor_actions.cpp +++ b/src/game/editor/editor_actions.cpp @@ -320,7 +320,7 @@ void CEditorActionDeleteQuad::Redo() CEditorActionEditQuadPoint::CEditorActionEditQuadPoint(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, std::vector const &vPreviousPoints, std::vector const &vCurrentPoints) : CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_QuadIndex(QuadIndex), m_vPreviousPoints(vPreviousPoints), m_vCurrentPoints(vCurrentPoints) { - str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quad points"); + str_copy(m_aDisplayText, "Edit quad points"); } void CEditorActionEditQuadPoint::Undo() @@ -476,24 +476,24 @@ void CEditorActionBulk::Redo() // --------- -CEditorActionAutoMap::CEditorActionAutoMap(CEditor *pEditor, int GroupIndex, int LayerIndex, const EditorTileStateChangeHistory &Changes) : +CEditorActionTileChanges::CEditorActionTileChanges(CEditor *pEditor, int GroupIndex, int LayerIndex, const char *pAction, const EditorTileStateChangeHistory &Changes) : CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_Changes(Changes) { ComputeInfos(); - str_format(m_aDisplayText, sizeof(m_aDisplayText), "Auto map (x%d)", m_TotalChanges); + str_format(m_aDisplayText, sizeof(m_aDisplayText), "%s (x%d)", pAction, m_TotalChanges); } -void CEditorActionAutoMap::Undo() +void CEditorActionTileChanges::Undo() { Apply(true); } -void CEditorActionAutoMap::Redo() +void CEditorActionTileChanges::Redo() { Apply(false); } -void CEditorActionAutoMap::Apply(bool Undo) +void CEditorActionTileChanges::Apply(bool Undo) { auto &Map = m_pEditor->m_Map; std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); @@ -512,7 +512,7 @@ void CEditorActionAutoMap::Apply(bool Undo) Map.OnModify(); } -void CEditorActionAutoMap::ComputeInfos() +void CEditorActionTileChanges::ComputeInfos() { m_TotalChanges = 0; for(auto &Line : m_Changes) @@ -539,6 +539,22 @@ void CEditorActionAddLayer::Undo() { // Undo: remove layer from vector but keep it in case we want to add it back auto &vLayers = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_vpLayers; + + if(m_pLayer->m_Type == LAYERTYPE_TILES) + { + std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); + if(pLayerTiles->m_Front) + m_pEditor->m_Map.m_pFrontLayer = nullptr; + else if(pLayerTiles->m_Tele) + m_pEditor->m_Map.m_pTeleLayer = nullptr; + else if(pLayerTiles->m_Speedup) + m_pEditor->m_Map.m_pSpeedupLayer = nullptr; + else if(pLayerTiles->m_Switch) + m_pEditor->m_Map.m_pSwitchLayer = nullptr; + else if(pLayerTiles->m_Tune) + m_pEditor->m_Map.m_pTuneLayer = nullptr; + } + vLayers.erase(vLayers.begin() + m_LayerIndex); m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_Collapse = false; @@ -552,6 +568,22 @@ void CEditorActionAddLayer::Redo() { // Redo: add back the removed layer contained in this class auto &vLayers = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_vpLayers; + + if(m_pLayer->m_Type == LAYERTYPE_TILES) + { + std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); + if(pLayerTiles->m_Front) + m_pEditor->m_Map.m_pFrontLayer = std::static_pointer_cast(m_pLayer); + else if(pLayerTiles->m_Tele) + m_pEditor->m_Map.m_pTeleLayer = std::static_pointer_cast(m_pLayer); + else if(pLayerTiles->m_Speedup) + m_pEditor->m_Map.m_pSpeedupLayer = std::static_pointer_cast(m_pLayer); + else if(pLayerTiles->m_Switch) + m_pEditor->m_Map.m_pSwitchLayer = std::static_pointer_cast(m_pLayer); + else if(pLayerTiles->m_Tune) + m_pEditor->m_Map.m_pTuneLayer = std::static_pointer_cast(m_pLayer); + } + vLayers.insert(vLayers.begin() + m_LayerIndex, m_pLayer); m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_Collapse = false; @@ -628,7 +660,7 @@ CEditorActionGroup::CEditorActionGroup(CEditor *pEditor, int GroupIndex, bool De if(m_Delete) str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete group %d", m_GroupIndex); else - str_format(m_aDisplayText, sizeof(m_aDisplayText), "New group"); + str_copy(m_aDisplayText, "New group", sizeof(m_aDisplayText)); } void CEditorActionGroup::Undo() @@ -794,12 +826,14 @@ void CEditorActionEditLayerProp::Apply(int Value) if(m_Prop == ELayerProp::PROP_GROUP) { auto pCurrentGroup = m_pEditor->m_Map.m_vpGroups[Value == m_Previous ? m_Current : m_Previous]; - auto Position = std::find(pCurrentGroup->m_vpLayers.begin(), pCurrentGroup->m_vpLayers.end(), m_pLayer); - if(Position != pCurrentGroup->m_vpLayers.end()) - pCurrentGroup->m_vpLayers.erase(Position); - m_pEditor->m_Map.m_vpGroups[Value]->m_vpLayers.push_back(m_pLayer); + auto pPreviousGroup = m_pEditor->m_Map.m_vpGroups[Value]; + pCurrentGroup->m_vpLayers.erase(pCurrentGroup->m_vpLayers.begin() + pCurrentGroup->m_vpLayers.size() - 1); + if(Value == m_Previous) + pPreviousGroup->m_vpLayers.insert(pPreviousGroup->m_vpLayers.begin() + m_LayerIndex, m_pLayer); + else + pPreviousGroup->m_vpLayers.push_back(m_pLayer); m_pEditor->m_SelectedGroup = Value; - m_pEditor->SelectLayer(m_pEditor->m_Map.m_vpGroups[Value]->m_vpLayers.size() - 1); + m_pEditor->SelectLayer(m_LayerIndex); } else if(m_Prop == ELayerProp::PROP_HQ) { @@ -1196,7 +1230,7 @@ CEditorActionTileArt::CEditorActionTileArt(CEditor *pEditor, int PreviousImageCo IEditorAction(pEditor), m_PreviousImageCount(PreviousImageCount), m_vImageIndexMap(vImageIndexMap) { str_copy(m_aTileArtFile, pTileArtFile); - str_format(m_aDisplayText, sizeof(m_aDisplayText), "Tile art"); + str_copy(m_aDisplayText, "Tile art"); } void CEditorActionTileArt::Undo() @@ -1241,7 +1275,7 @@ void CEditorActionTileArt::Undo() void CEditorActionTileArt::Redo() { - if(!m_pEditor->Graphics()->LoadPNG(&m_pEditor->m_TileartImageInfo, m_aTileArtFile, IStorage::TYPE_ALL)) + if(!m_pEditor->Graphics()->LoadPng(m_pEditor->m_TileartImageInfo, m_aTileArtFile, IStorage::TYPE_ALL)) { m_pEditor->ShowFileDialogError("Failed to load image from file '%s'.", m_aTileArtFile); return; @@ -1264,7 +1298,7 @@ CEditorCommandAction::CEditorCommandAction(CEditor *pEditor, EType Type, int *pS switch(m_Type) { case EType::ADD: - str_format(m_aDisplayText, sizeof(m_aDisplayText), "Add command"); + str_copy(m_aDisplayText, "Add command"); break; case EType::EDIT: str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit command %d", m_CommandIndex); @@ -2065,3 +2099,23 @@ void CEditorActionNewQuad::Redo() std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); pLayerQuads->m_vQuads.emplace_back(m_Quad); } + +// -------------- + +CEditorActionMoveSoundSource::CEditorActionMoveSoundSource(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, CPoint OriginalPosition, CPoint CurrentPosition) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_SourceIndex(SourceIndex), m_OriginalPosition(OriginalPosition), m_CurrentPosition(CurrentPosition) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Move sound source %d of layer %d in group %d", SourceIndex, LayerIndex, GroupIndex); +} + +void CEditorActionMoveSoundSource::Undo() +{ + dbg_assert(m_pLayer->m_Type == LAYERTYPE_SOUNDS, "Layer type does not match a sound layer"); + std::static_pointer_cast(m_pLayer)->m_vSources[m_SourceIndex].m_Position = m_OriginalPosition; +} + +void CEditorActionMoveSoundSource::Redo() +{ + dbg_assert(m_pLayer->m_Type == LAYERTYPE_SOUNDS, "Layer type does not match a sound layer"); + std::static_pointer_cast(m_pLayer)->m_vSources[m_SourceIndex].m_Position = m_CurrentPosition; +} diff --git a/src/game/editor/editor_actions.h b/src/game/editor/editor_actions.h index 198e02a6f3..688a342176 100644 --- a/src/game/editor/editor_actions.h +++ b/src/game/editor/editor_actions.h @@ -155,10 +155,10 @@ class CEditorActionBulk : public IEditorAction // -class CEditorActionAutoMap : public CEditorActionLayerBase +class CEditorActionTileChanges : public CEditorActionLayerBase { public: - CEditorActionAutoMap(CEditor *pEditor, int GroupIndex, int LayerIndex, const EditorTileStateChangeHistory &Changes); + CEditorActionTileChanges(CEditor *pEditor, int GroupIndex, int LayerIndex, const char *pAction, const EditorTileStateChangeHistory &Changes); void Undo() override; void Redo() override; @@ -645,4 +645,18 @@ class CEditorActionNewQuad : public CEditorActionLayerBase CQuad m_Quad; }; +class CEditorActionMoveSoundSource : public CEditorActionLayerBase +{ +public: + CEditorActionMoveSoundSource(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, CPoint OriginalPosition, CPoint CurrentPosition); + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + CPoint m_OriginalPosition; + CPoint m_CurrentPosition; +}; + #endif diff --git a/src/game/editor/editor_history.cpp b/src/game/editor/editor_history.cpp index 46110de2d5..f020d42b12 100644 --- a/src/game/editor/editor_history.cpp +++ b/src/game/editor/editor_history.cpp @@ -17,6 +17,12 @@ void CEditorHistory::Execute(const std::shared_ptr &pAction, cons void CEditorHistory::RecordAction(const std::shared_ptr &pAction, const char *pDisplay) { + if(m_IsBulk) + { + m_vpBulkActions.push_back(pAction); + return; + } + m_vpRedoActions.clear(); if((int)m_vpUndoActions.size() >= g_Config.m_ClEditorMaxHistory) @@ -63,3 +69,27 @@ void CEditorHistory::Clear() m_vpUndoActions.clear(); m_vpRedoActions.clear(); } + +void CEditorHistory::BeginBulk() +{ + m_IsBulk = true; + m_vpBulkActions.clear(); +} + +void CEditorHistory::EndBulk(const char *pDisplay) +{ + if(!m_IsBulk) + return; + m_IsBulk = false; + + // Record bulk action + if(!m_vpBulkActions.empty()) + RecordAction(std::make_shared(m_pEditor, m_vpBulkActions, pDisplay, true)); + + m_vpBulkActions.clear(); +} + +void CEditorHistory::EndBulk(int DisplayToUse) +{ + EndBulk((DisplayToUse < 0 || DisplayToUse >= (int)m_vpBulkActions.size()) ? nullptr : m_vpBulkActions[DisplayToUse]->DisplayText()); +} diff --git a/src/game/editor/editor_history.h b/src/game/editor/editor_history.h index 144147140f..1a0dbf79fa 100644 --- a/src/game/editor/editor_history.h +++ b/src/game/editor/editor_history.h @@ -3,7 +3,9 @@ #include "editor_action.h" -#include +#include +#include +#include class CEditorHistory { @@ -11,6 +13,7 @@ class CEditorHistory CEditorHistory() { m_pEditor = nullptr; + m_IsBulk = false; } ~CEditorHistory() @@ -29,9 +32,17 @@ class CEditorHistory bool CanUndo() const { return !m_vpUndoActions.empty(); } bool CanRedo() const { return !m_vpRedoActions.empty(); } + void BeginBulk(); + void EndBulk(const char *pDisplay = nullptr); + void EndBulk(int DisplayToUse); + CEditor *m_pEditor; std::deque> m_vpUndoActions; std::deque> m_vpRedoActions; + +private: + std::vector> m_vpBulkActions; + bool m_IsBulk; }; #endif diff --git a/src/game/editor/editor_object.cpp b/src/game/editor/editor_object.cpp index 2154c3d10a..2889a068f2 100644 --- a/src/game/editor/editor_object.cpp +++ b/src/game/editor/editor_object.cpp @@ -2,15 +2,14 @@ #include "editor.h" -void CEditorObject::Init(CEditor *pEditor) +void CEditorObject::OnInit(CEditor *pEditor) { m_pEditor = pEditor; OnReset(); } -void CEditorObject::OnUpdate(CUIRect View) +void CEditorObject::OnUpdate() { - OnRender(View); if(IsActive()) OnActive(); else if(IsHot()) @@ -29,37 +28,37 @@ void CEditorObject::OnMapLoad() {} bool CEditorObject::IsHot() { - return UI()->HotItem() == this; + return Ui()->HotItem() == this; } void CEditorObject::SetHot() { - UI()->SetHotItem(this); + Ui()->SetHotItem(this); } void CEditorObject::UnsetHot() { if(IsHot()) - UI()->SetHotItem(nullptr); + Ui()->SetHotItem(nullptr); } void CEditorObject::OnHot() {} bool CEditorObject::IsActive() { - return UI()->CheckActiveItem(this); + return Ui()->CheckActiveItem(this); } void CEditorObject::SetActive() { SetHot(); - UI()->SetActiveItem(this); + Ui()->SetActiveItem(this); } void CEditorObject::SetInactive() { if(IsActive()) - UI()->SetActiveItem(nullptr); + Ui()->SetActiveItem(nullptr); } void CEditorObject::OnActive() {} @@ -75,5 +74,5 @@ IGraphics *CEditorObject::Graphics() { return m_pEditor->Graphics(); } ISound *CEditorObject::Sound() { return m_pEditor->Sound(); } ITextRender *CEditorObject::TextRender() { return m_pEditor->TextRender(); } IStorage *CEditorObject::Storage() { return m_pEditor->Storage(); } -CUI *CEditorObject::UI() { return m_pEditor->UI(); } +CUi *CEditorObject::Ui() { return m_pEditor->Ui(); } CRenderTools *CEditorObject::RenderTools() { return m_pEditor->RenderTools(); } diff --git a/src/game/editor/editor_object.h b/src/game/editor/editor_object.h index 87ec1df4e7..59fee4b2e4 100644 --- a/src/game/editor/editor_object.h +++ b/src/game/editor/editor_object.h @@ -6,7 +6,7 @@ #include #include -class CUI; +class CUi; class CEditor; class IClient; class CConfig; @@ -26,14 +26,14 @@ class CEditorObject /** * Initialise the component and interface pointers. * Needs to be the first function that is called. - * The default implentation also resets the component. + * The default implentation also resets the component. */ - virtual void Init(CEditor *pEditor); + virtual void OnInit(CEditor *pEditor); /** - * Calls `OnRender` and then maybe `OnHot` or `OnActive`. + * Maybe calls `OnHot` or `OnActive`. */ - void OnUpdate(CUIRect View); + virtual void OnUpdate(); /** * Gets called before `OnRender`. Should return true @@ -45,7 +45,6 @@ class CEditorObject /** * Gets called after `OnRender` when the component is hot but not active. - * I */ virtual void OnHot(); @@ -76,7 +75,7 @@ class CEditorObject ISound *Sound(); ITextRender *TextRender(); IStorage *Storage(); - CUI *UI(); + CUi *Ui(); CRenderTools *RenderTools(); private: diff --git a/src/game/editor/editor_props.cpp b/src/game/editor/editor_props.cpp index 7d57b9c8f2..34d1e92e98 100644 --- a/src/game/editor/editor_props.cpp +++ b/src/game/editor/editor_props.cpp @@ -1,54 +1,62 @@ #include "editor.h" +#include + #include #include -int CEditor::DoProperties(CUIRect *pToolbox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color) +using namespace FontIcons; + +const ColorRGBA CEditor::ms_DefaultPropColor = ColorRGBA(1, 1, 1, 0.5f); + +int CEditor::DoProperties(CUIRect *pToolbox, CProperty *pProps, int *pIds, int *pNewVal, const std::vector &vColors) { - auto Res = DoPropertiesWithState(pToolbox, pProps, pIDs, pNewVal, Color); + auto Res = DoPropertiesWithState(pToolbox, pProps, pIds, pNewVal, vColors); return Res.m_Value; } template -SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color) +SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pProps, int *pIds, int *pNewVal, const std::vector &vColors) { int Change = -1; - EEditState State = EEditState::EDITING; + EEditState State = EEditState::NONE; for(int i = 0; pProps[i].m_pName; i++) { + const ColorRGBA *pColor = i >= (int)vColors.size() ? &ms_DefaultPropColor : &vColors[i]; + CUIRect Slot; pToolBox->HSplitTop(13.0f, &Slot, pToolBox); CUIRect Label, Shifter; Slot.VSplitMid(&Label, &Shifter); Shifter.HMargin(1.0f, &Shifter); - UI()->DoLabel(&Label, pProps[i].m_pName, 10.0f, TEXTALIGN_ML); + Ui()->DoLabel(&Label, pProps[i].m_pName, 10.0f, TEXTALIGN_ML); - if(pProps[i].m_Type == PROPTYPE_INT_STEP) + if(pProps[i].m_Type == PROPTYPE_INT) { CUIRect Inc, Dec; char aBuf[64]; Shifter.VSplitRight(10.0f, &Shifter, &Inc); Shifter.VSplitLeft(10.0f, &Dec, &Shifter); - str_from_int(pProps[i].m_Value, aBuf); - auto NewValueRes = UiDoValueSelector((char *)&pIDs[i], &Shifter, "", pProps[i].m_Value, pProps[i].m_Min, pProps[i].m_Max, 1, 1.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text.", false, false, 0, &Color); + str_format(aBuf, sizeof(aBuf), "%d", pProps[i].m_Value); + auto NewValueRes = UiDoValueSelector((char *)&pIds[i], &Shifter, "", pProps[i].m_Value, pProps[i].m_Min, pProps[i].m_Max, 1, 1.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text.", false, false, 0, pColor); int NewValue = NewValueRes.m_Value; - if(NewValue != pProps[i].m_Value || NewValueRes.m_State != EEditState::EDITING) + if(NewValue != pProps[i].m_Value || (NewValueRes.m_State != EEditState::NONE && NewValueRes.m_State != EEditState::EDITING)) { *pNewVal = NewValue; Change = i; State = NewValueRes.m_State; } - if(DoButton_ButtonDec((char *)&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Decrease")) + if(DoButton_FontIcon((char *)&pIds[i] + 1, FONT_ICON_MINUS, 0, &Dec, 0, "Decrease", IGraphics::CORNER_L, 7.0f)) { - *pNewVal = pProps[i].m_Value - 1; + *pNewVal = clamp(pProps[i].m_Value - 1, pProps[i].m_Min, pProps[i].m_Max); Change = i; State = EEditState::ONE_GO; } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, nullptr, 0, &Inc, 0, "Increase")) + if(DoButton_FontIcon(((char *)&pIds[i]) + 2, FONT_ICON_PLUS, 0, &Inc, 0, "Increase", IGraphics::CORNER_R, 7.0f)) { - *pNewVal = pProps[i].m_Value + 1; + *pNewVal = clamp(pProps[i].m_Value + 1, pProps[i].m_Min, pProps[i].m_Max); Change = i; State = EEditState::ONE_GO; } @@ -57,30 +65,19 @@ SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pPro { CUIRect No, Yes; Shifter.VSplitMid(&No, &Yes); - if(DoButton_ButtonDec(&pIDs[i], "No", !pProps[i].m_Value, &No, 0, "")) + if(DoButton_Ex(&pIds[i], "No", !pProps[i].m_Value, &No, 0, "", IGraphics::CORNER_L)) { *pNewVal = 0; Change = i; State = EEditState::ONE_GO; } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 1, "Yes", pProps[i].m_Value, &Yes, 0, "")) + if(DoButton_Ex(((char *)&pIds[i]) + 1, "Yes", pProps[i].m_Value, &Yes, 0, "", IGraphics::CORNER_R)) { *pNewVal = 1; Change = i; State = EEditState::ONE_GO; } } - else if(pProps[i].m_Type == PROPTYPE_INT_SCROLL) - { - auto NewValueRes = UiDoValueSelector(&pIDs[i], &Shifter, "", pProps[i].m_Value, pProps[i].m_Min, pProps[i].m_Max, 1, 1.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text."); - int NewValue = NewValueRes.m_Value; - if(NewValue != pProps[i].m_Value || NewValueRes.m_State != EEditState::EDITING) - { - *pNewVal = NewValue; - Change = i; - State = NewValueRes.m_State; - } - } else if(pProps[i].m_Type == PROPTYPE_ANGLE_SCROLL) { CUIRect Inc, Dec; @@ -90,22 +87,22 @@ SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pPro int Step = Shift ? 1 : 45; int Value = pProps[i].m_Value; - auto NewValueRes = UiDoValueSelector(&pIDs[i], &Shifter, "", Value, pProps[i].m_Min, pProps[i].m_Max, Shift ? 1 : 45, Shift ? 1.0f : 10.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text.", false, false, 0); + auto NewValueRes = UiDoValueSelector(&pIds[i], &Shifter, "", Value, pProps[i].m_Min, pProps[i].m_Max, Shift ? 1 : 45, Shift ? 1.0f : 10.0f, "Use left mouse button to drag and change the value. Hold shift to be more precise. Rightclick to edit as text.", false, false, 0); int NewValue = NewValueRes.m_Value; - if(DoButton_ButtonDec(&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Decrease")) + if(DoButton_FontIcon(&pIds[i] + 1, FONT_ICON_MINUS, 0, &Dec, 0, "Decrease", IGraphics::CORNER_L, 7.0f)) { NewValue = (std::ceil((pProps[i].m_Value / (float)Step)) - 1) * Step; if(NewValue < 0) NewValue += 360; State = EEditState::ONE_GO; } - if(DoButton_ButtonInc(&pIDs[i] + 2, nullptr, 0, &Inc, 0, "Increase")) + if(DoButton_FontIcon(&pIds[i] + 2, FONT_ICON_PLUS, 0, &Inc, 0, "Increase", IGraphics::CORNER_R, 7.0f)) { NewValue = (pProps[i].m_Value + Step) / Step * Step; State = EEditState::ONE_GO; } - if(NewValue != pProps[i].m_Value || NewValueRes.m_State != EEditState::EDITING) + if(NewValue != pProps[i].m_Value || (NewValueRes.m_State != EEditState::NONE && NewValueRes.m_State != EEditState::EDITING)) { *pNewVal = NewValue % 360; Change = i; @@ -123,7 +120,7 @@ SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pPro State = m_ColorPickerPopupContext.m_State; } }; - DoColorPickerButton(&pIDs[i], &Shifter, ColorRGBA::UnpackAlphaLast(pProps[i].m_Value), SetColor); + DoColorPickerButton(&pIds[i], &Shifter, ColorRGBA::UnpackAlphaLast(pProps[i].m_Value), SetColor); } else if(pProps[i].m_Type == PROPTYPE_IMAGE) { @@ -133,8 +130,8 @@ SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pPro else pName = m_Map.m_vpImages[pProps[i].m_Value]->m_aName; - if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) - PopupSelectImageInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); + if(DoButton_Ex(&pIds[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) + PopupSelectImageInvoke(pProps[i].m_Value, Ui()->MouseX(), Ui()->MouseY()); int r = PopupSelectImageResult(); if(r >= -1) @@ -150,31 +147,31 @@ SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pPro Shifter.VSplitMid(&Left, &Up, 2.0f); Left.VSplitLeft(10.0f, &Left, &Shifter); Shifter.VSplitRight(10.0f, &Shifter, &Right); - Shifter.Draw(ColorRGBA(1, 1, 1, 0.5f), 0, 0.0f); - UI()->DoLabel(&Shifter, "X", 10.0f, TEXTALIGN_MC); + Shifter.Draw(ColorRGBA(1, 1, 1, 0.5f), IGraphics::CORNER_NONE, 0.0f); + Ui()->DoLabel(&Shifter, "X", 10.0f, TEXTALIGN_MC); Up.VSplitLeft(10.0f, &Up, &Shifter); Shifter.VSplitRight(10.0f, &Shifter, &Down); - Shifter.Draw(ColorRGBA(1, 1, 1, 0.5f), 0, 0.0f); - UI()->DoLabel(&Shifter, "Y", 10.0f, TEXTALIGN_MC); - if(DoButton_ButtonDec(&pIDs[i], "-", 0, &Left, 0, "Left")) + Shifter.Draw(ColorRGBA(1, 1, 1, 0.5f), IGraphics::CORNER_NONE, 0.0f); + Ui()->DoLabel(&Shifter, "Y", 10.0f, TEXTALIGN_MC); + if(DoButton_FontIcon(&pIds[i], FONT_ICON_MINUS, 0, &Left, 0, "Left", IGraphics::CORNER_L, 7.0f)) { *pNewVal = DIRECTION_LEFT; Change = i; State = EEditState::ONE_GO; } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 3, "+", 0, &Right, 0, "Right")) + if(DoButton_FontIcon(((char *)&pIds[i]) + 3, FONT_ICON_PLUS, 0, &Right, 0, "Right", IGraphics::CORNER_R, 7.0f)) { *pNewVal = DIRECTION_RIGHT; Change = i; State = EEditState::ONE_GO; } - if(DoButton_ButtonDec(((char *)&pIDs[i]) + 1, "-", 0, &Up, 0, "Up")) + if(DoButton_FontIcon(((char *)&pIds[i]) + 1, FONT_ICON_MINUS, 0, &Up, 0, "Up", IGraphics::CORNER_L, 7.0f)) { *pNewVal = DIRECTION_UP; Change = i; State = EEditState::ONE_GO; } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, "+", 0, &Down, 0, "Down")) + if(DoButton_FontIcon(((char *)&pIds[i]) + 2, FONT_ICON_PLUS, 0, &Down, 0, "Down", IGraphics::CORNER_R, 7.0f)) { *pNewVal = DIRECTION_DOWN; Change = i; @@ -189,8 +186,8 @@ SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pPro else pName = m_Map.m_vpSounds[pProps[i].m_Value]->m_aName; - if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) - PopupSelectSoundInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); + if(DoButton_Ex(&pIds[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) + PopupSelectSoundInvoke(pProps[i].m_Value, Ui()->MouseX(), Ui()->MouseY()); int r = PopupSelectSoundResult(); if(r >= -1) @@ -208,8 +205,8 @@ SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pPro else pName = m_Map.m_vpImages[pProps[i].m_Min]->m_AutoMapper.GetConfigName(pProps[i].m_Value); - if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) - PopupSelectConfigAutoMapInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); + if(DoButton_Ex(&pIds[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) + PopupSelectConfigAutoMapInvoke(pProps[i].m_Value, Ui()->MouseX(), Ui()->MouseY()); int r = PopupSelectConfigAutoMapResult(); if(r >= -1) @@ -242,22 +239,22 @@ SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pPro else aBuf[0] = '\0'; - auto NewValueRes = UiDoValueSelector((char *)&pIDs[i], &Shifter, aBuf, CurValue, 0, m_Map.m_vpEnvelopes.size(), 1, 1.0f, "Set Envelope", false, false, IGraphics::CORNER_NONE); + auto NewValueRes = UiDoValueSelector((char *)&pIds[i], &Shifter, aBuf, CurValue, 0, m_Map.m_vpEnvelopes.size(), 1, 1.0f, "Set Envelope", false, false, IGraphics::CORNER_NONE); int NewVal = NewValueRes.m_Value; - if(NewVal != CurValue || NewValueRes.m_State != EEditState::EDITING) + if(NewVal != CurValue || (NewValueRes.m_State != EEditState::NONE && NewValueRes.m_State != EEditState::EDITING)) { *pNewVal = NewVal; Change = i; State = NewValueRes.m_State; } - if(DoButton_ButtonDec((char *)&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Previous Envelope")) + if(DoButton_FontIcon((char *)&pIds[i] + 1, FONT_ICON_MINUS, 0, &Dec, 0, "Previous Envelope", IGraphics::CORNER_L, 7.0f)) { *pNewVal = pProps[i].m_Value - 1; Change = i; State = EEditState::ONE_GO; } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, nullptr, 0, &Inc, 0, "Next Envelope")) + if(DoButton_FontIcon(((char *)&pIds[i]) + 2, FONT_ICON_PLUS, 0, &Inc, 0, "Next Envelope", IGraphics::CORNER_R, 7.0f)) { *pNewVal = pProps[i].m_Value + 1; Change = i; @@ -269,14 +266,14 @@ SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pPro return SEditResult{State, static_cast(Change)}; } -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); -template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, ColorRGBA); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); +template SEditResult CEditor::DoPropertiesWithState(CUIRect *, CProperty *, int *, int *, const std::vector &); diff --git a/src/game/editor/editor_server_settings.cpp b/src/game/editor/editor_server_settings.cpp new file mode 100644 index 0000000000..5d97c8cb44 --- /dev/null +++ b/src/game/editor/editor_server_settings.cpp @@ -0,0 +1,2244 @@ +#include "editor_server_settings.h" +#include "editor.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +using namespace FontIcons; + +static const int FONT_SIZE = 12.0f; + +struct IMapSetting +{ + enum EType + { + SETTING_INT, + SETTING_COMMAND, + }; + const char *m_pName; + const char *m_pHelp; + EType m_Type; + + IMapSetting(const char *pName, const char *pHelp, EType Type) : + m_pName(pName), m_pHelp(pHelp), m_Type(Type) {} +}; +struct SMapSettingInt : public IMapSetting +{ + int m_Default; + int m_Min; + int m_Max; + + SMapSettingInt(const char *pName, const char *pHelp, int Default, int Min, int Max) : + IMapSetting(pName, pHelp, IMapSetting::SETTING_INT), m_Default(Default), m_Min(Min), m_Max(Max) {} +}; +struct SMapSettingCommand : public IMapSetting +{ + const char *m_pArgs; + + SMapSettingCommand(const char *pName, const char *pHelp, const char *pArgs) : + IMapSetting(pName, pHelp, IMapSetting::SETTING_COMMAND), m_pArgs(pArgs) {} +}; + +void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast) +{ + static int s_CommandSelectedIndex = -1; + static CListBox s_ListBox; + s_ListBox.SetActive(!m_MapSettingsCommandContext.m_DropdownContext.m_ListBox.Active() && m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen()); + + bool GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); + const bool CurrentInputValid = m_MapSettingsCommandContext.Valid(); // Use the context to validate the input + + CUIRect ToolBar, Button, Label, List, DragBar; + View.HSplitTop(22.0f, &DragBar, nullptr); + DragBar.y -= 2.0f; + DragBar.w += 2.0f; + DragBar.h += 4.0f; + DoEditorDragBar(View, &DragBar, EDragSide::SIDE_TOP, &m_aExtraEditorSplits[EXTRAEDITOR_SERVER_SETTINGS]); + View.HSplitTop(20.0f, &ToolBar, &View); + View.HSplitTop(2.0f, nullptr, &List); + ToolBar.HMargin(2.0f, &ToolBar); + + // delete button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); + static int s_DeleteButton = 0; + if(DoButton_FontIcon(&s_DeleteButton, FONT_ICON_TRASH, GotSelection ? 0 : -1, &Button, 0, "[Delete] Delete the selected command from the command list.", IGraphics::CORNER_ALL, 9.0f) == 1 || (GotSelection && CLineInput::GetActiveInput() == nullptr && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(CUi::HOTKEY_DELETE))) + { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); + + m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + s_CommandSelectedIndex); + if(s_CommandSelectedIndex >= (int)m_Map.m_vSettings.size()) + s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; + if(s_CommandSelectedIndex >= 0) + m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); + else + m_SettingsCommandInput.Clear(); + m_Map.OnModify(); + m_MapSettingsCommandContext.Update(); + s_ListBox.ScrollToSelected(); + } + + // move down button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + const bool CanMoveDown = GotSelection && s_CommandSelectedIndex < (int)m_Map.m_vSettings.size() - 1; + static int s_DownButton = 0; + if(DoButton_FontIcon(&s_DownButton, FONT_ICON_SORT_DOWN, CanMoveDown ? 0 : -1, &Button, 0, "[Alt+Down] Move the selected command down.", IGraphics::CORNER_R, 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && Ui()->ConsumeHotkey(CUi::HOTKEY_DOWN))) + { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::MOVE_DOWN, &s_CommandSelectedIndex, s_CommandSelectedIndex)); + + std::swap(m_Map.m_vSettings[s_CommandSelectedIndex], m_Map.m_vSettings[s_CommandSelectedIndex + 1]); + s_CommandSelectedIndex++; + m_Map.OnModify(); + s_ListBox.ScrollToSelected(); + } + + // move up button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); + const bool CanMoveUp = GotSelection && s_CommandSelectedIndex > 0; + static int s_UpButton = 0; + if(DoButton_FontIcon(&s_UpButton, FONT_ICON_SORT_UP, CanMoveUp ? 0 : -1, &Button, 0, "[Alt+Up] Move the selected command up.", IGraphics::CORNER_L, 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && Ui()->ConsumeHotkey(CUi::HOTKEY_UP))) + { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::MOVE_UP, &s_CommandSelectedIndex, s_CommandSelectedIndex)); + + std::swap(m_Map.m_vSettings[s_CommandSelectedIndex], m_Map.m_vSettings[s_CommandSelectedIndex - 1]); + s_CommandSelectedIndex--; + m_Map.OnModify(); + s_ListBox.ScrollToSelected(); + } + + // redo button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + static int s_RedoButton = 0; + if(DoButton_FontIcon(&s_RedoButton, FONT_ICON_REDO, m_ServerSettingsHistory.CanRedo() ? 0 : -1, &Button, 0, "[Ctrl+Y] Redo command edit", IGraphics::CORNER_R, 11.0f) == 1 || (CanMoveDown && Input()->AltIsPressed() && Ui()->ConsumeHotkey(CUi::HOTKEY_DOWN))) + { + m_ServerSettingsHistory.Redo(); + } + + // undo button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(5.0f, &ToolBar, nullptr); + static int s_UndoButton = 0; + if(DoButton_FontIcon(&s_UndoButton, FONT_ICON_UNDO, m_ServerSettingsHistory.CanUndo() ? 0 : -1, &Button, 0, "[Ctrl+Z] Undo command edit", IGraphics::CORNER_L, 11.0f) == 1 || (CanMoveUp && Input()->AltIsPressed() && Ui()->ConsumeHotkey(CUi::HOTKEY_UP))) + { + m_ServerSettingsHistory.Undo(); + } + + GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); + + int CollidingCommandIndex = -1; + ECollisionCheckResult CheckResult = ECollisionCheckResult::ERROR; + if(CurrentInputValid) + CollidingCommandIndex = m_MapSettingsCommandContext.CheckCollision(CheckResult); + + // update button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + const bool CanAdd = CheckResult == ECollisionCheckResult::ADD; + const bool CanReplace = CheckResult == ECollisionCheckResult::REPLACE; + + const bool CanUpdate = GotSelection && CurrentInputValid && str_comp(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, m_SettingsCommandInput.GetString()) != 0; + + static int s_UpdateButton = 0; + if(DoButton_FontIcon(&s_UpdateButton, FONT_ICON_PENCIL, CanUpdate ? 0 : -1, &Button, 0, "[Alt+Enter] Update the selected command based on the entered value.", IGraphics::CORNER_R, 9.0f) == 1 || (CanUpdate && Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) + { + if(CollidingCommandIndex == -1) + { + bool Found = false; + int i; + for(i = 0; i < (int)m_Map.m_vSettings.size(); ++i) + { + if(i != s_CommandSelectedIndex && !str_comp(m_Map.m_vSettings[i].m_aCommand, m_SettingsCommandInput.GetString())) + { + Found = true; + break; + } + } + if(Found) + { + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); + m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + s_CommandSelectedIndex); + s_CommandSelectedIndex = i > s_CommandSelectedIndex ? i - 1 : i; + } + else + { + const char *pStr = m_SettingsCommandInput.GetString(); + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); + str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); + } + } + else + { + if(s_CommandSelectedIndex == CollidingCommandIndex) + { // If we are editing the currently collinding line, then we can just call EDIT on it + const char *pStr = m_SettingsCommandInput.GetString(); + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); + str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); + } + else + { // If not, then editing the current selected line will result in the deletion of the colliding line, and the editing of the selected line + const char *pStr = m_SettingsCommandInput.GetString(); + + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "Delete command %d; Edit command %d", CollidingCommandIndex, s_CommandSelectedIndex); + + m_ServerSettingsHistory.BeginBulk(); + // Delete the colliding command + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::DELETE, &s_CommandSelectedIndex, CollidingCommandIndex, m_Map.m_vSettings[CollidingCommandIndex].m_aCommand)); + m_Map.m_vSettings.erase(m_Map.m_vSettings.begin() + CollidingCommandIndex); + // Edit the selected command + s_CommandSelectedIndex = s_CommandSelectedIndex > CollidingCommandIndex ? s_CommandSelectedIndex - 1 : s_CommandSelectedIndex; + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); + str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); + + m_ServerSettingsHistory.EndBulk(aBuf); + } + } + + m_Map.OnModify(); + s_ListBox.ScrollToSelected(); + m_SettingsCommandInput.Clear(); + m_MapSettingsCommandContext.Reset(); // Reset context + Ui()->SetActiveItem(&m_SettingsCommandInput); + } + + // add button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(100.0f, &ToolBar, nullptr); + + static int s_AddButton = 0; + if(DoButton_FontIcon(&s_AddButton, CanReplace ? FONT_ICON_ARROWS_ROTATE : FONT_ICON_PLUS, CanAdd || CanReplace ? 0 : -1, &Button, 0, CanReplace ? "[Enter] Replace the corresponding command in the command list." : "[Enter] Add a command to the command list.", IGraphics::CORNER_L) == 1 || ((CanAdd || CanReplace) && !Input()->AltIsPressed() && m_Dialog == DIALOG_NONE && Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) + { + if(CanReplace) + { + dbg_assert(CollidingCommandIndex != -1, "Could not replace command"); + s_CommandSelectedIndex = CollidingCommandIndex; + + const char *pStr = m_SettingsCommandInput.GetString(); + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::EDIT, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr)); + str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, pStr); + } + else if(CanAdd) + { + m_Map.m_vSettings.emplace_back(m_SettingsCommandInput.GetString()); + s_CommandSelectedIndex = m_Map.m_vSettings.size() - 1; + m_ServerSettingsHistory.RecordAction(std::make_shared(this, CEditorCommandAction::EType::ADD, &s_CommandSelectedIndex, s_CommandSelectedIndex, m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand)); + } + + m_Map.OnModify(); + s_ListBox.ScrollToSelected(); + m_SettingsCommandInput.Clear(); + m_MapSettingsCommandContext.Reset(); // Reset context + Ui()->SetActiveItem(&m_SettingsCommandInput); + } + + // command input (use remaining toolbar width) + if(!ShowServerSettingsEditorLast) // Just activated + Ui()->SetActiveItem(&m_SettingsCommandInput); + m_SettingsCommandInput.SetEmptyText("Command"); + + TextRender()->TextColor(TextRender()->DefaultTextColor()); + + // command list + s_ListBox.DoStart(15.0f, m_Map.m_vSettings.size(), 1, 3, s_CommandSelectedIndex, &List); + + for(size_t i = 0; i < m_Map.m_vSettings.size(); i++) + { + const CListboxItem Item = s_ListBox.DoNextItem(&m_Map.m_vSettings[i], s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex == i); + if(!Item.m_Visible) + continue; + + Item.m_Rect.VMargin(5.0f, &Label); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + Props.m_EllipsisAtEnd = true; + Ui()->DoLabel(&Label, m_Map.m_vSettings[i].m_aCommand, 10.0f, TEXTALIGN_ML, Props); + } + + const int NewSelected = s_ListBox.DoEnd(); + if(s_CommandSelectedIndex != NewSelected || s_ListBox.WasItemSelected()) + { + s_CommandSelectedIndex = NewSelected; + if(m_SettingsCommandInput.IsEmpty() || !Input()->ModifierIsPressed()) // Allow ctrl+click to only change selection + { + m_SettingsCommandInput.Set(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand); + m_MapSettingsCommandContext.Update(); + m_MapSettingsCommandContext.UpdateCursor(true); + } + m_MapSettingsCommandContext.m_DropdownContext.m_ShouldHide = true; + Ui()->SetActiveItem(&m_SettingsCommandInput); + } + + // Map setting input + DoMapSettingsEditBox(&m_MapSettingsCommandContext, &ToolBar, FONT_SIZE, List.h); +} + +void CEditor::DoMapSettingsEditBox(CMapSettingsBackend::CContext *pContext, const CUIRect *pRect, float FontSize, float DropdownMaxHeight, int Corners, const char *pToolTip) +{ + // Main method to do the full featured map settings edit box + + auto *pLineInput = pContext->LineInput(); + auto &Context = *pContext; + Context.SetFontSize(FontSize); + + // Set current active context if input is active + if(pLineInput->IsActive()) + CMapSettingsBackend::ms_pActiveContext = pContext; + + // Small utility to render a floating part above the input rect. + // Use to display either the error or the current argument name + const float PartMargin = 4.0f; + auto &&RenderFloatingPart = [&](CUIRect *pInputRect, float x, const char *pStr) { + CUIRect Background; + Background.x = x - PartMargin; + Background.y = pInputRect->y - pInputRect->h - 6.0f; + Background.w = TextRender()->TextWidth(FontSize, pStr) + 2 * PartMargin; + Background.h = pInputRect->h; + Background.Draw(ColorRGBA(0, 0, 0, 0.9f), IGraphics::CORNER_ALL, 3.0f); + + CUIRect Label; + Background.VSplitLeft(PartMargin, nullptr, &Label); + TextRender()->TextColor(0.8f, 0.8f, 0.8f, 1.0f); + Ui()->DoLabel(&Label, pStr, FontSize, TEXTALIGN_ML); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + }; + + // If we have a valid command, display the help in the tooltip + if(Context.CommandIsValid() && pLineInput->IsActive() && Ui()->HotItem() == nullptr) + Context.GetCommandHelpText(m_aTooltip, sizeof(m_aTooltip)); + + CUIRect ToolBar = *pRect; + CUIRect Button; + ToolBar.VSplitRight(ToolBar.h, &ToolBar, &Button); + + // Do the unknown command toggle button + if(DoButton_FontIcon(&Context.m_AllowUnknownCommands, FONT_ICON_QUESTION, Context.m_AllowUnknownCommands, &Button, 0, "Disallow/allow unknown commands", IGraphics::CORNER_R)) + { + Context.m_AllowUnknownCommands = !Context.m_AllowUnknownCommands; + Context.Update(); + } + + // Color the arguments + std::vector vColorSplits; + Context.ColorArguments(vColorSplits); + + // Do and render clearable edit box with the colors + if(DoClearableEditBox(pLineInput, &ToolBar, FontSize, IGraphics::CORNER_L, "Enter a server setting.", vColorSplits)) + { + Context.Update(); // Update the context when contents change + Context.m_DropdownContext.m_ShouldHide = false; + } + + // Update/track the cursor + if(Context.UpdateCursor()) + Context.m_DropdownContext.m_ShouldHide = false; + + // Calculate x position of the dropdown and the floating part + float x = ToolBar.x + Context.CurrentArgPos() - pLineInput->GetScrollOffset(); + x = clamp(x, ToolBar.x + PartMargin, ToolBar.x + ToolBar.w); + + if(pLineInput->IsActive()) + { + // If line input is active, let's display a floating part for either the current argument name + // or for the error, if any. The error is only displayed when the cursor is at the end of the input. + const bool IsAtEnd = pLineInput->GetCursorOffset() >= (m_MapSettingsCommandContext.CommentOffset() != -1 ? m_MapSettingsCommandContext.CommentOffset() : pLineInput->GetLength()); + + if(Context.CurrentArgName() && (!Context.HasError() || !IsAtEnd)) // Render argument name + RenderFloatingPart(&ToolBar, x, Context.CurrentArgName()); + else if(Context.HasError() && IsAtEnd) // Render error + RenderFloatingPart(&ToolBar, ToolBar.x + PartMargin, Context.Error()); + } + + // If we have possible matches for the current argument, let's display an editbox suggestions dropdown + const auto &vPossibleCommands = Context.PossibleMatches(); + int Selected = DoEditBoxDropdown(&Context.m_DropdownContext, pLineInput, &ToolBar, x - PartMargin, DropdownMaxHeight, Context.CurrentArg() >= 0, vPossibleCommands, MapSettingsDropdownRenderCallback); + + // If the dropdown just became visible, update the context + // This is needed when input loses focus and then we click a command in the map settings list + if(Context.m_DropdownContext.m_DidBecomeVisible) + { + Context.Update(); + Context.UpdateCursor(true); + } + + if(!vPossibleCommands.empty()) + { + // Check if the completion index has changed + if(Selected != pContext->m_CurrentCompletionIndex) + { + // If so, we should autocomplete the selected option + if(Selected != -1) + { + const char *pStr = vPossibleCommands[Selected].m_pValue; + int Len = pContext->m_CurrentCompletionIndex == -1 ? str_length(Context.CurrentArgValue()) : (pContext->m_CurrentCompletionIndex < (int)vPossibleCommands.size() ? str_length(vPossibleCommands[pContext->m_CurrentCompletionIndex].m_pValue) : 0); + size_t Start = Context.CurrentArgOffset(); + size_t End = Start + Len; + pLineInput->SetRange(pStr, Start, End); + } + + pContext->m_CurrentCompletionIndex = Selected; + } + } + else + { + Context.m_DropdownContext.m_ListBox.SetActive(false); + } +} + +template +int CEditor::DoEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CLineInput *pLineInput, const CUIRect *pEditBoxRect, int x, float MaxHeight, bool AutoWidth, const std::vector &vData, const FDropdownRenderCallback &pfnMatchCallback) +{ + // Do an edit box with a possible dropdown + // This is a generic method which can display any data we want + + pDropdown->m_Selected = clamp(pDropdown->m_Selected, -1, (int)vData.size() - 1); + + if(Input()->KeyPress(KEY_SPACE) && Input()->ModifierIsPressed()) + { // Handle Ctrl+Space to show available options + pDropdown->m_ShortcutUsed = true; + // Remove inserted space + pLineInput->SetRange("", pLineInput->GetCursorOffset() - 1, pLineInput->GetCursorOffset()); + } + + if((!pDropdown->m_ShouldHide && !pLineInput->IsEmpty() && (pLineInput->IsActive() || pDropdown->m_MousePressedInside)) || pDropdown->m_ShortcutUsed) + { + if(!pDropdown->m_Visible) + { + pDropdown->m_DidBecomeVisible = true; + pDropdown->m_Visible = true; + } + else if(pDropdown->m_DidBecomeVisible) + pDropdown->m_DidBecomeVisible = false; + + if(!pLineInput->IsEmpty() || !pLineInput->IsActive()) + pDropdown->m_ShortcutUsed = false; + + int CurrentSelected = pDropdown->m_Selected; + + // Use tab to navigate through entries + if(Ui()->ConsumeHotkey(CUi::HOTKEY_TAB) && !vData.empty()) + { + int Direction = Input()->ShiftIsPressed() ? -1 : 1; + + pDropdown->m_Selected += Direction; + if(pDropdown->m_Selected < 0) + pDropdown->m_Selected = (int)vData.size() - 1; + pDropdown->m_Selected %= vData.size(); + } + + int Selected = RenderEditBoxDropdown(pDropdown, *pEditBoxRect, pLineInput, x, MaxHeight, AutoWidth, vData, pfnMatchCallback); + if(Selected != -1) + pDropdown->m_Selected = Selected; + + if(CurrentSelected != pDropdown->m_Selected) + pDropdown->m_ListBox.ScrollToSelected(); + + return pDropdown->m_Selected; + } + else + { + pDropdown->m_ShortcutUsed = false; + pDropdown->m_Visible = false; + pDropdown->m_ListBox.SetActive(false); + pDropdown->m_Selected = -1; + } + + return -1; +} + +template +int CEditor::RenderEditBoxDropdown(SEditBoxDropdownContext *pDropdown, CUIRect View, CLineInput *pLineInput, int x, float MaxHeight, bool AutoWidth, const std::vector &vData, const FDropdownRenderCallback &pfnMatchCallback) +{ + // Render a dropdown tied to an edit box/line input + auto *pListBox = &pDropdown->m_ListBox; + + pListBox->SetActive(m_Dialog == DIALOG_NONE && !Ui()->IsPopupOpen() && pLineInput->IsActive()); + pListBox->SetScrollbarWidth(15.0f); + + const int NumEntries = vData.size(); + + // Setup the rect + CUIRect CommandsDropdown = View; + CommandsDropdown.y += View.h + 0.1f; + CommandsDropdown.x = x; + if(AutoWidth) + CommandsDropdown.w = pDropdown->m_Width + pListBox->ScrollbarWidth(); + + pListBox->SetActive(NumEntries > 0); + if(NumEntries > 0) + { + // Draw the background + CommandsDropdown.h = minimum(NumEntries * 15.0f + 1.0f, MaxHeight); + CommandsDropdown.Draw(ColorRGBA(0.1f, 0.1f, 0.1f, 0.9f), IGraphics::CORNER_ALL, 3.0f); + + if(Ui()->MouseButton(0) && Ui()->MouseInside(&CommandsDropdown)) + pDropdown->m_MousePressedInside = true; + + // Do the list box + int Selected = pDropdown->m_Selected; + pListBox->DoStart(15.0f, NumEntries, 1, 3, Selected, &CommandsDropdown); + CUIRect Label; + + int NewIndex = Selected; + float LargestWidth = 0; + for(int i = 0; i < NumEntries; i++) + { + const CListboxItem Item = pListBox->DoNextItem(&vData[i], Selected == i); + + Item.m_Rect.VMargin(4.0f, &Label); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + Props.m_EllipsisAtEnd = true; + + // Call the callback to fill the current line string + char aBuf[128]; + pfnMatchCallback(vData.at(i), aBuf, Props.m_vColorSplits); + + LargestWidth = maximum(LargestWidth, TextRender()->TextWidth(12.0f, aBuf) + 10.0f); + if(!Item.m_Visible) + continue; + + Ui()->DoLabel(&Label, aBuf, 12.0f, TEXTALIGN_ML, Props); + + if(Ui()->ActiveItem() == &vData[i]) + { + // If we selected an item (by clicking on it for example), then set the active item back to the + // line input so we don't loose focus + NewIndex = i; + Ui()->SetActiveItem(pLineInput); + } + } + + pDropdown->m_Width = LargestWidth; + + int EndIndex = pListBox->DoEnd(); + if(NewIndex == Selected) + NewIndex = EndIndex; + + if(pDropdown->m_MousePressedInside && !Ui()->MouseButton(0)) + { + Ui()->SetActiveItem(pLineInput); + pDropdown->m_MousePressedInside = false; + } + + if(NewIndex != Selected) + { + Ui()->SetActiveItem(pLineInput); + return NewIndex; + } + } + return -1; +} + +void CEditor::RenderMapSettingsErrorDialog() +{ + auto &LoadedMapSettings = m_MapSettingsBackend.m_LoadedMapSettings; + auto &vSettingsInvalid = LoadedMapSettings.m_vSettingsInvalid; + auto &vSettingsValid = LoadedMapSettings.m_vSettingsValid; + auto &SettingsDuplicate = LoadedMapSettings.m_SettingsDuplicate; + + Ui()->MapScreen(); + CUIRect Overlay = *Ui()->Screen(); + + Overlay.Draw(ColorRGBA(0, 0, 0, 0.33f), IGraphics::CORNER_NONE, 0.0f); + CUIRect Background; + Overlay.VMargin(150.0f, &Background); + Background.HMargin(50.0f, &Background); + Background.Draw(ColorRGBA(0, 0, 0, 0.80f), IGraphics::CORNER_ALL, 5.0f); + + CUIRect View; + Background.Margin(10.0f, &View); + + CUIRect Title, ButtonBar, Label; + View.HSplitTop(18.0f, &Title, &View); + View.HSplitTop(5.0f, nullptr, &View); // some spacing + View.HSplitBottom(18.0f, &View, &ButtonBar); + View.HSplitBottom(10.0f, &View, nullptr); // some spacing + + // title bar + Title.Draw(ColorRGBA(1, 1, 1, 0.25f), IGraphics::CORNER_ALL, 4.0f); + Title.VMargin(10.0f, &Title); + Ui()->DoLabel(&Title, "Map settings error", 12.0f, TEXTALIGN_ML); + + // Render body + { + static CLineInputBuffered<256> s_Input; + static CMapSettingsBackend::CContext s_Context = m_MapSettingsBackend.NewContext(&s_Input); + + // Some text + SLabelProperties Props; + CUIRect Text; + View.HSplitTop(30.0f, &Text, &View); + Props.m_MaxWidth = Text.w; + Ui()->DoLabel(&Text, "Below is a report of the invalid map settings found when loading the map. Please fix them before proceeding further.", 10.0f, TEXTALIGN_MC, Props); + + // Mixed list + CUIRect List = View; + View.Draw(ColorRGBA(1, 1, 1, 0.25f), IGraphics::CORNER_ALL, 3.0f); + + const float RowHeight = 18.0f; + static CScrollRegion s_ScrollRegion; + vec2 ScrollOffset(0.0f, 0.0f); + CScrollRegionParams ScrollParams; + ScrollParams.m_ScrollUnit = 120.0f; + s_ScrollRegion.Begin(&List, &ScrollOffset, &ScrollParams); + const float EndY = List.y + List.h; + List.y += ScrollOffset.y; + + List.HSplitTop(20.0f, nullptr, &List); + + static int s_FixingCommandIndex = -1; + + auto &&SetInput = [&](const char *pString) { + s_Input.Set(pString); + s_Context.Update(); + s_Context.UpdateCursor(true); + Ui()->SetActiveItem(&s_Input); + }; + + CUIRect FixInput; + bool DisplayFixInput = false; + float DropdownHeight = 110.0f; + + for(int i = 0; i < (int)m_Map.m_vSettings.size(); i++) + { + CUIRect Slot; + + auto pInvalidSetting = std::find_if(vSettingsInvalid.begin(), vSettingsInvalid.end(), [i](const SInvalidSetting &Setting) { return Setting.m_Index == i; }); + if(pInvalidSetting != vSettingsInvalid.end()) + { // This setting is invalid, only display it if its not a duplicate + if(!(pInvalidSetting->m_Type & SInvalidSetting::TYPE_DUPLICATE)) + { + bool IsFixing = s_FixingCommandIndex == i; + List.HSplitTop(RowHeight, &Slot, &List); + + // Draw a reddish background if setting is marked as deleted + if(pInvalidSetting->m_Context.m_Deleted) + Slot.Draw(ColorRGBA(0.85f, 0.0f, 0.0f, 0.15f), IGraphics::CORNER_ALL, 3.0f); + + Slot.VMargin(5.0f, &Slot); + Slot.HMargin(1.0f, &Slot); + + if(!IsFixing && !pInvalidSetting->m_Context.m_Fixed) + { // Display "Fix" and "delete" buttons if we're not fixing the command and the command has not been fixed + CUIRect FixBtn, DelBtn; + Slot.VSplitRight(30.0f, &Slot, &DelBtn); + Slot.VSplitRight(5.0f, &Slot, nullptr); + DelBtn.HMargin(1.0f, &DelBtn); + + Slot.VSplitRight(30.0f, &Slot, &FixBtn); + Slot.VSplitRight(10.0f, &Slot, nullptr); + FixBtn.HMargin(1.0f, &FixBtn); + + // Delete button + if(DoButton_FontIcon(&pInvalidSetting->m_Context.m_Deleted, FONT_ICON_TRASH, pInvalidSetting->m_Context.m_Deleted, &DelBtn, 0, "Delete this command", IGraphics::CORNER_ALL, 10.0f)) + pInvalidSetting->m_Context.m_Deleted = !pInvalidSetting->m_Context.m_Deleted; + + // Fix button + if(DoButton_Editor(&pInvalidSetting->m_Context.m_Fixed, "Fix", !pInvalidSetting->m_Context.m_Deleted ? (s_FixingCommandIndex == -1 ? 0 : (IsFixing ? 1 : -1)) : -1, &FixBtn, 0, "Fix this command")) + { + s_FixingCommandIndex = i; + SetInput(pInvalidSetting->m_aSetting); + } + } + else if(IsFixing) + { // If we're fixing this command, then display "Done" and "Cancel" buttons + // Also setup the input rect + CUIRect OkBtn, CancelBtn; + Slot.VSplitRight(50.0f, &Slot, &CancelBtn); + Slot.VSplitRight(5.0f, &Slot, nullptr); + CancelBtn.HMargin(1.0f, &CancelBtn); + + Slot.VSplitRight(30.0f, &Slot, &OkBtn); + Slot.VSplitRight(10.0f, &Slot, nullptr); + OkBtn.HMargin(1.0f, &OkBtn); + + // Buttons + static int s_Cancel = 0, s_Ok = 0; + if(DoButton_Editor(&s_Cancel, "Cancel", 0, &CancelBtn, 0, "Cancel fixing this command") || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + s_FixingCommandIndex = -1; + s_Input.Clear(); + } + + // "Done" button only enabled if the fixed setting is valid + // For that we use a local CContext s_Context and use it to check + // that the setting is valid and that it is not a duplicate + ECollisionCheckResult Res = ECollisionCheckResult::ERROR; + s_Context.CheckCollision(vSettingsValid, Res); + bool Valid = s_Context.Valid() && Res == ECollisionCheckResult::ADD; + + if(DoButton_Editor(&s_Ok, "Done", Valid ? 0 : -1, &OkBtn, 0, "Confirm editing of this command") || (s_Input.IsActive() && Valid && Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) + { + // Mark the setting is being fixed + pInvalidSetting->m_Context.m_Fixed = true; + str_copy(pInvalidSetting->m_aSetting, s_Input.GetString()); + // Add it to the list for future collision checks + vSettingsValid.emplace_back(s_Input.GetString()); + + // Clear the input & fixing command index + s_FixingCommandIndex = -1; + s_Input.Clear(); + } + } + + Label = Slot; + Props.m_EllipsisAtEnd = true; + Props.m_MaxWidth = Label.w; + + if(IsFixing) + { + // Setup input rect, which will be used to draw the map settings input later + Label.HMargin(1.0, &FixInput); + DisplayFixInput = true; + DropdownHeight = minimum(DropdownHeight, EndY - FixInput.y - 16.0f); + } + else + { + // Draw label in case we're not fixing this setting. + // Deleted settings are shown in gray with a red line through them + // Fixed settings are shown in green + // Invalid settings are shown in red + if(!pInvalidSetting->m_Context.m_Deleted) + { + if(pInvalidSetting->m_Context.m_Fixed) + TextRender()->TextColor(0.0f, 1.0f, 0.0f, 1.0f); + else + TextRender()->TextColor(1.0f, 0.0f, 0.0f, 1.0f); + Ui()->DoLabel(&Label, pInvalidSetting->m_aSetting, 10.0f, TEXTALIGN_ML, Props); + } + else + { + TextRender()->TextColor(0.3f, 0.3f, 0.3f, 1.0f); + Ui()->DoLabel(&Label, pInvalidSetting->m_aSetting, 10.0f, TEXTALIGN_ML, Props); + + CUIRect Line = Label; + Line.y = Label.y + Label.h / 2; + Line.h = 1; + Line.Draw(ColorRGBA(1, 0, 0, 1), IGraphics::CORNER_NONE, 0.0f); + } + } + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + } + else + { // This setting is valid + // Check for duplicates + const std::vector &vDuplicates = SettingsDuplicate.at(i); + int Chosen = -1; // This is the chosen duplicate setting. -1 means the first valid setting that was found which was not a duplicate + for(int d = 0; d < (int)vDuplicates.size(); d++) + { + int DupIndex = vDuplicates[d]; + if(vSettingsInvalid[DupIndex].m_Context.m_Chosen) + { + Chosen = d; + break; + } + } + + List.HSplitTop(RowHeight * (vDuplicates.size() + 1) + 2.0f, &Slot, &List); + Slot.HMargin(1.0f, &Slot); + + // Draw a background to highlight group of duplicates + if(!vDuplicates.empty()) + Slot.Draw(ColorRGBA(1, 1, 1, 0.15f), IGraphics::CORNER_ALL, 3.0f); + + Slot.VMargin(5.0f, &Slot); + Slot.HSplitTop(RowHeight, &Label, &Slot); + Label.HMargin(1.0f, &Label); + + // Draw a "choose" button next to the label in case we have duplicates for this line + if(!vDuplicates.empty()) + { + CUIRect ChooseBtn; + Label.VSplitRight(50.0f, &Label, &ChooseBtn); + Label.VSplitRight(5.0f, &Label, nullptr); + ChooseBtn.HMargin(1.0f, &ChooseBtn); + if(DoButton_Editor(&vDuplicates, "Choose", Chosen == -1, &ChooseBtn, 0, "Choose this command")) + { + if(Chosen != -1) + vSettingsInvalid[vDuplicates[Chosen]].m_Context.m_Chosen = false; + Chosen = -1; // Choosing this means that we do not choose any of the duplicates + } + } + + // Draw the label + Props.m_MaxWidth = Label.w; + Ui()->DoLabel(&Label, m_Map.m_vSettings[i].m_aCommand, 10.0f, TEXTALIGN_ML, Props); + + // Draw the list of duplicates, with a "Choose" button for each duplicate + // In case a duplicate is also invalid, then we draw a "Fix" button which behaves like the fix button above + // Duplicate settings name are shown in light blue, or in purple if they are also invalid + Slot.VSplitLeft(10.0f, nullptr, &Slot); + for(int DuplicateIndex = 0; DuplicateIndex < (int)vDuplicates.size(); DuplicateIndex++) + { + auto &Duplicate = vSettingsInvalid.at(vDuplicates[DuplicateIndex]); + bool IsFixing = s_FixingCommandIndex == Duplicate.m_Index; + bool IsInvalid = Duplicate.m_Type & SInvalidSetting::TYPE_INVALID; + + ColorRGBA Color(0.329f, 0.714f, 0.859f, 1.0f); + CUIRect SubSlot; + Slot.HSplitTop(RowHeight, &SubSlot, &Slot); + SubSlot.HMargin(1.0f, &SubSlot); + + if(!IsFixing) + { + // If not fixing, then display "Choose" and maybe "Fix" buttons. + + CUIRect ChooseBtn; + SubSlot.VSplitRight(50.0f, &SubSlot, &ChooseBtn); + SubSlot.VSplitRight(5.0f, &SubSlot, nullptr); + ChooseBtn.HMargin(1.0f, &ChooseBtn); + if(DoButton_Editor(&Duplicate.m_Context.m_Chosen, "Choose", IsInvalid && !Duplicate.m_Context.m_Fixed ? -1 : Duplicate.m_Context.m_Chosen, &ChooseBtn, 0, "Override with this command")) + { + Duplicate.m_Context.m_Chosen = !Duplicate.m_Context.m_Chosen; + if(Chosen != -1 && Chosen != DuplicateIndex) + vSettingsInvalid[vDuplicates[Chosen]].m_Context.m_Chosen = false; + Chosen = DuplicateIndex; + } + + if(IsInvalid) + { + if(!Duplicate.m_Context.m_Fixed) + { + Color = ColorRGBA(1, 0, 1, 1); + CUIRect FixBtn; + SubSlot.VSplitRight(30.0f, &SubSlot, &FixBtn); + SubSlot.VSplitRight(10.0f, &SubSlot, nullptr); + FixBtn.HMargin(1.0f, &FixBtn); + if(DoButton_Editor(&Duplicate.m_Context.m_Fixed, "Fix", s_FixingCommandIndex == -1 ? 0 : (IsFixing ? 1 : -1), &FixBtn, 0, "Fix this command (needed before it can be chosen)")) + { + s_FixingCommandIndex = Duplicate.m_Index; + SetInput(Duplicate.m_aSetting); + } + } + else + { + Color = ColorRGBA(0.329f, 0.714f, 0.859f, 1.0f); + } + } + } + else + { + // If we're fixing, display "Done" and "Cancel" buttons + CUIRect OkBtn, CancelBtn; + SubSlot.VSplitRight(50.0f, &SubSlot, &CancelBtn); + SubSlot.VSplitRight(5.0f, &SubSlot, nullptr); + CancelBtn.HMargin(1.0f, &CancelBtn); + + SubSlot.VSplitRight(30.0f, &SubSlot, &OkBtn); + SubSlot.VSplitRight(10.0f, &SubSlot, nullptr); + OkBtn.HMargin(1.0f, &OkBtn); + + static int s_Cancel = 0, s_Ok = 0; + if(DoButton_Editor(&s_Cancel, "Cancel", 0, &CancelBtn, 0, "Cancel fixing this command") || Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + s_FixingCommandIndex = -1; + s_Input.Clear(); + } + + // Use the local CContext s_Context to validate the input + // We also need to make sure the fixed setting matches the initial duplicate setting + // For example: + // sv_deepfly 0 + // sv_deepfly 5 <- This is invalid and duplicate. We can only fix it by writing "sv_deepfly 0" or "sv_deepfly 1". + // If we write any other setting, like "sv_hit 1", it won't work as it does not match "sv_deepfly". + // To do that, we use the context and we check for collision with the current map setting + ECollisionCheckResult Res = ECollisionCheckResult::ERROR; + s_Context.CheckCollision({m_Map.m_vSettings[i]}, Res); + bool Valid = s_Context.Valid() && Res == ECollisionCheckResult::REPLACE; + + if(DoButton_Editor(&s_Ok, "Done", Valid ? 0 : -1, &OkBtn, 0, "Confirm editing of this command") || (s_Input.IsActive() && Valid && Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) + { + if(Valid) // Just to make sure + { + // Mark the setting as fixed + Duplicate.m_Context.m_Fixed = true; + str_copy(Duplicate.m_aSetting, s_Input.GetString()); + + s_FixingCommandIndex = -1; + s_Input.Clear(); + } + } + } + + Label = SubSlot; + Props.m_MaxWidth = Label.w; + + if(IsFixing) + { + // Setup input rect in case we are fixing the setting + Label.HMargin(1.0, &FixInput); + DisplayFixInput = true; + DropdownHeight = minimum(DropdownHeight, EndY - FixInput.y - 16.0f); + } + else + { + // Otherwise, render the setting label + TextRender()->TextColor(Color); + Ui()->DoLabel(&Label, Duplicate.m_aSetting, 10.0f, TEXTALIGN_ML, Props); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + } + } + + // Finally, add the slot to the scroll region + s_ScrollRegion.AddRect(Slot); + } + + // Add some padding to the bottom so the dropdown can actually display some values in case we + // fix an invalid setting at the bottom of the list + CUIRect PaddingBottom; + List.HSplitTop(30.0f, &PaddingBottom, &List); + s_ScrollRegion.AddRect(PaddingBottom); + + // Display the map settings edit box after having rendered all the lines, so the dropdown shows in + // front of everything, but is still being clipped by the scroll region. + if(DisplayFixInput) + DoMapSettingsEditBox(&s_Context, &FixInput, 10.0f, maximum(DropdownHeight, 30.0f)); + + s_ScrollRegion.End(); + } + + // Confirm button + static int s_ConfirmButton = 0, s_CancelButton = 0, s_FixAllButton = 0; + CUIRect ConfimButton, CancelButton, FixAllUnknownButton; + ButtonBar.VSplitLeft(110.0f, &CancelButton, &ButtonBar); + ButtonBar.VSplitRight(110.0f, &ButtonBar, &ConfimButton); + ButtonBar.VSplitRight(5.0f, &ButtonBar, nullptr); + ButtonBar.VSplitRight(150.0f, &ButtonBar, &FixAllUnknownButton); + + bool CanConfirm = true; + bool CanFixAllUnknown = false; + for(auto &InvalidSetting : vSettingsInvalid) + { + if(!InvalidSetting.m_Context.m_Fixed && !InvalidSetting.m_Context.m_Deleted && !(InvalidSetting.m_Type & SInvalidSetting::TYPE_DUPLICATE)) + { + CanConfirm = false; + if(InvalidSetting.m_Unknown) + CanFixAllUnknown = true; + break; + } + } + + auto &&Execute = [&]() { + // Execute will modify the actual map settings according to the fixes that were just made within the dialog. + + // Fix fixed settings, erase deleted settings + for(auto &FixedSetting : vSettingsInvalid) + { + if(FixedSetting.m_Context.m_Fixed) + { + str_copy(m_Map.m_vSettings[FixedSetting.m_Index].m_aCommand, FixedSetting.m_aSetting); + } + } + + // Choose chosen settings + // => Erase settings that don't match + // => Erase settings that were not chosen + std::vector vSettingsToErase; + for(auto &Setting : vSettingsInvalid) + { + if(Setting.m_Type & SInvalidSetting::TYPE_DUPLICATE) + { + if(!Setting.m_Context.m_Chosen) + vSettingsToErase.emplace_back(Setting.m_aSetting); + else + vSettingsToErase.emplace_back(m_Map.m_vSettings[Setting.m_CollidingIndex].m_aCommand); + } + } + + // Erase deleted settings + for(auto &DeletedSetting : vSettingsInvalid) + { + if(DeletedSetting.m_Context.m_Deleted) + { + m_Map.m_vSettings.erase( + std::remove_if(m_Map.m_vSettings.begin(), m_Map.m_vSettings.end(), [&](const CEditorMapSetting &MapSetting) { + return str_comp_nocase(MapSetting.m_aCommand, DeletedSetting.m_aSetting) == 0; + }), + m_Map.m_vSettings.end()); + } + } + + // Erase settings to erase + for(auto &Setting : vSettingsToErase) + { + m_Map.m_vSettings.erase( + std::remove_if(m_Map.m_vSettings.begin(), m_Map.m_vSettings.end(), [&](const CEditorMapSetting &MapSetting) { + return str_comp_nocase(MapSetting.m_aCommand, Setting.m_aCommand) == 0; + }), + m_Map.m_vSettings.end()); + } + + m_Map.OnModify(); + }; + + auto &&FixAllUnknown = [&] { + // Mark unknown settings as fixed + for(auto &InvalidSetting : vSettingsInvalid) + if(!InvalidSetting.m_Context.m_Fixed && !InvalidSetting.m_Context.m_Deleted && !(InvalidSetting.m_Type & SInvalidSetting::TYPE_DUPLICATE) && InvalidSetting.m_Unknown) + InvalidSetting.m_Context.m_Fixed = true; + }; + + // Fix all unknown settings + if(DoButton_Editor(&s_FixAllButton, "Allow all unknown settings", CanFixAllUnknown ? 0 : -1, &FixAllUnknownButton, 0, nullptr)) + { + FixAllUnknown(); + } + + // Confirm - execute the fixes + if(DoButton_Editor(&s_ConfirmButton, "Confirm", CanConfirm ? 0 : -1, &ConfimButton, 0, nullptr) || (CanConfirm && Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) + { + Execute(); + m_Dialog = DIALOG_NONE; + } + + // Cancel - we load a new empty map + if(DoButton_Editor(&s_CancelButton, "Cancel", 0, &CancelButton, 0, nullptr) || (Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE))) + { + Reset(); + m_aFileName[0] = 0; + m_Dialog = DIALOG_NONE; + } +} + +void CEditor::MapSettingsDropdownRenderCallback(const SPossibleValueMatch &Match, char (&aOutput)[128], std::vector &vColorSplits) +{ + // Check the match argument index. + // If it's -1, we're displaying the list of available map settings names + // If its >= 0, we're displaying the list of possible values matches for that argument + if(Match.m_ArgIndex == -1) + { + IMapSetting *pInfo = (IMapSetting *)Match.m_pData; + vColorSplits = { + {str_length(pInfo->m_pName) + 1, -1, ColorRGBA(0.6f, 0.6f, 0.6f, 1)}, // Darker arguments + }; + + if(pInfo->m_Type == IMapSetting::SETTING_INT) + { + str_format(aOutput, sizeof(aOutput), "%s i[value]", pInfo->m_pName); + } + else if(pInfo->m_Type == IMapSetting::SETTING_COMMAND) + { + SMapSettingCommand *pCommand = (SMapSettingCommand *)pInfo; + str_format(aOutput, sizeof(aOutput), "%s %s", pCommand->m_pName, pCommand->m_pArgs); + } + } + else + { + str_copy(aOutput, Match.m_pValue); + } +} + +// ---------------------------------------- + +CMapSettingsBackend::CContext *CMapSettingsBackend::ms_pActiveContext = nullptr; + +void CMapSettingsBackend::OnInit(CEditor *pEditor) +{ + CEditorComponent::OnInit(pEditor); + + // Register values loader + InitValueLoaders(); + + // Load settings/commands + LoadAllMapSettings(); + + CValuesBuilder Builder(&m_PossibleValuesPerCommand); + + // Load and parse static map settings so we can use them here + for(auto &pSetting : m_vpMapSettings) + { + // We want to parse the arguments of each map setting so we can autocomplete them later + // But that depends on the type of the setting. + // If we have a INT setting, then we know we can only ever have 1 argument which is a integer value + // If we have a COMMAND setting, then we need to parse its arguments + if(pSetting->m_Type == IMapSetting::SETTING_INT) + LoadSettingInt(std::static_pointer_cast(pSetting)); + else if(pSetting->m_Type == IMapSetting::SETTING_COMMAND) + LoadSettingCommand(std::static_pointer_cast(pSetting)); + + LoadPossibleValues(Builder(pSetting->m_pName), pSetting); + } + + // Init constraints + LoadConstraints(); +} + +void CMapSettingsBackend::LoadAllMapSettings() +{ + // Gather all config variables having the flag CFGFLAG_GAME + Editor()->ConfigManager()->PossibleConfigVariables("", CFGFLAG_GAME, PossibleConfigVariableCallback, this); + + // Load list of commands + LoadCommand("tune", "s[tuning] f[value]", "Tune variable to value or show current value"); + LoadCommand("tune_zone", "i[zone] s[tuning] f[value]", "Tune in zone a variable to value"); + LoadCommand("tune_zone_enter", "i[zone] r[message]", "which message to display on zone enter; use 0 for normal area"); + LoadCommand("tune_zone_leave", "i[zone] r[message]", "which message to display on zone leave; use 0 for normal area"); + LoadCommand("mapbug", "s[mapbug]", "Enable map compatibility mode using the specified bug (example: grenade-doubleexplosion@ddnet.tw)"); + LoadCommand("switch_open", "i[switch]", "Whether a switch is deactivated by default (otherwise activated)"); +} + +void CMapSettingsBackend::LoadCommand(const char *pName, const char *pArgs, const char *pHelp) +{ + m_vpMapSettings.emplace_back(std::make_shared(pName, pHelp, pArgs)); +} + +void CMapSettingsBackend::LoadSettingInt(const std::shared_ptr &pSetting) +{ + // We load an int argument here + m_ParsedCommandArgs[pSetting].emplace_back(); + auto &Arg = m_ParsedCommandArgs[pSetting].back(); + str_copy(Arg.m_aName, "value"); + Arg.m_Type = 'i'; +} + +void CMapSettingsBackend::LoadSettingCommand(const std::shared_ptr &pSetting) +{ + // This method parses a setting into its arguments (name and type) so we can later + // use them to validate the current input as well as display the current argument value + // over the line input. + + m_ParsedCommandArgs[pSetting].clear(); + const char *pIterator = pSetting->m_pArgs; + + char Type; + + while(*pIterator) + { + if(*pIterator == '?') // Skip optional values as a map setting should not have optional values + pIterator++; + + Type = *pIterator; + pIterator++; + while(*pIterator && *pIterator != '[') + pIterator++; + pIterator++; // skip '[' + + const char *pNameStart = pIterator; + + while(*pIterator && *pIterator != ']') + pIterator++; + + size_t Len = pIterator - pNameStart; + pIterator++; // Skip ']' + + dbg_assert(Len + 1 < sizeof(SParsedMapSettingArg::m_aName), "Length of server setting name exceeds limit."); + + // Append parsed arg + m_ParsedCommandArgs[pSetting].emplace_back(); + auto &Arg = m_ParsedCommandArgs[pSetting].back(); + str_copy(Arg.m_aName, pNameStart, Len + 1); + Arg.m_Type = Type; + + pIterator = str_skip_whitespaces_const(pIterator); + } +} + +void CMapSettingsBackend::LoadPossibleValues(const CSettingValuesBuilder &Builder, const std::shared_ptr &pSetting) +{ + // Call the value loader for that setting + auto Iter = m_LoaderFunctions.find(pSetting->m_pName); + if(Iter == m_LoaderFunctions.end()) + return; + + (*Iter->second)(Builder); +} + +void CMapSettingsBackend::RegisterLoader(const char *pSettingName, const FLoaderFunction &pfnLoader) +{ + // Registers a value loader function for a specific setting name + m_LoaderFunctions[pSettingName] = pfnLoader; +} + +void CMapSettingsBackend::LoadConstraints() +{ + // Make an instance of constraint builder + CCommandArgumentConstraintBuilder Command(&m_ArgConstraintsPerCommand); + + // Define constraints like this + // This is still a bit sad as we have to do it manually here. + Command("tune", 2).Unique(0); + Command("tune_zone", 3).Multiple(0).Unique(1); + Command("tune_zone_enter", 2).Unique(0); + Command("tune_zone_leave", 2).Unique(0); + Command("switch_open", 1).Unique(0); + Command("mapbug", 1).Unique(0); +} + +void CMapSettingsBackend::PossibleConfigVariableCallback(const SConfigVariable *pVariable, void *pUserData) +{ + CMapSettingsBackend *pBackend = (CMapSettingsBackend *)pUserData; + + if(pVariable->m_Type == SConfigVariable::VAR_INT) + { + SIntConfigVariable *pIntVariable = (SIntConfigVariable *)pVariable; + pBackend->m_vpMapSettings.emplace_back(std::make_shared( + pIntVariable->m_pScriptName, + pIntVariable->m_pHelp, + pIntVariable->m_Default, + pIntVariable->m_Min, + pIntVariable->m_Max)); + } +} + +void CMapSettingsBackend::CContext::Reset() +{ + m_LastCursorOffset = 0; + m_CursorArgIndex = -1; + m_pCurrentSetting = nullptr; + m_vCurrentArgs.clear(); + m_aCommand[0] = '\0'; + m_DropdownContext.m_Selected = -1; + m_CurrentCompletionIndex = -1; + m_DropdownContext.m_ShortcutUsed = false; + m_DropdownContext.m_MousePressedInside = false; + m_DropdownContext.m_Visible = false; + m_DropdownContext.m_ShouldHide = false; + m_CommentOffset = -1; + + ClearError(); +} + +void CMapSettingsBackend::CContext::Update() +{ + UpdateFromString(InputString()); +} + +void CMapSettingsBackend::CContext::UpdateFromString(const char *pStr) +{ + // This is the main method that does all the argument parsing and validating. + // It fills pretty much all the context values, the arguments, their position, + // if they are valid or not, etc. + + m_pCurrentSetting = nullptr; + m_vCurrentArgs.clear(); + m_CommentOffset = -1; + + const char *pIterator = pStr; + + // Check for comment + const char *pEnd = pStr; + int InString = 0; + + while(*pEnd) + { + if(*pEnd == '"') + InString ^= 1; + else if(*pEnd == '\\') // Escape sequences + { + if(pEnd[1] == '"') + pEnd++; + } + else if(!InString) + { + if(*pEnd == '#') // Found comment + { + m_CommentOffset = pEnd - pStr; + break; + } + } + + pEnd++; + } + + if(m_CommentOffset == 0) + return; + + // End command at start of comment, if any + char aInputString[256]; + str_copy(aInputString, pStr, m_CommentOffset != -1 ? m_CommentOffset + 1 : sizeof(aInputString)); + pIterator = aInputString; + + // Get the command/setting + m_aCommand[0] = '\0'; + while(pIterator && *pIterator != ' ' && *pIterator != '\0') + pIterator++; + + str_copy(m_aCommand, aInputString, (pIterator - aInputString) + 1); + + // Get the command if it is a recognized one + for(auto &pSetting : m_pBackend->m_vpMapSettings) + { + if(str_comp_nocase(m_aCommand, pSetting->m_pName) == 0) + { + m_pCurrentSetting = pSetting; + break; + } + } + + // Parse args + ParseArgs(aInputString, pIterator); +} + +void CMapSettingsBackend::CContext::ParseArgs(const char *pLineInputStr, const char *pStr) +{ + // This method parses the arguments of the current command, starting at pStr + + ClearError(); + + const char *pIterator = pStr; + + if(!pStr || *pStr == '\0') + return; // No arguments + + // NextArg is used to get the contents of the current argument and go to the next argument position + // It outputs the length of the argument in pLength and returns a boolean indicating if the parsing + // of that argument is valid or not (only the case when using strings with quotes (")) + auto &&NextArg = [&](const char *pArg, int *pLength) { + if(*pIterator == '"') + { + pIterator++; + bool Valid = true; + bool IsEscape = false; + + while(true) + { + if(pIterator[0] == '"' && !IsEscape) + break; + else if(pIterator[0] == 0) + { + Valid = false; + break; + } + + if(pIterator[0] == '\\' && !IsEscape) + IsEscape = true; + else if(IsEscape) + IsEscape = false; + + pIterator++; + } + const char *pEnd = ++pIterator; + pIterator = str_skip_to_whitespace_const(pIterator); + + // Make sure there are no other characters at the end, otherwise the string is invalid. + // E.g. "abcd"ef is invalid + Valid = Valid && pIterator == pEnd; + *pLength = pEnd - pArg; + + return Valid; + } + else + { + pIterator = str_skip_to_whitespace_const(pIterator); + *pLength = pIterator - pArg; + return true; + } + }; + + // Simple validation of string. Checks that it does not contain unescaped " in the middle of it. + auto &&ValidateStr = [](const char *pString) -> bool { + const char *pIt = pString; + bool IsEscape = false; + while(*pIt) + { + if(pIt[0] == '"' && !IsEscape) + return false; + + if(pIt[0] == '\\' && !IsEscape) + IsEscape = true; + else if(IsEscape) + IsEscape = false; + + pIt++; + } + return true; + }; + + const int CommandArgCount = m_pCurrentSetting != nullptr ? m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting).size() : 0; + int ArgIndex = 0; + SCommandParseError::EErrorType Error = SCommandParseError::ERROR_NONE; + + // Also keep track of the visual X position of each argument within the input + float PosX = 0; + const float WW = m_pBackend->TextRender()->TextWidth(m_FontSize, " "); + PosX += m_pBackend->TextRender()->TextWidth(m_FontSize, m_aCommand); + + // Parsing beings + while(*pIterator) + { + Error = SCommandParseError::ERROR_NONE; + pIterator++; // Skip whitespace + PosX += WW; // Add whitespace width + + // Insert argument here + char Char = *pIterator; + const char *pArgStart = pIterator; + int Length; + bool Valid = NextArg(pArgStart, &Length); // Get contents and go to next argument position + size_t Offset = pArgStart - pLineInputStr; // Compute offset from the start of the input + + // Add new argument, copy the argument contents + m_vCurrentArgs.emplace_back(); + auto &NewArg = m_vCurrentArgs.back(); + // Fill argument value, with a maximum length of 256 + str_copy(NewArg.m_aValue, pArgStart, minimum((int)sizeof(SCurrentSettingArg::m_aValue), Length + 1)); + + // Validate argument from the parsed argument of the current setting. + // If current setting is not valid, then there are no arguments which results in an error. + + char Type = 'u'; // u = unknown, only possible for unknown commands when m_AllowUnknownCommands is true. + if(ArgIndex < CommandArgCount) + { + SParsedMapSettingArg &Arg = m_pBackend->m_ParsedCommandArgs[m_pCurrentSetting].at(ArgIndex); + if(Arg.m_Type == 'r') + { + // Rest of string, should add all the string if there was no quotes + // Otherwise, only get the contents in the quotes, and consider content after that as other arguments + if(Char != '"') + { + while(*pIterator) + pIterator++; + Length = pIterator - pArgStart; + str_copy(NewArg.m_aValue, pArgStart, Length + 1); + } + + if(!Valid) + Error = SCommandParseError::ERROR_INVALID_VALUE; + } + else if(Arg.m_Type == 'i') + { + // Validate int + if(!str_toint(NewArg.m_aValue, nullptr)) + Error = SCommandParseError::ERROR_INVALID_VALUE; + } + else if(Arg.m_Type == 'f') + { + // Validate float + if(!str_tofloat(NewArg.m_aValue, nullptr)) + Error = SCommandParseError::ERROR_INVALID_VALUE; + } + else if(Arg.m_Type == 's') + { + // Validate string + if(!Valid || (Char != '"' && !ValidateStr(NewArg.m_aValue))) + Error = SCommandParseError::ERROR_INVALID_VALUE; + } + + // Extended argument validation: + // for int settings it checks that the value is in range + // for command settings, it checks that the value is one of the possible values if there are any + EValidationResult Result = ValidateArg(ArgIndex, NewArg.m_aValue); + if(Length && !Error && Result != EValidationResult::VALID) + { + if(Result == EValidationResult::ERROR) + Error = SCommandParseError::ERROR_INVALID_VALUE; // Invalid argument value (invalid int, invalid float) + else if(Result == EValidationResult::UNKNOWN) + Error = SCommandParseError::ERROR_UNKNOWN_VALUE; // Unknown argument value + else if(Result == EValidationResult::INCOMPLETE) + Error = SCommandParseError::ERROR_INCOMPLETE; // Incomplete argument in case of possible values + else if(Result == EValidationResult::OUT_OF_RANGE) + Error = SCommandParseError::ERROR_OUT_OF_RANGE; // Out of range argument value in case of int settings + else + Error = SCommandParseError::ERROR_UNKNOWN; // Unknown error + } + + Type = Arg.m_Type; + } + else + { + // Error: too many arguments if no comment after + if(m_CommentOffset == -1) + Error = SCommandParseError::ERROR_TOO_MANY_ARGS; + else + { // Otherwise, check if there are any arguments left between this argument and the comment + const char *pSubIt = pArgStart; + pSubIt = str_skip_whitespaces_const(pSubIt); + if(*pSubIt != '\0') + { // If there aren't only spaces between the last argument and the comment, then this is an error + Error = SCommandParseError::ERROR_TOO_MANY_ARGS; + } + else // If there are, then just exit the loop to avoid getting an error + { + m_vCurrentArgs.pop_back(); + break; + } + } + } + + // Fill argument informations + NewArg.m_X = PosX; + NewArg.m_Start = Offset; + NewArg.m_End = Offset + Length; + NewArg.m_Error = Error != SCommandParseError::ERROR_NONE || Length == 0 || m_Error.m_Type != SCommandParseError::ERROR_NONE; + NewArg.m_ExpectedType = Type; + + // Do not emit an error if we allow unknown commands and the current setting is invalid + if(m_AllowUnknownCommands && m_pCurrentSetting == nullptr) + NewArg.m_Error = false; + + // Check error and fill the error field with different messages + if(Error == SCommandParseError::ERROR_INVALID_VALUE || Error == SCommandParseError::ERROR_UNKNOWN_VALUE || Error == SCommandParseError::ERROR_OUT_OF_RANGE || Error == SCommandParseError::ERROR_INCOMPLETE) + { + // Only keep first error + if(!m_Error.m_aMessage[0]) + { + int ErrorArgIndex = (int)m_vCurrentArgs.size() - 1; + SCurrentSettingArg &ErrorArg = m_vCurrentArgs.back(); + SParsedMapSettingArg &SettingArg = m_pBackend->m_ParsedCommandArgs[m_pCurrentSetting].at(ArgIndex); + char aFormattedValue[256]; + FormatDisplayValue(ErrorArg.m_aValue, aFormattedValue); + + if(Error == SCommandParseError::ERROR_INVALID_VALUE || Error == SCommandParseError::ERROR_UNKNOWN_VALUE || Error == SCommandParseError::ERROR_INCOMPLETE) + { + static const std::map s_Names = { + {SCommandParseError::ERROR_INVALID_VALUE, "Invalid"}, + {SCommandParseError::ERROR_UNKNOWN_VALUE, "Unknown"}, + {SCommandParseError::ERROR_INCOMPLETE, "Incomplete"}, + }; + str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "%s argument value: %s at position %d for argument '%s'", s_Names.at(Error), aFormattedValue, (int)ErrorArg.m_Start, SettingArg.m_aName); + } + else + { + std::shared_ptr pSettingInt = std::static_pointer_cast(m_pCurrentSetting); + str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "Invalid argument value: %s at position %d for argument '%s': out of range [%d, %d]", aFormattedValue, (int)ErrorArg.m_Start, SettingArg.m_aName, pSettingInt->m_Min, pSettingInt->m_Max); + } + m_Error.m_ArgIndex = ErrorArgIndex; + m_Error.m_Type = Error; + } + } + else if(Error == SCommandParseError::ERROR_TOO_MANY_ARGS) + { + // Only keep first error + if(!m_Error.m_aMessage[0]) + { + if(m_pCurrentSetting != nullptr) + { + str_copy(m_Error.m_aMessage, "Too many arguments"); + m_Error.m_ArgIndex = ArgIndex; + break; + } + else if(!m_AllowUnknownCommands) + { + char aFormattedValue[256]; + FormatDisplayValue(m_aCommand, aFormattedValue); + str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "Unknown server setting: %s", aFormattedValue); + m_Error.m_ArgIndex = -1; + break; + } + m_Error.m_Type = Error; + } + } + + PosX += m_pBackend->TextRender()->TextWidth(m_FontSize, pArgStart, Length); // Advance argument position + ArgIndex++; + } +} + +void CMapSettingsBackend::CContext::ClearError() +{ + m_Error.m_aMessage[0] = '\0'; + m_Error.m_Type = SCommandParseError::ERROR_NONE; +} + +bool CMapSettingsBackend::CContext::UpdateCursor(bool Force) +{ + // This method updates the cursor offset in this class from + // the cursor offset of the line input. + // It also updates the argument index where the cursor is at + // and the possible values matches if the argument index changes. + // Returns true in case the cursor changed position + + if(!m_pLineInput) + return false; + + size_t Offset = m_pLineInput->GetCursorOffset(); + if(Offset == m_LastCursorOffset && !Force) + return false; + + m_LastCursorOffset = Offset; + int NewArg = m_CursorArgIndex; + + // Update current argument under cursor + if(m_CommentOffset != -1 && Offset >= (size_t)m_CommentOffset) + { + NewArg = (int)m_vCurrentArgs.size(); + } + else + { + bool FoundArg = false; + for(int i = (int)m_vCurrentArgs.size() - 1; i >= 0; i--) + { + if(Offset >= m_vCurrentArgs[i].m_Start) + { + NewArg = i; + FoundArg = true; + break; + } + } + + if(!FoundArg) + NewArg = -1; + } + + bool ShouldUpdate = NewArg != m_CursorArgIndex; + m_CursorArgIndex = NewArg; + + // Do not show error if current argument is incomplete, as we are editing it + if(m_pLineInput != nullptr) + { + if(Offset == m_pLineInput->GetLength() && m_Error.m_aMessage[0] && m_Error.m_ArgIndex == m_CursorArgIndex && m_Error.m_Type == SCommandParseError::ERROR_INCOMPLETE) + ClearError(); + } + + if(m_DropdownContext.m_Selected == -1 || ShouldUpdate || Force) + { + // Update possible commands from cursor + UpdatePossibleMatches(); + } + + return true; +} + +EValidationResult CMapSettingsBackend::CContext::ValidateArg(int Index, const char *pArg) +{ + if(!m_pCurrentSetting) + return EValidationResult::ERROR; + + // Check if this argument is valid against current argument + if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_INT) + { + std::shared_ptr pSetting = std::static_pointer_cast(m_pCurrentSetting); + if(Index > 0) + return EValidationResult::ERROR; + + int Value; + if(!str_toint(pArg, &Value)) // Try parse the integer + return EValidationResult::ERROR; + + return Value >= pSetting->m_Min && Value <= pSetting->m_Max ? EValidationResult::VALID : EValidationResult::OUT_OF_RANGE; + } + else if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_COMMAND) + { + auto &vArgs = m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting); + if(Index < (int)vArgs.size()) + { + auto It = m_pBackend->m_PossibleValuesPerCommand.find(m_pCurrentSetting->m_pName); + if(It != m_pBackend->m_PossibleValuesPerCommand.end()) + { + auto ValuesIt = It->second.find(Index); + if(ValuesIt != It->second.end()) + { + // This means that we have possible values for this argument for this setting + // In order to validate such arg, we have to check if it maches any of the possible values + const bool EqualsAny = std::any_of(ValuesIt->second.begin(), ValuesIt->second.end(), [pArg](auto *pValue) { return str_comp_nocase(pArg, pValue) == 0; }); + + // If equals, then argument is valid + if(EqualsAny) + return EValidationResult::VALID; + + // Here we check if argument is incomplete + const bool StartsAny = std::any_of(ValuesIt->second.begin(), ValuesIt->second.end(), [pArg](auto *pValue) { return str_startswith_nocase(pValue, pArg) != nullptr; }); + if(StartsAny) + return EValidationResult::INCOMPLETE; + + return EValidationResult::UNKNOWN; + } + } + } + + // If we get here, it means there are no posssible values for that specific argument. + // The validation for specific types such as int and floats were done earlier so if we get here + // we know the argument is valid. + // String and "rest of string" types are valid by default. + return EValidationResult::VALID; + } + + return EValidationResult::ERROR; +} + +void CMapSettingsBackend::CContext::UpdatePossibleMatches() +{ + // This method updates the possible values matches based on the cursor position within the current argument in the line input. + // For example ("|" is the cursor): + // - Typing "sv_deep|" will show "sv_deepfly" as a possible match in the dropdown + // Moving the cursor: "sv_|deep" will show all possible commands starting with "sv_" + // - Typing "tune ground_frict|" will show "ground_friction" as possible match + // Moving the cursor: "tune ground_|frict" will show all possible values starting with "ground_" for that argument (argument 0 of "tune" setting) + + m_vPossibleMatches.clear(); + m_DropdownContext.m_Selected = -1; + + if(m_CommentOffset == 0) + return; + + // First case: argument index under cursor is -1 => we're on the command/setting name + if(m_CursorArgIndex == -1) + { + // Use a substring from the start of the input to the cursor offset + char aSubString[128]; + str_copy(aSubString, m_aCommand, minimum(m_LastCursorOffset + 1, sizeof(aSubString))); + + // Iterate through available map settings and find those which the beginning matches with the command/setting name we are writing + for(auto &pSetting : m_pBackend->m_vpMapSettings) + { + if(str_startswith_nocase(pSetting->m_pName, aSubString)) + { + m_vPossibleMatches.emplace_back(SPossibleValueMatch{ + pSetting->m_pName, + m_CursorArgIndex, + pSetting.get(), + }); + } + } + + // If there are no matches, then the command is unknown + if(m_vPossibleMatches.empty() && !m_AllowUnknownCommands) + { + // Fill the error if we do not allow unknown commands + char aFormattedValue[256]; + FormatDisplayValue(m_aCommand, aFormattedValue); + str_format(m_Error.m_aMessage, sizeof(m_Error.m_aMessage), "Unknown server setting: %s", aFormattedValue); + m_Error.m_ArgIndex = -1; + } + } + else + { + // Second case: we are on an argument + if(!m_pCurrentSetting) // If we are on an argument of an unknown setting, we can't handle it => no possible values, ever. + return; + + if(m_pCurrentSetting->m_Type == IMapSetting::SETTING_INT) + { + // No possible values for int settings. + // Maybe we can add "0" and "1" as possible values for settings that are binary. + } + else + { + // Get the parsed arguments for the current setting + auto &vArgs = m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting); + // Make sure we are not out of bounds + if(m_CursorArgIndex < (int)vArgs.size() && m_CursorArgIndex < (int)m_vCurrentArgs.size()) + { + // Check if there are possible values for this command + auto It = m_pBackend->m_PossibleValuesPerCommand.find(m_pCurrentSetting->m_pName); + if(It != m_pBackend->m_PossibleValuesPerCommand.end()) + { + // If that's the case, then check if there are possible values for the current argument index the cursor is on + auto ValuesIt = It->second.find(m_CursorArgIndex); + if(ValuesIt != It->second.end()) + { + // If that's the case, then do the same as previously, we check for each value if they match + // with the current argument value + + auto &CurrentArg = m_vCurrentArgs.at(m_CursorArgIndex); + int SubstringLength = minimum(m_LastCursorOffset, CurrentArg.m_End) - CurrentArg.m_Start; + + // Substring based on the cursor position inside that argument + char aSubString[256]; + str_copy(aSubString, CurrentArg.m_aValue, SubstringLength + 1); + + for(auto &pValue : ValuesIt->second) + { + if(str_startswith_nocase(pValue, aSubString)) + { + m_vPossibleMatches.emplace_back(SPossibleValueMatch{ + pValue, + m_CursorArgIndex, + nullptr, + }); + } + } + } + } + } + } + } +} + +bool CMapSettingsBackend::CContext::OnInput(const IInput::CEvent &Event) +{ + if(!m_pLineInput) + return false; + + if(!m_pLineInput->IsActive()) + return false; + + if(Event.m_Flags & (IInput::FLAG_PRESS | IInput::FLAG_TEXT) && !m_pBackend->Input()->ModifierIsPressed() && !m_pBackend->Input()->AltIsPressed()) + { + // How to make this better? + // This checks when we press any key that is not handled by the dropdown + // When that's the case, it means we confirm the completion if we have a valid completion index + if(Event.m_Key != KEY_TAB && Event.m_Key != KEY_LSHIFT && Event.m_Key != KEY_RSHIFT && Event.m_Key != KEY_UP && Event.m_Key != KEY_DOWN && !(Event.m_Key >= KEY_MOUSE_1 && Event.m_Key <= KEY_MOUSE_WHEEL_RIGHT)) + { + if(m_CurrentCompletionIndex != -1) + { + m_CurrentCompletionIndex = -1; + m_DropdownContext.m_Selected = -1; + Update(); + UpdateCursor(true); + } + } + } + + return false; +} + +const char *CMapSettingsBackend::CContext::InputString() const +{ + if(!m_pLineInput) + return nullptr; + return m_pBackend->Input()->HasComposition() ? m_CompositionStringBuffer.c_str() : m_pLineInput->GetString(); +} + +const ColorRGBA CMapSettingsBackend::CContext::ms_ArgumentStringColor = ColorRGBA(84 / 255.0f, 1.0f, 1.0f, 1.0f); +const ColorRGBA CMapSettingsBackend::CContext::ms_ArgumentNumberColor = ColorRGBA(0.1f, 0.9f, 0.05f, 1.0f); +const ColorRGBA CMapSettingsBackend::CContext::ms_ArgumentUnknownColor = ColorRGBA(0.6f, 0.6f, 0.6f, 1.0f); +const ColorRGBA CMapSettingsBackend::CContext::ms_CommentColor = ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f); +const ColorRGBA CMapSettingsBackend::CContext::ms_ErrorColor = ColorRGBA(240 / 255.0f, 70 / 255.0f, 70 / 255.0f, 1.0f); + +void CMapSettingsBackend::CContext::ColorArguments(std::vector &vColorSplits) const +{ + // Get argument color based on its type + auto &&GetArgumentColor = [](char Type) -> ColorRGBA { + if(Type == 'u') + return ms_ArgumentUnknownColor; + else if(Type == 's' || Type == 'r') + return ms_ArgumentStringColor; + else if(Type == 'i' || Type == 'f') + return ms_ArgumentNumberColor; + return ms_ErrorColor; // Invalid arg type + }; + + // Iterate through all the current arguments and color them + for(int i = 0; i < ArgCount(); i++) + { + const auto &Argument = Arg(i); + // Color is based on the error flag and the type of the argument + auto Color = Argument.m_Error ? ms_ErrorColor : GetArgumentColor(Argument.m_ExpectedType); + vColorSplits.emplace_back(Argument.m_Start, Argument.m_End - Argument.m_Start, Color); + } + + if(m_pLineInput && !m_pLineInput->IsEmpty()) + { + if(!CommandIsValid() && !m_AllowUnknownCommands && m_CommentOffset != 0) + { + // If command is invalid, override color splits with red, but not comment + int ErrorLength = m_CommentOffset == -1 ? -1 : m_CommentOffset; + vColorSplits = {{0, ErrorLength, ms_ErrorColor}}; + } + else if(HasError()) + { + // If there is an error, then color the wrong part of the input, excluding comment + int ErrorLength = m_CommentOffset == -1 ? -1 : m_CommentOffset - ErrorOffset(); + vColorSplits.emplace_back(ErrorOffset(), ErrorLength, ms_ErrorColor); + } + if(m_CommentOffset != -1) + { // Color comment if there is one + vColorSplits.emplace_back(m_CommentOffset, -1, ms_CommentColor); + } + } + + std::sort(vColorSplits.begin(), vColorSplits.end(), [](const STextColorSplit &a, const STextColorSplit &b) { + return a.m_CharIndex < b.m_CharIndex; + }); +} + +int CMapSettingsBackend::CContext::CheckCollision(ECollisionCheckResult &Result) const +{ + return CheckCollision(m_pBackend->Editor()->m_Map.m_vSettings, Result); +} + +int CMapSettingsBackend::CContext::CheckCollision(const std::vector &vSettings, ECollisionCheckResult &Result) const +{ + return CheckCollision(InputString(), vSettings, Result); +} + +int CMapSettingsBackend::CContext::CheckCollision(const char *pInputString, const std::vector &vSettings, ECollisionCheckResult &Result) const +{ + // Checks for a collision with the current map settings. + // A collision is when a setting with the same arguments already exists and that it can't be added multiple times. + // For this, we use argument constraints that we define in CMapSettingsCommandObject::LoadConstraints(). + // For example, the "tune" command can be added multiple times, but only if the actual tune argument is different, thus + // the tune argument must be defined as UNIQUE. + // This method CheckCollision(ECollisionCheckResult&) returns an integer which is the index of the colliding line. If no + // colliding line was found, then it returns -1. + + if(m_CommentOffset == 0) + { // Ignore comments + Result = ECollisionCheckResult::ADD; + return -1; + } + + const int InputLength = str_length(pInputString); + + struct SArgument + { + char m_aValue[128]; + SArgument(const char *pStr) + { + str_copy(m_aValue, pStr); + } + }; + + struct SLineArgs + { + int m_Index; + std::vector m_vArgs; + }; + + // For now we split each map setting corresponding to the setting we want to add by spaces + auto &&SplitSetting = [](const char *pStr) { + std::vector vaArgs; + const char *pIt = pStr; + char aBuffer[128]; + while((pIt = str_next_token(pIt, " ", aBuffer, sizeof(aBuffer)))) + vaArgs.emplace_back(aBuffer); + return vaArgs; + }; + + // Define the result of the check + Result = ECollisionCheckResult::ERROR; + + // First case: the command is not a valid (recognized) command. + if(!CommandIsValid()) + { + // If we don't allow unknown commands, then we know there is no collision + // and the check results in an error. + if(!m_AllowUnknownCommands) + return -1; + + if(InputLength == 0) + return -1; + + // If we get here, it means we allow unknown commands. + // For them, we need to check if a similar exact command exists or not in the settings list. + // If it does, then we found a collision, and the result is REPLACE. + for(int i = 0; i < (int)vSettings.size(); i++) + { + if(str_comp_nocase(vSettings[i].m_aCommand, pInputString) == 0) + { + Result = ECollisionCheckResult::REPLACE; + return i; + } + } + + // If nothing was found, then we must ensure that the command, although unknown, is somewhat valid + // by checking if the command contains a space and that there is at least one non-empty argument. + const char *pSpace = str_find(pInputString, " "); + if(!pSpace || !*(pSpace + 1)) + Result = ECollisionCheckResult::ERROR; + else + Result = ECollisionCheckResult::ADD; + + return -1; // No collision + } + + // Second case: the command is valid. + // In this case, we know we have a valid setting name, which means we can use everything we have in this class which are + // related to valid map settings, such as parsed command arguments, etc. + + const std::shared_ptr &pSetting = Setting(); + if(pSetting->m_Type == IMapSetting::SETTING_INT) + { + // For integer settings, the check is quite simple as we know + // we can only ever have 1 argument. + + // The integer setting cannot be added multiple times, which means if a collision was found, then the only result we + // can have is REPLACE. + // In this case, the collision is found only by checking the command name for every setting in the current map settings. + char aBuffer[256]; + auto It = std::find_if(vSettings.begin(), vSettings.end(), [&](const CEditorMapSetting &Setting) { + const char *pLineSettingValue = Setting.m_aCommand; // Get the map setting command + pLineSettingValue = str_next_token(pLineSettingValue, " ", aBuffer, sizeof(aBuffer)); // Get the first token before the first space + return str_comp_nocase(aBuffer, pSetting->m_pName) == 0; // Check if that equals our current command + }); + + if(It == vSettings.end()) + { + // If nothing was found, then there is no collision and we can add that command to the list + Result = ECollisionCheckResult::ADD; + return -1; + } + else + { + // Otherwise, we can only replace it + Result = ECollisionCheckResult::REPLACE; + return It - vSettings.begin(); // This is the index of the colliding line + } + } + else if(pSetting->m_Type == IMapSetting::SETTING_COMMAND) + { + // For "command" settings, this is a bit more complex as we have to use argument constraints. + // The general idea is to split every map setting in their arguments separated by spaces. + // Then, for each argument, we check if it collides with any of the map settings. When that's the case, + // we need to check the constraint of the argument. If set to UNIQUE, then that's a collision and we can only + // replace the command in the list. + // If set to anything else, we consider that it is not a collision and we move to the next argument. + // This system is simple and somewhat flexible as we only need to declare the constraints, the rest should be + // handled automatically. + + std::shared_ptr pSettingCommand = std::static_pointer_cast(pSetting); + // Get matching lines for that command + std::vector vLineArgs; + for(int i = 0; i < (int)vSettings.size(); i++) + { + const auto &Setting = vSettings.at(i); + + // Split this setting into its arguments + std::vector vArgs = SplitSetting(Setting.m_aCommand); + // Only keep settings that match with the current input setting name + if(!vArgs.empty() && str_comp_nocase(vArgs[0].m_aValue, pSettingCommand->m_pName) == 0) + { + // When that's the case, we save them + vArgs.erase(vArgs.begin()); + vLineArgs.push_back(SLineArgs{ + i, + vArgs, + }); + } + } + + // Here is the simple algorithm to check for collisions according to argument constraints + bool Error = false; + int CollidingLineIndex = -1; + for(int ArgIndex = 0; ArgIndex < ArgCount(); ArgIndex++) + { + bool Collide = false; + const char *pValue = Arg(ArgIndex).m_aValue; + for(auto &Line : vLineArgs) + { + // Check first colliding line + if(str_comp_nocase(pValue, Line.m_vArgs[ArgIndex].m_aValue) == 0) + { + Collide = true; + CollidingLineIndex = Line.m_Index; + Error = m_pBackend->ArgConstraint(pSetting->m_pName, ArgIndex) == CMapSettingsBackend::EArgConstraint::UNIQUE; + } + if(Error) + break; + } + + // If we did not collide with any of the lines for that argument, we're good to go + // (or if we had an error) + if(!Collide || Error) + break; + + // Otherwise, remove non-colliding args from the list + vLineArgs.erase( + std::remove_if(vLineArgs.begin(), vLineArgs.end(), [&](const SLineArgs &Line) { + return str_comp_nocase(pValue, Line.m_vArgs[ArgIndex].m_aValue) != 0; + }), + vLineArgs.end()); + } + + // The result is either REPLACE when we found a collision, or ADD + Result = Error ? ECollisionCheckResult::REPLACE : ECollisionCheckResult::ADD; + return CollidingLineIndex; + } + + return -1; +} + +bool CMapSettingsBackend::CContext::Valid() const +{ + // Check if the entire setting is valid or not + + if(m_CommentOffset == 0) + return true; // A "comment" setting is considered valid. + + // Check if command is valid + if(m_pCurrentSetting) + { + // Check if all arguments are valid + const bool ArgumentsValid = std::all_of(m_vCurrentArgs.begin(), m_vCurrentArgs.end(), [](const SCurrentSettingArg &Arg) { + return !Arg.m_Error; + }); + + if(!ArgumentsValid) + return false; + + // Check that we have the same number of arguments + return m_vCurrentArgs.size() == m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting).size(); + } + else + { + // If we have an invalid setting, then we consider the entire setting as valid if we allow unknown commands + // as we cannot handle them. + return m_AllowUnknownCommands; + } +} + +void CMapSettingsBackend::CContext::GetCommandHelpText(char *pStr, int Length) const +{ + if(!m_pCurrentSetting) + return; + + str_copy(pStr, m_pCurrentSetting->m_pHelp, Length); +} + +void CMapSettingsBackend::CContext::UpdateCompositionString() +{ + if(!m_pLineInput) + return; + + const bool HasComposition = m_pBackend->Input()->HasComposition(); + + if(HasComposition) + { + const size_t CursorOffset = m_pLineInput->GetCursorOffset(); + const size_t DisplayCursorOffset = m_pLineInput->OffsetFromActualToDisplay(CursorOffset); + const std::string DisplayStr = std::string(m_pLineInput->GetString()); + std::string CompositionBuffer = DisplayStr.substr(0, DisplayCursorOffset) + m_pBackend->Input()->GetComposition() + DisplayStr.substr(DisplayCursorOffset); + if(CompositionBuffer != m_CompositionStringBuffer) + { + m_CompositionStringBuffer = CompositionBuffer; + Update(); + UpdateCursor(); + } + } +} + +template +void CMapSettingsBackend::CContext::FormatDisplayValue(const char *pValue, char (&aOut)[N]) +{ + const int MaxLength = 32; + if(str_length(pValue) > MaxLength) + { + str_copy(aOut, pValue, MaxLength); + str_append(aOut, "..."); + } + else + { + str_copy(aOut, pValue); + } +} + +bool CMapSettingsBackend::OnInput(const IInput::CEvent &Event) +{ + if(ms_pActiveContext) + return ms_pActiveContext->OnInput(Event); + + return false; +} + +void CMapSettingsBackend::OnUpdate() +{ + if(ms_pActiveContext && ms_pActiveContext->m_pLineInput && ms_pActiveContext->m_pLineInput->IsActive()) + ms_pActiveContext->UpdateCompositionString(); +} + +void CMapSettingsBackend::OnMapLoad() +{ + // Load & validate all map settings + m_LoadedMapSettings.Reset(); + + auto &vLoadedMapSettings = Editor()->m_Map.m_vSettings; + + // Keep a vector of valid map settings, to check collision against: m_vValidLoadedMapSettings + + // Create a local context with no lineinput, only used to parse the commands + CContext LocalContext = NewContext(nullptr); + + // Iterate through map settings + // Two steps: + // 1. Save valid and invalid settings + // 2. Check for duplicates + + std::vector> vSettingsInvalid; + + for(int i = 0; i < (int)vLoadedMapSettings.size(); i++) + { + CEditorMapSetting &Setting = vLoadedMapSettings.at(i); + // Parse the setting using the context + LocalContext.UpdateFromString(Setting.m_aCommand); + + bool Valid = LocalContext.Valid(); + ECollisionCheckResult Result = ECollisionCheckResult::ERROR; + LocalContext.CheckCollision(Setting.m_aCommand, m_LoadedMapSettings.m_vSettingsValid, Result); + + if(Valid && Result == ECollisionCheckResult::ADD) + m_LoadedMapSettings.m_vSettingsValid.emplace_back(Setting); + else + vSettingsInvalid.emplace_back(i, Valid, Setting); + + LocalContext.Reset(); + + // Empty duplicates for this line, might be filled later + m_LoadedMapSettings.m_SettingsDuplicate.insert({i, {}}); + } + + for(const auto &[Index, Valid, Setting] : vSettingsInvalid) + { + LocalContext.UpdateFromString(Setting.m_aCommand); + + ECollisionCheckResult Result = ECollisionCheckResult::ERROR; + int CollidingLineIndex = LocalContext.CheckCollision(Setting.m_aCommand, m_LoadedMapSettings.m_vSettingsValid, Result); + int RealCollidingLineIndex = CollidingLineIndex; + + if(CollidingLineIndex != -1) + RealCollidingLineIndex = std::find_if(vLoadedMapSettings.begin(), vLoadedMapSettings.end(), [&](const CEditorMapSetting &MapSetting) { + return str_comp_nocase(MapSetting.m_aCommand, m_LoadedMapSettings.m_vSettingsValid.at(CollidingLineIndex).m_aCommand) == 0; + }) - vLoadedMapSettings.begin(); + + int Type = 0; + if(!Valid) + Type |= SInvalidSetting::TYPE_INVALID; + if(Result == ECollisionCheckResult::REPLACE) + Type |= SInvalidSetting::TYPE_DUPLICATE; + + m_LoadedMapSettings.m_vSettingsInvalid.emplace_back(Index, Setting.m_aCommand, Type, RealCollidingLineIndex, !LocalContext.CommandIsValid()); + if(Type & SInvalidSetting::TYPE_DUPLICATE) + m_LoadedMapSettings.m_SettingsDuplicate[RealCollidingLineIndex].emplace_back(m_LoadedMapSettings.m_vSettingsInvalid.size() - 1); + + LocalContext.Reset(); + } + + if(!m_LoadedMapSettings.m_vSettingsInvalid.empty()) + Editor()->m_Dialog = DIALOG_MAPSETTINGS_ERROR; +} + +// ------ loaders + +void CMapSettingsBackend::InitValueLoaders() +{ + // Load the different possible values for some specific settings + RegisterLoader("tune", SValueLoader::LoadTuneValues); + RegisterLoader("tune_zone", SValueLoader::LoadTuneZoneValues); + RegisterLoader("mapbug", SValueLoader::LoadMapBugs); +} + +void SValueLoader::LoadTuneValues(const CSettingValuesBuilder &TuneBuilder) +{ + // Add available tuning names to argument 0 of setting "tune" + LoadArgumentTuneValues(TuneBuilder.Argument(0)); +} + +void SValueLoader::LoadTuneZoneValues(const CSettingValuesBuilder &TuneZoneBuilder) +{ + // Add available tuning names to argument 1 of setting "tune_zone" + LoadArgumentTuneValues(TuneZoneBuilder.Argument(1)); +} + +void SValueLoader::LoadMapBugs(const CSettingValuesBuilder &BugBuilder) +{ + // Get argument 0 of setting "mapbug" + auto ArgBuilder = BugBuilder.Argument(0); + // Add available map bugs options + ArgBuilder.Add("grenade-doubleexplosion@ddnet.tw"); +} + +void SValueLoader::LoadArgumentTuneValues(CArgumentValuesListBuilder &&ArgBuilder) +{ + // Iterate through available tunings add their name to the list + for(int i = 0; i < CTuningParams::Num(); i++) + { + ArgBuilder.Add(CTuningParams::Name(i)); + } +} diff --git a/src/game/editor/editor_server_settings.h b/src/game/editor/editor_server_settings.h new file mode 100644 index 0000000000..f4f1d6e292 --- /dev/null +++ b/src/game/editor/editor_server_settings.h @@ -0,0 +1,416 @@ +#ifndef GAME_EDITOR_EDITOR_SERVER_SETTINGS_H +#define GAME_EDITOR_EDITOR_SERVER_SETTINGS_H + +#include "component.h" +#include "editor_ui.h" + +#include +#include +#include + +class CEditor; +struct SMapSettingInt; +struct SMapSettingCommand; +struct IMapSetting; +class CLineInput; + +struct CEditorMapSetting +{ + char m_aCommand[256]; + + CEditorMapSetting(const char *pCommand) + { + str_copy(m_aCommand, pCommand); + } +}; + +// A parsed map setting argument, storing the name and the type +// Used for validation and to display arguments names +struct SParsedMapSettingArg +{ + char m_aName[32]; + char m_Type; +}; + +// An argument for the current setting +struct SCurrentSettingArg +{ + char m_aValue[256]; // Value of the argument + float m_X; // The X position + size_t m_Start; // Start offset within the input string + size_t m_End; // End offset within the input string + bool m_Error; // If the argument is wrong or not + char m_ExpectedType; // The expected type +}; + +struct SPossibleValueMatch +{ + const char *m_pValue; // Possible value string + int m_ArgIndex; // Argument for that possible value + const void *m_pData; // Generic pointer to pass specific data +}; + +struct SCommandParseError +{ + char m_aMessage[256]; + int m_ArgIndex; + + enum EErrorType + { + ERROR_NONE = 0, + ERROR_TOO_MANY_ARGS, + ERROR_INVALID_VALUE, + ERROR_UNKNOWN_VALUE, + ERROR_INCOMPLETE, + ERROR_OUT_OF_RANGE, + ERROR_UNKNOWN + }; + EErrorType m_Type; +}; + +struct SInvalidSetting +{ + enum + { + TYPE_INVALID = 1 << 0, + TYPE_DUPLICATE = 1 << 1 + }; + int m_Index; // Index of the command in the loaded map settings list + char m_aSetting[256]; // String of the setting + int m_Type; // Type of that invalid setting + int m_CollidingIndex; // The colliding line index in case type is TYPE_DUPLICATE + bool m_Unknown; // Is the command unknown + + struct SContext + { + bool m_Fixed; + bool m_Deleted; + bool m_Chosen; + } m_Context; + + SInvalidSetting(int Index, const char *pSetting, int Type, int CollidingIndex, bool Unknown) : + m_Index(Index), m_Type(Type), m_CollidingIndex(CollidingIndex), m_Unknown(Unknown), m_Context() + { + str_copy(m_aSetting, pSetting); + } +}; + +// -------------------------------------- +// Builder classes & methods to generate list of possible values +// easily for specific settings and arguments. +// It uses a container stored inside CMapSettingsBackend. +// Usage: +// CValuesBuilder Builder(&m_Container); +// // Either do it in one go: +// Builder("tune").Argument(0).Add("value_1").Add("value_2"); +// // Or reference the builder (useful when using in a loop): +// auto TuneBuilder = Builder("tune").Argument(0); +// TuneBuilder.Add("value_1"); +// TuneBuilder.Add("value_2"); +// // ... + +using TArgumentValuesList = std::vector; // List of possible values +using TSettingValues = std::map; // Possible values per argument +using TSettingsArgumentValues = std::map; // Possible values per argument, per command/setting name + +class CValuesBuilder; +class CSettingValuesBuilder; +class CArgumentValuesListBuilder +{ +public: + CArgumentValuesListBuilder &Add(const char *pString) + { + m_pContainer->emplace_back(pString); + return *this; + } + +private: + CArgumentValuesListBuilder(std::vector *pContainer) : + m_pContainer(pContainer) {} + + std::vector *m_pContainer; + friend class CSettingValuesBuilder; +}; + +class CSettingValuesBuilder +{ +public: + CArgumentValuesListBuilder Argument(int Arg) const + { + return CArgumentValuesListBuilder(&(*m_pContainer)[Arg]); + } + +private: + CSettingValuesBuilder(TSettingValues *pContainer) : + m_pContainer(pContainer) {} + + friend class CValuesBuilder; + TSettingValues *m_pContainer; +}; + +class CValuesBuilder +{ +public: + CValuesBuilder(TSettingsArgumentValues *pContainer) : + m_pContainer(pContainer) + { + } + + CSettingValuesBuilder operator()(const char *pSettingName) const + { + return CSettingValuesBuilder(&(*m_pContainer)[pSettingName]); + } + +private: + TSettingsArgumentValues *m_pContainer; +}; + +// -------------------------------------- + +struct SValueLoader +{ + static void LoadTuneValues(const CSettingValuesBuilder &TuneBuilder); + static void LoadTuneZoneValues(const CSettingValuesBuilder &TuneZoneBuilder); + static void LoadMapBugs(const CSettingValuesBuilder &BugBuilder); + static void LoadArgumentTuneValues(CArgumentValuesListBuilder &&ArgBuilder); +}; + +enum class EValidationResult +{ + VALID = 0, + ERROR, + INCOMPLETE, + UNKNOWN, + OUT_OF_RANGE, +}; + +enum class ECollisionCheckResult +{ + ERROR, + REPLACE, + ADD +}; + +class CMapSettingsBackend : public CEditorComponent +{ + typedef void (*FLoaderFunction)(const CSettingValuesBuilder &); + +public: // General methods + CMapSettingsBackend() = default; + + void OnInit(CEditor *pEditor) override; + bool OnInput(const IInput::CEvent &Event) override; + void OnUpdate() override; + void OnMapLoad() override; + +public: // Constraints methods + enum class EArgConstraint + { + DEFAULT = 0, + UNIQUE, + MULTIPLE, + }; + + EArgConstraint ArgConstraint(const char *pSettingName, int Arg) const + { + return m_ArgConstraintsPerCommand.at(pSettingName).at(Arg); + } + +public: // Backend methods + const std::vector &ParsedArgs(const std::shared_ptr &pSetting) const + { + return m_ParsedCommandArgs.at(pSetting); + } + +public: // CContext + class CContext + { + static const ColorRGBA ms_ArgumentStringColor; + static const ColorRGBA ms_ArgumentNumberColor; + static const ColorRGBA ms_ArgumentUnknownColor; + static const ColorRGBA ms_CommentColor; + static const ColorRGBA ms_ErrorColor; + + friend class CMapSettingsBackend; + + public: + bool CommandIsValid() const { return m_pCurrentSetting != nullptr; } + int CurrentArg() const { return m_CursorArgIndex; } + const char *CurrentArgName() const { return (!m_pCurrentSetting || m_CursorArgIndex < 0 || m_CursorArgIndex >= (int)m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting).size()) ? nullptr : m_pBackend->m_ParsedCommandArgs.at(m_pCurrentSetting).at(m_CursorArgIndex).m_aName; } + float CurrentArgPos() const { return (m_CursorArgIndex < 0 || m_CursorArgIndex >= (int)m_vCurrentArgs.size()) ? 0 : m_vCurrentArgs[m_CursorArgIndex].m_X; } + size_t CurrentArgOffset() const { return m_CursorArgIndex == -1 ? 0 : m_vCurrentArgs[m_CursorArgIndex].m_Start; } + const char *CurrentArgValue() const { return m_CursorArgIndex == -1 ? m_aCommand : m_vCurrentArgs[m_CursorArgIndex].m_aValue; } + const std::vector &PossibleMatches() const { return m_vPossibleMatches; } + bool HasError() const { return m_Error.m_aMessage[0] != '\0'; } + size_t ErrorOffset() const { return m_Error.m_ArgIndex < 0 ? 0 : m_vCurrentArgs.at(m_Error.m_ArgIndex).m_Start; } + const char *Error() const { return m_Error.m_aMessage; } + int ArgCount() const { return (int)m_vCurrentArgs.size(); } + const SCurrentSettingArg &Arg(int Index) const { return m_vCurrentArgs.at(Index); } + const std::shared_ptr &Setting() const { return m_pCurrentSetting; } + CLineInput *LineInput() const { return m_pLineInput; } + void SetFontSize(float FontSize) { m_FontSize = FontSize; } + int CommentOffset() const { return m_CommentOffset; }; + + int CheckCollision(ECollisionCheckResult &Result) const; + int CheckCollision(const std::vector &vSettings, ECollisionCheckResult &Result) const; + int CheckCollision(const char *pInputString, const std::vector &vSettings, ECollisionCheckResult &Result) const; + + void Update(); + void UpdateFromString(const char *pStr); + bool UpdateCursor(bool Force = false); + void Reset(); + void GetCommandHelpText(char *pStr, int Length) const; + bool Valid() const; + void ColorArguments(std::vector &vColorSplits) const; + + bool m_AllowUnknownCommands; + SEditBoxDropdownContext m_DropdownContext; + int m_CurrentCompletionIndex; + + private: // Methods + CContext(CMapSettingsBackend *pMaster, CLineInput *pLineinput) : + m_DropdownContext(), m_pLineInput(pLineinput), m_pBackend(pMaster) + { + m_AllowUnknownCommands = false; + Reset(); + } + + void ClearError(); + EValidationResult ValidateArg(int Index, const char *pArg); + void UpdatePossibleMatches(); + void ParseArgs(const char *pLineInputStr, const char *pStr); + bool OnInput(const IInput::CEvent &Event); + const char *InputString() const; + void UpdateCompositionString(); + + template + void FormatDisplayValue(const char *pValue, char (&aOut)[N]); + + private: // Fields + std::shared_ptr m_pCurrentSetting; // Current setting, can be nullptr in case of invalid setting name + std::vector m_vCurrentArgs; // Current parsed arguments from lineinput string + int m_CursorArgIndex; // The current argument the cursor is over + std::vector m_vPossibleMatches; // The current matches from cursor argument + size_t m_LastCursorOffset; // Last cursor offset + CLineInput *m_pLineInput; + char m_aCommand[128]; // The current command, not necessarily valid + SCommandParseError m_Error; // Error + int m_CommentOffset; // Position of the comment, if there is one + + CMapSettingsBackend *m_pBackend; + std::string m_CompositionStringBuffer; + float m_FontSize; + }; + + CContext NewContext(CLineInput *pLineInput) + { + return CContext(this, pLineInput); + } + +private: // Loader methods + void LoadAllMapSettings(); + void LoadCommand(const char *pName, const char *pArgs, const char *pHelp); + void LoadSettingInt(const std::shared_ptr &pSetting); + void LoadSettingCommand(const std::shared_ptr &pSetting); + void InitValueLoaders(); + void LoadPossibleValues(const CSettingValuesBuilder &Builder, const std::shared_ptr &pSetting); + void RegisterLoader(const char *pSettingName, const FLoaderFunction &pfnLoader); + void LoadConstraints(); + + static void PossibleConfigVariableCallback(const struct SConfigVariable *pVariable, void *pUserData); + +private: // Argument constraints + using TArgumentConstraints = std::map; // Constraint per argument index + using TCommandArgumentConstraints = std::map; // Constraints per command/setting name + + // Argument constraints builder + // Used to define arguments constraints for specific commands + // It uses a container stored in CMapSettingsBackend. + // Usage: + // CCommandArgumentConstraintBuilder Command(&m_Container); + // Command("tune", 2).Unique(0); // Defines argument 0 of command "tune" having 2 args as UNIQUE + // Command("tune_zone", 3).Multiple(0).Unique(1); + // // ^ Multiple() currently is only for readable purposes. It can be omited: + // // Command("tune_zone", 3).Unique(1); + // + + class CCommandArgumentConstraintBuilder; + + class CArgumentConstraintsBuilder + { + friend class CCommandArgumentConstraintBuilder; + + private: + CArgumentConstraintsBuilder(TArgumentConstraints *pContainer) : + m_pContainer(pContainer){}; + + TArgumentConstraints *m_pContainer; + + public: + CArgumentConstraintsBuilder &Multiple(int Arg) + { + // Define a multiple argument constraint + (*m_pContainer)[Arg] = EArgConstraint::MULTIPLE; + return *this; + } + + CArgumentConstraintsBuilder &Unique(int Arg) + { + // Define a unique argument constraint + (*m_pContainer)[Arg] = EArgConstraint::UNIQUE; + return *this; + } + }; + + class CCommandArgumentConstraintBuilder + { + public: + CCommandArgumentConstraintBuilder(TCommandArgumentConstraints *pContainer) : + m_pContainer(pContainer) {} + + CArgumentConstraintsBuilder operator()(const char *pSettingName, int ArgCount) + { + for(int i = 0; i < ArgCount; i++) + (*m_pContainer)[pSettingName][i] = EArgConstraint::DEFAULT; + return CArgumentConstraintsBuilder(&(*m_pContainer)[pSettingName]); + } + + private: + TCommandArgumentConstraints *m_pContainer; + }; + + TCommandArgumentConstraints m_ArgConstraintsPerCommand; + +private: // Backend fields + std::vector> m_vpMapSettings; + std::map, std::vector> m_ParsedCommandArgs; // Parsed available settings arguments, used for validation + TSettingsArgumentValues m_PossibleValuesPerCommand; + std::map m_LoaderFunctions; + + static CContext *ms_pActiveContext; + + friend class CEditor; + +private: // Map settings validation on load + struct SLoadedMapSettings + { + std::vector m_vSettingsInvalid; + std::vector m_vSettingsValid; + std::map> m_SettingsDuplicate; + + SLoadedMapSettings() : + m_vSettingsInvalid(), m_vSettingsValid(), m_SettingsDuplicate() + { + } + + void Reset() + { + m_vSettingsInvalid.clear(); + m_vSettingsValid.clear(); + m_SettingsDuplicate.clear(); + } + + } m_LoadedMapSettings; +}; + +#endif diff --git a/src/game/editor/editor_trackers.cpp b/src/game/editor/editor_trackers.cpp index 8f3660422f..3200f67b81 100644 --- a/src/game/editor/editor_trackers.cpp +++ b/src/game/editor/editor_trackers.cpp @@ -15,13 +15,15 @@ CQuadEditTracker::~CQuadEditTracker() m_vSelectedQuads.clear(); } -void CQuadEditTracker::BeginQuadTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads) +void CQuadEditTracker::BeginQuadTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, int GroupIndex, int LayerIndex) { if(m_Tracking) return; m_Tracking = true; m_vSelectedQuads.clear(); m_pLayer = pLayer; + m_GroupIndex = GroupIndex < 0 ? m_pEditor->m_SelectedGroup : GroupIndex; + m_LayerIndex = LayerIndex < 0 ? m_pEditor->m_vSelectedLayers[0] : LayerIndex; // Init all points for(auto QuadIndex : vSelectedQuads) { @@ -37,26 +39,25 @@ void CQuadEditTracker::EndQuadTrack() return; m_Tracking = false; - int GroupIndex = m_pEditor->m_SelectedGroup; - int LayerIndex = m_pEditor->m_vSelectedLayers[0]; - // Record all moved stuff std::vector> vpActions; for(auto QuadIndex : m_vSelectedQuads) { auto &pQuad = m_pLayer->m_vQuads[QuadIndex]; auto vCurrentPoints = std::vector(pQuad.m_aPoints, pQuad.m_aPoints + 5); - vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); + vpActions.push_back(std::make_shared(m_pEditor, m_GroupIndex, m_LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); } m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions)); } -void CQuadEditTracker::BeginQuadPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, EQuadProp Prop) +void CQuadEditTracker::BeginQuadPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, EQuadProp Prop, int GroupIndex, int LayerIndex) { if(m_TrackedProp != EQuadProp::PROP_NONE) return; m_TrackedProp = Prop; m_pLayer = pLayer; + m_GroupIndex = GroupIndex < 0 ? m_pEditor->m_SelectedGroup : GroupIndex; + m_LayerIndex = LayerIndex < 0 ? m_pEditor->m_vSelectedLayers[0] : LayerIndex; m_vSelectedQuads = vSelectedQuads; m_PreviousValues.clear(); @@ -83,16 +84,13 @@ void CQuadEditTracker::EndQuadPropTrack(EQuadProp Prop) std::vector> vpActions; - int GroupIndex = m_pEditor->m_SelectedGroup; - int LayerIndex = m_pEditor->m_vSelectedLayers[0]; - for(auto QuadIndex : m_vSelectedQuads) { auto &Quad = m_pLayer->m_vQuads[QuadIndex]; if(Prop == EQuadProp::PROP_POS_X || Prop == EQuadProp::PROP_POS_Y) { auto vCurrentPoints = std::vector(Quad.m_aPoints, Quad.m_aPoints + 5); - vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); + vpActions.push_back(std::make_shared(m_pEditor, m_GroupIndex, m_LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); } else { @@ -107,7 +105,7 @@ void CQuadEditTracker::EndQuadPropTrack(EQuadProp Prop) Value = Quad.m_ColorEnvOffset; if(Value != m_PreviousValues[QuadIndex]) - vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, Prop, m_PreviousValues[QuadIndex], Value)); + vpActions.push_back(std::make_shared(m_pEditor, m_GroupIndex, m_LayerIndex, QuadIndex, Prop, m_PreviousValues[QuadIndex], Value)); } } @@ -115,12 +113,14 @@ void CQuadEditTracker::EndQuadPropTrack(EQuadProp Prop) m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions)); } -void CQuadEditTracker::BeginQuadPointPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, int SelectedQuadPoints) +void CQuadEditTracker::BeginQuadPointPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, int SelectedQuadPoints, int GroupIndex, int LayerIndex) { if(!m_vTrackedProps.empty()) return; m_pLayer = pLayer; + m_GroupIndex = GroupIndex < 0 ? m_pEditor->m_SelectedGroup : GroupIndex; + m_LayerIndex = LayerIndex < 0 ? m_pEditor->m_vSelectedLayers[0] : LayerIndex; m_SelectedQuadPoints = SelectedQuadPoints; m_vSelectedQuads = vSelectedQuads; m_PreviousValuesPoint.clear(); @@ -187,16 +187,13 @@ void CQuadEditTracker::EndQuadPointPropTrack(EQuadPointProp Prop) std::vector> vpActions; - int GroupIndex = m_pEditor->m_SelectedGroup; - int LayerIndex = m_pEditor->m_vSelectedLayers[0]; - for(auto QuadIndex : m_vSelectedQuads) { auto &Quad = m_pLayer->m_vQuads[QuadIndex]; if(Prop == EQuadPointProp::PROP_POS_X || Prop == EQuadPointProp::PROP_POS_Y) { auto vCurrentPoints = std::vector(Quad.m_aPoints, Quad.m_aPoints + 5); - vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); + vpActions.push_back(std::make_shared(m_pEditor, m_GroupIndex, m_LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); } else { @@ -219,7 +216,7 @@ void CQuadEditTracker::EndQuadPointPropTrack(EQuadPointProp Prop) } if(Value != m_PreviousValuesPoint[QuadIndex][v][Prop]) - vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, v, Prop, m_PreviousValuesPoint[QuadIndex][v][Prop], Value)); + vpActions.push_back(std::make_shared(m_pEditor, m_GroupIndex, m_LayerIndex, QuadIndex, v, Prop, m_PreviousValuesPoint[QuadIndex][v][Prop], Value)); } } } @@ -234,16 +231,13 @@ void CQuadEditTracker::EndQuadPointPropTrackAll() std::vector> vpActions; for(auto &Prop : m_vTrackedProps) { - int GroupIndex = m_pEditor->m_SelectedGroup; - int LayerIndex = m_pEditor->m_vSelectedLayers[0]; - for(auto QuadIndex : m_vSelectedQuads) { auto &Quad = m_pLayer->m_vQuads[QuadIndex]; if(Prop == EQuadPointProp::PROP_POS_X || Prop == EQuadPointProp::PROP_POS_Y) { auto vCurrentPoints = std::vector(Quad.m_aPoints, Quad.m_aPoints + 5); - vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); + vpActions.push_back(std::make_shared(m_pEditor, m_GroupIndex, m_LayerIndex, QuadIndex, m_InitalPoints[QuadIndex], vCurrentPoints)); } else { @@ -266,7 +260,7 @@ void CQuadEditTracker::EndQuadPointPropTrackAll() } if(Value != m_PreviousValuesPoint[QuadIndex][v][Prop]) - vpActions.push_back(std::make_shared(m_pEditor, GroupIndex, LayerIndex, QuadIndex, v, Prop, m_PreviousValuesPoint[QuadIndex][v][Prop], Value)); + vpActions.push_back(std::make_shared(m_pEditor, m_GroupIndex, m_LayerIndex, QuadIndex, v, Prop, m_PreviousValuesPoint[QuadIndex][v][Prop], Value)); } } } @@ -279,22 +273,6 @@ void CQuadEditTracker::EndQuadPointPropTrackAll() m_vTrackedProps.clear(); } -void CEditorPropTracker::BeginPropTrack(int Prop, int Value) -{ - if(m_TrackedProp != -1 || m_TrackedProp == Prop) - return; - m_TrackedProp = Prop; - m_PreviousValue = Value; -} - -void CEditorPropTracker::StopPropTrack(int Prop, int Value) -{ - if(Prop != m_TrackedProp) - return; - m_TrackedProp = -1; - m_CurrentValue = Value; -} - // ----------------------------- void CEnvelopeEditorOperationTracker::Begin(EEnvelopeEditorOp Operation) @@ -385,20 +363,81 @@ void CEnvelopeEditorOperationTracker::HandlePointDragEnd(bool Switch) m_SavedValues.clear(); } +// ----------------------------------------------------------------------- +CSoundSourceOperationTracker::CSoundSourceOperationTracker(CEditor *pEditor) : + m_pEditor(pEditor), m_pSource(nullptr), m_TrackedOp(ESoundSourceOp::OP_NONE), m_LayerIndex(-1) +{ +} + +void CSoundSourceOperationTracker::Begin(CSoundSource *pSource, ESoundSourceOp Operation, int LayerIndex) +{ + if(m_TrackedOp == Operation || m_TrackedOp != ESoundSourceOp::OP_NONE) + return; + + m_TrackedOp = Operation; + m_pSource = pSource; + m_LayerIndex = LayerIndex; + + if(m_TrackedOp == ESoundSourceOp::OP_MOVE) + HandlePointMove(EState::STATE_BEGIN); +} + +void CSoundSourceOperationTracker::End() +{ + if(m_TrackedOp == ESoundSourceOp::OP_NONE) + return; + + if(m_TrackedOp == ESoundSourceOp::OP_MOVE) + HandlePointMove(EState::STATE_END); + + m_TrackedOp = ESoundSourceOp::OP_NONE; +} + +void CSoundSourceOperationTracker::HandlePointMove(EState State) +{ + if(State == EState::STATE_BEGIN) + { + m_Data.m_OriginalPoint = m_pSource->m_Position; + } + else if(State == EState::STATE_END) + { + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_LayerIndex, m_pEditor->m_SelectedSource, m_Data.m_OriginalPoint, m_pSource->m_Position)); + } +} + +// ----------------------------------------------------------------------- + +int SPropTrackerHelper::GetDefaultGroupIndex(CEditor *pEditor) +{ + return pEditor->m_SelectedGroup; +} + +int SPropTrackerHelper::GetDefaultLayerIndex(CEditor *pEditor) +{ + return pEditor->m_vSelectedLayers[0]; +} + // ----------------------------------------------------------------------- void CLayerPropTracker::OnEnd(ELayerProp Prop, int Value) { - m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], Prop, m_OriginalValue, Value)); + if(Prop == ELayerProp::PROP_GROUP) + { + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_OriginalGroupIndex, std::vector{m_OriginalLayerIndex}, m_CurrentGroupIndex, std::vector{m_CurrentLayerIndex})); + } + else + { + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_CurrentGroupIndex, m_CurrentLayerIndex, Prop, m_OriginalValue, Value)); + } } int CLayerPropTracker::PropToValue(ELayerProp Prop) { switch(Prop) { - case ELayerProp::PROP_GROUP: return m_pEditor->m_SelectedGroup; + case ELayerProp::PROP_GROUP: return m_CurrentGroupIndex; case ELayerProp::PROP_HQ: return m_pObject->m_Flags; - case ELayerProp::PROP_ORDER: return m_pEditor->m_vSelectedLayers[0]; + case ELayerProp::PROP_ORDER: return m_CurrentLayerIndex; default: return 0; } } @@ -439,7 +478,7 @@ void CLayerTilesPropTracker::OnStart(ETilesProp Prop) void CLayerTilesPropTracker::OnEnd(ETilesProp Prop, int Value) { - auto pAction = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], Prop, m_OriginalValue, Value); + auto pAction = std::make_shared(m_pEditor, m_OriginalGroupIndex, m_OriginalLayerIndex, Prop, m_OriginalValue, Value); pAction->SetSavedLayers(m_SavedLayers); m_SavedLayers.clear(); @@ -490,7 +529,7 @@ void CLayerTilesCommonPropTracker::OnEnd(ETilesCommonProp Prop, int Value) for(auto &pLayer : m_vpLayers) { int LayerIndex = m_vLayerIndices[j++]; - auto pAction = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, LayerIndex, s_PropMap[Prop], m_OriginalValue, Value); + auto pAction = std::make_shared(m_pEditor, m_OriginalGroupIndex, LayerIndex, s_PropMap[Prop], m_OriginalValue, Value); pAction->SetSavedLayers(m_SavedLayers[pLayer]); vpActions.push_back(pAction); } @@ -549,7 +588,7 @@ int CLayerGroupPropTracker::PropToValue(EGroupProp Prop) void CLayerQuadsPropTracker::OnEnd(ELayerQuadsProp Prop, int Value) { - auto pAction = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], Prop, m_OriginalValue, Value); + auto pAction = std::make_shared(m_pEditor, m_OriginalGroupIndex, m_OriginalLayerIndex, Prop, m_OriginalValue, Value); m_pEditor->m_EditorHistory.RecordAction(pAction); } @@ -564,7 +603,7 @@ int CLayerQuadsPropTracker::PropToValue(ELayerQuadsProp Prop) void CLayerSoundsPropTracker::OnEnd(ELayerSoundsProp Prop, int Value) { - auto pAction = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], Prop, m_OriginalValue, Value); + auto pAction = std::make_shared(m_pEditor, m_OriginalGroupIndex, m_OriginalLayerIndex, Prop, m_OriginalValue, Value); m_pEditor->m_EditorHistory.RecordAction(pAction); } @@ -579,7 +618,7 @@ int CLayerSoundsPropTracker::PropToValue(ELayerSoundsProp Prop) void CSoundSourcePropTracker::OnEnd(ESoundProp Prop, int Value) { - m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], m_pEditor->m_SelectedSource, Prop, m_OriginalValue, Value)); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_OriginalGroupIndex, m_OriginalLayerIndex, m_pEditor->m_SelectedSource, Prop, m_OriginalValue, Value)); } int CSoundSourcePropTracker::PropToValue(ESoundProp Prop) @@ -604,7 +643,7 @@ int CSoundSourcePropTracker::PropToValue(ESoundProp Prop) void CSoundSourceRectShapePropTracker::OnEnd(ERectangleShapeProp Prop, int Value) { - m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], m_pEditor->m_SelectedSource, Prop, m_OriginalValue, Value)); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_OriginalGroupIndex, m_OriginalLayerIndex, m_pEditor->m_SelectedSource, Prop, m_OriginalValue, Value)); } int CSoundSourceRectShapePropTracker::PropToValue(ERectangleShapeProp Prop) @@ -619,7 +658,7 @@ int CSoundSourceRectShapePropTracker::PropToValue(ERectangleShapeProp Prop) void CSoundSourceCircleShapePropTracker::OnEnd(ECircleShapeProp Prop, int Value) { - m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], m_pEditor->m_SelectedSource, Prop, m_OriginalValue, Value)); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_OriginalGroupIndex, m_OriginalLayerIndex, m_pEditor->m_SelectedSource, Prop, m_OriginalValue, Value)); } int CSoundSourceCircleShapePropTracker::PropToValue(ECircleShapeProp Prop) diff --git a/src/game/editor/editor_trackers.h b/src/game/editor/editor_trackers.h index 156096b6af..4767e732df 100644 --- a/src/game/editor/editor_trackers.h +++ b/src/game/editor/editor_trackers.h @@ -21,13 +21,13 @@ class CQuadEditTracker CQuadEditTracker(); ~CQuadEditTracker(); - void BeginQuadTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads); + void BeginQuadTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, int GroupIndex = -1, int LayerIndex = -1); void EndQuadTrack(); - void BeginQuadPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, EQuadProp Prop); + void BeginQuadPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, EQuadProp Prop, int GroupIndex = -1, int LayerIndex = -1); void EndQuadPropTrack(EQuadProp Prop); - void BeginQuadPointPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, int SelectedQuadPoints); + void BeginQuadPointPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, int SelectedQuadPoints, int GroupIndex = -1, int LayerIndex = -1); void AddQuadPointPropTrack(EQuadPointProp Prop); void EndQuadPointPropTrack(EQuadPointProp Prop); void EndQuadPointPropTrackAll(); @@ -46,23 +46,8 @@ class CQuadEditTracker std::vector m_vTrackedProps; std::map m_PreviousValues; std::map>> m_PreviousValuesPoint; -}; - -class CEditorPropTracker -{ -public: - CEditorPropTracker() = default; - - void BeginPropTrack(int Prop, int Value); - void StopPropTrack(int Prop, int Value); - inline void Reset() { m_TrackedProp = -1; } - - CEditor *m_pEditor; - int m_PreviousValue; - int m_CurrentValue; - -private: - int m_TrackedProp = -1; + int m_LayerIndex; + int m_GroupIndex; }; enum class EEnvelopeEditorOp @@ -77,6 +62,13 @@ enum class EEnvelopeEditorOp OP_SCALE }; +enum class ESoundSourceOp +{ + OP_NONE = 0, + OP_MOVE, + OP_CONTEXT_MENU, +}; + class CEnvelopeEditorOperationTracker { public: @@ -104,34 +96,81 @@ class CEnvelopeEditorOperationTracker void HandlePointDragEnd(bool Switch); }; +class CSoundSourceOperationTracker +{ +public: + CSoundSourceOperationTracker(CEditor *pEditor); + + void Begin(CSoundSource *pSource, ESoundSourceOp Operation, int LayerIndex); + void End(); + +private: + CEditor *m_pEditor; + CSoundSource *m_pSource; + ESoundSourceOp m_TrackedOp; + int m_LayerIndex; + + struct SData + { + CPoint m_OriginalPoint; + }; + SData m_Data; + + enum EState + { + STATE_BEGIN, + STATE_EDITING, + STATE_END + }; + void HandlePointMove(EState State); +}; + +struct SPropTrackerHelper +{ + static int GetDefaultGroupIndex(CEditor *pEditor); + static int GetDefaultLayerIndex(CEditor *pEditor); +}; + template class CPropTracker { public: CPropTracker(CEditor *pEditor) : - m_pEditor(pEditor), m_OriginalValue(0) {} + m_pEditor(pEditor), m_OriginalValue(0), m_pObject(nullptr), m_OriginalLayerIndex(-1), m_OriginalGroupIndex(-1), m_CurrentLayerIndex(-1), m_CurrentGroupIndex(-1), m_Tracking(false) {} CEditor *m_pEditor; - void Begin(T *pObject, E Prop, EEditState State) + void Begin(T *pObject, E Prop, EEditState State, int GroupIndex = -1, int LayerIndex = -1) { - if(Prop == static_cast(-1)) + if(m_Tracking || Prop == static_cast(-1)) return; m_pObject = pObject; + + m_OriginalGroupIndex = GroupIndex < 0 ? SPropTrackerHelper::GetDefaultGroupIndex(m_pEditor) : GroupIndex; + m_OriginalLayerIndex = LayerIndex < 0 ? SPropTrackerHelper::GetDefaultLayerIndex(m_pEditor) : LayerIndex; + m_CurrentGroupIndex = m_OriginalGroupIndex; + m_CurrentLayerIndex = m_OriginalLayerIndex; + int Value = PropToValue(Prop); if(StartChecker(Prop, State, Value)) { + m_Tracking = true; m_OriginalValue = Value; OnStart(Prop); } } - void End(E Prop, EEditState State) + void End(E Prop, EEditState State, int GroupIndex = -1, int LayerIndex = -1) { - if(Prop == static_cast(-1)) + if(!m_Tracking || Prop == static_cast(-1)) return; + + m_CurrentGroupIndex = GroupIndex < 0 ? SPropTrackerHelper::GetDefaultGroupIndex(m_pEditor) : GroupIndex; + m_CurrentLayerIndex = LayerIndex < 0 ? SPropTrackerHelper::GetDefaultLayerIndex(m_pEditor) : LayerIndex; + int Value = PropToValue(Prop); if(EndChecker(Prop, State, Value)) { + m_Tracking = false; OnEnd(Prop, Value); } } @@ -151,6 +190,11 @@ class CPropTracker int m_OriginalValue; T *m_pObject; + int m_OriginalLayerIndex; + int m_OriginalGroupIndex; + int m_CurrentLayerIndex; + int m_CurrentGroupIndex; + bool m_Tracking; }; class CLayerPropTracker : public CPropTracker diff --git a/src/game/editor/editor_ui.h b/src/game/editor/editor_ui.h new file mode 100644 index 0000000000..b3e2b54822 --- /dev/null +++ b/src/game/editor/editor_ui.h @@ -0,0 +1,22 @@ +#ifndef GAME_EDITOR_EDITOR_UI_H +#define GAME_EDITOR_EDITOR_UI_H + +#include + +struct SEditBoxDropdownContext +{ + bool m_Visible = false; + int m_Selected = -1; + CListBox m_ListBox; + bool m_ShortcutUsed = false; + bool m_DidBecomeVisible = false; + bool m_MousePressedInside = false; + bool m_ShouldHide = false; + int m_Width = 0; +}; + +namespace EditorFontSizes { +MAYBE_UNUSED static constexpr float MENU = 10.0f; +} + +#endif diff --git a/src/game/editor/enums.h b/src/game/editor/enums.h new file mode 100644 index 0000000000..d14679b95d --- /dev/null +++ b/src/game/editor/enums.h @@ -0,0 +1,36 @@ +#ifndef GAME_EDITOR_ENUMS_H +#define GAME_EDITOR_ENUMS_H + +constexpr const char *g_apGametileOpNames[] = { + "Air", + "Hookable", + "Death", + "Unhookable", + "Hookthrough", + "Freeze", + "Unfreeze", + "Deep Freeze", + "Deep Unfreeze", + "Blue Check-Tele", + "Red Check-Tele", + "Live Freeze", + "Live Unfreeze", +}; +enum class EGameTileOp +{ + AIR, + HOOKABLE, + DEATH, + UNHOOKABLE, + HOOKTHROUGH, + FREEZE, + UNFREEZE, + DEEP_FREEZE, + DEEP_UNFREEZE, + BLUE_CHECK_TELE, + RED_CHECK_TELE, + LIVE_FREEZE, + LIVE_UNFREEZE, +}; + +#endif diff --git a/src/game/editor/explanations.cpp b/src/game/editor/explanations.cpp index a25404ab0e..ad52b5d26e 100644 --- a/src/game/editor/explanations.cpp +++ b/src/game/editor/explanations.cpp @@ -14,7 +14,7 @@ const char *CEditor::ExplainDDNet(int Tile, int Layer) return "HOOKABLE: It's possible to hook and collide with it."; break; case TILE_DEATH: - if(Layer == LAYER_GAME) + if(Layer == LAYER_GAME || Layer == LAYER_FRONT) return "KILL: Kills the tee."; break; case TILE_NOHOOK: diff --git a/src/game/editor/layer_selector.cpp b/src/game/editor/layer_selector.cpp new file mode 100644 index 0000000000..4db70b67c8 --- /dev/null +++ b/src/game/editor/layer_selector.cpp @@ -0,0 +1,52 @@ +#include + +#include "editor.h" + +#include "layer_selector.h" + +void CLayerSelector::OnInit(CEditor *pEditor) +{ + CEditorComponent::OnInit(pEditor); + + m_SelectionOffset = 0; +} + +bool CLayerSelector::SelectByTile() +{ + // ctrl+rightclick a map index to select the layer that has a tile there + if(Ui()->HotItem() != &Editor()->m_MapEditorId) + return false; + if(!Input()->ModifierIsPressed() || !Ui()->MouseButtonClicked(1)) + return false; + if(!g_Config.m_EdLayerSelector) + return false; + + int MatchedGroup = -1; + int MatchedLayer = -1; + int Matches = 0; + bool IsFound = false; + for(auto HoverTile : Editor()->HoverTiles()) + { + if(MatchedGroup == -1) + { + MatchedGroup = HoverTile.m_Group; + MatchedLayer = HoverTile.m_Layer; + } + if(++Matches > m_SelectionOffset) + { + m_SelectionOffset++; + MatchedGroup = HoverTile.m_Group; + MatchedLayer = HoverTile.m_Layer; + IsFound = true; + break; + } + } + if(MatchedGroup != -1 && MatchedLayer != -1) + { + if(!IsFound) + m_SelectionOffset = 1; + Editor()->SelectLayer(MatchedLayer, MatchedGroup); + return true; + } + return false; +} diff --git a/src/game/editor/layer_selector.h b/src/game/editor/layer_selector.h new file mode 100644 index 0000000000..1ea088ac24 --- /dev/null +++ b/src/game/editor/layer_selector.h @@ -0,0 +1,15 @@ +#ifndef GAME_EDITOR_LAYER_SELECTOR_H +#define GAME_EDITOR_LAYER_SELECTOR_H + +#include "component.h" + +class CLayerSelector : public CEditorComponent +{ + int m_SelectionOffset; + +public: + void OnInit(CEditor *pEditor) override; + bool SelectByTile(); +}; + +#endif diff --git a/src/game/editor/map_grid.cpp b/src/game/editor/map_grid.cpp index e241642aa6..6b090610fa 100644 --- a/src/game/editor/map_grid.cpp +++ b/src/game/editor/map_grid.cpp @@ -4,6 +4,9 @@ #include "editor.h" +static constexpr int MIN_GRID_FACTOR = 1; +static constexpr int MAX_GRID_FACTOR = 15; + void CMapGrid::OnReset() { m_GridActive = false; @@ -23,7 +26,7 @@ void CMapGrid::OnRender(CUIRect View) float aGroupPoints[4]; pGroup->Mapping(aGroupPoints); - const CUIRect *pScreen = UI()->Screen(); + const CUIRect *pScreen = Ui()->Screen(); int LineDistance = GridLineDistance(); @@ -60,7 +63,11 @@ void CMapGrid::OnRender(CUIRect View) int CMapGrid::GridLineDistance() const { - if(Editor()->MapView()->Zoom()->GetValue() <= 100.0f) + if(Editor()->MapView()->Zoom()->GetValue() <= 10.0f) + return 4; + else if(Editor()->MapView()->Zoom()->GetValue() <= 50.0f) + return 8; + else if(Editor()->MapView()->Zoom()->GetValue() <= 100.0f) return 16; else if(Editor()->MapView()->Zoom()->GetValue() <= 250.0f) return 32; @@ -96,19 +103,47 @@ int CMapGrid::Factor() const return m_GridFactor; } -void CMapGrid::ResetFactor() +void CMapGrid::SetFactor(int Factor) { - m_GridFactor = 1; + m_GridFactor = clamp(Factor, MIN_GRID_FACTOR, MAX_GRID_FACTOR); } -void CMapGrid::IncreaseFactor() +void CMapGrid::DoSettingsPopup(vec2 Position) { - if(m_GridFactor < 15) - m_GridFactor++; + Ui()->DoPopupMenu(&m_PopupGridSettingsId, Position.x, Position.y, 120.0f, 37.0f, this, PopupGridSettings); } -void CMapGrid::DecreaseFactor() +CUi::EPopupMenuFunctionResult CMapGrid::PopupGridSettings(void *pContext, CUIRect View, bool Active) { - if(m_GridFactor > 1) - m_GridFactor--; + CMapGrid *pMapGrid = static_cast(pContext); + + enum + { + PROP_SIZE = 0, + NUM_PROPS, + }; + CProperty aProps[] = { + {"Size", pMapGrid->Factor(), PROPTYPE_INT, MIN_GRID_FACTOR, MAX_GRID_FACTOR}, + {nullptr}, + }; + + static int s_aIds[NUM_PROPS]; + int NewVal; + int Prop = pMapGrid->Editor()->DoProperties(&View, aProps, s_aIds, &NewVal); + + if(Prop == PROP_SIZE) + { + pMapGrid->SetFactor(NewVal); + } + + CUIRect Button; + View.HSplitBottom(12.0f, &View, &Button); + + static char s_DefaultButton; + if(pMapGrid->Editor()->DoButton_Ex(&s_DefaultButton, "Default", 0, &Button, 0, "Normal grid size", IGraphics::CORNER_ALL)) + { + pMapGrid->SetFactor(1); + } + + return CUi::POPUP_KEEP_OPEN; } diff --git a/src/game/editor/map_grid.h b/src/game/editor/map_grid.h index 3a6e26a3be..0a4110cb51 100644 --- a/src/game/editor/map_grid.h +++ b/src/game/editor/map_grid.h @@ -3,6 +3,8 @@ #include "component.h" +#include + class CMapGrid : public CEditorComponent { public: @@ -20,13 +22,16 @@ class CMapGrid : public CEditorComponent void Toggle(); int Factor() const; - void ResetFactor(); - void IncreaseFactor(); - void DecreaseFactor(); + void SetFactor(int Factor); + + void DoSettingsPopup(vec2 Position); private: bool m_GridActive; int m_GridFactor; + + SPopupMenuId m_PopupGridSettingsId; + static CUi::EPopupMenuFunctionResult PopupGridSettings(void *pContext, CUIRect View, bool Active); }; #endif diff --git a/src/game/editor/map_view.cpp b/src/game/editor/map_view.cpp index 99337b8f76..355eb8b91a 100644 --- a/src/game/editor/map_view.cpp +++ b/src/game/editor/map_view.cpp @@ -8,9 +8,9 @@ #include "editor.h" -void CMapView::Init(CEditor *pEditor) +void CMapView::OnInit(CEditor *pEditor) { - CEditorComponent::Init(pEditor); + CEditorComponent::OnInit(pEditor); RegisterSubComponent(m_MapGrid); RegisterSubComponent(m_ProofMode); InitSubComponents(); @@ -19,7 +19,7 @@ void CMapView::Init(CEditor *pEditor) void CMapView::OnReset() { m_Zoom = CSmoothValue(200.0f, 10.0f, 2000.0f); - m_Zoom.Init(Editor()); + m_Zoom.OnInit(Editor()); m_WorldZoom = 1.0f; SetWorldOffset({0, 0}); @@ -152,8 +152,8 @@ void CMapView::ZoomMouseTarget(float ZoomFactor) float WorldWidth = aPoints[2] - aPoints[0]; float WorldHeight = aPoints[3] - aPoints[1]; - float Mwx = aPoints[0] + WorldWidth * (UI()->MouseX() / UI()->Screen()->w); - float Mwy = aPoints[1] + WorldHeight * (UI()->MouseY() / UI()->Screen()->h); + float Mwx = aPoints[0] + WorldWidth * (Ui()->MouseX() / Ui()->Screen()->w); + float Mwy = aPoints[1] + WorldHeight * (Ui()->MouseY() / Ui()->Screen()->h); // adjust camera OffsetWorld((vec2(Mwx, Mwy) - GetWorldOffset()) * (1.0f - ZoomFactor)); diff --git a/src/game/editor/map_view.h b/src/game/editor/map_view.h index 5aa86254d6..ca99ca836d 100644 --- a/src/game/editor/map_view.h +++ b/src/game/editor/map_view.h @@ -13,7 +13,7 @@ class CLayerGroup; class CMapView : public CEditorComponent { public: - void Init(CEditor *pEditor) override; + void OnInit(CEditor *pEditor) override; void OnReset() override; void OnMapLoad() override; diff --git a/src/game/editor/mapitems/envelope.cpp b/src/game/editor/mapitems/envelope.cpp index 5f0524eb2a..174ee96cc2 100644 --- a/src/game/editor/mapitems/envelope.cpp +++ b/src/game/editor/mapitems/envelope.cpp @@ -98,10 +98,10 @@ std::pair CEnvelope::GetValueRange(int ChannelMask) return {Bottom, Top}; } -int CEnvelope::Eval(float Time, ColorRGBA &Color) +void CEnvelope::Eval(float Time, ColorRGBA &Result, size_t Channels) { - CRenderTools::RenderEvalEnvelope(&m_PointsAccess, GetChannels(), std::chrono::nanoseconds((int64_t)((double)Time * (double)std::chrono::nanoseconds(1s).count())), Color); - return GetChannels(); + Channels = minimum(Channels, GetChannels(), CEnvPoint::MAX_CHANNELS); + CRenderTools::RenderEvalEnvelope(&m_PointsAccess, std::chrono::nanoseconds((int64_t)((double)Time * (double)std::chrono::nanoseconds(1s).count())), Result, Channels); } void CEnvelope::AddPoint(int Time, int v0, int v1, int v2, int v3) diff --git a/src/game/editor/mapitems/envelope.h b/src/game/editor/mapitems/envelope.h index 2b25c9f362..b3a00a258f 100644 --- a/src/game/editor/mapitems/envelope.h +++ b/src/game/editor/mapitems/envelope.h @@ -21,7 +21,7 @@ class CEnvelope explicit CEnvelope(int NumChannels); std::pair GetValueRange(int ChannelMask); - int Eval(float Time, ColorRGBA &Color); + void Eval(float Time, ColorRGBA &Result, size_t Channels); void AddPoint(int Time, int v0, int v1 = 0, int v2 = 0, int v3 = 0); float EndTime() const; int GetChannels() const; diff --git a/src/game/editor/mapitems/image.cpp b/src/game/editor/mapitems/image.cpp index 514a1cadf7..0266ccb99d 100644 --- a/src/game/editor/mapitems/image.cpp +++ b/src/game/editor/mapitems/image.cpp @@ -5,7 +5,7 @@ CEditorImage::CEditorImage(CEditor *pEditor) : m_AutoMapper(pEditor) { - Init(pEditor); + OnInit(pEditor); m_Texture.Invalidate(); } @@ -16,9 +16,9 @@ CEditorImage::~CEditorImage() m_pData = nullptr; } -void CEditorImage::Init(CEditor *pEditor) +void CEditorImage::OnInit(CEditor *pEditor) { - CEditorComponent::Init(pEditor); + CEditorComponent::OnInit(pEditor); RegisterSubComponent(m_AutoMapper); InitSubComponents(); } @@ -27,22 +27,20 @@ void CEditorImage::AnalyseTileFlags() { mem_zero(m_aTileFlags, sizeof(m_aTileFlags)); - int tw = m_Width / 16; // tilesizes - int th = m_Height / 16; + size_t tw = m_Width / 16; // tilesizes + size_t th = m_Height / 16; if(tw == th && m_Format == CImageInfo::FORMAT_RGBA) { - unsigned char *pPixelData = (unsigned char *)m_pData; - - int TileID = 0; - for(int ty = 0; ty < 16; ty++) - for(int tx = 0; tx < 16; tx++, TileID++) + int TileId = 0; + for(size_t ty = 0; ty < 16; ty++) + for(size_t tx = 0; tx < 16; tx++, TileId++) { bool Opaque = true; - for(int x = 0; x < tw; x++) - for(int y = 0; y < th; y++) + for(size_t x = 0; x < tw; x++) + for(size_t y = 0; y < th; y++) { - int p = (ty * tw + y) * m_Width + tx * tw + x; - if(pPixelData[p * 4 + 3] < 250) + size_t p = (ty * tw + y) * m_Width + tx * tw + x; + if(m_pData[p * 4 + 3] < 250) { Opaque = false; break; @@ -50,7 +48,7 @@ void CEditorImage::AnalyseTileFlags() } if(Opaque) - m_aTileFlags[TileID] |= TILEFLAG_OPAQUE; + m_aTileFlags[TileId] |= TILEFLAG_OPAQUE; } } } diff --git a/src/game/editor/mapitems/image.h b/src/game/editor/mapitems/image.h index cffbd6c7e7..7e2142f630 100644 --- a/src/game/editor/mapitems/image.h +++ b/src/game/editor/mapitems/image.h @@ -12,7 +12,7 @@ class CEditorImage : public CImageInfo, public CEditorComponent explicit CEditorImage(CEditor *pEditor); ~CEditorImage(); - void Init(CEditor *pEditor) override; + void OnInit(CEditor *pEditor) override; void AnalyseTileFlags(); IGraphics::CTextureHandle m_Texture; diff --git a/src/game/editor/mapitems/layer.h b/src/game/editor/mapitems/layer.h index 238c9e4ccd..3f62e04f94 100644 --- a/src/game/editor/mapitems/layer.h +++ b/src/game/editor/mapitems/layer.h @@ -46,8 +46,8 @@ class CLayer virtual void BrushSelecting(CUIRect Rect) {} virtual int BrushGrab(std::shared_ptr pBrush, CUIRect Rect) { return 0; } virtual void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) {} - virtual void BrushDraw(std::shared_ptr pBrush, float x, float y) {} - virtual void BrushPlace(std::shared_ptr pBrush, float x, float y) {} + virtual void BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) {} + virtual void BrushPlace(std::shared_ptr pBrush, vec2 WorldPos) {} virtual void BrushFlipX() {} virtual void BrushFlipY() {} virtual void BrushRotate(float Amount) {} @@ -55,7 +55,7 @@ class CLayer virtual bool IsEntitiesLayer() const { return false; } virtual void Render(bool Tileset = false) {} - virtual CUI::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) { return CUI::POPUP_KEEP_OPEN; } + virtual CUi::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) { return CUi::POPUP_KEEP_OPEN; } virtual void ModifyImageIndex(FIndexModifyFunction pfnFunc) {} virtual void ModifyEnvelopeIndex(FIndexModifyFunction pfnFunc) {} diff --git a/src/game/editor/mapitems/layer_front.cpp b/src/game/editor/mapitems/layer_front.cpp index f046299c4b..b553ead212 100644 --- a/src/game/editor/mapitems/layer_front.cpp +++ b/src/game/editor/mapitems/layer_front.cpp @@ -27,12 +27,7 @@ void CLayerFront::SetTile(int x, int y, CTile Tile) { CTile air = {TILE_AIR}; CLayerTiles::SetTile(x, y, air); - if(!m_pEditor->m_PreventUnusedTilesWasWarned) - { - m_pEditor->m_PopupEventType = CEditor::POPEVENT_PREVENTUNUSEDTILES; - m_pEditor->m_PopupEventActivated = true; - m_pEditor->m_PreventUnusedTilesWasWarned = true; - } + ShowPreventUnusedTilesWarning(); } } diff --git a/src/game/editor/mapitems/layer_game.cpp b/src/game/editor/mapitems/layer_game.cpp index eb0ee541fc..14dfd27e14 100644 --- a/src/game/editor/mapitems/layer_game.cpp +++ b/src/game/editor/mapitems/layer_game.cpp @@ -17,8 +17,8 @@ CTile CLayerGame::GetTile(int x, int y) { if(m_pEditor->m_Map.m_pFrontLayer && m_pEditor->m_Map.m_pFrontLayer->GetTile(x, y).m_Index == TILE_THROUGH_CUT) { - CTile through_cut = {TILE_THROUGH_CUT}; - return through_cut; + CTile ThroughCut = {TILE_THROUGH_CUT}; + return ThroughCut; } else { @@ -38,8 +38,8 @@ void CLayerGame::SetTile(int x, int y, CTile Tile) } CTile nohook = {TILE_NOHOOK}; CLayerTiles::SetTile(x, y, nohook); - CTile through_cut = {TILE_THROUGH_CUT}; - m_pEditor->m_Map.m_pFrontLayer->CLayerTiles::SetTile(x, y, through_cut); // NOLINT(bugprone-parent-virtual-call) + CTile ThroughCut = {TILE_THROUGH_CUT}; + m_pEditor->m_Map.m_pFrontLayer->CLayerTiles::SetTile(x, y, ThroughCut); // NOLINT(bugprone-parent-virtual-call) } else { @@ -56,19 +56,14 @@ void CLayerGame::SetTile(int x, int y, CTile Tile) { CTile air = {TILE_AIR}; CLayerTiles::SetTile(x, y, air); - if(!m_pEditor->m_PreventUnusedTilesWasWarned) - { - m_pEditor->m_PopupEventType = CEditor::POPEVENT_PREVENTUNUSEDTILES; - m_pEditor->m_PopupEventActivated = true; - m_pEditor->m_PreventUnusedTilesWasWarned = true; - } + ShowPreventUnusedTilesWarning(); } } } -CUI::EPopupMenuFunctionResult CLayerGame::RenderProperties(CUIRect *pToolbox) +CUi::EPopupMenuFunctionResult CLayerGame::RenderProperties(CUIRect *pToolbox) { - const CUI::EPopupMenuFunctionResult Result = CLayerTiles::RenderProperties(pToolbox); + const CUi::EPopupMenuFunctionResult Result = CLayerTiles::RenderProperties(pToolbox); m_Image = -1; return Result; } diff --git a/src/game/editor/mapitems/layer_game.h b/src/game/editor/mapitems/layer_game.h index ec6f1d78fa..4fd7b71b86 100644 --- a/src/game/editor/mapitems/layer_game.h +++ b/src/game/editor/mapitems/layer_game.h @@ -13,7 +13,7 @@ class CLayerGame : public CLayerTiles void SetTile(int x, int y, CTile Tile) override; const char *TypeName() const override; - CUI::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; + CUi::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; }; #endif diff --git a/src/game/editor/mapitems/layer_quads.cpp b/src/game/editor/mapitems/layer_quads.cpp index 3f69708fd5..b523f815d2 100644 --- a/src/game/editor/mapitems/layer_quads.cpp +++ b/src/game/editor/mapitems/layer_quads.cpp @@ -138,7 +138,7 @@ int CLayerQuads::BrushGrab(std::shared_ptr pBrush, CUIRect Rect) return pGrabbed->m_vQuads.empty() ? 0 : 1; } -void CLayerQuads::BrushPlace(std::shared_ptr pBrush, float wx, float wy) +void CLayerQuads::BrushPlace(std::shared_ptr pBrush, vec2 WorldPos) { std::shared_ptr pQuadLayer = std::static_pointer_cast(pBrush); std::vector vAddedQuads; @@ -148,8 +148,8 @@ void CLayerQuads::BrushPlace(std::shared_ptr pBrush, float wx, float wy) for(auto &Point : n.m_aPoints) { - Point.x += f2fx(wx); - Point.y += f2fx(wy); + Point.x += f2fx(WorldPos.x); + Point.y += f2fx(WorldPos.y); } m_vQuads.push_back(n); @@ -221,7 +221,7 @@ void CLayerQuads::GetSize(float *pWidth, float *pHeight) } } -CUI::EPopupMenuFunctionResult CLayerQuads::RenderProperties(CUIRect *pToolBox) +CUi::EPopupMenuFunctionResult CLayerQuads::RenderProperties(CUIRect *pToolBox) { CProperty aProps[] = { {"Image", m_Image, PROPTYPE_IMAGE, -1, 0}, @@ -231,7 +231,7 @@ CUI::EPopupMenuFunctionResult CLayerQuads::RenderProperties(CUIRect *pToolBox) static int s_aIds[(int)ELayerQuadsProp::NUM_PROPS] = {0}; int NewVal = 0; auto [State, Prop] = m_pEditor->DoPropertiesWithState(pToolBox, aProps, s_aIds, &NewVal); - if(Prop != ELayerQuadsProp::PROP_NONE) + if(Prop != ELayerQuadsProp::PROP_NONE && (State == EEditState::END || State == EEditState::ONE_GO)) { m_pEditor->m_Map.OnModify(); } @@ -249,7 +249,7 @@ CUI::EPopupMenuFunctionResult CLayerQuads::RenderProperties(CUIRect *pToolBox) s_Tracker.End(Prop, State); - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } void CLayerQuads::ModifyImageIndex(FIndexModifyFunction Func) diff --git a/src/game/editor/mapitems/layer_quads.h b/src/game/editor/mapitems/layer_quads.h index dd826b385e..e0026aaff0 100644 --- a/src/game/editor/mapitems/layer_quads.h +++ b/src/game/editor/mapitems/layer_quads.h @@ -16,12 +16,12 @@ class CLayerQuads : public CLayer void BrushSelecting(CUIRect Rect) override; int BrushGrab(std::shared_ptr pBrush, CUIRect Rect) override; - void BrushPlace(std::shared_ptr pBrush, float wx, float wy) override; + void BrushPlace(std::shared_ptr pBrush, vec2 WorldPos) override; void BrushFlipX() override; void BrushFlipY() override; void BrushRotate(float Amount) override; - CUI::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; + CUi::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; void ModifyImageIndex(FIndexModifyFunction pfnFunc) override; void ModifyEnvelopeIndex(FIndexModifyFunction pfnFunc) override; diff --git a/src/game/editor/mapitems/layer_sounds.cpp b/src/game/editor/mapitems/layer_sounds.cpp index ff8250617a..d25de9a2c6 100644 --- a/src/game/editor/mapitems/layer_sounds.cpp +++ b/src/game/editor/mapitems/layer_sounds.cpp @@ -34,41 +34,31 @@ void CLayerSounds::Render(bool Tileset) Graphics()->SetColor(0.6f, 0.8f, 1.0f, 0.4f); for(const auto &Source : m_vSources) { - float OffsetX = 0; - float OffsetY = 0; - - if(Source.m_PosEnv >= 0) - { - ColorRGBA Channels; - CEditor::EnvelopeEval(Source.m_PosEnvOffset, Source.m_PosEnv, Channels, m_pEditor); - OffsetX = Channels.r; - OffsetY = Channels.g; - } + ColorRGBA Offset = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + CEditor::EnvelopeEval(Source.m_PosEnvOffset, Source.m_PosEnv, Offset, 2, m_pEditor); + const vec2 Position = vec2(fx2f(Source.m_Position.x) + Offset.r, fx2f(Source.m_Position.y) + Offset.g); + const float Falloff = Source.m_Falloff / 255.0f; switch(Source.m_Shape.m_Type) { case CSoundShape::SHAPE_CIRCLE: { - m_pEditor->Graphics()->DrawCircle(fx2f(Source.m_Position.x) + OffsetX, fx2f(Source.m_Position.y) + OffsetY, - Source.m_Shape.m_Circle.m_Radius, 32); - - float Falloff = ((float)Source.m_Falloff / 255.0f); + m_pEditor->Graphics()->DrawCircle(Position.x, Position.y, Source.m_Shape.m_Circle.m_Radius, 32); if(Falloff > 0.0f) - m_pEditor->Graphics()->DrawCircle(fx2f(Source.m_Position.x) + OffsetX, fx2f(Source.m_Position.y) + OffsetY, - Source.m_Shape.m_Circle.m_Radius * Falloff, 32); + { + m_pEditor->Graphics()->DrawCircle(Position.x, Position.y, Source.m_Shape.m_Circle.m_Radius * Falloff, 32); + } break; } case CSoundShape::SHAPE_RECTANGLE: { - float Width = fx2f(Source.m_Shape.m_Rectangle.m_Width); - float Height = fx2f(Source.m_Shape.m_Rectangle.m_Height); - m_pEditor->Graphics()->DrawRectExt(fx2f(Source.m_Position.x) + OffsetX - Width / 2, fx2f(Source.m_Position.y) + OffsetY - Height / 2, - Width, Height, 0.0f, IGraphics::CORNER_NONE); - - float Falloff = ((float)Source.m_Falloff / 255.0f); + const float Width = fx2f(Source.m_Shape.m_Rectangle.m_Width); + const float Height = fx2f(Source.m_Shape.m_Rectangle.m_Height); + m_pEditor->Graphics()->DrawRectExt(Position.x - Width / 2, Position.y - Height / 2, Width, Height, 0.0f, IGraphics::CORNER_NONE); if(Falloff > 0.0f) - m_pEditor->Graphics()->DrawRectExt(fx2f(Source.m_Position.x) + OffsetX - Falloff * Width / 2, fx2f(Source.m_Position.y) + OffsetY - Falloff * Height / 2, - Width * Falloff, Height * Falloff, 0.0f, IGraphics::CORNER_NONE); + { + m_pEditor->Graphics()->DrawRectExt(Position.x - Falloff * Width / 2, Position.y - Falloff * Height / 2, Width * Falloff, Height * Falloff, 0.0f, IGraphics::CORNER_NONE); + } break; } } @@ -84,18 +74,10 @@ void CLayerSounds::Render(bool Tileset) m_pEditor->RenderTools()->SelectSprite(SPRITE_AUDIO_SOURCE); for(const auto &Source : m_vSources) { - float OffsetX = 0; - float OffsetY = 0; - - if(Source.m_PosEnv >= 0) - { - ColorRGBA Channels; - CEditor::EnvelopeEval(Source.m_PosEnvOffset, Source.m_PosEnv, Channels, m_pEditor); - OffsetX = Channels.r; - OffsetY = Channels.g; - } - - m_pEditor->RenderTools()->DrawSprite(fx2f(Source.m_Position.x) + OffsetX, fx2f(Source.m_Position.y) + OffsetY, m_pEditor->MapView()->ScaleLength(s_SourceVisualSize)); + ColorRGBA Offset = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + CEditor::EnvelopeEval(Source.m_PosEnvOffset, Source.m_PosEnv, Offset, 2, m_pEditor); + const vec2 Position = vec2(fx2f(Source.m_Position.x) + Offset.r, fx2f(Source.m_Position.y) + Offset.g); + m_pEditor->RenderTools()->DrawSprite(Position.x, Position.y, m_pEditor->MapView()->ScaleLength(s_SourceVisualSize)); } Graphics()->QuadsEnd(); @@ -168,7 +150,7 @@ int CLayerSounds::BrushGrab(std::shared_ptr pBrush, CUIRect Rect) return pGrabbed->m_vSources.empty() ? 0 : 1; } -void CLayerSounds::BrushPlace(std::shared_ptr pBrush, float wx, float wy) +void CLayerSounds::BrushPlace(std::shared_ptr pBrush, vec2 WorldPos) { std::shared_ptr pSoundLayer = std::static_pointer_cast(pBrush); std::vector vAddedSources; @@ -176,8 +158,8 @@ void CLayerSounds::BrushPlace(std::shared_ptr pBrush, float wx, float wy { CSoundSource n = Source; - n.m_Position.x += f2fx(wx); - n.m_Position.y += f2fx(wy); + n.m_Position.x += f2fx(WorldPos.x); + n.m_Position.y += f2fx(WorldPos.y); m_vSources.push_back(n); vAddedSources.push_back(n); @@ -186,7 +168,7 @@ void CLayerSounds::BrushPlace(std::shared_ptr pBrush, float wx, float wy m_pEditor->m_Map.OnModify(); } -CUI::EPopupMenuFunctionResult CLayerSounds::RenderProperties(CUIRect *pToolBox) +CUi::EPopupMenuFunctionResult CLayerSounds::RenderProperties(CUIRect *pToolBox) { CProperty aProps[] = { {"Sound", m_Sound, PROPTYPE_SOUND, -1, 0}, @@ -196,7 +178,7 @@ CUI::EPopupMenuFunctionResult CLayerSounds::RenderProperties(CUIRect *pToolBox) static int s_aIds[(int)ELayerSoundsProp::NUM_PROPS] = {0}; int NewVal = 0; auto [State, Prop] = m_pEditor->DoPropertiesWithState(pToolBox, aProps, s_aIds, &NewVal); - if(Prop != ELayerSoundsProp::PROP_NONE) + if(Prop != ELayerSoundsProp::PROP_NONE && (State == EEditState::END || State == EEditState::ONE_GO)) { m_pEditor->m_Map.OnModify(); } @@ -214,7 +196,7 @@ CUI::EPopupMenuFunctionResult CLayerSounds::RenderProperties(CUIRect *pToolBox) s_Tracker.End(Prop, State); - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } void CLayerSounds::ModifySoundIndex(FIndexModifyFunction Func) diff --git a/src/game/editor/mapitems/layer_sounds.h b/src/game/editor/mapitems/layer_sounds.h index bc71b13642..5efe11d515 100644 --- a/src/game/editor/mapitems/layer_sounds.h +++ b/src/game/editor/mapitems/layer_sounds.h @@ -15,9 +15,9 @@ class CLayerSounds : public CLayer void BrushSelecting(CUIRect Rect) override; int BrushGrab(std::shared_ptr pBrush, CUIRect Rect) override; - void BrushPlace(std::shared_ptr pBrush, float wx, float wy) override; + void BrushPlace(std::shared_ptr pBrush, vec2 WorldPos) override; - CUI::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; + CUi::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; void ModifyEnvelopeIndex(FIndexModifyFunction pfnFunc) override; void ModifySoundIndex(FIndexModifyFunction pfnFunc) override; diff --git a/src/game/editor/mapitems/layer_speedup.cpp b/src/game/editor/mapitems/layer_speedup.cpp index 02795a4316..e963da8e81 100644 --- a/src/game/editor/mapitems/layer_speedup.cpp +++ b/src/game/editor/mapitems/layer_speedup.cpp @@ -65,14 +65,14 @@ bool CLayerSpeedup::IsEmpty(const std::shared_ptr &pLayer) return true; } -void CLayerSpeedup::BrushDraw(std::shared_ptr pBrush, float wx, float wy) +void CLayerSpeedup::BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) { if(m_Readonly) return; std::shared_ptr pSpeedupLayer = std::static_pointer_cast(pBrush); - int sx = ConvertX(wx); - int sy = ConvertY(wy); + int sx = ConvertX(WorldPos.x); + int sy = ConvertY(WorldPos.y); if(str_comp(pSpeedupLayer->m_aFileName, m_pEditor->m_aFileName)) { m_pEditor->m_SpeedupAngle = pSpeedupLayer->m_SpeedupAngle; @@ -144,6 +144,9 @@ void CLayerSpeedup::BrushDraw(std::shared_ptr pBrush, float wx, float wy m_pSpeedupTile[Index].m_Angle = 0; m_pSpeedupTile[Index].m_Type = 0; m_pTiles[Index].m_Index = 0; + + if(pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index != TILE_AIR) + ShowPreventUnusedTilesWarning(); } SSpeedupTileStateChange::SData Current{ @@ -258,6 +261,9 @@ void CLayerSpeedup::FillSelection(bool Empty, std::shared_ptr pBrush, CU m_pTiles[TgtIndex].m_Index = 0; m_pSpeedupTile[TgtIndex].m_Force = 0; m_pSpeedupTile[TgtIndex].m_Angle = 0; + + if(!Empty) + ShowPreventUnusedTilesWarning(); } else { diff --git a/src/game/editor/mapitems/layer_speedup.h b/src/game/editor/mapitems/layer_speedup.h index 990f488e7d..5b298d5288 100644 --- a/src/game/editor/mapitems/layer_speedup.h +++ b/src/game/editor/mapitems/layer_speedup.h @@ -31,7 +31,7 @@ class CLayerSpeedup : public CLayerTiles void Resize(int NewW, int NewH) override; void Shift(int Direction) override; bool IsEmpty(const std::shared_ptr &pLayer) override; - void BrushDraw(std::shared_ptr pBrush, float wx, float wy) override; + void BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) override; void BrushFlipX() override; void BrushFlipY() override; void BrushRotate(float Amount) override; diff --git a/src/game/editor/mapitems/layer_switch.cpp b/src/game/editor/mapitems/layer_switch.cpp index 6d59903b6a..25e1bc35ee 100644 --- a/src/game/editor/mapitems/layer_switch.cpp +++ b/src/game/editor/mapitems/layer_switch.cpp @@ -10,6 +10,8 @@ CLayerSwitch::CLayerSwitch(CEditor *pEditor, int w, int h) : m_pSwitchTile = new CSwitchTile[w * h]; mem_zero(m_pSwitchTile, (size_t)w * h * sizeof(CSwitchTile)); + m_GotoSwitchLastPos = ivec2(-1, -1); + m_GotoSwitchOffset = 0; } CLayerSwitch::CLayerSwitch(const CLayerSwitch &Other) : @@ -65,14 +67,14 @@ bool CLayerSwitch::IsEmpty(const std::shared_ptr &pLayer) return true; } -void CLayerSwitch::BrushDraw(std::shared_ptr pBrush, float wx, float wy) +void CLayerSwitch::BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) { if(m_Readonly) return; std::shared_ptr pSwitchLayer = std::static_pointer_cast(pBrush); - int sx = ConvertX(wx); - int sy = ConvertY(wy); + int sx = ConvertX(WorldPos.x); + int sy = ConvertY(WorldPos.y); if(str_comp(pSwitchLayer->m_aFileName, m_pEditor->m_aFileName)) { m_pEditor->m_SwitchNum = pSwitchLayer->m_SwitchNumber; @@ -144,6 +146,9 @@ void CLayerSwitch::BrushDraw(std::shared_ptr pBrush, float wx, float wy) m_pSwitchTile[Index].m_Flags = 0; m_pSwitchTile[Index].m_Delay = 0; m_pTiles[Index].m_Index = 0; + + if(pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index != TILE_AIR) + ShowPreventUnusedTilesWarning(); } SSwitchTileStateChange::SData Current{ @@ -265,6 +270,9 @@ void CLayerSwitch::FillSelection(bool Empty, std::shared_ptr pBrush, CUI m_pSwitchTile[TgtIndex].m_Type = 0; m_pSwitchTile[TgtIndex].m_Number = 0; m_pSwitchTile[TgtIndex].m_Delay = 0; + + if(!Empty) + ShowPreventUnusedTilesWarning(); } else { @@ -322,6 +330,58 @@ bool CLayerSwitch::ContainsElementWithId(int Id) return false; } +void CLayerSwitch::GetPos(int Number, int Offset, ivec2 &SwitchPos) +{ + int Match = -1; + ivec2 MatchPos = ivec2(-1, -1); + SwitchPos = ivec2(-1, -1); + + auto FindTile = [this, &Match, &MatchPos, &Number, &Offset]() { + for(int x = 0; x < m_Width; x++) + { + for(int y = 0; y < m_Height; y++) + { + int i = y * m_Width + x; + int Switch = m_pSwitchTile[i].m_Number; + if(Number == Switch) + { + Match++; + if(Offset != -1) + { + if(Match == Offset) + { + MatchPos = ivec2(x, y); + m_GotoSwitchOffset = Match; + return; + } + continue; + } + MatchPos = ivec2(x, y); + if(m_GotoSwitchLastPos != ivec2(-1, -1)) + { + if(distance(m_GotoSwitchLastPos, MatchPos) < 10.0f) + { + m_GotoSwitchOffset++; + continue; + } + } + m_GotoSwitchLastPos = MatchPos; + if(Match == m_GotoSwitchOffset) + return; + } + } + } + }; + FindTile(); + + if(MatchPos == ivec2(-1, -1)) + return; + if(Match < m_GotoSwitchOffset) + m_GotoSwitchOffset = -1; + SwitchPos = MatchPos; + m_GotoSwitchOffset++; +} + std::shared_ptr CLayerSwitch::Duplicate() const { return std::make_shared(*this); diff --git a/src/game/editor/mapitems/layer_switch.h b/src/game/editor/mapitems/layer_switch.h index 7a8aae79b2..810cbf9efe 100644 --- a/src/game/editor/mapitems/layer_switch.h +++ b/src/game/editor/mapitems/layer_switch.h @@ -30,12 +30,16 @@ class CLayerSwitch : public CLayerTiles void Resize(int NewW, int NewH) override; void Shift(int Direction) override; bool IsEmpty(const std::shared_ptr &pLayer) override; - void BrushDraw(std::shared_ptr pBrush, float wx, float wy) override; + void BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) override; void BrushFlipX() override; void BrushFlipY() override; void BrushRotate(float Amount) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; virtual bool ContainsElementWithId(int Id); + virtual void GetPos(int Number, int Offset, ivec2 &SwitchPos); + + int m_GotoSwitchOffset; + ivec2 m_GotoSwitchLastPos; EditorTileStateChangeHistory m_History; inline void ClearHistory() override diff --git a/src/game/editor/mapitems/layer_tele.cpp b/src/game/editor/mapitems/layer_tele.cpp index 0006a74c90..3e57388f71 100644 --- a/src/game/editor/mapitems/layer_tele.cpp +++ b/src/game/editor/mapitems/layer_tele.cpp @@ -10,6 +10,9 @@ CLayerTele::CLayerTele(CEditor *pEditor, int w, int h) : m_pTeleTile = new CTeleTile[w * h]; mem_zero(m_pTeleTile, (size_t)w * h * sizeof(CTeleTile)); + + m_GotoTeleOffset = 0; + m_GotoTeleLastPos = ivec2(-1, -1); } CLayerTele::CLayerTele(const CLayerTele &Other) : @@ -65,14 +68,14 @@ bool CLayerTele::IsEmpty(const std::shared_ptr &pLayer) return true; } -void CLayerTele::BrushDraw(std::shared_ptr pBrush, float wx, float wy) +void CLayerTele::BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) { if(m_Readonly) return; std::shared_ptr pTeleLayer = std::static_pointer_cast(pBrush); - int sx = ConvertX(wx); - int sy = ConvertY(wy); + int sx = ConvertX(WorldPos.x); + int sy = ConvertY(WorldPos.y); if(str_comp(pTeleLayer->m_aFileName, m_pEditor->m_aFileName)) m_pEditor->m_TeleNumber = pTeleLayer->m_TeleNum; @@ -98,23 +101,28 @@ void CLayerTele::BrushDraw(std::shared_ptr pBrush, float wx, float wy) if((m_pEditor->m_AllowPlaceUnusedTiles || IsValidTeleTile(pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index)) && pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index != TILE_AIR) { - if(!IsTeleTileNumberUsed(pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index)) + bool IsCheckpoint = IsTeleTileCheckpoint(pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index); + if(!IsCheckpoint && !IsTeleTileNumberUsed(pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index, false)) { // Tele tile number is unused. Set a known value which is not 0, // as tiles with number 0 would be ignored by previous versions. m_pTeleTile[Index].m_Number = 255; } - else if(m_pEditor->m_TeleNumber != pTeleLayer->m_TeleNum) + else if(!IsCheckpoint && m_pEditor->m_TeleNumber != pTeleLayer->m_TeleNum) { m_pTeleTile[Index].m_Number = m_pEditor->m_TeleNumber; } + else if(IsCheckpoint && m_pEditor->m_TeleCheckpointNumber != pTeleLayer->m_TeleCheckpointNum) + { + m_pTeleTile[Index].m_Number = m_pEditor->m_TeleCheckpointNumber; + } else if(pTeleLayer->m_pTeleTile[y * pTeleLayer->m_Width + x].m_Number) { m_pTeleTile[Index].m_Number = pTeleLayer->m_pTeleTile[y * pTeleLayer->m_Width + x].m_Number; } else { - if(!m_pEditor->m_TeleNumber) + if((!IsCheckpoint && !m_pEditor->m_TeleNumber) || (IsCheckpoint && !m_pEditor->m_TeleCheckpointNumber)) { m_pTeleTile[Index].m_Number = 0; m_pTeleTile[Index].m_Type = 0; @@ -130,7 +138,7 @@ void CLayerTele::BrushDraw(std::shared_ptr pBrush, float wx, float wy) } else { - m_pTeleTile[Index].m_Number = m_pEditor->m_TeleNumber; + m_pTeleTile[Index].m_Number = IsCheckpoint ? m_pEditor->m_TeleCheckpointNumber : m_pEditor->m_TeleNumber; } } @@ -142,6 +150,9 @@ void CLayerTele::BrushDraw(std::shared_ptr pBrush, float wx, float wy) m_pTeleTile[Index].m_Number = 0; m_pTeleTile[Index].m_Type = 0; m_pTiles[Index].m_Index = 0; + + if(pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index != TILE_AIR) + ShowPreventUnusedTilesWarning(); } STeleTileStateChange::SData Current{ @@ -254,6 +265,9 @@ void CLayerTele::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe m_pTiles[TgtIndex].m_Index = 0; m_pTeleTile[TgtIndex].m_Type = 0; m_pTeleTile[TgtIndex].m_Number = 0; + + if(!Empty) + ShowPreventUnusedTilesWarning(); } else { @@ -261,15 +275,18 @@ void CLayerTele::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe if(pLt->m_Tele && m_pTiles[TgtIndex].m_Index > 0) { m_pTeleTile[TgtIndex].m_Type = m_pTiles[TgtIndex].m_Index; + bool IsCheckpoint = IsTeleTileCheckpoint(m_pTiles[TgtIndex].m_Index); - if(!IsTeleTileNumberUsed(m_pTeleTile[TgtIndex].m_Type)) + if(!IsCheckpoint && !IsTeleTileNumberUsed(m_pTeleTile[TgtIndex].m_Type, false)) { // Tele tile number is unused. Set a known value which is not 0, // as tiles with number 0 would be ignored by previous versions. m_pTeleTile[TgtIndex].m_Number = 255; } - else if((pLt->m_pTeleTile[SrcIndex].m_Number == 0 && m_pEditor->m_TeleNumber) || m_pEditor->m_TeleNumber != pLt->m_TeleNum) + else if(!IsCheckpoint && ((pLt->m_pTeleTile[SrcIndex].m_Number == 0 && m_pEditor->m_TeleNumber) || m_pEditor->m_TeleNumber != pLt->m_TeleNum)) m_pTeleTile[TgtIndex].m_Number = m_pEditor->m_TeleNumber; + else if(IsCheckpoint && ((pLt->m_pTeleTile[SrcIndex].m_Number == 0 && m_pEditor->m_TeleCheckpointNumber) || m_pEditor->m_TeleCheckpointNumber != pLt->m_TeleCheckpointNum)) + m_pTeleTile[TgtIndex].m_Number = m_pEditor->m_TeleCheckpointNumber; else m_pTeleTile[TgtIndex].m_Number = pLt->m_pTeleTile[SrcIndex].m_Number; } @@ -286,13 +303,13 @@ void CLayerTele::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe FlagModified(sx, sy, w, h); } -bool CLayerTele::ContainsElementWithId(int Id) +bool CLayerTele::ContainsElementWithId(int Id, bool Checkpoint) { for(int y = 0; y < m_Height; ++y) { for(int x = 0; x < m_Width; ++x) { - if(IsTeleTileNumberUsed(m_pTeleTile[y * m_Width + x].m_Type) && m_pTeleTile[y * m_Width + x].m_Number == Id) + if(IsTeleTileNumberUsed(m_pTeleTile[y * m_Width + x].m_Type, Checkpoint) && m_pTeleTile[y * m_Width + x].m_Number == Id) { return true; } @@ -302,6 +319,60 @@ bool CLayerTele::ContainsElementWithId(int Id) return false; } +void CLayerTele::GetPos(int Number, int Offset, int &TeleX, int &TeleY) +{ + int Match = -1; + ivec2 MatchPos = ivec2(-1, -1); + TeleX = -1; + TeleY = -1; + + auto FindTile = [this, &Match, &MatchPos, &Number, &Offset]() { + for(int x = 0; x < m_Width; x++) + { + for(int y = 0; y < m_Height; y++) + { + int i = y * m_Width + x; + int Tele = m_pTeleTile[i].m_Number; + if(Number == Tele) + { + Match++; + if(Offset != -1) + { + if(Match == Offset) + { + MatchPos = ivec2(x, y); + m_GotoTeleOffset = Match; + return; + } + continue; + } + MatchPos = ivec2(x, y); + if(m_GotoTeleLastPos != ivec2(-1, -1)) + { + if(distance(m_GotoTeleLastPos, MatchPos) < 10.0f) + { + m_GotoTeleOffset++; + continue; + } + } + m_GotoTeleLastPos = MatchPos; + if(Match == m_GotoTeleOffset) + return; + } + } + } + }; + FindTile(); + + if(MatchPos == ivec2(-1, -1)) + return; + if(Match < m_GotoTeleOffset) + m_GotoTeleOffset = -1; + TeleX = MatchPos.x; + TeleY = MatchPos.y; + m_GotoTeleOffset++; +} + std::shared_ptr CLayerTele::Duplicate() const { return std::make_shared(*this); diff --git a/src/game/editor/mapitems/layer_tele.h b/src/game/editor/mapitems/layer_tele.h index 569dd305e4..1a373e79ad 100644 --- a/src/game/editor/mapitems/layer_tele.h +++ b/src/game/editor/mapitems/layer_tele.h @@ -23,16 +23,21 @@ class CLayerTele : public CLayerTiles CTeleTile *m_pTeleTile; unsigned char m_TeleNum; + unsigned char m_TeleCheckpointNum; void Resize(int NewW, int NewH) override; void Shift(int Direction) override; bool IsEmpty(const std::shared_ptr &pLayer) override; - void BrushDraw(std::shared_ptr pBrush, float wx, float wy) override; + void BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) override; void BrushFlipX() override; void BrushFlipY() override; void BrushRotate(float Amount) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; - virtual bool ContainsElementWithId(int Id); + virtual bool ContainsElementWithId(int Id, bool Checkpoint); + virtual void GetPos(int Number, int Offset, int &TeleX, int &TeleY); + + int m_GotoTeleOffset; + ivec2 m_GotoTeleLastPos; EditorTileStateChangeHistory m_History; inline void ClearHistory() override diff --git a/src/game/editor/mapitems/layer_tiles.cpp b/src/game/editor/mapitems/layer_tiles.cpp index a3ed05c33a..a6a040a6d0 100644 --- a/src/game/editor/mapitems/layer_tiles.cpp +++ b/src/game/editor/mapitems/layer_tiles.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -64,7 +65,7 @@ CLayerTiles::CLayerTiles(const CLayerTiles &Other) : m_Switch = Other.m_Switch; m_Tune = Other.m_Tune; - mem_copy(m_aFileName, Other.m_aFileName, IO_MAX_PATH_LENGTH); + str_copy(m_aFileName, Other.m_aFileName); } CLayerTiles::~CLayerTiles() @@ -146,13 +147,14 @@ void CLayerTiles::Render(bool Tileset) Texture = m_pEditor->GetTuneTexture(); Graphics()->TextureSet(Texture); - ColorRGBA Color = ColorRGBA(m_Color.r / 255.0f, m_Color.g / 255.0f, m_Color.b / 255.0f, m_Color.a / 255.0f); + ColorRGBA ColorEnv = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + CEditor::EnvelopeEval(m_ColorEnvOffset, m_ColorEnv, ColorEnv, 4, m_pEditor); + const ColorRGBA Color = ColorRGBA(m_Color.r / 255.0f, m_Color.g / 255.0f, m_Color.b / 255.0f, m_Color.a / 255.0f).Multiply(ColorEnv); + Graphics()->BlendNone(); - m_pEditor->RenderTools()->RenderTilemap(m_pTiles, m_Width, m_Height, 32.0f, Color, LAYERRENDERFLAG_OPAQUE, - CEditor::EnvelopeEval, m_pEditor, m_ColorEnv, m_ColorEnvOffset); + m_pEditor->RenderTools()->RenderTilemap(m_pTiles, m_Width, m_Height, 32.0f, Color, LAYERRENDERFLAG_OPAQUE); Graphics()->BlendNormal(); - m_pEditor->RenderTools()->RenderTilemap(m_pTiles, m_Width, m_Height, 32.0f, Color, LAYERRENDERFLAG_TRANSPARENT, - CEditor::EnvelopeEval, m_pEditor, m_ColorEnv, m_ColorEnvOffset); + m_pEditor->RenderTools()->RenderTilemap(m_pTiles, m_Width, m_Height, 32.0f, Color, LAYERRENDERFLAG_TRANSPARENT); // Render DDRace Layers if(!Tileset) @@ -251,12 +253,12 @@ bool CLayerTiles::IsEmpty(const std::shared_ptr &pLayer) void CLayerTiles::BrushSelecting(CUIRect Rect) { Graphics()->TextureClear(); - m_pEditor->Graphics()->QuadsBegin(); - m_pEditor->Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f); + Graphics()->QuadsBegin(); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 0.4f); Snap(&Rect); IGraphics::CQuadItem QuadItem(Rect.x, Rect.y, Rect.w, Rect.h); - m_pEditor->Graphics()->QuadsDrawTL(&QuadItem, 1); - m_pEditor->Graphics()->QuadsEnd(); + Graphics()->QuadsDrawTL(&QuadItem, 1); + Graphics()->QuadsEnd(); char aBuf[16]; str_format(aBuf, sizeof(aBuf), "%d⨯%d", ConvertX(Rect.w), ConvertY(Rect.h)); TextRender()->Text(Rect.x + 3.0f, Rect.y + 3.0f, m_pEditor->m_ShowPicker ? 15.0f : m_pEditor->MapView()->ScaleLength(15.0f), aBuf, -1.0f); @@ -308,12 +310,18 @@ int CLayerTiles::BrushGrab(std::shared_ptr pBrush, CUIRect Rect) for(int x = 0; x < r.w; x++) { pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x] = static_cast(this)->m_pTeleTile[(r.y + y) * m_Width + (r.x + x)]; - if(IsValidTeleTile(pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type) && IsTeleTileNumberUsed(pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type)) + if(IsValidTeleTile(pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type)) { - m_pEditor->m_TeleNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number; + if(IsTeleTileNumberUsed(pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type, false)) + m_pEditor->m_TeleNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number; + else if(IsTeleTileNumberUsed(pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Type, true)) + m_pEditor->m_TeleCheckpointNumber = pGrabbed->m_pTeleTile[y * pGrabbed->m_Width + x].m_Number; } } + pGrabbed->m_TeleNum = m_pEditor->m_TeleNumber; + pGrabbed->m_TeleCheckpointNum = m_pEditor->m_TeleCheckpointNumber; + str_copy(pGrabbed->m_aFileName, m_pEditor->m_aFileName); } else if(this->m_Speedup) @@ -479,15 +487,15 @@ void CLayerTiles::FillSelection(bool Empty, std::shared_ptr pBrush, CUIR FlagModified(sx, sy, w, h); } -void CLayerTiles::BrushDraw(std::shared_ptr pBrush, float wx, float wy) +void CLayerTiles::BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) { if(m_Readonly) return; // std::shared_ptr pTileLayer = std::static_pointer_cast(pBrush); - int sx = ConvertX(wx); - int sy = ConvertY(wy); + int sx = ConvertX(WorldPos.x); + int sy = ConvertY(WorldPos.y); bool Destructive = m_pEditor->m_BrushDrawDestructive || IsEmpty(pTileLayer); @@ -669,7 +677,7 @@ void CLayerTiles::ShowInfo() } else { - str_from_int(m_pTiles[c].m_Index, aBuf); + str_format(aBuf, sizeof(aBuf), "%d", m_pTiles[c].m_Index); } m_pEditor->Graphics()->QuadsText(x * 32, y * 32, 16.0f, aBuf); @@ -686,92 +694,126 @@ void CLayerTiles::ShowInfo() Graphics()->MapScreen(ScreenX0, ScreenY0, ScreenX1, ScreenY1); } -CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) +void CLayerTiles::FillGameTiles(EGameTileOp Fill) { - CUIRect Button; - - const bool EntitiesLayer = IsEntitiesLayer(); + if(!CanFillGameTiles()) + return; std::shared_ptr pGroup = m_pEditor->m_Map.m_vpGroups[m_pEditor->m_SelectedGroup]; - // Game tiles can only be constructed if the layer is relative to the game layer - if(!EntitiesLayer && !(pGroup->m_OffsetX % 32) && !(pGroup->m_OffsetY % 32) && pGroup->m_ParallaxX == 100 && pGroup->m_ParallaxY == 100) + int Result = (int)Fill; + switch(Fill) { - pToolBox->HSplitBottom(12.0f, pToolBox, &Button); - static int s_GameTilesButton = 0; - if(m_pEditor->DoButton_Editor(&s_GameTilesButton, "Game tiles", 0, &Button, 0, "Constructs game tiles from this layer")) - m_pEditor->PopupSelectGametileOpInvoke(m_pEditor->UI()->MouseX(), m_pEditor->UI()->MouseY()); - int Result = m_pEditor->PopupSelectGameTileOpResult(); - switch(Result) + case EGameTileOp::HOOKTHROUGH: + Result = TILE_THROUGH_CUT; + break; + case EGameTileOp::FREEZE: + Result = TILE_FREEZE; + break; + case EGameTileOp::UNFREEZE: + Result = TILE_UNFREEZE; + break; + case EGameTileOp::DEEP_FREEZE: + Result = TILE_DFREEZE; + break; + case EGameTileOp::DEEP_UNFREEZE: + Result = TILE_DUNFREEZE; + break; + case EGameTileOp::BLUE_CHECK_TELE: + Result = TILE_TELECHECKIN; + break; + case EGameTileOp::RED_CHECK_TELE: + Result = TILE_TELECHECKINEVIL; + break; + case EGameTileOp::LIVE_FREEZE: + Result = TILE_LFREEZE; + break; + case EGameTileOp::LIVE_UNFREEZE: + Result = TILE_LUNFREEZE; + break; + default: + break; + } + if(Result > -1) + { + const int OffsetX = -pGroup->m_OffsetX / 32; + const int OffsetY = -pGroup->m_OffsetY / 32; + + std::vector> vpActions; + std::shared_ptr pGLayer = m_pEditor->m_Map.m_pGameLayer; + int GameLayerIndex = std::find(m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(), m_pEditor->m_Map.m_pGameGroup->m_vpLayers.end(), pGLayer) - m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(); + + if(Result != TILE_TELECHECKIN && Result != TILE_TELECHECKINEVIL) { - case 4: - Result = TILE_THROUGH_CUT; - break; - case 5: - Result = TILE_FREEZE; - break; - case 6: - Result = TILE_UNFREEZE; - break; - case 7: - Result = TILE_DFREEZE; - break; - case 8: - Result = TILE_DUNFREEZE; - break; - case 9: - Result = TILE_TELECHECKIN; - break; - case 10: - Result = TILE_TELECHECKINEVIL; - break; - case 11: - Result = TILE_LFREEZE; - break; - case 12: - Result = TILE_LUNFREEZE; - break; - default: - break; + if(pGLayer->m_Width < m_Width + OffsetX || pGLayer->m_Height < m_Height + OffsetY) + { + std::map> savedLayers; + savedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate(); + savedLayers[LAYERTYPE_GAME] = savedLayers[LAYERTYPE_TILES]; + + int PrevW = pGLayer->m_Width; + int PrevH = pGLayer->m_Height; + const int NewW = pGLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pGLayer->m_Width; + const int NewH = pGLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pGLayer->m_Height; + pGLayer->Resize(NewW, NewH); + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW)); + const std::shared_ptr &Action1 = std::static_pointer_cast(vpActions[vpActions.size() - 1]); + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH)); + const std::shared_ptr &Action2 = std::static_pointer_cast(vpActions[vpActions.size() - 1]); + + Action1->SetSavedLayers(savedLayers); + Action2->SetSavedLayers(savedLayers); + } + + int Changes = 0; + for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++) + { + for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++) + { + if(GetTile(x, y).m_Index) + { + const CTile ResultTile = {(unsigned char)Result}; + pGLayer->SetTile(x + OffsetX, y + OffsetY, ResultTile); + Changes++; + } + } + } + + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup)); + char aDisplay[256]; + str_format(aDisplay, sizeof(aDisplay), "Construct '%s' game tiles (x%d)", g_apGametileOpNames[(int)Fill], Changes); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, aDisplay, true)); } - if(Result > -1) + else { - const int OffsetX = -pGroup->m_OffsetX / 32; - const int OffsetY = -pGroup->m_OffsetY / 32; - - static const char *s_apGametileOpNames[] = { - "Air", - "Hookable", - "Death", - "Unhookable", - "Hookthrough", - "Freeze", - "Unfreeze", - "Deep Freeze", - "Deep Unfreeze", - "Blue Check-Tele", - "Red Check-Tele", - "Live Freeze", - "Live Unfreeze", - }; + if(!m_pEditor->m_Map.m_pTeleLayer) + { + std::shared_ptr pLayer = std::make_shared(m_pEditor, m_Width, m_Height); + m_pEditor->m_Map.MakeTeleLayer(pLayer); + m_pEditor->m_Map.m_pGameGroup->AddLayer(pLayer); - std::vector> vpActions; - std::shared_ptr pGLayer = m_pEditor->m_Map.m_pGameLayer; - int GameLayerIndex = std::find(m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(), m_pEditor->m_Map.m_pGameGroup->m_vpLayers.end(), pGLayer) - m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(); + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_Map.m_pGameGroup->m_vpLayers.size() - 1)); - if(Result != TILE_TELECHECKIN && Result != TILE_TELECHECKINEVIL) - { - if(pGLayer->m_Width < m_Width + OffsetX || pGLayer->m_Height < m_Height + OffsetY) + if(m_Width != pGLayer->m_Width || m_Height > pGLayer->m_Height) { std::map> savedLayers; savedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate(); savedLayers[LAYERTYPE_GAME] = savedLayers[LAYERTYPE_TILES]; + int NewW = pGLayer->m_Width; + int NewH = pGLayer->m_Height; + if(m_Width > pGLayer->m_Width) + { + NewW = m_Width; + } + if(m_Height > pGLayer->m_Height) + { + NewH = m_Height; + } + int PrevW = pGLayer->m_Width; int PrevH = pGLayer->m_Height; - const int NewW = pGLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pGLayer->m_Width; - const int NewH = pGLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pGLayer->m_Height; - pGLayer->Resize(NewW, NewH); + pLayer->Resize(NewW, NewH); vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW)); const std::shared_ptr &Action1 = std::static_pointer_cast(vpActions[vpActions.size() - 1]); vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH)); @@ -780,124 +822,94 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) Action1->SetSavedLayers(savedLayers); Action2->SetSavedLayers(savedLayers); } + } - int Changes = 0; - for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++) - { - for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++) - { - if(GetTile(x, y).m_Index) - { - const CTile ResultTile = {(unsigned char)Result}; - pGLayer->SetTile(x + OffsetX, y + OffsetY, ResultTile); - Changes++; - } - } - } + std::shared_ptr pTLayer = m_pEditor->m_Map.m_pTeleLayer; + int TeleLayerIndex = std::find(m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(), m_pEditor->m_Map.m_pGameGroup->m_vpLayers.end(), pTLayer) - m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(); - vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup)); - char aDisplay[256]; - str_format(aDisplay, sizeof(aDisplay), "Construct '%s' game tiles (x%d)", s_apGametileOpNames[Result], Changes); - m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, aDisplay, true)); + if(pTLayer->m_Width < m_Width + OffsetX || pTLayer->m_Height < m_Height + OffsetY) + { + std::map> savedLayers; + savedLayers[LAYERTYPE_TILES] = pTLayer->Duplicate(); + savedLayers[LAYERTYPE_TELE] = savedLayers[LAYERTYPE_TILES]; + + int PrevW = pTLayer->m_Width; + int PrevH = pTLayer->m_Height; + int NewW = pTLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pTLayer->m_Width; + int NewH = pTLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pTLayer->m_Height; + pTLayer->Resize(NewW, NewH); + std::shared_ptr Action1, Action2; + vpActions.push_back(Action1 = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, TeleLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW)); + vpActions.push_back(Action2 = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, TeleLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH)); + + Action1->SetSavedLayers(savedLayers); + Action2->SetSavedLayers(savedLayers); } - else + + int Changes = 0; + for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++) { - if(!m_pEditor->m_Map.m_pTeleLayer) + for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++) { - std::shared_ptr pLayer = std::make_shared(m_pEditor, m_Width, m_Height); - m_pEditor->m_Map.MakeTeleLayer(pLayer); - m_pEditor->m_Map.m_pGameGroup->AddLayer(pLayer); - - vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_Map.m_pGameGroup->m_vpLayers.size() - 1)); - - if(m_Width != pGLayer->m_Width || m_Height > pGLayer->m_Height) + if(GetTile(x, y).m_Index) { - std::map> savedLayers; - savedLayers[LAYERTYPE_TILES] = pGLayer->Duplicate(); - savedLayers[LAYERTYPE_GAME] = savedLayers[LAYERTYPE_TILES]; - - int NewW = pGLayer->m_Width; - int NewH = pGLayer->m_Height; - if(m_Width > pGLayer->m_Width) - { - NewW = m_Width; - } - if(m_Height > pGLayer->m_Height) - { - NewH = m_Height; - } - - int PrevW = pGLayer->m_Width; - int PrevH = pGLayer->m_Height; - pLayer->Resize(NewW, NewH); - vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW)); - const std::shared_ptr &Action1 = std::static_pointer_cast(vpActions[vpActions.size() - 1]); - vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, GameLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH)); - const std::shared_ptr &Action2 = std::static_pointer_cast(vpActions[vpActions.size() - 1]); - - Action1->SetSavedLayers(savedLayers); - Action2->SetSavedLayers(savedLayers); - } - } + auto TileIndex = (y + OffsetY) * pTLayer->m_Width + x + OffsetX; + Changes++; - std::shared_ptr pTLayer = m_pEditor->m_Map.m_pTeleLayer; - int TeleLayerIndex = std::find(m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(), m_pEditor->m_Map.m_pGameGroup->m_vpLayers.end(), pTLayer) - m_pEditor->m_Map.m_pGameGroup->m_vpLayers.begin(); + STeleTileStateChange::SData Previous{ + pTLayer->m_pTeleTile[TileIndex].m_Number, + pTLayer->m_pTeleTile[TileIndex].m_Type, + pTLayer->m_pTiles[TileIndex].m_Index}; - if(pTLayer->m_Width < m_Width + OffsetX || pTLayer->m_Height < m_Height + OffsetY) - { - std::map> savedLayers; - savedLayers[LAYERTYPE_TILES] = pTLayer->Duplicate(); - savedLayers[LAYERTYPE_TELE] = savedLayers[LAYERTYPE_TILES]; - - int PrevW = pTLayer->m_Width; - int PrevH = pTLayer->m_Height; - int NewW = pTLayer->m_Width < m_Width + OffsetX ? m_Width + OffsetX : pTLayer->m_Width; - int NewH = pTLayer->m_Height < m_Height + OffsetY ? m_Height + OffsetY : pTLayer->m_Height; - pTLayer->Resize(NewW, NewH); - std::shared_ptr Action1, Action2; - vpActions.push_back(Action1 = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, TeleLayerIndex, ETilesProp::PROP_WIDTH, PrevW, NewW)); - vpActions.push_back(Action2 = std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, TeleLayerIndex, ETilesProp::PROP_HEIGHT, PrevH, NewH)); + pTLayer->m_pTiles[TileIndex].m_Index = TILE_AIR + Result; + pTLayer->m_pTeleTile[TileIndex].m_Number = 1; + pTLayer->m_pTeleTile[TileIndex].m_Type = TILE_AIR + Result; - Action1->SetSavedLayers(savedLayers); - Action2->SetSavedLayers(savedLayers); - } + STeleTileStateChange::SData Current{ + pTLayer->m_pTeleTile[TileIndex].m_Number, + pTLayer->m_pTeleTile[TileIndex].m_Type, + pTLayer->m_pTiles[TileIndex].m_Index}; - int Changes = 0; - for(int y = OffsetY < 0 ? -OffsetY : 0; y < m_Height; y++) - { - for(int x = OffsetX < 0 ? -OffsetX : 0; x < m_Width; x++) - { - if(GetTile(x, y).m_Index) - { - auto TileIndex = (y + OffsetY) * pTLayer->m_Width + x + OffsetX; - Changes++; - - STeleTileStateChange::SData Previous{ - pTLayer->m_pTeleTile[TileIndex].m_Number, - pTLayer->m_pTeleTile[TileIndex].m_Type, - pTLayer->m_pTiles[TileIndex].m_Index}; - - pTLayer->m_pTiles[TileIndex].m_Index = TILE_AIR + Result; - pTLayer->m_pTeleTile[TileIndex].m_Number = 1; - pTLayer->m_pTeleTile[TileIndex].m_Type = TILE_AIR + Result; - - STeleTileStateChange::SData Current{ - pTLayer->m_pTeleTile[TileIndex].m_Number, - pTLayer->m_pTeleTile[TileIndex].m_Type, - pTLayer->m_pTiles[TileIndex].m_Index}; - - pTLayer->RecordStateChange(x, y, Previous, Current); - } + pTLayer->RecordStateChange(x, y, Previous, Current); } } - - vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup)); - char aDisplay[256]; - str_format(aDisplay, sizeof(aDisplay), "Construct 'tele' game tiles (x%d)", Changes); - m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, aDisplay, true)); } + + vpActions.push_back(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup)); + char aDisplay[256]; + str_format(aDisplay, sizeof(aDisplay), "Construct 'tele' game tiles (x%d)", Changes); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, aDisplay, true)); } } +} + +bool CLayerTiles::CanFillGameTiles() const +{ + const bool EntitiesLayer = IsEntitiesLayer(); + if(EntitiesLayer) + return false; + + std::shared_ptr pGroup = m_pEditor->m_Map.m_vpGroups[m_pEditor->m_SelectedGroup]; + + // Game tiles can only be constructed if the layer is relative to the game layer + return !(pGroup->m_OffsetX % 32) && !(pGroup->m_OffsetY % 32) && pGroup->m_ParallaxX == 100 && pGroup->m_ParallaxY == 100; +} + +CUi::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) +{ + CUIRect Button; + + const bool EntitiesLayer = IsEntitiesLayer(); + + if(CanFillGameTiles()) + { + pToolBox->HSplitBottom(12.0f, pToolBox, &Button); + static int s_GameTilesButton = 0; + if(m_pEditor->DoButton_Editor(&s_GameTilesButton, "Game tiles", 0, &Button, 0, "Constructs game tiles from this layer")) + m_pEditor->PopupSelectGametileOpInvoke(m_pEditor->Ui()->MouseX(), m_pEditor->Ui()->MouseY()); + const int Selected = m_pEditor->PopupSelectGameTileOpResult(); + FillGameTiles((EGameTileOp)Selected); + } if(m_pEditor->m_Map.m_pGameLayer.get() != this) { @@ -918,7 +930,7 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) if(!m_TilesHistory.empty()) // Sometimes pressing that button causes the automap to run so we should be able to undo that { // record undo - m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], m_TilesHistory)); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], "Auto map", m_TilesHistory)); ClearHistory(); } } @@ -929,9 +941,9 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) { m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.Proceed(this, m_AutoMapperConfig, m_Seed); // record undo - m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], m_TilesHistory)); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], "Auto map", m_TilesHistory)); ClearHistory(); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } } } @@ -939,16 +951,16 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) int Color = PackColor(m_Color); CProperty aProps[] = { - {"Width", m_Width, PROPTYPE_INT_SCROLL, 1, 100000}, - {"Height", m_Height, PROPTYPE_INT_SCROLL, 1, 100000}, + {"Width", m_Width, PROPTYPE_INT, 1, 100000}, + {"Height", m_Height, PROPTYPE_INT, 1, 100000}, {"Shift", 0, PROPTYPE_SHIFT, 0, 0}, - {"Shift by", m_pEditor->m_ShiftBy, PROPTYPE_INT_SCROLL, 1, 100000}, + {"Shift by", m_pEditor->m_ShiftBy, PROPTYPE_INT, 1, 100000}, {"Image", m_Image, PROPTYPE_IMAGE, 0, 0}, {"Color", Color, PROPTYPE_COLOR, 0, 0}, {"Color Env", m_ColorEnv + 1, PROPTYPE_ENVELOPE, 0, 0}, - {"Color TO", m_ColorEnvOffset, PROPTYPE_INT_SCROLL, -1000000, 1000000}, + {"Color TO", m_ColorEnvOffset, PROPTYPE_INT, -1000000, 1000000}, {"Auto Rule", m_AutoMapperConfig, PROPTYPE_AUTOMAPPER, m_Image, 0}, - {"Seed", m_Seed, PROPTYPE_INT_SCROLL, 0, 1000000000}, + {"Seed", m_Seed, PROPTYPE_INT, 0, 1000000000}, {nullptr}, }; @@ -970,6 +982,7 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) static CLayerTilesPropTracker s_Tracker(m_pEditor); s_Tracker.Begin(this, Prop, State); + m_pEditor->m_EditorHistory.BeginBulk(); if(Prop == ETilesProp::PROP_WIDTH && NewVal > 1) { @@ -1058,17 +1071,30 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) m_AutoMapperConfig = -1; } - if(Prop != ETilesProp::PROP_NONE && Prop != ETilesProp::PROP_SHIFT_BY) + s_Tracker.End(Prop, State); + + // Check if modified property could have an effect on automapper + if((State == EEditState::END || State == EEditState::ONE_GO) && HasAutomapEffect(Prop)) { FlagModified(0, 0, m_Width, m_Height); + + // Record undo if automapper was ran + if(m_AutoAutoMap && !m_TilesHistory.empty()) + { + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], "Auto map", m_TilesHistory)); + ClearHistory(); + } } - s_Tracker.End(Prop, State); + // End undo bulk, taking the first action display as the displayed text in the history + // This is usually the resulting text of the edit layer tiles prop action + // Since we may also squeeze a tile changes action, we want both to appear as one, thus using a bulk + m_pEditor->m_EditorHistory.EndBulk(0); - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector> &vpLayers, std::vector &vLayerIndices) +CUi::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector> &vpLayers, std::vector &vLayerIndices) { if(State.m_Modified) { @@ -1169,16 +1195,16 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta pEditor->TextRender()->TextColor(ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f)); SLabelProperties Props; Props.m_MaxWidth = Warning.w; - pEditor->UI()->DoLabel(&Warning, "Editing multiple layers", 9.0f, TEXTALIGN_ML, Props); + pEditor->Ui()->DoLabel(&Warning, "Editing multiple layers", 9.0f, TEXTALIGN_ML, Props); pEditor->TextRender()->TextColor(ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f)); pToolbox->HSplitTop(2.0f, nullptr, pToolbox); } CProperty aProps[] = { - {"Width", State.m_Width, PROPTYPE_INT_SCROLL, 1, 100000}, - {"Height", State.m_Height, PROPTYPE_INT_SCROLL, 1, 100000}, + {"Width", State.m_Width, PROPTYPE_INT, 1, 100000}, + {"Height", State.m_Height, PROPTYPE_INT, 1, 100000}, {"Shift", 0, PROPTYPE_SHIFT, 0, 0}, - {"Shift by", pEditor->m_ShiftBy, PROPTYPE_INT_SCROLL, 1, 100000}, + {"Shift by", pEditor->m_ShiftBy, PROPTYPE_INT, 1, 100000}, {"Color", State.m_Color, PROPTYPE_COLOR, 0, 0}, {nullptr}, }; @@ -1229,16 +1255,19 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta s_Tracker.End(Prop, PropState); - if(Prop == ETilesCommonProp::PROP_WIDTH || Prop == ETilesCommonProp::PROP_HEIGHT) - { - State.m_Modified |= SCommonPropState::MODIFIED_SIZE; - } - else if(Prop == ETilesCommonProp::PROP_COLOR) + if(PropState == EEditState::END || PropState == EEditState::ONE_GO) { - State.m_Modified |= SCommonPropState::MODIFIED_COLOR; + if(Prop == ETilesCommonProp::PROP_WIDTH || Prop == ETilesCommonProp::PROP_HEIGHT) + { + State.m_Modified |= SCommonPropState::MODIFIED_SIZE; + } + else if(Prop == ETilesCommonProp::PROP_COLOR) + { + State.m_Modified |= SCommonPropState::MODIFIED_COLOR; + } } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } void CLayerTiles::FlagModified(int x, int y, int w, int h) @@ -1259,3 +1288,30 @@ void CLayerTiles::ModifyEnvelopeIndex(FIndexModifyFunction Func) { Func(&m_ColorEnv); } + +void CLayerTiles::ShowPreventUnusedTilesWarning() +{ + if(!m_pEditor->m_PreventUnusedTilesWasWarned) + { + m_pEditor->m_PopupEventType = CEditor::POPEVENT_PREVENTUNUSEDTILES; + m_pEditor->m_PopupEventActivated = true; + m_pEditor->m_PreventUnusedTilesWasWarned = true; + } +} + +bool CLayerTiles::HasAutomapEffect(ETilesProp Prop) +{ + switch(Prop) + { + case ETilesProp::PROP_WIDTH: + case ETilesProp::PROP_HEIGHT: + case ETilesProp::PROP_SHIFT: + case ETilesProp::PROP_IMAGE: + case ETilesProp::PROP_AUTOMAPPER: + case ETilesProp::PROP_SEED: + return true; + default: + return false; + } + return false; +} diff --git a/src/game/editor/mapitems/layer_tiles.h b/src/game/editor/mapitems/layer_tiles.h index ca8e84e889..b6ca148d7e 100644 --- a/src/game/editor/mapitems/layer_tiles.h +++ b/src/game/editor/mapitems/layer_tiles.h @@ -2,6 +2,7 @@ #define GAME_EDITOR_MAPITEMS_LAYER_TILES_H #include +#include #include #include "layer.h" @@ -122,7 +123,9 @@ class CLayerTiles : public CLayer void BrushSelecting(CUIRect Rect) override; int BrushGrab(std::shared_ptr pBrush, CUIRect Rect) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; - void BrushDraw(std::shared_ptr pBrush, float wx, float wy) override; + void FillGameTiles(EGameTileOp Fill); + bool CanFillGameTiles() const; + void BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) override; void BrushFlipX() override; void BrushFlipY() override; void BrushRotate(float Amount) override; @@ -131,7 +134,7 @@ class CLayerTiles : public CLayer const char *TypeName() const override; virtual void ShowInfo(); - CUI::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; + CUi::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; struct SCommonPropState { @@ -145,7 +148,7 @@ class CLayerTiles : public CLayer int m_Height = -1; int m_Color = 0; }; - static CUI::EPopupMenuFunctionResult RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector> &vpLayers, std::vector &vLayerIndices); + static CUi::EPopupMenuFunctionResult RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector> &vpLayers, std::vector &vLayerIndices); void ModifyImageIndex(FIndexModifyFunction pfnFunc) override; void ModifyEnvelopeIndex(FIndexModifyFunction pfnFunc) override; @@ -185,9 +188,13 @@ class CLayerTiles : public CLayer EditorTileStateChangeHistory m_TilesHistory; inline virtual void ClearHistory() { m_TilesHistory.clear(); } + static bool HasAutomapEffect(ETilesProp Prop); + protected: void RecordStateChange(int x, int y, CTile Previous, CTile Tile); + void ShowPreventUnusedTilesWarning(); + friend class CAutoMapper; }; diff --git a/src/game/editor/mapitems/layer_tune.cpp b/src/game/editor/mapitems/layer_tune.cpp index 0a41fcc7b2..a417599221 100644 --- a/src/game/editor/mapitems/layer_tune.cpp +++ b/src/game/editor/mapitems/layer_tune.cpp @@ -65,14 +65,14 @@ bool CLayerTune::IsEmpty(const std::shared_ptr &pLayer) return true; } -void CLayerTune::BrushDraw(std::shared_ptr pBrush, float wx, float wy) +void CLayerTune::BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) { if(m_Readonly) return; std::shared_ptr pTuneLayer = std::static_pointer_cast(pBrush); - int sx = ConvertX(wx); - int sy = ConvertY(wy); + int sx = ConvertX(WorldPos.x); + int sy = ConvertY(WorldPos.y); if(str_comp(pTuneLayer->m_aFileName, m_pEditor->m_aFileName)) { m_pEditor->m_TuningNum = pTuneLayer->m_TuningNumber; @@ -127,6 +127,9 @@ void CLayerTune::BrushDraw(std::shared_ptr pBrush, float wx, float wy) m_pTuneTile[Index].m_Number = 0; m_pTuneTile[Index].m_Type = 0; m_pTiles[Index].m_Index = 0; + + if(pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index != TILE_AIR) + ShowPreventUnusedTilesWarning(); } STuneTileStateChange::SData Current{ @@ -235,6 +238,9 @@ void CLayerTune::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe m_pTiles[TgtIndex].m_Index = 0; m_pTuneTile[TgtIndex].m_Type = 0; m_pTuneTile[TgtIndex].m_Number = 0; + + if(!Empty) + ShowPreventUnusedTilesWarning(); } else { diff --git a/src/game/editor/mapitems/layer_tune.h b/src/game/editor/mapitems/layer_tune.h index cf35b95693..6ea607d8dc 100644 --- a/src/game/editor/mapitems/layer_tune.h +++ b/src/game/editor/mapitems/layer_tune.h @@ -27,7 +27,7 @@ class CLayerTune : public CLayerTiles void Resize(int NewW, int NewH) override; void Shift(int Direction) override; bool IsEmpty(const std::shared_ptr &pLayer) override; - void BrushDraw(std::shared_ptr pBrush, float wx, float wy) override; + void BrushDraw(std::shared_ptr pBrush, vec2 WorldPos) override; void BrushFlipX() override; void BrushFlipY() override; void BrushRotate(float Amount) override; diff --git a/src/game/editor/mapitems/map_io.cpp b/src/game/editor/mapitems/map_io.cpp index 30a88ee40e..30f85c6764 100644 --- a/src/game/editor/mapitems/map_io.cpp +++ b/src/game/editor/mapitems/map_io.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -33,18 +34,25 @@ struct CSoundSource_DEPRECATED int m_SoundEnvOffset; }; -bool CEditorMap::Save(const char *pFileName) +bool CEditorMap::Save(const char *pFileName, const std::function &ErrorHandler) { char aFileNameTmp[IO_MAX_PATH_LENGTH]; - str_format(aFileNameTmp, sizeof(aFileNameTmp), "%s.%d.tmp", pFileName, pid()); + IStorage::FormatTmpPath(aFileNameTmp, sizeof(aFileNameTmp), pFileName); + char aBuf[IO_MAX_PATH_LENGTH + 64]; str_format(aBuf, sizeof(aBuf), "saving to '%s'...", aFileNameTmp); m_pEditor->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "editor", aBuf); + + if(!PerformPreSaveSanityChecks(ErrorHandler)) + { + return false; + } + CDataFileWriter Writer; if(!Writer.Open(m_pEditor->Storage(), aFileNameTmp)) { - str_format(aBuf, sizeof(aBuf), "failed to open file '%s'...", aFileNameTmp); - m_pEditor->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "editor", aBuf); + str_format(aBuf, sizeof(aBuf), "Error: Failed to open file '%s' for writing.", aFileNameTmp); + ErrorHandler(aBuf); return false; } @@ -110,27 +118,8 @@ bool CEditorMap::Save(const char *pFileName) } else { - const size_t PixelSize = CImageInfo::PixelSize(CImageInfo::FORMAT_RGBA); - const size_t DataSize = (size_t)Item.m_Width * Item.m_Height * PixelSize; - if(pImg->m_Format == CImageInfo::FORMAT_RGB) - { - // Convert to RGBA - unsigned char *pDataRGBA = (unsigned char *)malloc(DataSize); - unsigned char *pDataRGB = (unsigned char *)pImg->m_pData; - for(int j = 0; j < Item.m_Width * Item.m_Height; j++) - { - pDataRGBA[j * PixelSize] = pDataRGB[j * 3]; - pDataRGBA[j * PixelSize + 1] = pDataRGB[j * 3 + 1]; - pDataRGBA[j * PixelSize + 2] = pDataRGB[j * 3 + 2]; - pDataRGBA[j * PixelSize + 3] = 255; - } - Item.m_ImageData = Writer.AddData(DataSize, pDataRGBA); - free(pDataRGBA); - } - else - { - Item.m_ImageData = Writer.AddData(DataSize, pImg->m_pData); - } + dbg_assert(pImg->m_Format == CImageInfo::FORMAT_RGBA, "Embedded images must be in RGBA format"); + Item.m_ImageData = Writer.AddData(pImg->DataSize(), pImg->m_pData); } Writer.AddItem(MAPITEMTYPE_IMAGE, i, sizeof(Item), &Item); } @@ -146,6 +135,7 @@ bool CEditorMap::Save(const char *pFileName) Item.m_External = 0; Item.m_SoundName = Writer.AddDataString(pSound->m_aName); Item.m_SoundData = Writer.AddData(pSound->m_DataSize, pSound->m_pData); + // Value is not read in new versions, but we still need to write it for compatibility with old versions. Item.m_SoundDataSize = pSound->m_DataSize; Writer.AddItem(MAPITEMTYPE_SOUND, i, sizeof(Item), &Item); @@ -172,7 +162,7 @@ bool CEditorMap::Save(const char *pFileName) GItem.m_NumLayers = 0; // save group name - StrToInts(GItem.m_aName, sizeof(GItem.m_aName) / sizeof(int), pGroup->m_aName); + StrToInts(GItem.m_aName, std::size(GItem.m_aName), pGroup->m_aName); for(const std::shared_ptr &pLayer : pGroup->m_vpLayers) { @@ -241,8 +231,9 @@ bool CEditorMap::Save(const char *pFileName) Item.m_Data = Writer.AddData((size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTile), pLayerTiles->m_pTiles); // save layer name - StrToInts(Item.m_aName, sizeof(Item.m_aName) / sizeof(int), pLayerTiles->m_aName); + StrToInts(Item.m_aName, std::size(Item.m_aName), pLayerTiles->m_aName); + // save item Writer.AddItem(MAPITEMTYPE_LAYER, LayerCount, sizeof(Item), &Item); // save auto mapper of each tile layer (not physics layer) @@ -261,61 +252,75 @@ bool CEditorMap::Save(const char *pFileName) Writer.AddItem(MAPITEMTYPE_AUTOMAPPER_CONFIG, AutomapperCount, sizeof(ItemAutomapper), &ItemAutomapper); AutomapperCount++; } - - GItem.m_NumLayers++; - LayerCount++; } else if(pLayer->m_Type == LAYERTYPE_QUADS) { m_pEditor->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "editor", "saving quads layer"); std::shared_ptr pLayerQuads = std::static_pointer_cast(pLayer); + CMapItemLayerQuads Item; + Item.m_Version = 2; + Item.m_Layer.m_Version = 0; // was previously uninitialized, do not rely on it being 0 + Item.m_Layer.m_Flags = pLayerQuads->m_Flags; + Item.m_Layer.m_Type = pLayerQuads->m_Type; + Item.m_Image = pLayerQuads->m_Image; + + Item.m_NumQuads = 0; + Item.m_Data = -1; if(!pLayerQuads->m_vQuads.empty()) { - CMapItemLayerQuads Item; - Item.m_Version = 2; - Item.m_Layer.m_Version = 0; // was previously uninitialized, do not rely on it being 0 - Item.m_Layer.m_Flags = pLayerQuads->m_Flags; - Item.m_Layer.m_Type = pLayerQuads->m_Type; - Item.m_Image = pLayerQuads->m_Image; - // add the data Item.m_NumQuads = pLayerQuads->m_vQuads.size(); Item.m_Data = Writer.AddDataSwapped(pLayerQuads->m_vQuads.size() * sizeof(CQuad), pLayerQuads->m_vQuads.data()); + } + else + { + // add dummy data for backwards compatibility + // this allows the layer to be loaded with an empty array since m_NumQuads is 0 while saving + CQuad Dummy{}; + Item.m_Data = Writer.AddDataSwapped(sizeof(CQuad), &Dummy); + } - // save layer name - StrToInts(Item.m_aName, sizeof(Item.m_aName) / sizeof(int), pLayerQuads->m_aName); - - Writer.AddItem(MAPITEMTYPE_LAYER, LayerCount, sizeof(Item), &Item); + // save layer name + StrToInts(Item.m_aName, std::size(Item.m_aName), pLayerQuads->m_aName); - GItem.m_NumLayers++; - LayerCount++; - } + // save item + Writer.AddItem(MAPITEMTYPE_LAYER, LayerCount, sizeof(Item), &Item); } else if(pLayer->m_Type == LAYERTYPE_SOUNDS) { m_pEditor->Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "editor", "saving sounds layer"); std::shared_ptr pLayerSounds = std::static_pointer_cast(pLayer); + CMapItemLayerSounds Item; + Item.m_Version = CMapItemLayerSounds::CURRENT_VERSION; + Item.m_Layer.m_Version = 0; // was previously uninitialized, do not rely on it being 0 + Item.m_Layer.m_Flags = pLayerSounds->m_Flags; + Item.m_Layer.m_Type = pLayerSounds->m_Type; + Item.m_Sound = pLayerSounds->m_Sound; + + Item.m_NumSources = 0; if(!pLayerSounds->m_vSources.empty()) { - CMapItemLayerSounds Item; - Item.m_Version = CMapItemLayerSounds::CURRENT_VERSION; - Item.m_Layer.m_Version = 0; // was previously uninitialized, do not rely on it being 0 - Item.m_Layer.m_Flags = pLayerSounds->m_Flags; - Item.m_Layer.m_Type = pLayerSounds->m_Type; - Item.m_Sound = pLayerSounds->m_Sound; - // add the data Item.m_NumSources = pLayerSounds->m_vSources.size(); Item.m_Data = Writer.AddDataSwapped(pLayerSounds->m_vSources.size() * sizeof(CSoundSource), pLayerSounds->m_vSources.data()); + } + else + { + // add dummy data for backwards compatibility + // this allows the layer to be loaded with an empty array since m_NumSources is 0 while saving + CSoundSource Dummy{}; + Item.m_Data = Writer.AddDataSwapped(sizeof(CSoundSource), &Dummy); + } - // save layer name - StrToInts(Item.m_aName, sizeof(Item.m_aName) / sizeof(int), pLayerSounds->m_aName); + // save layer name + StrToInts(Item.m_aName, std::size(Item.m_aName), pLayerSounds->m_aName); - Writer.AddItem(MAPITEMTYPE_LAYER, LayerCount, sizeof(Item), &Item); - GItem.m_NumLayers++; - LayerCount++; - } + // save item + Writer.AddItem(MAPITEMTYPE_LAYER, LayerCount, sizeof(Item), &Item); } + + GItem.m_NumLayers++; + LayerCount++; } Writer.AddItem(MAPITEMTYPE_GROUP, GroupCount, sizeof(GItem), &GItem); @@ -333,7 +338,7 @@ bool CEditorMap::Save(const char *pFileName) Item.m_StartPoint = PointCount; Item.m_NumPoints = m_vpEnvelopes[e]->m_vPoints.size(); Item.m_Synchronized = m_vpEnvelopes[e]->m_Synchronized; - StrToInts(Item.m_aName, sizeof(Item.m_aName) / sizeof(int), m_vpEnvelopes[e]->m_aName); + StrToInts(Item.m_aName, std::size(Item.m_aName), m_vpEnvelopes[e]->m_aName); Writer.AddItem(MAPITEMTYPE_ENVELOPE, e, sizeof(Item), &Item); PointCount += Item.m_NumPoints; @@ -403,11 +408,42 @@ bool CEditorMap::Save(const char *pFileName) return true; } +bool CEditorMap::PerformPreSaveSanityChecks(const std::function &ErrorHandler) +{ + bool Success = true; + char aErrorMessage[256]; + + for(const std::shared_ptr &pImage : m_vpImages) + { + if(!pImage->m_External && pImage->m_pData == nullptr) + { + str_format(aErrorMessage, sizeof(aErrorMessage), "Error: Saving is not possible because the image '%s' could not be loaded. Remove or replace this image.", pImage->m_aName); + ErrorHandler(aErrorMessage); + Success = false; + } + } + + for(const std::shared_ptr &pSound : m_vpSounds) + { + if(pSound->m_pData == nullptr) + { + str_format(aErrorMessage, sizeof(aErrorMessage), "Error: Saving is not possible because the sound '%s' could not be loaded. Remove or replace this sound.", pSound->m_aName); + ErrorHandler(aErrorMessage); + Success = false; + } + } + + return Success; +} + bool CEditorMap::Load(const char *pFileName, int StorageType, const std::function &ErrorHandler) { CDataFileReader DataFile; if(!DataFile.Open(m_pEditor->Storage(), pFileName, StorageType)) + { + ErrorHandler("Error: Failed to open map file. See local console for details."); return false; + } // check version const CMapItemVersion *pItemVersion = static_cast(DataFile.FindItem(MAPITEMTYPE_VERSION, 0)); @@ -426,9 +462,9 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio for(int i = Start; i < Start + Num; i++) { int ItemSize = DataFile.GetItemSize(Start); - int ItemID; - CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)DataFile.GetItem(i, nullptr, &ItemID); - if(!pItem || ItemID != 0) + int ItemId; + CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)DataFile.GetItem(i, nullptr, &ItemId); + if(!pItem || ItemId != 0) continue; const auto &&ReadStringInfo = [&](int Index, char *pBuffer, size_t BufferSize, const char *pErrorContext) { @@ -491,43 +527,55 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio else str_copy(pImg->m_aName, pName); - const CImageInfo::EImageFormat Format = pItem->m_Version < CMapItemImage_v2::CURRENT_VERSION ? CImageInfo::FORMAT_RGBA : CImageInfo::ImageFormatFromInt(pItem->m_Format); - if(pImg->m_External || (Format != CImageInfo::FORMAT_RGB && Format != CImageInfo::FORMAT_RGBA)) + if(pItem->m_Version > 1 && pItem->m_MustBe1 != 1) + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "Error: Unsupported image type of image %d '%s'.", i, pImg->m_aName); + ErrorHandler(aBuf); + } + + if(pImg->m_External || (pItem->m_Version > 1 && pItem->m_MustBe1 != 1)) { char aBuf[IO_MAX_PATH_LENGTH]; str_format(aBuf, sizeof(aBuf), "mapres/%s.png", pImg->m_aName); // load external CImageInfo ImgInfo; - if(m_pEditor->Graphics()->LoadPNG(&ImgInfo, aBuf, IStorage::TYPE_ALL)) + if(m_pEditor->Graphics()->LoadPng(ImgInfo, aBuf, IStorage::TYPE_ALL)) { pImg->m_Width = ImgInfo.m_Width; pImg->m_Height = ImgInfo.m_Height; pImg->m_Format = ImgInfo.m_Format; pImg->m_pData = ImgInfo.m_pData; + ConvertToRgba(*pImg); + int TextureLoadFlag = m_pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; - if(ImgInfo.m_Width % 16 != 0 || ImgInfo.m_Height % 16 != 0) + if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0) TextureLoadFlag = 0; - pImg->m_Texture = m_pEditor->Graphics()->LoadTextureRaw(ImgInfo.m_Width, ImgInfo.m_Height, ImgInfo.m_Format, ImgInfo.m_pData, TextureLoadFlag, aBuf); - ImgInfo.m_pData = nullptr; pImg->m_External = 1; + pImg->m_Texture = m_pEditor->Graphics()->LoadTextureRaw(*pImg, TextureLoadFlag, aBuf); + } + else + { + str_format(aBuf, sizeof(aBuf), "Error: Failed to load external image '%s'.", pImg->m_aName); + ErrorHandler(aBuf); } } else { pImg->m_Width = pItem->m_Width; pImg->m_Height = pItem->m_Height; - pImg->m_Format = Format; + pImg->m_Format = CImageInfo::FORMAT_RGBA; // copy image data void *pData = DataFile.GetData(pItem->m_ImageData); - const size_t DataSize = (size_t)pImg->m_Width * pImg->m_Height * CImageInfo::PixelSize(Format); - pImg->m_pData = malloc(DataSize); + const size_t DataSize = pImg->DataSize(); + pImg->m_pData = static_cast(malloc(DataSize)); mem_copy(pImg->m_pData, pData, DataSize); int TextureLoadFlag = m_pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; if(pImg->m_Width % 16 != 0 || pImg->m_Height % 16 != 0) TextureLoadFlag = 0; - pImg->m_Texture = m_pEditor->Graphics()->LoadTextureRaw(pImg->m_Width, pImg->m_Height, pImg->m_Format, pImg->m_pData, TextureLoadFlag); + pImg->m_Texture = m_pEditor->Graphics()->LoadTextureRaw(*pImg, TextureLoadFlag, pImg->m_aName); } // load auto mapper file @@ -570,18 +618,21 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio // load external if(m_pEditor->Storage()->ReadFile(aBuf, IStorage::TYPE_ALL, &pSound->m_pData, &pSound->m_DataSize)) { - pSound->m_SoundID = m_pEditor->Sound()->LoadOpusFromMem(pSound->m_pData, pSound->m_DataSize, true); + pSound->m_SoundId = m_pEditor->Sound()->LoadOpusFromMem(pSound->m_pData, pSound->m_DataSize, true); + } + else + { + str_format(aBuf, sizeof(aBuf), "Error: Failed to load external sound '%s'.", pSound->m_aName); + ErrorHandler(aBuf); } } else { - pSound->m_DataSize = pItem->m_SoundDataSize; - - // copy sample data + pSound->m_DataSize = DataFile.GetDataSize(pItem->m_SoundData); void *pData = DataFile.GetData(pItem->m_SoundData); pSound->m_pData = malloc(pSound->m_DataSize); mem_copy(pSound->m_pData, pData, pSound->m_DataSize); - pSound->m_SoundID = m_pEditor->Sound()->LoadOpusFromMem(pSound->m_pData, pSound->m_DataSize, true); + pSound->m_SoundId = m_pEditor->Sound()->LoadOpusFromMem(pSound->m_pData, pSound->m_DataSize, true); } m_vpSounds.push_back(pSound); @@ -624,7 +675,7 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio // load group name if(pGItem->m_Version >= 3) - IntsToStr(pGItem->m_aName, sizeof(pGroup->m_aName) / sizeof(int), pGroup->m_aName); + IntsToStr(pGItem->m_aName, std::size(pGItem->m_aName), pGroup->m_aName, std::size(pGroup->m_aName)); for(int l = 0; l < pGItem->m_NumLayers; l++) { @@ -700,7 +751,7 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio // load layer name if(pTilemapItem->m_Version >= 3) - IntsToStr(pTilemapItem->m_aName, sizeof(pTiles->m_aName) / sizeof(int), pTiles->m_aName); + IntsToStr(pTilemapItem->m_aName, std::size(pTilemapItem->m_aName), pTiles->m_aName, std::size(pTiles->m_aName)); if(pTiles->m_Tele) { @@ -826,13 +877,17 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio // load layer name if(pQuadsItem->m_Version >= 2) - IntsToStr(pQuadsItem->m_aName, sizeof(pQuads->m_aName) / sizeof(int), pQuads->m_aName); + IntsToStr(pQuadsItem->m_aName, std::size(pQuadsItem->m_aName), pQuads->m_aName, std::size(pQuads->m_aName)); + + if(pQuadsItem->m_NumQuads > 0) + { + void *pData = DataFile.GetDataSwapped(pQuadsItem->m_Data); + pQuads->m_vQuads.resize(pQuadsItem->m_NumQuads); + mem_copy(pQuads->m_vQuads.data(), pData, sizeof(CQuad) * pQuadsItem->m_NumQuads); + DataFile.UnloadData(pQuadsItem->m_Data); + } - void *pData = DataFile.GetDataSwapped(pQuadsItem->m_Data); pGroup->AddLayer(pQuads); - pQuads->m_vQuads.resize(pQuadsItem->m_NumQuads); - mem_copy(pQuads->m_vQuads.data(), pData, sizeof(CQuad) * pQuadsItem->m_NumQuads); - DataFile.UnloadData(pQuadsItem->m_Data); } else if(pLayerItem->m_Type == LAYERTYPE_SOUNDS) { @@ -849,14 +904,18 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio pSounds->m_Sound = -1; // load layer name - IntsToStr(pSoundsItem->m_aName, sizeof(pSounds->m_aName) / sizeof(int), pSounds->m_aName); + IntsToStr(pSoundsItem->m_aName, std::size(pSoundsItem->m_aName), pSounds->m_aName, std::size(pSounds->m_aName)); // load data - void *pData = DataFile.GetDataSwapped(pSoundsItem->m_Data); + if(pSoundsItem->m_NumSources > 0) + { + void *pData = DataFile.GetDataSwapped(pSoundsItem->m_Data); + pSounds->m_vSources.resize(pSoundsItem->m_NumSources); + mem_copy(pSounds->m_vSources.data(), pData, sizeof(CSoundSource) * pSoundsItem->m_NumSources); + DataFile.UnloadData(pSoundsItem->m_Data); + } + pGroup->AddLayer(pSounds); - pSounds->m_vSources.resize(pSoundsItem->m_NumSources); - mem_copy(pSounds->m_vSources.data(), pData, sizeof(CSoundSource) * pSoundsItem->m_NumSources); - DataFile.UnloadData(pSoundsItem->m_Data); } else if(pLayerItem->m_Type == LAYERTYPE_SOUNDS_DEPRECATED) { @@ -874,7 +933,7 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio pSounds->m_Sound = -1; // load layer name - IntsToStr(pSoundsItem->m_aName, sizeof(pSounds->m_aName) / sizeof(int), pSounds->m_aName); + IntsToStr(pSoundsItem->m_aName, std::size(pSoundsItem->m_aName), pSounds->m_aName, std::size(pSounds->m_aName)); // load data CSoundSource_DEPRECATED *pData = (CSoundSource_DEPRECATED *)DataFile.GetDataSwapped(pSoundsItem->m_Data); @@ -916,7 +975,20 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio for(int e = 0; e < EnvNum; e++) { CMapItemEnvelope *pItem = (CMapItemEnvelope *)DataFile.GetItem(EnvStart + e); - std::shared_ptr pEnv = std::make_shared(pItem->m_Channels); + int Channels = pItem->m_Channels; + if(Channels <= 0 || Channels == 2 || Channels > CEnvPoint::MAX_CHANNELS) + { + // Fall back to showing all channels if the number of channels is unsupported + Channels = CEnvPoint::MAX_CHANNELS; + } + if(Channels != pItem->m_Channels) + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "Error: Envelope %d had an invalid number of channels, %d, which was changed to %d.", e, pItem->m_Channels, Channels); + ErrorHandler(aBuf); + } + + std::shared_ptr pEnv = std::make_shared(Channels); pEnv->m_vPoints.resize(pItem->m_NumPoints); for(int p = 0; p < pItem->m_NumPoints; p++) { @@ -928,7 +1000,7 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio mem_copy(&pEnv->m_vPoints[p].m_Bezier, pPointBezier, sizeof(CEnvPointBezier)); } if(pItem->m_aName[0] != -1) // compatibility with old maps - IntsToStr(pItem->m_aName, sizeof(pItem->m_aName) / sizeof(int), pEnv->m_aName); + IntsToStr(pItem->m_aName, std::size(pItem->m_aName), pEnv->m_aName, std::size(pEnv->m_aName)); m_vpEnvelopes.push_back(pEnv); if(pItem->m_Version >= CMapItemEnvelope_v2::CURRENT_VERSION) pEnv->m_Synchronized = pItem->m_Synchronized; @@ -996,7 +1068,7 @@ void CEditorMap::PerformSanityChecks(const std::functionm_Image = -1; char aBuf[IO_MAX_PATH_LENGTH + 128]; - str_format(aBuf, sizeof(aBuf), "Error: The image '%s' (size %dx%d) has a width or height that is not divisible by 16 and therefore cannot be used for tile layers. The image of layer #%" PRIzu " '%s' in group #%" PRIzu " '%s' has been unset.", pImage->m_aName, pImage->m_Width, pImage->m_Height, LayerIndex, pLayer->m_aName, GroupIndex, pGroup->m_aName); + str_format(aBuf, sizeof(aBuf), "Error: The image '%s' (size %" PRIzu "x%" PRIzu ") has a width or height that is not divisible by 16 and therefore cannot be used for tile layers. The image of layer #%" PRIzu " '%s' in group #%" PRIzu " '%s' has been unset.", pImage->m_aName, pImage->m_Width, pImage->m_Height, LayerIndex, pLayer->m_aName, GroupIndex, pGroup->m_aName); ErrorHandler(aBuf); } } diff --git a/src/game/editor/mapitems/sound.cpp b/src/game/editor/mapitems/sound.cpp index 6465f915b4..33b260091e 100644 --- a/src/game/editor/mapitems/sound.cpp +++ b/src/game/editor/mapitems/sound.cpp @@ -4,12 +4,12 @@ CEditorSound::CEditorSound(CEditor *pEditor) { - Init(pEditor); + OnInit(pEditor); } CEditorSound::~CEditorSound() { - Sound()->UnloadSample(m_SoundID); + Sound()->UnloadSample(m_SoundId); free(m_pData); m_pData = nullptr; } diff --git a/src/game/editor/mapitems/sound.h b/src/game/editor/mapitems/sound.h index 28b222972c..360d545468 100644 --- a/src/game/editor/mapitems/sound.h +++ b/src/game/editor/mapitems/sound.h @@ -10,7 +10,7 @@ class CEditorSound : public CEditorComponent explicit CEditorSound(CEditor *pEditor); ~CEditorSound(); - int m_SoundID = 0; + int m_SoundId = -1; char m_aName[IO_MAX_PATH_LENGTH] = ""; void *m_pData = nullptr; diff --git a/src/game/editor/popups.cpp b/src/game/editor/popups.cpp index e9bcb721c4..579954a5cc 100644 --- a/src/game/editor/popups.cpp +++ b/src/game/editor/popups.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -18,7 +19,9 @@ #include "editor.h" #include "editor_actions.h" -CUI::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect View, bool Active) +using namespace FontIcons; + +CUi::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -29,7 +32,6 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect Vie static int s_OpenButton = 0; static int s_OpenCurrentMapButton = 0; static int s_AppendButton = 0; - static int s_MapInfoButton = 0; static int s_ExitButton = 0; CUIRect Slot; @@ -46,7 +48,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect Vie pEditor->Reset(); pEditor->m_aFileName[0] = 0; } - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(10.0f, nullptr, &View); @@ -60,23 +62,15 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect Vie } else pEditor->InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_MAP, "Load map", "Load", "maps", false, CEditor::CallbackOpenMap, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(2.0f, nullptr, &View); View.HSplitTop(12.0f, &Slot, &View); - if(pEditor->DoButton_MenuItem(&s_OpenCurrentMapButton, "Load Current Map", 0, &Slot, 0, "Opens the current in game map for editing (ctrl+alt+l)")) + if(pEditor->DoButton_MenuItem(&s_OpenCurrentMapButton, pEditor->m_QuickActionLoadCurrentMap.Label(), 0, &Slot, 0, pEditor->m_QuickActionLoadCurrentMap.Description())) { - if(pEditor->HasUnsavedData()) - { - pEditor->m_PopupEventType = POPEVENT_LOADCURRENT; - pEditor->m_PopupEventActivated = true; - } - else - { - pEditor->LoadCurrentMap(); - } - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionLoadCurrentMap.Call(); + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(10.0f, nullptr, &View); @@ -84,7 +78,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect Vie if(pEditor->DoButton_MenuItem(&s_AppendButton, "Append", 0, &Slot, 0, "Opens a map and adds everything from that map to the current one (ctrl+a)")) { pEditor->InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_MAP, "Append map", "Append", "maps", false, CEditor::CallbackAppendMap, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(10.0f, nullptr, &View); @@ -99,15 +93,15 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect Vie } else pEditor->InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_MAP, "Save map", "Save", "maps", false, CEditor::CallbackSaveMap, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(2.0f, nullptr, &View); View.HSplitTop(12.0f, &Slot, &View); - if(pEditor->DoButton_MenuItem(&s_SaveAsButton, "Save As", 0, &Slot, 0, "Saves the current map under a new name (ctrl+shift+s)")) + if(pEditor->DoButton_MenuItem(&s_SaveAsButton, pEditor->m_QuickActionSaveAs.Label(), 0, &Slot, 0, pEditor->m_QuickActionSaveAs.Description())) { - pEditor->InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_MAP, "Save map", "Save", "maps", true, CEditor::CallbackSaveMap, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionSaveAs.Call(); + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(2.0f, nullptr, &View); @@ -115,21 +109,15 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect Vie if(pEditor->DoButton_MenuItem(&s_SaveCopyButton, "Save Copy", 0, &Slot, 0, "Saves a copy of the current map under a new name (ctrl+shift+alt+s)")) { pEditor->InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_MAP, "Save map", "Save", "maps", true, CEditor::CallbackSaveCopyMap, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(10.0f, nullptr, &View); View.HSplitTop(12.0f, &Slot, &View); - if(pEditor->DoButton_MenuItem(&s_MapInfoButton, "Map details", 0, &Slot, 0, "Adjust the map details of the current map")) + if(pEditor->DoButton_MenuItem(&pEditor->m_QuickActionMapDetails, pEditor->m_QuickActionMapDetails.Label(), 0, &Slot, 0, pEditor->m_QuickActionMapDetails.Description())) { - const CUIRect *pScreen = pEditor->UI()->Screen(); - pEditor->m_Map.m_MapInfoTmp.Copy(pEditor->m_Map.m_MapInfo); - static SPopupMenuId s_PopupMapInfoId; - constexpr float PopupWidth = 400.0f; - constexpr float PopupHeight = 170.0f; - pEditor->UI()->DoPopupMenu(&s_PopupMapInfoId, pScreen->w / 2.0f - PopupWidth / 2.0f, pScreen->h / 2.0f - PopupHeight / 2.0f, PopupWidth, PopupHeight, pEditor, PopupMapInfo); - pEditor->UI()->SetActiveItem(nullptr); - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionMapDetails.Call(); + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(10.0f, nullptr, &View); @@ -146,33 +134,33 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect Vie pEditor->OnClose(); g_Config.m_ClEditor = 0; } - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupMenuTools(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupMenuTools(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); CUIRect Slot; View.HSplitTop(12.0f, &Slot, &View); static int s_RemoveUnusedEnvelopesButton = 0; - static CUI::SConfirmPopupContext s_ConfirmPopupContext; + static CUi::SConfirmPopupContext s_ConfirmPopupContext; if(pEditor->DoButton_MenuItem(&s_RemoveUnusedEnvelopesButton, "Remove unused envelopes", 0, &Slot, 0, "Removes all unused envelopes from the map")) { s_ConfirmPopupContext.Reset(); s_ConfirmPopupContext.YesNoButtons(); str_copy(s_ConfirmPopupContext.m_aMessage, "Are you sure that you want to remove all unused envelopes from this map?"); - pEditor->UI()->ShowPopupConfirm(Slot.x + Slot.w, Slot.y, &s_ConfirmPopupContext); + pEditor->Ui()->ShowPopupConfirm(Slot.x + Slot.w, Slot.y, &s_ConfirmPopupContext); } - if(s_ConfirmPopupContext.m_Result == CUI::SConfirmPopupContext::CONFIRMED) + if(s_ConfirmPopupContext.m_Result == CUi::SConfirmPopupContext::CONFIRMED) pEditor->RemoveUnusedEnvelopes(); - if(s_ConfirmPopupContext.m_Result != CUI::SConfirmPopupContext::UNSET) + if(s_ConfirmPopupContext.m_Result != CUi::SConfirmPopupContext::UNSET) { s_ConfirmPopupContext.Reset(); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } static int s_BorderButton = 0; @@ -188,10 +176,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuTools(void *pContext, CUIRect Vi } else { - static CUI::SMessagePopupContext s_MessagePopupContext; + static CUi::SMessagePopupContext s_MessagePopupContext; s_MessagePopupContext.DefaultColor(pEditor->m_pTextRender); str_copy(s_MessagePopupContext.m_aMessage, "No tile layer selected"); - pEditor->UI()->ShowPopupMessage(Slot.x, Slot.y + Slot.h, &s_MessagePopupContext); + pEditor->Ui()->ShowPopupMessage(Slot.x, Slot.y + Slot.h, &s_MessagePopupContext); } } @@ -201,7 +189,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuTools(void *pContext, CUIRect Vi if(pEditor->DoButton_MenuItem(&s_GotoButton, "Goto XY", 0, &Slot, 0, "Go to a specified coordinate point on the map")) { static SPopupMenuId s_PopupGotoId; - pEditor->UI()->DoPopupMenu(&s_PopupGotoId, Slot.x, Slot.y + Slot.h, 120, 52, pEditor, PopupGoto); + pEditor->Ui()->DoPopupMenu(&s_PopupGotoId, Slot.x, Slot.y + Slot.h, 120, 52, pEditor, PopupGoto); } static int s_TileartButton = 0; @@ -210,10 +198,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuTools(void *pContext, CUIRect Vi if(pEditor->DoButton_MenuItem(&s_TileartButton, "Add tileart", 0, &Slot, 0, "Generate tileart from image")) { pEditor->InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_IMG, "Add tileart", "Open", "mapres", false, CallbackAddTileart, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } static int EntitiesListdirCallback(const char *pName, int IsDir, int StorageType, void *pUser) @@ -228,23 +216,24 @@ static int EntitiesListdirCallback(const char *pName, int IsDir, int StorageType return 0; } -CUI::EPopupMenuFunctionResult CEditor::PopupMenuSettings(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupMenuSettings(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); CUIRect Slot; View.HSplitTop(12.0f, &Slot, &View); - static int s_EntitiesButtonID = 0; + static int s_EntitiesButtonId = 0; char aButtonText[64]; str_format(aButtonText, sizeof(aButtonText), "Entities: %s", pEditor->m_SelectEntitiesImage.c_str()); - if(pEditor->DoButton_MenuItem(&s_EntitiesButtonID, aButtonText, 0, &Slot, 0, "Choose game layer entities image for different gametypes")) + if(pEditor->DoButton_MenuItem(&s_EntitiesButtonId, aButtonText, 0, &Slot, 0, "Choose game layer entities image for different gametypes")) { pEditor->m_vSelectEntitiesFiles.clear(); pEditor->Storage()->ListDirectory(IStorage::TYPE_ALL, "editor/entities", EntitiesListdirCallback, pEditor); std::sort(pEditor->m_vSelectEntitiesFiles.begin(), pEditor->m_vSelectEntitiesFiles.end()); + pEditor->m_vSelectEntitiesFiles.emplace_back("Custom…"); static SPopupMenuId s_PopupEntitiesId; - pEditor->UI()->DoPopupMenu(&s_PopupEntitiesId, Slot.x, Slot.y + Slot.h, 250, pEditor->m_vSelectEntitiesFiles.size() * 14.0f + 10.0f, pEditor, PopupEntities); + pEditor->Ui()->DoPopupMenu(&s_PopupEntitiesId, Slot.x, Slot.y + Slot.h, 250, pEditor->m_vSelectEntitiesFiles.size() * 14.0f + 10.0f, pEditor, PopupEntities); } View.HSplitTop(2.0f, nullptr, &View); @@ -257,14 +246,14 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuSettings(void *pContext, CUIRect CUIRect No, Yes; Selector.VSplitMid(&No, &Yes); - pEditor->UI()->DoLabel(&Label, "Brush coloring", 10.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Brush coloring", 10.0f, TEXTALIGN_ML); static int s_ButtonNo = 0; static int s_ButtonYes = 0; - if(pEditor->DoButton_ButtonDec(&s_ButtonNo, "No", !pEditor->m_BrushColorEnabled, &No, 0, "Disable brush coloring")) + if(pEditor->DoButton_Ex(&s_ButtonNo, "No", !pEditor->m_BrushColorEnabled, &No, 0, "Disable brush coloring", IGraphics::CORNER_L)) { pEditor->m_BrushColorEnabled = false; } - if(pEditor->DoButton_ButtonInc(&s_ButtonYes, "Yes", pEditor->m_BrushColorEnabled, &Yes, 0, "Enable brush coloring")) + if(pEditor->DoButton_Ex(&s_ButtonYes, "Yes", pEditor->m_BrushColorEnabled, &Yes, 0, "Enable brush coloring", IGraphics::CORNER_R)) { pEditor->m_BrushColorEnabled = true; } @@ -280,16 +269,16 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuSettings(void *pContext, CUIRect CUIRect No, Yes; Selector.VSplitMid(&No, &Yes); - pEditor->UI()->DoLabel(&Label, "Allow unused", 10.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Allow unused", 10.0f, TEXTALIGN_ML); if(pEditor->m_AllowPlaceUnusedTiles != -1) { static int s_ButtonNo = 0; static int s_ButtonYes = 0; - if(pEditor->DoButton_ButtonDec(&s_ButtonNo, "No", !pEditor->m_AllowPlaceUnusedTiles, &No, 0, "[ctrl+u] Disallow placing unused tiles")) + if(pEditor->DoButton_Ex(&s_ButtonNo, "No", !pEditor->m_AllowPlaceUnusedTiles, &No, 0, "[ctrl+u] Disallow placing unused tiles", IGraphics::CORNER_L)) { pEditor->m_AllowPlaceUnusedTiles = false; } - if(pEditor->DoButton_ButtonInc(&s_ButtonYes, "Yes", pEditor->m_AllowPlaceUnusedTiles, &Yes, 0, "[ctrl+u] Allow placing unused tiles")) + if(pEditor->DoButton_Ex(&s_ButtonYes, "Yes", pEditor->m_AllowPlaceUnusedTiles, &Yes, 0, "[ctrl+u] Allow placing unused tiles", IGraphics::CORNER_R)) { pEditor->m_AllowPlaceUnusedTiles = true; } @@ -307,24 +296,24 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuSettings(void *pContext, CUIRect Selector.VSplitLeft(Selector.w / 3.0f, &Off, &Selector); Selector.VSplitMid(&Dec, &Hex); - pEditor->UI()->DoLabel(&Label, "Show Info", 10.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Show Info", 10.0f, TEXTALIGN_ML); static int s_ButtonOff = 0; static int s_ButtonDec = 0; static int s_ButtonHex = 0; - if(pEditor->DoButton_ButtonDec(&s_ButtonOff, "Off", pEditor->m_ShowTileInfo == SHOW_TILE_OFF, &Off, 0, "Do not show tile information")) + CQuickAction *pAction = &pEditor->m_QuickActionShowInfoOff; + if(pEditor->DoButton_Ex(&s_ButtonOff, pAction->LabelShort(), pAction->Active(), &Off, 0, pAction->Description(), IGraphics::CORNER_L)) { - pEditor->m_ShowTileInfo = SHOW_TILE_OFF; - pEditor->m_ShowEnvelopePreview = SHOWENV_NONE; + pAction->Call(); } - if(pEditor->DoButton_Ex(&s_ButtonDec, "Dec", pEditor->m_ShowTileInfo == SHOW_TILE_DECIMAL, &Dec, 0, "[ctrl+i] Show tile information", IGraphics::CORNER_NONE)) + pAction = &pEditor->m_QuickActionShowInfoDec; + if(pEditor->DoButton_Ex(&s_ButtonDec, pAction->LabelShort(), pAction->Active(), &Dec, 0, pAction->Description(), IGraphics::CORNER_NONE)) { - pEditor->m_ShowTileInfo = SHOW_TILE_DECIMAL; - pEditor->m_ShowEnvelopePreview = SHOWENV_NONE; + pAction->Call(); } - if(pEditor->DoButton_ButtonInc(&s_ButtonHex, "Hex", pEditor->m_ShowTileInfo == SHOW_TILE_HEXADECIMAL, &Hex, 0, "[ctrl+shift+i] Show tile information in hexadecimal")) + pAction = &pEditor->m_QuickActionShowInfoHex; + if(pEditor->DoButton_Ex(&s_ButtonHex, pAction->LabelShort(), pAction->Active(), &Hex, 0, pAction->Description(), IGraphics::CORNER_R)) { - pEditor->m_ShowTileInfo = SHOW_TILE_HEXADECIMAL; - pEditor->m_ShowEnvelopePreview = SHOWENV_NONE; + pAction->Call(); } } @@ -338,19 +327,17 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuSettings(void *pContext, CUIRect CUIRect No, Yes; Selector.VSplitMid(&No, &Yes); - pEditor->UI()->DoLabel(&Label, "Align quads", 10.0f, TEXTALIGN_ML); - if(pEditor->m_AllowPlaceUnusedTiles != -1) + pEditor->Ui()->DoLabel(&Label, "Align quads", 10.0f, TEXTALIGN_ML); + + static int s_ButtonNo = 0; + static int s_ButtonYes = 0; + if(pEditor->DoButton_Ex(&s_ButtonNo, "No", !g_Config.m_EdAlignQuads, &No, 0, "Do not perform quad alignment to other quads/points when moving quads", IGraphics::CORNER_L)) { - static int s_ButtonNo = 0; - static int s_ButtonYes = 0; - if(pEditor->DoButton_ButtonDec(&s_ButtonNo, "No", !g_Config.m_EdAlignQuads, &No, 0, "Do not perform quad alignment to other quads/points when moving quads")) - { - g_Config.m_EdAlignQuads = false; - } - if(pEditor->DoButton_ButtonInc(&s_ButtonYes, "Yes", g_Config.m_EdAlignQuads, &Yes, 0, "Allow quad alignment to other quads/points when moving quads")) - { - g_Config.m_EdAlignQuads = true; - } + g_Config.m_EdAlignQuads = false; + } + if(pEditor->DoButton_Ex(&s_ButtonYes, "Yes", g_Config.m_EdAlignQuads, &Yes, 0, "Allow quad alignment to other quads/points when moving quads", IGraphics::CORNER_R)) + { + g_Config.m_EdAlignQuads = true; } } @@ -364,26 +351,72 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuSettings(void *pContext, CUIRect CUIRect No, Yes; Selector.VSplitMid(&No, &Yes); - pEditor->UI()->DoLabel(&Label, "Show quads bounds", 10.0f, TEXTALIGN_ML); - if(pEditor->m_AllowPlaceUnusedTiles != -1) + pEditor->Ui()->DoLabel(&Label, "Show quads bounds", 10.0f, TEXTALIGN_ML); + + static int s_ButtonNo = 0; + static int s_ButtonYes = 0; + if(pEditor->DoButton_Ex(&s_ButtonNo, "No", !g_Config.m_EdShowQuadsRect, &No, 0, "Do not show quad bounds when moving quads", IGraphics::CORNER_L)) { - static int s_ButtonNo = 0; - static int s_ButtonYes = 0; - if(pEditor->DoButton_ButtonDec(&s_ButtonNo, "No", !g_Config.m_EdShowQuadsRect, &No, 0, "Do not show quad bounds when moving quads")) - { - g_Config.m_EdShowQuadsRect = false; - } - if(pEditor->DoButton_ButtonInc(&s_ButtonYes, "Yes", g_Config.m_EdShowQuadsRect, &Yes, 0, "Show quad bounds when moving quads")) - { - g_Config.m_EdShowQuadsRect = true; - } + g_Config.m_EdShowQuadsRect = false; + } + if(pEditor->DoButton_Ex(&s_ButtonYes, "Yes", g_Config.m_EdShowQuadsRect, &Yes, 0, "Show quad bounds when moving quads", IGraphics::CORNER_R)) + { + g_Config.m_EdShowQuadsRect = true; + } + } + + View.HSplitTop(2.0f, nullptr, &View); + View.HSplitTop(12.0f, &Slot, &View); + { + Slot.VMargin(5.0f, &Slot); + + CUIRect Label, Selector; + Slot.VSplitMid(&Label, &Selector); + CUIRect No, Yes; + Selector.VSplitMid(&No, &Yes); + + pEditor->Ui()->DoLabel(&Label, "Auto map reload", 10.0f, TEXTALIGN_ML); + + static int s_ButtonNo = 0; + static int s_ButtonYes = 0; + if(pEditor->DoButton_Ex(&s_ButtonNo, "No", !g_Config.m_EdAutoMapReload, &No, 0, "Do not run 'hot_reload' on the local server while rcon authed on map save", IGraphics::CORNER_L)) + { + g_Config.m_EdAutoMapReload = false; + } + if(pEditor->DoButton_Ex(&s_ButtonYes, "Yes", g_Config.m_EdAutoMapReload, &Yes, 0, "Run 'hot_reload' on the local server while rcon authed on map save", IGraphics::CORNER_R)) + { + g_Config.m_EdAutoMapReload = true; + } + } + + View.HSplitTop(2.0f, nullptr, &View); + View.HSplitTop(12.0f, &Slot, &View); + { + Slot.VMargin(5.0f, &Slot); + + CUIRect Label, Selector; + Slot.VSplitMid(&Label, &Selector); + CUIRect No, Yes; + Selector.VSplitMid(&No, &Yes); + + pEditor->Ui()->DoLabel(&Label, "Select layers by tile", 10.0f, TEXTALIGN_ML); + + static int s_ButtonNo = 0; + static int s_ButtonYes = 0; + if(pEditor->DoButton_Ex(&s_ButtonNo, "No", !g_Config.m_EdLayerSelector, &No, 0, "Do not select layers when Ctrl+right clicking on a tile", IGraphics::CORNER_L)) + { + g_Config.m_EdLayerSelector = false; + } + if(pEditor->DoButton_Ex(&s_ButtonYes, "Yes", g_Config.m_EdLayerSelector, &Yes, 0, "Select layers when Ctrl+right clicking on a tile", IGraphics::CORNER_R)) + { + g_Config.m_EdLayerSelector = true; } } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -400,7 +433,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, true)); pEditor->m_Map.DeleteGroup(pEditor->m_SelectedGroup); pEditor->m_SelectedGroup = maximum(0, pEditor->m_SelectedGroup - 1); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } } else @@ -409,10 +442,14 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, { // gather all tile layers std::vector> vpLayers; - for(auto &pLayer : pEditor->m_Map.m_pGameGroup->m_vpLayers) + int GameLayerIndex = -1; + for(int LayerIndex = 0; LayerIndex < (int)pEditor->m_Map.m_pGameGroup->m_vpLayers.size(); LayerIndex++) { + auto &pLayer = pEditor->m_Map.m_pGameGroup->m_vpLayers.at(LayerIndex); if(pLayer != pEditor->m_Map.m_pGameLayer && pLayer->m_Type == LAYERTYPE_TILES) vpLayers.push_back(std::static_pointer_cast(pLayer)); + else if(pLayer == pEditor->m_Map.m_pGameLayer) + GameLayerIndex = LayerIndex; } // search for unneeded game tiles @@ -434,15 +471,31 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, } } - if(!Found) + CTile Tile = pGameLayer->GetTile(x, y); + if(!Found && Tile.m_Index != TILE_AIR) { - pGameLayer->m_pTiles[y * pGameLayer->m_Width + x].m_Index = TILE_AIR; + Tile.m_Index = TILE_AIR; + pGameLayer->SetTile(x, y, Tile); pEditor->m_Map.OnModify(); } } } - return CUI::POPUP_CLOSE_CURRENT; + if(!pGameLayer->m_TilesHistory.empty()) + { + if(GameLayerIndex == -1) + { + dbg_msg("editor", "failed to record action (GameLayerIndex not found)"); + } + else + { + // record undo + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, GameLayerIndex, "Clean up game tiles", pGameLayer->m_TilesHistory)); + } + pGameLayer->ClearHistory(); + } + + return CUi::POPUP_CLOSE_CURRENT; } } @@ -451,17 +504,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, // new tele layer View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Button); - static int s_NewTeleLayerButton = 0; - if(pEditor->DoButton_Editor(&s_NewTeleLayerButton, "Add tele layer", 0, &Button, 0, "Creates a new tele layer")) + if(pEditor->DoButton_Editor(&pEditor->m_QuickActionAddTeleLayer, pEditor->m_QuickActionAddTeleLayer.Label(), 0, &Button, 0, pEditor->m_QuickActionAddTeleLayer.Description())) { - std::shared_ptr pTeleLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); - pEditor->m_Map.MakeTeleLayer(pTeleLayer); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pTeleLayer); - int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; - pEditor->SelectLayer(LayerIndex); - pEditor->m_pBrush->Clear(); - pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionAddTeleLayer.Call(); + return CUi::POPUP_CLOSE_CURRENT; } } @@ -470,17 +516,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, // new speedup layer View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Button); - static int s_NewSpeedupLayerButton = 0; - if(pEditor->DoButton_Editor(&s_NewSpeedupLayerButton, "Add speedup layer", 0, &Button, 0, "Creates a new speedup layer")) + if(pEditor->DoButton_Editor(&pEditor->m_QuickActionAddSpeedupLayer, pEditor->m_QuickActionAddSpeedupLayer.Label(), 0, &Button, 0, pEditor->m_QuickActionAddSpeedupLayer.Description())) { - std::shared_ptr pSpeedupLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); - pEditor->m_Map.MakeSpeedupLayer(pSpeedupLayer); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pSpeedupLayer); - int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; - pEditor->SelectLayer(LayerIndex); - pEditor->m_pBrush->Clear(); - pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionAddSpeedupLayer.Call(); + return CUi::POPUP_CLOSE_CURRENT; } } @@ -489,17 +528,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, // new tune layer View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Button); - static int s_NewTuneLayerButton = 0; - if(pEditor->DoButton_Editor(&s_NewTuneLayerButton, "Add tune layer", 0, &Button, 0, "Creates a new tuning layer")) + if(pEditor->DoButton_Editor(&pEditor->m_QuickActionAddTuneLayer, pEditor->m_QuickActionAddTuneLayer.Label(), 0, &Button, 0, pEditor->m_QuickActionAddTuneLayer.Description())) { - std::shared_ptr pTuneLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); - pEditor->m_Map.MakeTuneLayer(pTuneLayer); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pTuneLayer); - int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; - pEditor->SelectLayer(LayerIndex); - pEditor->m_pBrush->Clear(); - pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionAddTuneLayer.Call(); + return CUi::POPUP_CLOSE_CURRENT; } } @@ -508,17 +540,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, // new front layer View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Button); - static int s_NewFrontLayerButton = 0; - if(pEditor->DoButton_Editor(&s_NewFrontLayerButton, "Add front layer", 0, &Button, 0, "Creates a new item layer")) + if(pEditor->DoButton_Editor(&pEditor->m_QuickActionAddFrontLayer, pEditor->m_QuickActionAddFrontLayer.Label(), 0, &Button, 0, pEditor->m_QuickActionAddFrontLayer.Description())) { - std::shared_ptr pFrontLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); - pEditor->m_Map.MakeFrontLayer(pFrontLayer); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pFrontLayer); - int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; - pEditor->SelectLayer(LayerIndex); - pEditor->m_pBrush->Clear(); - pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionAddFrontLayer.Call(); + return CUi::POPUP_CLOSE_CURRENT; } } @@ -527,64 +552,38 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, // new Switch layer View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Button); - static int s_NewSwitchLayerButton = 0; - if(pEditor->DoButton_Editor(&s_NewSwitchLayerButton, "Add switch layer", 0, &Button, 0, "Creates a new switch layer")) + if(pEditor->DoButton_Editor(&pEditor->m_QuickActionAddSwitchLayer, pEditor->m_QuickActionAddSwitchLayer.Label(), 0, &Button, 0, pEditor->m_QuickActionAddSwitchLayer.Description())) { - std::shared_ptr pSwitchLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); - pEditor->m_Map.MakeSwitchLayer(pSwitchLayer); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pSwitchLayer); - int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; - pEditor->SelectLayer(LayerIndex); - pEditor->m_pBrush->Clear(); - pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionAddSwitchLayer.Call(); + return CUi::POPUP_CLOSE_CURRENT; } } // new quad layer View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Button); - static int s_NewQuadLayerButton = 0; - if(pEditor->DoButton_Editor(&s_NewQuadLayerButton, "Add quads layer", 0, &Button, 0, "Creates a new quad layer")) + if(pEditor->DoButton_Editor(&pEditor->m_QuickActionAddQuadsLayer, pEditor->m_QuickActionAddQuadsLayer.Label(), 0, &Button, 0, pEditor->m_QuickActionAddQuadsLayer.Description())) { - std::shared_ptr pQuadLayer = std::make_shared(pEditor); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pQuadLayer); - int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; - pEditor->SelectLayer(LayerIndex); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_Collapse = false; - pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionAddQuadsLayer.Call(); + return CUi::POPUP_CLOSE_CURRENT; } // new tile layer View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Button); - static int s_NewTileLayerButton = 0; - if(pEditor->DoButton_Editor(&s_NewTileLayerButton, "Add tile layer", 0, &Button, 0, "Creates a new tile layer")) + if(pEditor->DoButton_Editor(&pEditor->m_QuickActionAddTileLayer, pEditor->m_QuickActionAddTileLayer.Label(), 0, &Button, 0, pEditor->m_QuickActionAddTileLayer.Description())) { - std::shared_ptr pTileLayer = std::make_shared(pEditor, pEditor->m_Map.m_pGameLayer->m_Width, pEditor->m_Map.m_pGameLayer->m_Height); - pTileLayer->m_pEditor = pEditor; - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pTileLayer); - int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; - pEditor->SelectLayer(LayerIndex); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_Collapse = false; - pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionAddTileLayer.Call(); + return CUi::POPUP_CLOSE_CURRENT; } // new sound layer View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Button); - static int s_NewSoundLayerButton = 0; - if(pEditor->DoButton_Editor(&s_NewSoundLayerButton, "Add sound layer", 0, &Button, 0, "Creates a new sound layer")) + if(pEditor->DoButton_Editor(&pEditor->m_QuickActionAddSoundLayer, pEditor->m_QuickActionAddSoundLayer.Label(), 0, &Button, 0, pEditor->m_QuickActionAddSoundLayer.Description())) { - std::shared_ptr pSoundLayer = std::make_shared(pEditor); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pSoundLayer); - int LayerIndex = pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1; - pEditor->SelectLayer(LayerIndex); - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_Collapse = false; - pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, LayerIndex)); - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionAddSoundLayer.Call(); + return CUi::POPUP_CLOSE_CURRENT; } // group name @@ -592,7 +591,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, { View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Button); - pEditor->UI()->DoLabel(&Button, "Name:", 10.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Button, "Name:", 10.0f, TEXTALIGN_ML); Button.VSplitLeft(40.0f, nullptr, &Button); static CLineInput s_NameInput; s_NameInput.SetBuffer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_aName, sizeof(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_aName)); @@ -601,16 +600,16 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, } CProperty aProps[] = { - {"Order", pEditor->m_SelectedGroup, PROPTYPE_INT_STEP, 0, (int)pEditor->m_Map.m_vpGroups.size() - 1}, - {"Pos X", -pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetX, PROPTYPE_INT_SCROLL, -1000000, 1000000}, - {"Pos Y", -pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetY, PROPTYPE_INT_SCROLL, -1000000, 1000000}, - {"Para X", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ParallaxX, PROPTYPE_INT_SCROLL, -1000000, 1000000}, - {"Para Y", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ParallaxY, PROPTYPE_INT_SCROLL, -1000000, 1000000}, + {"Order", pEditor->m_SelectedGroup, PROPTYPE_INT, 0, (int)pEditor->m_Map.m_vpGroups.size() - 1}, + {"Pos X", -pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetX, PROPTYPE_INT, -1000000, 1000000}, + {"Pos Y", -pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetY, PROPTYPE_INT, -1000000, 1000000}, + {"Para X", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ParallaxX, PROPTYPE_INT, -1000000, 1000000}, + {"Para Y", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ParallaxY, PROPTYPE_INT, -1000000, 1000000}, {"Use Clipping", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_UseClipping, PROPTYPE_BOOL, 0, 1}, - {"Clip X", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipX, PROPTYPE_INT_SCROLL, -1000000, 1000000}, - {"Clip Y", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipY, PROPTYPE_INT_SCROLL, -1000000, 1000000}, - {"Clip W", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipW, PROPTYPE_INT_SCROLL, 0, 1000000}, - {"Clip H", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipH, PROPTYPE_INT_SCROLL, 0, 1000000}, + {"Clip X", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipX, PROPTYPE_INT, -1000000, 1000000}, + {"Clip Y", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipY, PROPTYPE_INT, -1000000, 1000000}, + {"Clip W", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipW, PROPTYPE_INT, 0, 1000000}, + {"Clip H", pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipH, PROPTYPE_INT, 0, 1000000}, {nullptr}, }; @@ -621,7 +620,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, static int s_aIds[(int)EGroupProp::NUM_PROPS] = {0}; int NewVal = 0; auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); - if(Prop != EGroupProp::PROP_NONE) + if(Prop != EGroupProp::PROP_NONE && (State == EEditState::END || State == EEditState::ONE_GO)) { pEditor->m_Map.OnModify(); } @@ -647,11 +646,11 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, } else if(Prop == EGroupProp::PROP_POS_X) { - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetX = NewVal; + pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetX = -NewVal; } else if(Prop == EGroupProp::PROP_POS_Y) { - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetY = NewVal; + pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_OffsetY = -NewVal; } else if(Prop == EGroupProp::PROP_USE_CLIPPING) { @@ -677,10 +676,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, s_Tracker.End(Prop, State); - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, bool Active) { SLayerPopupContext *pPopup = (SLayerPopupContext *)pContext; CEditor *pEditor = pPopup->m_pEditor; @@ -700,24 +699,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, { CUIRect DeleteButton; View.HSplitBottom(12.0f, &View, &DeleteButton); - static int s_DeleteButton = 0; - if(pEditor->DoButton_Editor(&s_DeleteButton, "Delete layer", 0, &DeleteButton, 0, "Deletes the layer")) + if(pEditor->DoButton_Editor(&pEditor->m_QuickActionDeleteLayer, pEditor->m_QuickActionDeleteLayer.Label(), 0, &DeleteButton, 0, pEditor->m_QuickActionDeleteLayer.Description())) { - pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, pEditor->m_vSelectedLayers[0])); - - if(pCurrentLayer == pEditor->m_Map.m_pFrontLayer) - pEditor->m_Map.m_pFrontLayer = nullptr; - if(pCurrentLayer == pEditor->m_Map.m_pTeleLayer) - pEditor->m_Map.m_pTeleLayer = nullptr; - if(pCurrentLayer == pEditor->m_Map.m_pSpeedupLayer) - pEditor->m_Map.m_pSpeedupLayer = nullptr; - if(pCurrentLayer == pEditor->m_Map.m_pSwitchLayer) - pEditor->m_Map.m_pSwitchLayer = nullptr; - if(pCurrentLayer == pEditor->m_Map.m_pTuneLayer) - pEditor->m_Map.m_pTuneLayer = nullptr; - pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->DeleteLayer(pEditor->m_vSelectedLayers[0]); - - return CUI::POPUP_CLOSE_CURRENT; + pEditor->m_QuickActionDeleteLayer.Call(); + return CUi::POPUP_CLOSE_CURRENT; } } @@ -732,7 +717,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->DuplicateLayer(pEditor->m_vSelectedLayers[0]); pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, pEditor->m_SelectedGroup, pEditor->m_vSelectedLayers[0] + 1, true)); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } } @@ -743,7 +728,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, View.HSplitBottom(5.0f, &View, nullptr); View.HSplitBottom(12.0f, &View, &Label); Label.VSplitLeft(40.0f, &Label, &EditBox); - pEditor->UI()->DoLabel(&Label, "Name:", 10.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Name:", 10.0f, TEXTALIGN_ML); static CLineInput s_NameInput; s_NameInput.SetBuffer(pCurrentLayer->m_aName, sizeof(pCurrentLayer->m_aName)); if(pEditor->DoEditBox(&s_NameInput, &EditBox, 10.0f)) @@ -755,8 +740,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, View.HSplitBottom(10.0f, &View, nullptr); CProperty aProps[] = { - {"Group", pEditor->m_SelectedGroup, PROPTYPE_INT_STEP, 0, (int)pEditor->m_Map.m_vpGroups.size() - 1}, - {"Order", pEditor->m_vSelectedLayers[0], PROPTYPE_INT_STEP, 0, (int)pCurrentGroup->m_vpLayers.size() - 1}, + {"Group", pEditor->m_SelectedGroup, PROPTYPE_INT, 0, (int)pEditor->m_Map.m_vpGroups.size() - 1}, + {"Order", pEditor->m_vSelectedLayers[0], PROPTYPE_INT, 0, (int)pCurrentGroup->m_vpLayers.size() - 1}, {"Detail", pCurrentLayer->m_Flags & LAYERFLAG_DETAIL, PROPTYPE_BOOL, 0, 1}, {nullptr}, }; @@ -768,16 +753,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, aProps[2].m_Type = PROPTYPE_NULL; } - // don't use Detail from the selection if this is a sound layer - if(pCurrentLayer->m_Type == LAYERTYPE_SOUNDS) - { - aProps[2].m_Type = PROPTYPE_NULL; - } - static int s_aIds[(int)ELayerProp::NUM_PROPS] = {0}; int NewVal = 0; auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); - if(Prop != ELayerProp::PROP_NONE) + if(Prop != ELayerProp::PROP_NONE && (State == EEditState::END || State == EEditState::ONE_GO)) { pEditor->m_Map.OnModify(); } @@ -792,7 +771,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, } else if(Prop == ELayerProp::PROP_GROUP) { - if(NewVal >= 0 && (size_t)NewVal < pEditor->m_Map.m_vpGroups.size()) + if(NewVal >= 0 && (size_t)NewVal < pEditor->m_Map.m_vpGroups.size() && NewVal != pEditor->m_SelectedGroup) { auto Position = std::find(pCurrentGroup->m_vpLayers.begin(), pCurrentGroup->m_vpLayers.end(), pCurrentLayer); if(Position != pCurrentGroup->m_vpLayers.end()) @@ -814,10 +793,12 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, return pCurrentLayer->RenderProperties(&View); } -CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); std::vector vpQuads = pEditor->GetSelectedQuads(); + if(pEditor->m_SelectedQuadIndex < 0 || pEditor->m_SelectedQuadIndex >= (int)vpQuads.size()) + return CUi::POPUP_CLOSE_CURRENT; CQuad *pCurrentQuad = vpQuads[pEditor->m_SelectedQuadIndex]; std::shared_ptr pLayer = std::static_pointer_cast(pEditor->GetSelectedLayerType(0, LAYERTYPE_QUADS)); @@ -833,7 +814,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b pEditor->m_Map.OnModify(); pEditor->DeleteSelectedQuads(); } - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } // aspect ratio button @@ -875,8 +856,39 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b } pEditor->m_QuadTracker.EndQuadTrack(); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; + } + } + + // center pivot button + View.HSplitBottom(6.0f, &View, nullptr); + View.HSplitBottom(12.0f, &View, &Button); + static int s_CenterButton = 0; + if(pEditor->DoButton_Editor(&s_CenterButton, "Center pivot", 0, &Button, 0, "Centers the pivot of the current quad")) + { + pEditor->m_QuadTracker.BeginQuadTrack(pLayer, pEditor->m_vSelectedQuads); + int Top = pCurrentQuad->m_aPoints[0].y; + int Left = pCurrentQuad->m_aPoints[0].x; + int Bottom = pCurrentQuad->m_aPoints[0].y; + int Right = pCurrentQuad->m_aPoints[0].x; + + for(int k = 1; k < 4; k++) + { + if(pCurrentQuad->m_aPoints[k].y < Top) + Top = pCurrentQuad->m_aPoints[k].y; + if(pCurrentQuad->m_aPoints[k].x < Left) + Left = pCurrentQuad->m_aPoints[k].x; + if(pCurrentQuad->m_aPoints[k].y > Bottom) + Bottom = pCurrentQuad->m_aPoints[k].y; + if(pCurrentQuad->m_aPoints[k].x > Right) + Right = pCurrentQuad->m_aPoints[k].x; } + + pCurrentQuad->m_aPoints[4].x = Left + (Right - Left) / 2; + pCurrentQuad->m_aPoints[4].y = Top + (Bottom - Top) / 2; + pEditor->m_QuadTracker.EndQuadTrack(); + pEditor->m_Map.OnModify(); + return CUi::POPUP_CLOSE_CURRENT; } // align button @@ -896,7 +908,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b pEditor->m_Map.OnModify(); } pEditor->m_QuadTracker.EndQuadTrack(); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } // square button @@ -936,7 +948,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b pEditor->m_Map.OnModify(); } pEditor->m_QuadTracker.EndQuadTrack(); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } // slice button @@ -947,32 +959,27 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b { pEditor->m_QuadKnifeCount = 0; pEditor->m_QuadKnifeActive = true; - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } const int NumQuads = pLayer ? (int)pLayer->m_vQuads.size() : 0; CProperty aProps[] = { - {"Order", pEditor->m_vSelectedQuads[pEditor->m_SelectedQuadIndex], PROPTYPE_INT_STEP, 0, NumQuads}, - {"Pos X", fx2i(pCurrentQuad->m_aPoints[4].x), PROPTYPE_INT_SCROLL, -1000000, 1000000}, - {"Pos Y", fx2i(pCurrentQuad->m_aPoints[4].y), PROPTYPE_INT_SCROLL, -1000000, 1000000}, + {"Order", pEditor->m_vSelectedQuads[pEditor->m_SelectedQuadIndex], PROPTYPE_INT, 0, NumQuads}, + {"Pos X", fx2i(pCurrentQuad->m_aPoints[4].x), PROPTYPE_INT, -1000000, 1000000}, + {"Pos Y", fx2i(pCurrentQuad->m_aPoints[4].y), PROPTYPE_INT, -1000000, 1000000}, {"Pos. Env", pCurrentQuad->m_PosEnv + 1, PROPTYPE_ENVELOPE, 0, 0}, - {"Pos. TO", pCurrentQuad->m_PosEnvOffset, PROPTYPE_INT_SCROLL, -1000000, 1000000}, + {"Pos. TO", pCurrentQuad->m_PosEnvOffset, PROPTYPE_INT, -1000000, 1000000}, {"Color Env", pCurrentQuad->m_ColorEnv + 1, PROPTYPE_ENVELOPE, 0, 0}, - {"Color TO", pCurrentQuad->m_ColorEnvOffset, PROPTYPE_INT_SCROLL, -1000000, 1000000}, + {"Color TO", pCurrentQuad->m_ColorEnvOffset, PROPTYPE_INT, -1000000, 1000000}, {nullptr}, }; static int s_aIds[(int)EQuadProp::NUM_PROPS] = {0}; int NewVal = 0; - auto PropRes = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); - EQuadProp Prop = PropRes.m_Value; - if(Prop != EQuadProp::PROP_NONE) + auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + if(Prop != EQuadProp::PROP_NONE && (State == EEditState::START || State == EEditState::ONE_GO)) { - pEditor->m_Map.OnModify(); - if(PropRes.m_State == EEditState::START || PropRes.m_State == EEditState::ONE_GO) - { - pEditor->m_QuadTracker.BeginQuadPropTrack(pLayer, pEditor->m_vSelectedQuads, Prop); - } + pEditor->m_QuadTracker.BeginQuadPropTrack(pLayer, pEditor->m_vSelectedQuads, Prop); } const float OffsetX = i2fx(NewVal) - pCurrentQuad->m_aPoints[4].x; @@ -1038,23 +1045,21 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b } } - if(Prop != EQuadProp::PROP_NONE) + if(Prop != EQuadProp::PROP_NONE && (State == EEditState::END || State == EEditState::ONE_GO)) { - if(PropRes.m_State == EEditState::END || PropRes.m_State == EEditState::ONE_GO) - { - pEditor->m_QuadTracker.EndQuadPropTrack(Prop); - } + pEditor->m_QuadTracker.EndQuadPropTrack(Prop); + pEditor->m_Map.OnModify(); } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); CSoundSource *pSource = pEditor->GetSelectedSource(); if(!pSource) - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; CUIRect Button; @@ -1068,7 +1073,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, { pEditor->m_EditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedGroup, pEditor->m_vSelectedLayers[0], pEditor->m_SelectedSource)); } - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } // Sound shape button @@ -1089,23 +1094,23 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, } CProperty aProps[] = { - {"Pos X", pSource->m_Position.x / 1000, PROPTYPE_INT_SCROLL, -1000000, 1000000}, - {"Pos Y", pSource->m_Position.y / 1000, PROPTYPE_INT_SCROLL, -1000000, 1000000}, + {"Pos X", pSource->m_Position.x / 1000, PROPTYPE_INT, -1000000, 1000000}, + {"Pos Y", pSource->m_Position.y / 1000, PROPTYPE_INT, -1000000, 1000000}, {"Loop", pSource->m_Loop, PROPTYPE_BOOL, 0, 1}, {"Pan", pSource->m_Pan, PROPTYPE_BOOL, 0, 1}, - {"Delay", pSource->m_TimeDelay, PROPTYPE_INT_SCROLL, 0, 1000000}, - {"Falloff", pSource->m_Falloff, PROPTYPE_INT_SCROLL, 0, 255}, + {"Delay", pSource->m_TimeDelay, PROPTYPE_INT, 0, 1000000}, + {"Falloff", pSource->m_Falloff, PROPTYPE_INT, 0, 255}, {"Pos. Env", pSource->m_PosEnv + 1, PROPTYPE_ENVELOPE, 0, 0}, - {"Pos. TO", pSource->m_PosEnvOffset, PROPTYPE_INT_SCROLL, -1000000, 1000000}, + {"Pos. TO", pSource->m_PosEnvOffset, PROPTYPE_INT, -1000000, 1000000}, {"Sound Env", pSource->m_SoundEnv + 1, PROPTYPE_ENVELOPE, 0, 0}, - {"Sound. TO", pSource->m_SoundEnvOffset, PROPTYPE_INT_SCROLL, -1000000, 1000000}, + {"Sound. TO", pSource->m_SoundEnvOffset, PROPTYPE_INT, -1000000, 1000000}, {nullptr}, }; static int s_aIds[(int)ESoundProp::NUM_PROPS] = {0}; int NewVal = 0; auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); - if(Prop != ESoundProp::PROP_NONE) + if(Prop != ESoundProp::PROP_NONE && (State == EEditState::END || State == EEditState::ONE_GO)) { pEditor->m_Map.OnModify(); } @@ -1180,14 +1185,14 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, case CSoundShape::SHAPE_CIRCLE: { CProperty aCircleProps[] = { - {"Radius", pSource->m_Shape.m_Circle.m_Radius, PROPTYPE_INT_SCROLL, 0, 1000000}, + {"Radius", pSource->m_Shape.m_Circle.m_Radius, PROPTYPE_INT, 0, 1000000}, {nullptr}, }; static int s_aCircleIds[(int)ECircleShapeProp::NUM_CIRCLE_PROPS] = {0}; NewVal = 0; auto [LocalState, LocalProp] = pEditor->DoPropertiesWithState(&View, aCircleProps, s_aCircleIds, &NewVal); - if(LocalProp != ECircleShapeProp::PROP_NONE) + if(LocalProp != ECircleShapeProp::PROP_NONE && (LocalState == EEditState::END || LocalState == EEditState::ONE_GO)) { pEditor->m_Map.OnModify(); } @@ -1207,15 +1212,15 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, case CSoundShape::SHAPE_RECTANGLE: { CProperty aRectangleProps[] = { - {"Width", pSource->m_Shape.m_Rectangle.m_Width / 1024, PROPTYPE_INT_SCROLL, 0, 1000000}, - {"Height", pSource->m_Shape.m_Rectangle.m_Height / 1024, PROPTYPE_INT_SCROLL, 0, 1000000}, + {"Width", pSource->m_Shape.m_Rectangle.m_Width / 1024, PROPTYPE_INT, 0, 1000000}, + {"Height", pSource->m_Shape.m_Rectangle.m_Height / 1024, PROPTYPE_INT, 0, 1000000}, {nullptr}, }; static int s_aRectangleIds[(int)ERectangleShapeProp::NUM_RECTANGLE_PROPS] = {0}; NewVal = 0; auto [LocalState, LocalProp] = pEditor->DoPropertiesWithState(&View, aRectangleProps, s_aRectangleIds, &NewVal); - if(LocalProp != ERectangleShapeProp::PROP_NONE) + if(LocalProp != ERectangleShapeProp::PROP_NONE && (LocalState == EEditState::END || LocalState == EEditState::ONE_GO)) { pEditor->m_Map.OnModify(); } @@ -1237,15 +1242,15 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, } } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); std::vector vpQuads = pEditor->GetSelectedQuads(); if(!in_range(pEditor->m_SelectedQuadIndex, 0, vpQuads.size() - 1)) - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; CQuad *pCurrentQuad = vpQuads[pEditor->m_SelectedQuadIndex]; std::shared_ptr pLayer = std::static_pointer_cast(pEditor->GetSelectedLayerType(0, LAYERTYPE_QUADS)); @@ -1257,26 +1262,21 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, const int TextureV = fx2f(pCurrentQuad->m_aTexcoords[pEditor->m_SelectedQuadPoint].y) * 1024; CProperty aProps[] = { - {"Pos X", X, PROPTYPE_INT_SCROLL, -1000000, 1000000}, - {"Pos Y", Y, PROPTYPE_INT_SCROLL, -1000000, 1000000}, + {"Pos X", X, PROPTYPE_INT, -1000000, 1000000}, + {"Pos Y", Y, PROPTYPE_INT, -1000000, 1000000}, {"Color", Color, PROPTYPE_COLOR, 0, 0}, - {"Tex U", TextureU, PROPTYPE_INT_SCROLL, -1000000, 1000000}, - {"Tex V", TextureV, PROPTYPE_INT_SCROLL, -1000000, 1000000}, + {"Tex U", TextureU, PROPTYPE_INT, -1000000, 1000000}, + {"Tex V", TextureV, PROPTYPE_INT, -1000000, 1000000}, {nullptr}, }; static int s_aIds[(int)EQuadPointProp::NUM_PROPS] = {0}; int NewVal = 0; - auto PropRes = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); - EQuadPointProp Prop = PropRes.m_Value; - if(Prop != EQuadPointProp::PROP_NONE) + auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + if(Prop != EQuadPointProp::PROP_NONE && (State == EEditState::START || State == EEditState::ONE_GO)) { - pEditor->m_Map.OnModify(); - if(PropRes.m_State == EEditState::START || PropRes.m_State == EEditState::ONE_GO) - { - pEditor->m_QuadTracker.BeginQuadPointPropTrack(pLayer, pEditor->m_vSelectedQuads, pEditor->m_SelectedQuadPoints); - pEditor->m_QuadTracker.AddQuadPointPropTrack(Prop); - } + pEditor->m_QuadTracker.BeginQuadPointPropTrack(pLayer, pEditor->m_vSelectedQuads, pEditor->m_SelectedQuadPoints); + pEditor->m_QuadTracker.AddQuadPointPropTrack(Prop); } for(CQuad *pQuad : vpQuads) @@ -1320,23 +1320,20 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, } } - if(Prop != EQuadPointProp::PROP_NONE) + if(Prop != EQuadPointProp::PROP_NONE && (State == EEditState::END || State == EEditState::ONE_GO)) { + pEditor->m_QuadTracker.EndQuadPointPropTrack(Prop); pEditor->m_Map.OnModify(); - if(PropRes.m_State == EEditState::END || PropRes.m_State == EEditState::ONE_GO) - { - pEditor->m_QuadTracker.EndQuadPointPropTrack(Prop); - } } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); if(pEditor->m_SelectedEnvelope < 0 || pEditor->m_SelectedEnvelope >= (int)pEditor->m_Map.m_vpEnvelopes.size()) - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; const float RowHeight = 12.0f; CUIRect Row, Label, EditBox; @@ -1345,13 +1342,13 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie std::shared_ptr pEnvelope = pEditor->m_Map.m_vpEnvelopes[pEditor->m_SelectedEnvelope]; - if(pEnvelope->GetChannels() == 4) + if(pEnvelope->GetChannels() == 4 && !pEditor->IsTangentSelected()) { View.HSplitTop(RowHeight, &Row, &View); View.HSplitTop(4.0f, nullptr, &View); Row.VSplitLeft(60.0f, &Label, &Row); Row.VSplitLeft(10.0f, nullptr, &EditBox); - pEditor->UI()->DoLabel(&Label, "Color:", RowHeight - 2.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Color:", RowHeight - 2.0f, TEXTALIGN_ML); const auto SelectedPoint = pEditor->m_vSelectedEnvelopePoints.front(); const int SelectedIndex = SelectedPoint.first; @@ -1420,14 +1417,14 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie View.HSplitTop(RowHeight, &Row, &View); Row.VSplitLeft(60.0f, &Label, &Row); Row.VSplitLeft(10.0f, nullptr, &EditBox); - pEditor->UI()->DoLabel(&Label, "Value:", RowHeight - 2.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Value:", RowHeight - 2.0f, TEXTALIGN_ML); pEditor->DoEditBox(&s_CurValueInput, &EditBox, RowHeight - 2.0f, IGraphics::CORNER_ALL, "The value of the selected envelope point"); View.HSplitTop(4.0f, nullptr, &View); View.HSplitTop(RowHeight, &Row, &View); Row.VSplitLeft(60.0f, &Label, &Row); Row.VSplitLeft(10.0f, nullptr, &EditBox); - pEditor->UI()->DoLabel(&Label, "Time (in s):", RowHeight - 2.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Time (in s):", RowHeight - 2.0f, TEXTALIGN_ML); pEditor->DoEditBox(&s_CurTimeInput, &EditBox, RowHeight - 2.0f, IGraphics::CORNER_ALL, "The time of the selected envelope point"); if(pEditor->Input()->KeyIsPressed(KEY_RETURN) || pEditor->Input()->KeyIsPressed(KEY_KP_ENTER)) @@ -1478,10 +1475,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie View.HSplitTop(6.0f, nullptr, &View); View.HSplitTop(RowHeight, &Row, &View); - static int s_DeleteButtonID = 0; + static int s_DeleteButtonId = 0; const char *pButtonText = pEditor->IsTangentSelected() ? "Reset" : "Delete"; const char *pTooltip = pEditor->IsTangentSelected() ? "Reset tangent point to default value." : "Delete current envelope point in all channels."; - if(pEditor->DoButton_Editor(&s_DeleteButtonID, pButtonText, 0, &Row, 0, pTooltip)) + if(pEditor->DoButton_Editor(&s_DeleteButtonId, pButtonText, 0, &Row, 0, pTooltip)) { if(pEditor->IsTangentInSelected()) { @@ -1499,64 +1496,64 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex)); } - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupEnvPointMulti(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupEnvPointMulti(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); const float RowHeight = 12.0f; - static int s_CurveButtonID = 0; + static int s_CurveButtonId = 0; CUIRect CurveButton; View.HSplitTop(RowHeight, &CurveButton, &View); - if(pEditor->DoButton_Editor(&s_CurveButtonID, "Project onto", 0, &CurveButton, 0, "Project all selected envelopes onto the curve between the first and last selected envelope.")) + if(pEditor->DoButton_Editor(&s_CurveButtonId, "Project onto", 0, &CurveButton, 0, "Project all selected envelopes onto the curve between the first and last selected envelope.")) { static SPopupMenuId s_PopupCurveTypeId; - pEditor->UI()->DoPopupMenu(&s_PopupCurveTypeId, pEditor->UI()->MouseX(), pEditor->UI()->MouseY(), 80, 80, pEditor, PopupEnvPointCurveType); + pEditor->Ui()->DoPopupMenu(&s_PopupCurveTypeId, pEditor->Ui()->MouseX(), pEditor->Ui()->MouseY(), 80, 80, pEditor, PopupEnvPointCurveType); } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupEnvPointCurveType(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupEnvPointCurveType(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); const float RowHeight = 14.0f; int CurveType = -1; - static int s_ButtonLinearID; + static int s_ButtonLinearId; CUIRect ButtonLinear; View.HSplitTop(RowHeight, &ButtonLinear, &View); - if(pEditor->DoButton_MenuItem(&s_ButtonLinearID, "Linear", 0, &ButtonLinear)) + if(pEditor->DoButton_MenuItem(&s_ButtonLinearId, "Linear", 0, &ButtonLinear)) CurveType = CURVETYPE_LINEAR; - static int s_ButtonSlowID; + static int s_ButtonSlowId; CUIRect ButtonSlow; View.HSplitTop(RowHeight, &ButtonSlow, &View); - if(pEditor->DoButton_MenuItem(&s_ButtonSlowID, "Slow", 0, &ButtonSlow)) + if(pEditor->DoButton_MenuItem(&s_ButtonSlowId, "Slow", 0, &ButtonSlow)) CurveType = CURVETYPE_SLOW; - static int s_ButtonFastID; + static int s_ButtonFastId; CUIRect ButtonFast; View.HSplitTop(RowHeight, &ButtonFast, &View); - if(pEditor->DoButton_MenuItem(&s_ButtonFastID, "Fast", 0, &ButtonFast)) + if(pEditor->DoButton_MenuItem(&s_ButtonFastId, "Fast", 0, &ButtonFast)) CurveType = CURVETYPE_FAST; - static int s_ButtonStepID; + static int s_ButtonStepId; CUIRect ButtonStep; View.HSplitTop(RowHeight, &ButtonStep, &View); - if(pEditor->DoButton_MenuItem(&s_ButtonStepID, "Step", 0, &ButtonStep)) + if(pEditor->DoButton_MenuItem(&s_ButtonStepId, "Step", 0, &ButtonStep)) CurveType = CURVETYPE_STEP; - static int s_ButtonSmoothID; + static int s_ButtonSmoothId; CUIRect ButtonSmooth; View.HSplitTop(RowHeight, &ButtonSmooth, &View); - if(pEditor->DoButton_MenuItem(&s_ButtonSmoothID, "Smooth", 0, &ButtonSmooth)) + if(pEditor->DoButton_MenuItem(&s_ButtonSmoothId, "Smooth", 0, &ButtonSmooth)) CurveType = CURVETYPE_SMOOTH; std::vector> vpActions; @@ -1595,8 +1592,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPointCurveType(void *pContext, CU if(SelectedIndex != FirstSelectedIndex && SelectedIndex != LastSelectedIndex) { CEnvPoint &CurrentPoint = pEnvelope->m_vPoints[SelectedIndex]; - ColorRGBA Channels; - HelperEnvelope.Eval(CurrentPoint.m_Time / 1000.0f, Channels); + ColorRGBA Channels = ColorRGBA(0.0f, 0.0f, 0.0f, 0.0f); + HelperEnvelope.Eval(CurrentPoint.m_Time / 1000.0f, Channels, 1); int PrevValue = CurrentPoint.m_aValues[c]; CurrentPoint.m_aValues[c] = f2fx(Channels.r); vpActions.push_back(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, CEditorActionEnvelopeEditPoint::EEditType::VALUE, PrevValue, CurrentPoint.m_aValues[c])); @@ -1612,10 +1609,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPointCurveType(void *pContext, CU } pEditor->m_Map.OnModify(); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } static const auto &&gs_ModifyIndexDeleted = [](int DeletedIndex) { @@ -1627,12 +1624,12 @@ static const auto &&gs_ModifyIndexDeleted = [](int DeletedIndex) { }; }; -CUI::EPopupMenuFunctionResult CEditor::PopupImage(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupImage(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); static int s_ExternalButton = 0; - static int s_ReaddButton = 0; + static int s_ReimportButton = 0; static int s_ReplaceButton = 0; static int s_RemoveButton = 0; static int s_ExportButton = 0; @@ -1652,7 +1649,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupImage(void *pContext, CUIRect View, Slot.VMargin(5.0f, &Slot); Slot.VSplitLeft(35.0f, &Label, &Slot); Slot.VSplitLeft(RowHeight - 2.0f, nullptr, &EditBox); - pEditor->UI()->DoLabel(&Label, "Name:", RowHeight - 2.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Name:", RowHeight - 2.0f, TEXTALIGN_ML); s_RenameInput.SetBuffer(pImg->m_aName, sizeof(pImg->m_aName)); if(pEditor->DoEditBox(&s_RenameInput, &EditBox, RowHeight - 2.0f)) @@ -1666,8 +1663,13 @@ CUI::EPopupMenuFunctionResult CEditor::PopupImage(void *pContext, CUIRect View, { if(pEditor->DoButton_MenuItem(&s_ExternalButton, "Embed", 0, &Slot, 0, "Embeds the image into the map file.")) { + if(pImg->m_pData == nullptr) + { + pEditor->ShowFileDialogError("Embedding is not possible because the image could not be loaded."); + return CUi::POPUP_KEEP_OPEN; + } pImg->m_External = 0; - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(5.0f, nullptr, &View); View.HSplitTop(RowHeight, &Slot, &View); @@ -1677,16 +1679,16 @@ CUI::EPopupMenuFunctionResult CEditor::PopupImage(void *pContext, CUIRect View, if(pEditor->DoButton_MenuItem(&s_ExternalButton, "Make external", 0, &Slot, 0, "Removes the image from the map file.")) { pImg->m_External = 1; - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(5.0f, nullptr, &View); View.HSplitTop(RowHeight, &Slot, &View); } - static CUI::SSelectionPopupContext s_SelectionPopupContext; + static CUi::SSelectionPopupContext s_SelectionPopupContext; static CScrollRegion s_SelectionPopupScrollRegion; s_SelectionPopupContext.m_pScrollRegion = &s_SelectionPopupScrollRegion; - if(pEditor->DoButton_MenuItem(&s_ReaddButton, "Readd", 0, &Slot, 0, "Reloads the image from the mapres folder")) + if(pEditor->DoButton_MenuItem(&s_ReimportButton, "Re-import", 0, &Slot, 0, "Re-imports the image from the mapres folder")) { char aFilename[IO_MAX_PATH_LENGTH]; str_format(aFilename, sizeof(aFilename), "%s.png", pImg->m_aName); @@ -1706,7 +1708,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupImage(void *pContext, CUIRect View, else { str_copy(s_SelectionPopupContext.m_aMessage, "Select the wanted image:"); - pEditor->UI()->ShowPopupSelection(pEditor->UI()->MouseX(), pEditor->UI()->MouseY(), &s_SelectionPopupContext); + pEditor->Ui()->ShowPopupSelection(pEditor->Ui()->MouseX(), pEditor->Ui()->MouseY(), &s_SelectionPopupContext); } } if(s_SelectionPopupContext.m_pSelection != nullptr) @@ -1715,7 +1717,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupImage(void *pContext, CUIRect View, const bool Result = pEditor->ReplaceImage(s_SelectionPopupContext.m_pSelection->c_str(), IStorage::TYPE_ALL, false); pImg->m_External = WasExternal; s_SelectionPopupContext.Reset(); - return Result ? CUI::POPUP_CLOSE_CURRENT : CUI::POPUP_KEEP_OPEN; + return Result ? CUi::POPUP_CLOSE_CURRENT : CUi::POPUP_KEEP_OPEN; } View.HSplitTop(5.0f, nullptr, &View); @@ -1723,7 +1725,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupImage(void *pContext, CUIRect View, if(pEditor->DoButton_MenuItem(&s_ReplaceButton, "Replace", 0, &Slot, 0, "Replaces the image with a new one")) { pEditor->InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_IMG, "Replace Image", "Replace", "mapres", false, ReplaceImageCallback, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(5.0f, nullptr, &View); @@ -1732,7 +1734,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupImage(void *pContext, CUIRect View, { pEditor->m_Map.m_vpImages.erase(pEditor->m_Map.m_vpImages.begin() + pEditor->m_SelectedImage); pEditor->m_Map.ModifyImageIndex(gs_ModifyIndexDeleted(pEditor->m_SelectedImage)); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } if(!pImg->m_External) @@ -1741,20 +1743,25 @@ CUI::EPopupMenuFunctionResult CEditor::PopupImage(void *pContext, CUIRect View, View.HSplitTop(RowHeight, &Slot, &View); if(pEditor->DoButton_MenuItem(&s_ExportButton, "Export", 0, &Slot, 0, "Export the image")) { + if(pImg->m_pData == nullptr) + { + pEditor->ShowFileDialogError("Exporting is not possible because the image could not be loaded."); + return CUi::POPUP_KEEP_OPEN; + } pEditor->InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_IMG, "Save image", "Save", "mapres", false, CallbackSaveImage, pEditor); pEditor->m_FileDialogFileNameInput.Set(pImg->m_aName); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupSound(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupSound(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); - static int s_ReaddButton = 0; + static int s_ReimportButton = 0; static int s_ReplaceButton = 0; static int s_RemoveButton = 0; static int s_ExportButton = 0; @@ -1765,7 +1772,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSound(void *pContext, CUIRect View, View.HSplitTop(RowHeight, &Slot, &View); std::shared_ptr pSound = pEditor->m_Map.m_vpSounds[pEditor->m_SelectedSound]; - static CUI::SSelectionPopupContext s_SelectionPopupContext; + static CUi::SSelectionPopupContext s_SelectionPopupContext; static CScrollRegion s_SelectionPopupScrollRegion; s_SelectionPopupContext.m_pScrollRegion = &s_SelectionPopupScrollRegion; @@ -1776,7 +1783,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSound(void *pContext, CUIRect View, Slot.VMargin(5.0f, &Slot); Slot.VSplitLeft(35.0f, &Label, &Slot); Slot.VSplitLeft(RowHeight - 2.0f, nullptr, &EditBox); - pEditor->UI()->DoLabel(&Label, "Name:", RowHeight - 2.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Name:", RowHeight - 2.0f, TEXTALIGN_ML); s_RenameInput.SetBuffer(pSound->m_aName, sizeof(pSound->m_aName)); if(pEditor->DoEditBox(&s_RenameInput, &EditBox, RowHeight - 2.0f)) @@ -1785,7 +1792,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSound(void *pContext, CUIRect View, View.HSplitTop(5.0f, nullptr, &View); View.HSplitTop(RowHeight, &Slot, &View); - if(pEditor->DoButton_MenuItem(&s_ReaddButton, "Readd", 0, &Slot, 0, "Reloads the sound from the mapres folder")) + if(pEditor->DoButton_MenuItem(&s_ReimportButton, "Re-import", 0, &Slot, 0, "Re-imports the sound from the mapres folder")) { char aFilename[IO_MAX_PATH_LENGTH]; str_format(aFilename, sizeof(aFilename), "%s.opus", pSound->m_aName); @@ -1805,14 +1812,14 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSound(void *pContext, CUIRect View, else { str_copy(s_SelectionPopupContext.m_aMessage, "Select the wanted sound:"); - pEditor->UI()->ShowPopupSelection(pEditor->UI()->MouseX(), pEditor->UI()->MouseY(), &s_SelectionPopupContext); + pEditor->Ui()->ShowPopupSelection(pEditor->Ui()->MouseX(), pEditor->Ui()->MouseY(), &s_SelectionPopupContext); } } if(s_SelectionPopupContext.m_pSelection != nullptr) { const bool Result = pEditor->ReplaceSound(s_SelectionPopupContext.m_pSelection->c_str(), IStorage::TYPE_ALL, false); s_SelectionPopupContext.Reset(); - return Result ? CUI::POPUP_CLOSE_CURRENT : CUI::POPUP_KEEP_OPEN; + return Result ? CUi::POPUP_CLOSE_CURRENT : CUi::POPUP_KEEP_OPEN; } View.HSplitTop(5.0f, nullptr, &View); @@ -1820,7 +1827,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSound(void *pContext, CUIRect View, if(pEditor->DoButton_MenuItem(&s_ReplaceButton, "Replace", 0, &Slot, 0, "Replaces the sound with a new one")) { pEditor->InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_SOUND, "Replace sound", "Replace", "mapres", false, ReplaceSoundCallback, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(5.0f, nullptr, &View); @@ -1829,22 +1836,27 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSound(void *pContext, CUIRect View, { pEditor->m_Map.m_vpSounds.erase(pEditor->m_Map.m_vpSounds.begin() + pEditor->m_SelectedSound); pEditor->m_Map.ModifySoundIndex(gs_ModifyIndexDeleted(pEditor->m_SelectedSound)); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(5.0f, nullptr, &View); View.HSplitTop(RowHeight, &Slot, &View); if(pEditor->DoButton_MenuItem(&s_ExportButton, "Export", 0, &Slot, 0, "Export sound")) { + if(pSound->m_pData == nullptr) + { + pEditor->ShowFileDialogError("Exporting is not possible because the sound could not be loaded."); + return CUi::POPUP_KEEP_OPEN; + } pEditor->InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_SOUND, "Save sound", "Save", "mapres", false, CallbackSaveSound, pEditor); pEditor->m_FileDialogFileNameInput.Set(pSound->m_aName); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupNewFolder(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupNewFolder(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -1855,12 +1867,12 @@ CUI::EPopupMenuFunctionResult CEditor::PopupNewFolder(void *pContext, CUIRect Vi // title View.HSplitTop(20.0f, &Label, &View); - pEditor->UI()->DoLabel(&Label, "Create new folder", 20.0f, TEXTALIGN_MC); + pEditor->Ui()->DoLabel(&Label, "Create new folder", 20.0f, TEXTALIGN_MC); View.HSplitTop(10.0f, nullptr, &View); // folder name View.HSplitTop(20.0f, &Label, &View); - pEditor->UI()->DoLabel(&Label, "Name:", 10.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Name:", 10.0f, TEXTALIGN_ML); Label.VSplitLeft(50.0f, nullptr, &Button); Button.HMargin(2.0f, &Button); pEditor->DoEditBox(&pEditor->m_FileDialogNewFolderNameInput, &Button, 12.0f); @@ -1869,11 +1881,11 @@ CUI::EPopupMenuFunctionResult CEditor::PopupNewFolder(void *pContext, CUIRect Vi ButtonBar.VSplitLeft(110.0f, &Button, &ButtonBar); static int s_CancelButton = 0; if(pEditor->DoButton_Editor(&s_CancelButton, "Cancel", 0, &Button, 0, nullptr)) - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; ButtonBar.VSplitRight(110.0f, &ButtonBar, &Button); static int s_CreateButton = 0; - if(pEditor->DoButton_Editor(&s_CreateButton, "Create", 0, &Button, 0, nullptr) || (Active && pEditor->UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + if(pEditor->DoButton_Editor(&s_CreateButton, "Create", 0, &Button, 0, nullptr) || (Active && pEditor->Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) { // create the folder if(!pEditor->m_FileDialogNewFolderNameInput.IsEmpty()) @@ -1883,7 +1895,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupNewFolder(void *pContext, CUIRect Vi if(pEditor->Storage()->CreateFolder(aBuf, IStorage::TYPE_SAVE)) { pEditor->FilelistPopulate(IStorage::TYPE_SAVE); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } else { @@ -1892,10 +1904,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupNewFolder(void *pContext, CUIRect Vi } } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupMapInfo(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupMapInfo(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -1906,12 +1918,12 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMapInfo(void *pContext, CUIRect View // title View.HSplitTop(20.0f, &Label, &View); - pEditor->UI()->DoLabel(&Label, "Map details", 20.0f, TEXTALIGN_MC); + pEditor->Ui()->DoLabel(&Label, "Map details", 20.0f, TEXTALIGN_MC); View.HSplitTop(10.0f, nullptr, &View); // author box View.HSplitTop(20.0f, &Label, &View); - pEditor->UI()->DoLabel(&Label, "Author:", 10.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Author:", 10.0f, TEXTALIGN_ML); Label.VSplitLeft(60.0f, nullptr, &Button); Button.HMargin(3.0f, &Button); static CLineInput s_AuthorInput; @@ -1920,7 +1932,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMapInfo(void *pContext, CUIRect View // version box View.HSplitTop(20.0f, &Label, &View); - pEditor->UI()->DoLabel(&Label, "Version:", 10.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Version:", 10.0f, TEXTALIGN_ML); Label.VSplitLeft(60.0f, nullptr, &Button); Button.HMargin(3.0f, &Button); static CLineInput s_VersionInput; @@ -1929,7 +1941,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMapInfo(void *pContext, CUIRect View // credits box View.HSplitTop(20.0f, &Label, &View); - pEditor->UI()->DoLabel(&Label, "Credits:", 10.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "Credits:", 10.0f, TEXTALIGN_ML); Label.VSplitLeft(60.0f, nullptr, &Button); Button.HMargin(3.0f, &Button); static CLineInput s_CreditsInput; @@ -1938,7 +1950,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMapInfo(void *pContext, CUIRect View // license box View.HSplitTop(20.0f, &Label, &View); - pEditor->UI()->DoLabel(&Label, "License:", 10.0f, TEXTALIGN_ML); + pEditor->Ui()->DoLabel(&Label, "License:", 10.0f, TEXTALIGN_ML); Label.VSplitLeft(60.0f, nullptr, &Button); Button.HMargin(3.0f, &Button); static CLineInput s_LicenseInput; @@ -1949,11 +1961,11 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMapInfo(void *pContext, CUIRect View ButtonBar.VSplitLeft(110.0f, &Label, &ButtonBar); static int s_CancelButton = 0; if(pEditor->DoButton_Editor(&s_CancelButton, "Cancel", 0, &Label, 0, nullptr)) - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; ButtonBar.VSplitRight(110.0f, &ButtonBar, &Label); static int s_ConfirmButton = 0; - if(pEditor->DoButton_Editor(&s_ConfirmButton, "Confirm", 0, &Label, 0, nullptr) || (Active && pEditor->UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + if(pEditor->DoButton_Editor(&s_ConfirmButton, "Confirm", 0, &Label, 0, nullptr) || (Active && pEditor->Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) { bool AuthorDifferent = str_comp(pEditor->m_Map.m_MapInfoTmp.m_aAuthor, pEditor->m_Map.m_MapInfo.m_aAuthor) != 0; bool VersionDifferent = str_comp(pEditor->m_Map.m_MapInfoTmp.m_aVersion, pEditor->m_Map.m_MapInfo.m_aVersion) != 0; @@ -1964,13 +1976,13 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMapInfo(void *pContext, CUIRect View pEditor->m_Map.OnModify(); pEditor->m_Map.m_MapInfo.Copy(pEditor->m_Map.m_MapInfoTmp); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupEvent(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupEvent(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -2057,7 +2069,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEvent(void *pContext, CUIRect View, else { dbg_assert(false, "m_PopupEventType invalid"); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } CUIRect Label, ButtonBar, Button; @@ -2067,12 +2079,12 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEvent(void *pContext, CUIRect View, // title View.HSplitTop(20.0f, &Label, &View); - pEditor->UI()->DoLabel(&Label, pTitle, 20.0f, TEXTALIGN_MC); + pEditor->Ui()->DoLabel(&Label, pTitle, 20.0f, TEXTALIGN_MC); // message SLabelProperties Props; Props.m_MaxWidth = View.w; - pEditor->UI()->DoLabel(&View, pMessage, 10.0f, TEXTALIGN_ML, Props); + pEditor->Ui()->DoLabel(&View, pMessage, 10.0f, TEXTALIGN_ML, Props); // button bar ButtonBar.VSplitLeft(110.0f, &Button, &ButtonBar); @@ -2092,17 +2104,16 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEvent(void *pContext, CUIRect View, if(pEditor->m_PopupEventType == POPEVENT_PIXELART_BIG_IMAGE || pEditor->m_PopupEventType == POPEVENT_PIXELART_MANY_COLORS) { - free(pEditor->m_TileartImageInfo.m_pData); - pEditor->m_TileartImageInfo.m_pData = nullptr; + pEditor->m_TileartImageInfo.Free(); } - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } } ButtonBar.VSplitRight(110.0f, &ButtonBar, &Button); static int s_ConfirmButton = 0; - if(pEditor->DoButton_Editor(&s_ConfirmButton, "Confirm", 0, &Button, 0, nullptr) || (Active && pEditor->UI()->ConsumeHotkey(CUI::HOTKEY_ENTER))) + if(pEditor->DoButton_Editor(&s_ConfirmButton, "Confirm", 0, &Button, 0, nullptr) || (Active && pEditor->Ui()->ConsumeHotkey(CUi::HOTKEY_ENTER))) { if(pEditor->m_PopupEventType == POPEVENT_EXIT) { @@ -2132,22 +2143,22 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEvent(void *pContext, CUIRect View, else if(pEditor->m_PopupEventType == POPEVENT_SAVE) { CallbackSaveMap(pEditor->m_aFileSaveName, IStorage::TYPE_SAVE, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } else if(pEditor->m_PopupEventType == POPEVENT_SAVE_COPY) { CallbackSaveCopyMap(pEditor->m_aFileSaveName, IStorage::TYPE_SAVE, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } else if(pEditor->m_PopupEventType == POPEVENT_SAVE_IMG) { CallbackSaveImage(pEditor->m_aFileSaveName, IStorage::TYPE_SAVE, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } else if(pEditor->m_PopupEventType == POPEVENT_SAVE_SOUND) { CallbackSaveSound(pEditor->m_aFileSaveName, IStorage::TYPE_SAVE, pEditor); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } else if(pEditor->m_PopupEventType == POPEVENT_PLACE_BORDER_TILES) { @@ -2162,16 +2173,16 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEvent(void *pContext, CUIRect View, pEditor->AddTileart(); } pEditor->m_PopupEventWasActivated = false; - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } static int g_SelectImageSelected = -100; static int g_SelectImageCurrent = -100; -CUI::EPopupMenuFunctionResult CEditor::PopupSelectImage(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupSelectImage(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -2184,32 +2195,32 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSelectImage(void *pContext, CUIRect const float ButtonHeight = 12.0f; const float ButtonMargin = 2.0f; - static CScrollRegion s_ScrollRegion; - vec2 ScrollOffset(0.0f, 0.0f); - CScrollRegionParams ScrollParams; - ScrollParams.m_ScrollbarWidth = 10.0f; - ScrollParams.m_ScrollbarMargin = 3.0f; - ScrollParams.m_ScrollUnit = (ButtonHeight + ButtonMargin) * 5; - s_ScrollRegion.Begin(&ButtonBar, &ScrollOffset, &ScrollParams); - ButtonBar.y += ScrollOffset.y; + static CListBox s_ListBox; + s_ListBox.DoStart(ButtonHeight, pEditor->m_Map.m_vpImages.size() + 1, 1, 4, g_SelectImageCurrent + 1, &ButtonBar, false); + s_ListBox.DoAutoSpacing(ButtonMargin); - for(int i = -1; i < (int)pEditor->m_Map.m_vpImages.size(); i++) + for(int i = 0; i <= (int)pEditor->m_Map.m_vpImages.size(); i++) { - CUIRect Button; - ButtonBar.HSplitTop(ButtonMargin, nullptr, &ButtonBar); - ButtonBar.HSplitTop(ButtonHeight, &Button, &ButtonBar); - if(s_ScrollRegion.AddRect(Button)) - { - if(pEditor->UI()->MouseInside(&Button)) - ShowImage = i; + static int s_NoneButton = 0; + CListboxItem Item = s_ListBox.DoNextItem(i == 0 ? (void *)&s_NoneButton : &pEditor->m_Map.m_vpImages[i - 1], (i - 1) == g_SelectImageCurrent, 3.0f); + if(!Item.m_Visible) + continue; - static int s_NoneButton = 0; - if(pEditor->DoButton_MenuItem(i == -1 ? (void *)&s_NoneButton : &pEditor->m_Map.m_vpImages[i], i == -1 ? "None" : pEditor->m_Map.m_vpImages[i]->m_aName, i == g_SelectImageCurrent, &Button)) - g_SelectImageSelected = i; - } + if(pEditor->Ui()->MouseInside(&Item.m_Rect)) + ShowImage = i - 1; + + CUIRect Label; + Item.m_Rect.VMargin(5.0f, &Label); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + Props.m_EllipsisAtEnd = true; + pEditor->Ui()->DoLabel(&Label, i == 0 ? "None" : pEditor->m_Map.m_vpImages[i - 1]->m_aName, EditorFontSizes::MENU, TEXTALIGN_ML, Props); } - s_ScrollRegion.End(); + int NewSelected = s_ListBox.DoEnd() - 1; + if(NewSelected != g_SelectImageCurrent) + g_SelectImageSelected = NewSelected; if(ShowImage >= 0 && (size_t)ShowImage < pEditor->m_Map.m_vpImages.size()) { @@ -2230,7 +2241,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSelectImage(void *pContext, CUIRect pEditor->Graphics()->WrapNormal(); } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } void CEditor::PopupSelectImageInvoke(int Current, float x, float y) @@ -2238,7 +2249,7 @@ void CEditor::PopupSelectImageInvoke(int Current, float x, float y) static SPopupMenuId s_PopupSelectImageId; g_SelectImageSelected = -100; g_SelectImageCurrent = Current; - UI()->DoPopupMenu(&s_PopupSelectImageId, x, y, 450, 300, this, PopupSelectImage); + Ui()->DoPopupMenu(&s_PopupSelectImageId, x, y, 450, 300, this, PopupSelectImage); } int CEditor::PopupSelectImageResult() @@ -2254,38 +2265,38 @@ int CEditor::PopupSelectImageResult() static int g_SelectSoundSelected = -100; static int g_SelectSoundCurrent = -100; -CUI::EPopupMenuFunctionResult CEditor::PopupSelectSound(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupSelectSound(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); const float ButtonHeight = 12.0f; const float ButtonMargin = 2.0f; - static CScrollRegion s_ScrollRegion; - vec2 ScrollOffset(0.0f, 0.0f); - CScrollRegionParams ScrollParams; - ScrollParams.m_ScrollbarWidth = 10.0f; - ScrollParams.m_ScrollbarMargin = 3.0f; - ScrollParams.m_ScrollUnit = (ButtonHeight + ButtonMargin) * 5; - s_ScrollRegion.Begin(&View, &ScrollOffset, &ScrollParams); - View.y += ScrollOffset.y; + static CListBox s_ListBox; + s_ListBox.DoStart(ButtonHeight, pEditor->m_Map.m_vpSounds.size() + 1, 1, 4, g_SelectSoundCurrent + 1, &View, false); + s_ListBox.DoAutoSpacing(ButtonMargin); - for(int i = -1; i < (int)pEditor->m_Map.m_vpSounds.size(); i++) + for(int i = 0; i <= (int)pEditor->m_Map.m_vpSounds.size(); i++) { - CUIRect Button; - View.HSplitTop(ButtonMargin, nullptr, &View); - View.HSplitTop(ButtonHeight, &Button, &View); - if(s_ScrollRegion.AddRect(Button)) - { - static int s_NoneButton = 0; - if(pEditor->DoButton_MenuItem(i == -1 ? (void *)&s_NoneButton : &pEditor->m_Map.m_vpSounds[i], i == -1 ? "None" : pEditor->m_Map.m_vpSounds[i]->m_aName, i == g_SelectSoundCurrent, &Button)) - g_SelectSoundSelected = i; - } + static int s_NoneButton = 0; + CListboxItem Item = s_ListBox.DoNextItem(i == 0 ? (void *)&s_NoneButton : &pEditor->m_Map.m_vpSounds[i - 1], (i - 1) == g_SelectSoundCurrent, 3.0f); + if(!Item.m_Visible) + continue; + + CUIRect Label; + Item.m_Rect.VMargin(5.0f, &Label); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + Props.m_EllipsisAtEnd = true; + pEditor->Ui()->DoLabel(&Label, i == 0 ? "None" : pEditor->m_Map.m_vpSounds[i - 1]->m_aName, EditorFontSizes::MENU, TEXTALIGN_ML, Props); } - s_ScrollRegion.End(); + int NewSelected = s_ListBox.DoEnd() - 1; + if(NewSelected != g_SelectSoundCurrent) + g_SelectSoundSelected = NewSelected; - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } void CEditor::PopupSelectSoundInvoke(int Current, float x, float y) @@ -2293,7 +2304,7 @@ void CEditor::PopupSelectSoundInvoke(int Current, float x, float y) static SPopupMenuId s_PopupSelectSoundId; g_SelectSoundSelected = -100; g_SelectSoundCurrent = Current; - UI()->DoPopupMenu(&s_PopupSelectSoundId, x, y, 150, 300, this, PopupSelectSound); + Ui()->DoPopupMenu(&s_PopupSelectSoundId, x, y, 150, 300, this, PopupSelectSound); } int CEditor::PopupSelectSoundResult() @@ -2324,7 +2335,7 @@ static const char *s_apGametileOpButtonNames[] = { "Live Unfreeze", }; -CUI::EPopupMenuFunctionResult CEditor::PopupSelectGametileOp(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupSelectGametileOp(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -2339,14 +2350,14 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSelectGametileOp(void *pContext, CUI s_GametileOpSelected = i; } - return s_GametileOpSelected == PreviousSelected ? CUI::POPUP_KEEP_OPEN : CUI::POPUP_CLOSE_CURRENT; + return s_GametileOpSelected == PreviousSelected ? CUi::POPUP_KEEP_OPEN : CUi::POPUP_CLOSE_CURRENT; } void CEditor::PopupSelectGametileOpInvoke(float x, float y) { static SPopupMenuId s_PopupSelectGametileOpId; s_GametileOpSelected = -1; - UI()->DoPopupMenu(&s_PopupSelectGametileOpId, x, y, 120.0f, std::size(s_apGametileOpButtonNames) * 14.0f + 10.0f, this, PopupSelectGametileOp); + Ui()->DoPopupMenu(&s_PopupSelectGametileOpId, x, y, 120.0f, std::size(s_apGametileOpButtonNames) * 14.0f + 10.0f, this, PopupSelectGametileOp); } int CEditor::PopupSelectGameTileOpResult() @@ -2362,7 +2373,7 @@ int CEditor::PopupSelectGameTileOpResult() static int s_AutoMapConfigSelected = -100; static int s_AutoMapConfigCurrent = -100; -CUI::EPopupMenuFunctionResult CEditor::PopupSelectConfigAutoMap(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupSelectConfigAutoMap(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); std::shared_ptr pLayer = std::static_pointer_cast(pEditor->GetSelectedLayer(0)); @@ -2371,31 +2382,31 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSelectConfigAutoMap(void *pContext, const float ButtonHeight = 12.0f; const float ButtonMargin = 2.0f; - static CScrollRegion s_ScrollRegion; - vec2 ScrollOffset(0.0f, 0.0f); - CScrollRegionParams ScrollParams; - ScrollParams.m_ScrollbarWidth = 10.0f; - ScrollParams.m_ScrollbarMargin = 3.0f; - ScrollParams.m_ScrollUnit = (ButtonHeight + ButtonMargin) * 5; - s_ScrollRegion.Begin(&View, &ScrollOffset, &ScrollParams); - View.y += ScrollOffset.y; + static CListBox s_ListBox; + s_ListBox.DoStart(ButtonHeight, pAutoMapper->ConfigNamesNum() + 1, 1, 4, s_AutoMapConfigCurrent + 1, &View, false); + s_ListBox.DoAutoSpacing(ButtonMargin); - for(int i = -1; i < pAutoMapper->ConfigNamesNum(); i++) + for(int i = 0; i < pAutoMapper->ConfigNamesNum() + 1; i++) { - CUIRect Button; - View.HSplitTop(ButtonMargin, nullptr, &View); - View.HSplitTop(ButtonHeight, &Button, &View); - if(s_ScrollRegion.AddRect(Button)) - { - static int s_NoneButton = 0; - if(pEditor->DoButton_MenuItem(i == -1 ? (void *)&s_NoneButton : pAutoMapper->GetConfigName(i), i == -1 ? "None" : pAutoMapper->GetConfigName(i), i == s_AutoMapConfigCurrent, &Button)) - s_AutoMapConfigSelected = i; - } + static int s_NoneButton = 0; + CListboxItem Item = s_ListBox.DoNextItem(i == 0 ? (void *)&s_NoneButton : pAutoMapper->GetConfigName(i - 1), (i - 1) == s_AutoMapConfigCurrent, 3.0f); + if(!Item.m_Visible) + continue; + + CUIRect Label; + Item.m_Rect.VMargin(5.0f, &Label); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + Props.m_EllipsisAtEnd = true; + pEditor->Ui()->DoLabel(&Label, i == 0 ? "None" : pAutoMapper->GetConfigName(i - 1), EditorFontSizes::MENU, TEXTALIGN_ML, Props); } - s_ScrollRegion.End(); + int NewSelected = s_ListBox.DoEnd() - 1; + if(NewSelected != s_AutoMapConfigCurrent) + s_AutoMapConfigSelected = NewSelected; - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } void CEditor::PopupSelectConfigAutoMapInvoke(int Current, float x, float y) @@ -2406,7 +2417,7 @@ void CEditor::PopupSelectConfigAutoMapInvoke(int Current, float x, float y) std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayer(0)); const int ItemCount = minimum(m_Map.m_vpImages[pLayer->m_Image]->m_AutoMapper.ConfigNamesNum(), 10); // Width for buttons is 120, 15 is the scrollbar width, 2 is the margin between both. - UI()->DoPopupMenu(&s_PopupSelectConfigAutoMapId, x, y, 120.0f + 15.0f + 2.0f, 26.0f + 14.0f * ItemCount, this, PopupSelectConfigAutoMap); + Ui()->DoPopupMenu(&s_PopupSelectConfigAutoMapId, x, y, 120.0f + 15.0f + 2.0f, 26.0f + 14.0f * ItemCount, this, PopupSelectConfigAutoMap); } int CEditor::PopupSelectConfigAutoMapResult() @@ -2421,77 +2432,121 @@ int CEditor::PopupSelectConfigAutoMapResult() // DDRace -CUI::EPopupMenuFunctionResult CEditor::PopupTele(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupTele(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); - static int s_PreviousNumber = -1; + static int s_PreviousTeleNumber; + static int s_PreviousCheckpointNumber; + static int s_PreviousViewTeleNumber; CUIRect NumberPicker; CUIRect FindEmptySlot; + CUIRect FindFreeTeleSlot, FindFreeCheckpointSlot, FindFreeViewSlot; View.VSplitRight(15.f, &NumberPicker, &FindEmptySlot); NumberPicker.VSplitRight(2.f, &NumberPicker, nullptr); - FindEmptySlot.HSplitTop(13.0f, &FindEmptySlot, nullptr); - FindEmptySlot.HMargin(1.0f, &FindEmptySlot); - // find empty number button + FindEmptySlot.HSplitTop(13.0f, &FindFreeTeleSlot, &FindEmptySlot); + FindEmptySlot.HSplitTop(13.0f, &FindFreeCheckpointSlot, &FindEmptySlot); + FindEmptySlot.HSplitTop(13.0f, &FindFreeViewSlot, &FindEmptySlot); + + FindFreeTeleSlot.HMargin(1.0f, &FindFreeTeleSlot); + FindFreeCheckpointSlot.HMargin(1.0f, &FindFreeCheckpointSlot); + FindFreeViewSlot.HMargin(1.0f, &FindFreeViewSlot); + + auto ViewTele = [](CEditor *pEd) -> bool { + if(!pEd->m_ViewTeleNumber) + return false; + int TeleX, TeleY; + pEd->m_Map.m_pTeleLayer->GetPos(pEd->m_ViewTeleNumber, -1, TeleX, TeleY); + if(TeleX != -1 && TeleY != -1) + { + pEd->MapView()->SetWorldOffset({32.0f * TeleX + 0.5f, 32.0f * TeleY + 0.5f}); + return true; + } + return false; + }; + + static std::vector s_vColors = { + ColorRGBA(0.5f, 1, 0.5f, 0.5f), + ColorRGBA(0.5f, 1, 0.5f, 0.5f), + ColorRGBA(0.5f, 1, 0.5f, 0.5f), + }; + enum { - static int s_EmptySlotPid = 0; - if(pEditor->DoButton_Editor(&s_EmptySlotPid, "F", 0, &FindEmptySlot, 0, "[ctrl+f] Find empty slot") || (Active && pEditor->Input()->ModifierIsPressed() && pEditor->Input()->KeyPress(KEY_F))) + PROP_TELE = 0, + PROP_TELE_CP, + PROP_TELE_VIEW, + NUM_PROPS, + }; + + // find next free numbers buttons + { + // Pressing ctrl+f will find next free numbers for both tele and checkpoints + + static int s_NextFreeTelePid = 0; + if(pEditor->DoButton_Editor(&s_NextFreeTelePid, "F", 0, &FindFreeTeleSlot, 0, "[ctrl+f] Find next free tele number") || (Active && pEditor->Input()->ModifierIsPressed() && pEditor->Input()->KeyPress(KEY_F))) { - int number = -1; - for(int i = 1; i <= 255; i++) - { - if(!pEditor->m_Map.m_pTeleLayer->ContainsElementWithId(i)) - { - number = i; - break; - } - } + int TeleNumber = pEditor->FindNextFreeTeleNumber(); - if(number != -1) - { - pEditor->m_TeleNumber = number; - } + if(TeleNumber != -1) + pEditor->m_TeleNumber = TeleNumber; + } + + static int s_NextFreeCheckpointPid = 0; + if(pEditor->DoButton_Editor(&s_NextFreeCheckpointPid, "F", 0, &FindFreeCheckpointSlot, 0, "[ctrl+f] Find next free checkpoint number") || (Active && pEditor->Input()->ModifierIsPressed() && pEditor->Input()->KeyPress(KEY_F))) + { + int CPNumber = pEditor->FindNextFreeTeleNumber(true); + + if(CPNumber != -1) + pEditor->m_TeleCheckpointNumber = CPNumber; } + + static int s_NextFreeViewPid = 0; + int btn = pEditor->DoButton_Editor(&s_NextFreeViewPid, "N", 0, &FindFreeViewSlot, 0, "[n] Show next tele with this number"); + if(btn || (Active && pEditor->Input()->KeyPress(KEY_N))) + s_vColors[PROP_TELE_VIEW] = ViewTele(pEditor) ? ColorRGBA(0.5f, 1, 0.5f, 0.5f) : ColorRGBA(1, 0.5f, 0.5f, 0.5f); } // number picker { - static ColorRGBA s_Color = ColorRGBA(0.5f, 1, 0.5f, 0.5f); - - enum - { - PROP_TELE = 0, - NUM_PROPS, - }; CProperty aProps[] = { - {"Number", pEditor->m_TeleNumber, PROPTYPE_INT_STEP, 1, 255}, + {"Number", pEditor->m_TeleNumber, PROPTYPE_INT, 1, 255}, + {"Checkpoint", pEditor->m_TeleCheckpointNumber, PROPTYPE_INT, 1, 255}, + {"View", pEditor->m_ViewTeleNumber, PROPTYPE_INT, 1, 255}, {nullptr}, }; static int s_aIds[NUM_PROPS] = {0}; - static int NewVal = 0; - int Prop = pEditor->DoProperties(&NumberPicker, aProps, s_aIds, &NewVal, s_Color); + int NewVal = 0; + int Prop = pEditor->DoProperties(&NumberPicker, aProps, s_aIds, &NewVal, s_vColors); if(Prop == PROP_TELE) - { pEditor->m_TeleNumber = (NewVal - 1 + 255) % 255 + 1; - } + else if(Prop == PROP_TELE_CP) + pEditor->m_TeleCheckpointNumber = (NewVal - 1 + 255) % 255 + 1; + else if(Prop == PROP_TELE_VIEW) + pEditor->m_ViewTeleNumber = (NewVal - 1 + 255) % 255 + 1; - if(s_PreviousNumber == 1 || s_PreviousNumber != pEditor->m_TeleNumber) - { - s_Color = pEditor->m_Map.m_pTeleLayer->ContainsElementWithId(pEditor->m_TeleNumber) ? ColorRGBA(1, 0.5f, 0.5f, 0.5f) : ColorRGBA(0.5f, 1, 0.5f, 0.5f); - } + if(s_PreviousTeleNumber == 1 || s_PreviousTeleNumber != pEditor->m_TeleNumber) + s_vColors[PROP_TELE] = pEditor->m_Map.m_pTeleLayer->ContainsElementWithId(pEditor->m_TeleNumber, false) ? ColorRGBA(1, 0.5f, 0.5f, 0.5f) : ColorRGBA(0.5f, 1, 0.5f, 0.5f); + + if(s_PreviousCheckpointNumber == 1 || s_PreviousCheckpointNumber != pEditor->m_TeleCheckpointNumber) + s_vColors[PROP_TELE_CP] = pEditor->m_Map.m_pTeleLayer->ContainsElementWithId(pEditor->m_TeleCheckpointNumber, true) ? ColorRGBA(1, 0.5f, 0.5f, 0.5f) : ColorRGBA(0.5f, 1, 0.5f, 0.5f); + + if(s_PreviousViewTeleNumber != pEditor->m_ViewTeleNumber) + s_vColors[PROP_TELE_VIEW] = ViewTele(pEditor) ? ColorRGBA(0.5f, 1, 0.5f, 0.5f) : ColorRGBA(1, 0.5f, 0.5f, 0.5f); } - s_PreviousNumber = pEditor->m_TeleNumber; + s_PreviousTeleNumber = pEditor->m_TeleNumber; + s_PreviousCheckpointNumber = pEditor->m_TeleCheckpointNumber; + s_PreviousViewTeleNumber = pEditor->m_ViewTeleNumber; - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupSpeedup(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupSpeedup(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -2504,8 +2559,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSpeedup(void *pContext, CUIRect View }; CProperty aProps[] = { - {"Force", pEditor->m_SpeedupForce, PROPTYPE_INT_STEP, 1, 255}, - {"Max Speed", pEditor->m_SpeedupMaxSpeed, PROPTYPE_INT_STEP, 0, 255}, + {"Force", pEditor->m_SpeedupForce, PROPTYPE_INT, 1, 255}, + {"Max Speed", pEditor->m_SpeedupMaxSpeed, PROPTYPE_INT, 0, 255}, {"Angle", pEditor->m_SpeedupAngle, PROPTYPE_ANGLE_SCROLL, 0, 359}, {nullptr}, }; @@ -2527,63 +2582,82 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSpeedup(void *pContext, CUIRect View pEditor->m_SpeedupAngle = clamp(NewVal, 0, 359); } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupSwitch(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupSwitch(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); - CUIRect NumberPicker, FindEmptySlot; + CUIRect NumberPicker, FindEmptySlot, ViewEmptySlot; View.VSplitRight(15.0f, &NumberPicker, &FindEmptySlot); NumberPicker.VSplitRight(2.0f, &NumberPicker, nullptr); - FindEmptySlot.HSplitTop(13.0f, &FindEmptySlot, nullptr); + + FindEmptySlot.HSplitTop(13.0f, &FindEmptySlot, &ViewEmptySlot); + ViewEmptySlot.HSplitTop(13.0f, nullptr, &ViewEmptySlot); + FindEmptySlot.HMargin(1.0f, &FindEmptySlot); + ViewEmptySlot.HMargin(1.0f, &ViewEmptySlot); + + auto ViewSwitch = [pEditor]() -> bool { + if(!pEditor->m_ViewSwitch) + return false; + ivec2 SwitchPos; + pEditor->m_Map.m_pSwitchLayer->GetPos(pEditor->m_ViewSwitch, -1, SwitchPos); + if(SwitchPos != ivec2(-1, -1)) + { + pEditor->MapView()->SetWorldOffset({32.0f * SwitchPos.x + 0.5f, 32.0f * SwitchPos.y + 0.5f}); + return true; + } + return false; + }; + + static std::vector s_vColors = { + ColorRGBA(1, 1, 1, 0.5f), + ColorRGBA(1, 1, 1, 0.5f), + ColorRGBA(1, 1, 1, 0.5f), + }; + + enum + { + PROP_SWITCH_NUMBER = 0, + PROP_SWITCH_DELAY, + PROP_SWITCH_VIEW, + NUM_PROPS, + }; // find empty number button { static int s_EmptySlotPid = 0; if(pEditor->DoButton_Editor(&s_EmptySlotPid, "F", 0, &FindEmptySlot, 0, "[ctrl+f] Find empty slot") || (Active && pEditor->Input()->ModifierIsPressed() && pEditor->Input()->KeyPress(KEY_F))) { - int Number = -1; - for(int i = 1; i <= 255; i++) - { - if(!pEditor->m_Map.m_pSwitchLayer->ContainsElementWithId(i)) - { - Number = i; - break; - } - } + int Number = pEditor->FindNextFreeSwitchNumber(); if(Number != -1) - { pEditor->m_SwitchNum = Number; - } } + + static int s_NextViewPid = 0; + int ButtonResult = pEditor->DoButton_Editor(&s_NextViewPid, "N", 0, &ViewEmptySlot, 0, "[n] Show next switcher with this number"); + if(ButtonResult || (Active && pEditor->Input()->KeyPress(KEY_N))) + s_vColors[PROP_SWITCH_VIEW] = ViewSwitch() ? ColorRGBA(0.5f, 1, 0.5f, 0.5f) : ColorRGBA(1, 0.5f, 0.5f, 0.5f); } // number picker static int s_PreviousNumber = -1; + static int s_PreviousView = -1; { - static ColorRGBA s_Color = ColorRGBA(1, 1, 1, 0.5f); - - enum - { - PROP_SWITCH_NUMBER = 0, - PROP_SWITCH_DELAY, - NUM_PROPS, - }; - CProperty aProps[] = { - {"Number", pEditor->m_SwitchNum, PROPTYPE_INT_STEP, 0, 255}, - {"Delay", pEditor->m_SwitchDelay, PROPTYPE_INT_STEP, 0, 255}, + {"Number", pEditor->m_SwitchNum, PROPTYPE_INT, 0, 255}, + {"Delay", pEditor->m_SwitchDelay, PROPTYPE_INT, 0, 255}, + {"View", pEditor->m_ViewSwitch, PROPTYPE_INT, 0, 255}, {nullptr}, }; static int s_aIds[NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&NumberPicker, aProps, s_aIds, &NewVal, s_Color); + int Prop = pEditor->DoProperties(&NumberPicker, aProps, s_aIds, &NewVal, s_vColors); if(Prop == PROP_SWITCH_NUMBER) { @@ -2593,18 +2667,23 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSwitch(void *pContext, CUIRect View, { pEditor->m_SwitchDelay = (NewVal + 256) % 256; } - - if(s_PreviousNumber == 1 || s_PreviousNumber != pEditor->m_SwitchNum) + else if(Prop == PROP_SWITCH_VIEW) { - s_Color = pEditor->m_Map.m_pSwitchLayer->ContainsElementWithId(pEditor->m_SwitchNum) ? ColorRGBA(1, 0.5f, 0.5f, 0.5f) : ColorRGBA(0.5f, 1, 0.5f, 0.5f); + pEditor->m_ViewSwitch = (NewVal + 256) % 256; } + + if(s_PreviousNumber == 1 || s_PreviousNumber != pEditor->m_SwitchNum) + s_vColors[PROP_SWITCH_NUMBER] = pEditor->m_Map.m_pSwitchLayer->ContainsElementWithId(pEditor->m_SwitchNum) ? ColorRGBA(1, 0.5f, 0.5f, 0.5f) : ColorRGBA(0.5f, 1, 0.5f, 0.5f); + if(s_PreviousView != pEditor->m_ViewSwitch) + s_vColors[PROP_SWITCH_VIEW] = ViewSwitch() ? ColorRGBA(0.5f, 1, 0.5f, 0.5f) : ColorRGBA(1, 0.5f, 0.5f, 0.5f); } s_PreviousNumber = pEditor->m_SwitchNum; - return CUI::POPUP_KEEP_OPEN; + s_PreviousView = pEditor->m_ViewSwitch; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupTune(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupTune(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -2615,7 +2694,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupTune(void *pContext, CUIRect View, b }; CProperty aProps[] = { - {"Zone", pEditor->m_TuningNum, PROPTYPE_INT_STEP, 1, 255}, + {"Zone", pEditor->m_TuningNum, PROPTYPE_INT, 1, 255}, {nullptr}, }; @@ -2628,10 +2707,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupTune(void *pContext, CUIRect View, b pEditor->m_TuningNum = (NewVal - 1 + 255) % 255 + 1; } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupGoto(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupGoto(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -2645,14 +2724,14 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGoto(void *pContext, CUIRect View, b static ivec2 s_GotoPos(0, 0); CProperty aProps[] = { - {"X", s_GotoPos.x, PROPTYPE_INT_STEP, std::numeric_limits::min(), std::numeric_limits::max()}, - {"Y", s_GotoPos.y, PROPTYPE_INT_STEP, std::numeric_limits::min(), std::numeric_limits::max()}, + {"X", s_GotoPos.x, PROPTYPE_INT, std::numeric_limits::min(), std::numeric_limits::max()}, + {"Y", s_GotoPos.y, PROPTYPE_INT, std::numeric_limits::min(), std::numeric_limits::max()}, {nullptr}, }; static int s_aIds[NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal, ColorRGBA(1, 1, 1, 0.5f)); + int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); if(Prop == PROP_COORD_X) { @@ -2672,10 +2751,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGoto(void *pContext, CUIRect View, b pEditor->MapView()->SetWorldOffset({32.0f * s_GotoPos.x + 0.5f, 32.0f * s_GotoPos.y + 0.5f}); } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupEntities(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupEntities(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -2689,25 +2768,30 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEntities(void *pContext, CUIRect Vie { if(pEditor->m_vSelectEntitiesFiles[i] != pEditor->m_SelectEntitiesImage) { + if(i == pEditor->m_vSelectEntitiesFiles.size() - 1) + { + pEditor->InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_IMG, "Custom Entities", "Load", "assets/entities", false, CallbackCustomEntities, pEditor); + return CUi::POPUP_CLOSE_CURRENT; + } + pEditor->m_SelectEntitiesImage = pEditor->m_vSelectEntitiesFiles[i]; pEditor->m_AllowPlaceUnusedTiles = pEditor->m_SelectEntitiesImage == "DDNet" ? 0 : -1; pEditor->m_PreventUnusedTilesWasWarned = false; - if(pEditor->m_EntitiesTexture.IsValid()) - pEditor->Graphics()->UnloadTexture(&pEditor->m_EntitiesTexture); + pEditor->Graphics()->UnloadTexture(&pEditor->m_EntitiesTexture); char aBuf[IO_MAX_PATH_LENGTH]; str_format(aBuf, sizeof(aBuf), "editor/entities/%s.png", pName); pEditor->m_EntitiesTexture = pEditor->Graphics()->LoadTexture(aBuf, IStorage::TYPE_ALL, pEditor->GetTextureUsageFlag()); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } } } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CEditor::PopupProofMode(void *pContext, CUIRect View, bool Active) +CUi::EPopupMenuFunctionResult CEditor::PopupProofMode(void *pContext, CUIRect View, bool Active) { CEditor *pEditor = static_cast(pContext); @@ -2717,7 +2801,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupProofMode(void *pContext, CUIRect Vi if(pEditor->DoButton_MenuItem(&s_ButtonIngame, "Ingame", pEditor->MapView()->ProofMode()->IsModeIngame(), &Button, 0, "These borders represent what a player maximum can see.")) { pEditor->MapView()->ProofMode()->SetModeIngame(); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; } View.HSplitTop(2.0f, nullptr, &View); @@ -2726,8 +2810,65 @@ CUI::EPopupMenuFunctionResult CEditor::PopupProofMode(void *pContext, CUIRect Vi if(pEditor->DoButton_MenuItem(&s_ButtonMenu, "Menu", pEditor->MapView()->ProofMode()->IsModeMenu(), &Button, 0, "These borders represent what will be shown in the menu.")) { pEditor->MapView()->ProofMode()->SetModeMenu(); - return CUI::POPUP_CLOSE_CURRENT; + return CUi::POPUP_CLOSE_CURRENT; + } + + return CUi::POPUP_KEEP_OPEN; +} + +CUi::EPopupMenuFunctionResult CEditor::PopupAnimateSettings(void *pContext, CUIRect View, bool Active) +{ + CEditor *pEditor = static_cast(pContext); + + constexpr float MIN_ANIM_SPEED = 0.001f; + constexpr float MAX_ANIM_SPEED = 1000000.0f; + + CUIRect Row, Label, ButtonDecrease, EditBox, ButtonIncrease, ButtonReset; + View.HSplitTop(13.0f, &Row, &View); + Row.VSplitMid(&Label, &Row); + Row.HMargin(1.0f, &Row); + Row.VSplitLeft(10.0f, &ButtonDecrease, &Row); + Row.VSplitRight(10.0f, &EditBox, &ButtonIncrease); + View.HSplitBottom(12.0f, &View, &ButtonReset); + pEditor->Ui()->DoLabel(&Label, "Speed", 10.0f, TEXTALIGN_ML); + + static char s_DecreaseButton; + if(pEditor->DoButton_FontIcon(&s_DecreaseButton, FONT_ICON_MINUS, 0, &ButtonDecrease, 0, "Decrease animation speed", IGraphics::CORNER_L, 7.0f)) + { + pEditor->m_AnimateSpeed -= pEditor->m_AnimateSpeed <= 1.0f ? 0.1f : 0.5f; + pEditor->m_AnimateSpeed = maximum(pEditor->m_AnimateSpeed, MIN_ANIM_SPEED); + pEditor->m_AnimateUpdatePopup = true; + } + + static char s_IncreaseButton; + if(pEditor->DoButton_FontIcon(&s_IncreaseButton, FONT_ICON_PLUS, 0, &ButtonIncrease, 0, "Increase animation speed", IGraphics::CORNER_R, 7.0f)) + { + if(pEditor->m_AnimateSpeed < 0.1f) + pEditor->m_AnimateSpeed = 0.1f; + else + pEditor->m_AnimateSpeed += pEditor->m_AnimateSpeed < 1.0f ? 0.1f : 0.5f; + pEditor->m_AnimateSpeed = minimum(pEditor->m_AnimateSpeed, MAX_ANIM_SPEED); + pEditor->m_AnimateUpdatePopup = true; + } + + static char s_DefaultButton; + if(pEditor->DoButton_Ex(&s_DefaultButton, "Default", 0, &ButtonReset, 0, "Normal animation speed", IGraphics::CORNER_ALL)) + { + pEditor->m_AnimateSpeed = 1.0f; + pEditor->m_AnimateUpdatePopup = true; + } + + static CLineInputNumber s_SpeedInput; + if(pEditor->m_AnimateUpdatePopup) + { + s_SpeedInput.SetFloat(pEditor->m_AnimateSpeed); + pEditor->m_AnimateUpdatePopup = false; + } + + if(pEditor->DoEditBox(&s_SpeedInput, &EditBox, 10.0f, IGraphics::CORNER_NONE, "The animation speed")) + { + pEditor->m_AnimateSpeed = clamp(s_SpeedInput.GetFloat(), MIN_ANIM_SPEED, MAX_ANIM_SPEED); } - return CUI::POPUP_KEEP_OPEN; + return CUi::POPUP_KEEP_OPEN; } diff --git a/src/game/editor/prompt.cpp b/src/game/editor/prompt.cpp new file mode 100644 index 0000000000..822651e004 --- /dev/null +++ b/src/game/editor/prompt.cpp @@ -0,0 +1,166 @@ +#include +#include +#include + +#include "editor.h" + +#include "prompt.h" + +bool FuzzyMatch(const char *pHaystack, const char *pNeedle) +{ + if(!pNeedle || !pNeedle[0]) + return false; + char aBuf[2] = {0}; + const char *pHit = pHaystack; + int NeedleLen = str_length(pNeedle); + for(int i = 0; i < NeedleLen; i++) + { + if(!pHit) + return false; + aBuf[0] = pNeedle[i]; + pHit = str_find_nocase(pHit, aBuf); + if(pHit) + pHit++; + } + return pHit; +} + +bool CPrompt::IsActive() +{ + return CEditorComponent::IsActive() || Editor()->m_Dialog == DIALOG_QUICK_PROMPT; +} + +void CPrompt::SetActive() +{ + Editor()->m_Dialog = DIALOG_QUICK_PROMPT; + CEditorComponent::SetActive(); + + Ui()->SetActiveItem(&m_PromptInput); +} + +void CPrompt::SetInactive() +{ + m_ResetFilterResults = true; + m_PromptInput.Clear(); + if(Editor()->m_Dialog == DIALOG_QUICK_PROMPT) + Editor()->m_Dialog = DIALOG_NONE; + CEditorComponent::SetInactive(); +} + +bool CPrompt::OnInput(const IInput::CEvent &Event) +{ + if(Input()->ModifierIsPressed() && Input()->KeyIsPressed(KEY_P)) + { + SetActive(); + } + return false; +} + +void CPrompt::OnInit(CEditor *pEditor) +{ + CEditorComponent::OnInit(pEditor); + +#define REGISTER_QUICK_ACTION(name, text, callback, disabled, active, button_color, description) m_vQuickActions.emplace_back(&Editor()->m_QuickAction##name); +#include +#undef REGISTER_QUICK_ACTION +} + +void CPrompt::OnRender(CUIRect _) +{ + if(!IsActive()) + return; + + if(Ui()->ConsumeHotkey(CUi::HOTKEY_ESCAPE)) + { + SetInactive(); + return; + } + + static CListBox s_ListBox; + CUIRect Prompt, PromptBox; + CUIRect Suggestions; + + Ui()->MapScreen(); + CUIRect Overlay = *Ui()->Screen(); + + Overlay.Draw(ColorRGBA(0, 0, 0, 0.33f), IGraphics::CORNER_NONE, 0.0f); + CUIRect Background; + Overlay.VMargin(150.0f, &Background); + Background.HMargin(50.0f, &Background); + Background.Draw(ColorRGBA(0, 0, 0, 0.80f), IGraphics::CORNER_ALL, 5.0f); + + Background.Margin(10.0f, &Prompt); + + Prompt.VSplitMid(nullptr, &PromptBox); + + Prompt.HSplitTop(16.0f, &PromptBox, &Suggestions); + PromptBox.Draw(ColorRGBA(0, 0, 0, 0.75f), IGraphics::CORNER_ALL, 2.0f); + Suggestions.y += 6.0f; + + if(Ui()->DoClearableEditBox(&m_PromptInput, &PromptBox, 10.0f) || m_ResetFilterResults) + { + m_PromptSelectedIndex = 0; + m_vpFilteredPromptList.clear(); + if(m_ResetFilterResults && m_pLastAction && !m_pLastAction->Disabled()) + { + m_vpFilteredPromptList.push_back(m_pLastAction); + } + for(auto *pQuickAction : m_vQuickActions) + { + if(pQuickAction->Disabled()) + continue; + + if(m_PromptInput.IsEmpty() || FuzzyMatch(pQuickAction->Label(), m_PromptInput.GetString())) + { + if(!m_ResetFilterResults || pQuickAction != m_pLastAction) + m_vpFilteredPromptList.push_back(pQuickAction); + } + } + m_ResetFilterResults = false; + } + + s_ListBox.SetActive(!Ui()->IsPopupOpen()); + s_ListBox.DoStart(15.0f, m_vpFilteredPromptList.size(), 1, 5, m_PromptSelectedIndex, &Suggestions, false); + + float LabelWidth = Overlay.w > 855.0f ? 200.0f : 100.0f; + + for(size_t i = 0; i < m_vpFilteredPromptList.size(); i++) + { + const CListboxItem Item = s_ListBox.DoNextItem(m_vpFilteredPromptList[i]->ActionButtonId(), m_PromptSelectedIndex >= 0 && (size_t)m_PromptSelectedIndex == i); + if(!Item.m_Visible) + continue; + + CUIRect LabelColumn, DescColumn; + float Margin = 5.0f; + Item.m_Rect.VMargin(Margin, &LabelColumn); + LabelColumn.VSplitLeft(LabelWidth, &LabelColumn, &DescColumn); + DescColumn.VSplitLeft(Margin, nullptr, &DescColumn); + + SLabelProperties Props; + Props.m_MaxWidth = LabelColumn.w; + Props.m_EllipsisAtEnd = true; + Ui()->DoLabel(&LabelColumn, m_vpFilteredPromptList[i]->Label(), 10.0f, TEXTALIGN_ML, Props); + + Props.m_MaxWidth = DescColumn.w; + TextRender()->TextColor(TextRender()->DefaultTextColor().WithAlpha(Item.m_Selected ? 1.0f : 0.8f)); + Ui()->DoLabel(&DescColumn, m_vpFilteredPromptList[i]->Description(), 10.0f, TEXTALIGN_MR, Props); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + + const int NewSelected = s_ListBox.DoEnd(); + if(m_PromptSelectedIndex != NewSelected) + { + m_PromptSelectedIndex = NewSelected; + } + + if(s_ListBox.WasItemActivated()) + { + if(m_PromptSelectedIndex >= 0) + { + CQuickAction *pBtn = m_vpFilteredPromptList[m_PromptSelectedIndex]; + SetInactive(); + pBtn->Call(); + m_pLastAction = pBtn; + } + } +} diff --git a/src/game/editor/prompt.h b/src/game/editor/prompt.h new file mode 100644 index 0000000000..2d873e580c --- /dev/null +++ b/src/game/editor/prompt.h @@ -0,0 +1,29 @@ +#ifndef GAME_EDITOR_PROMPT_H +#define GAME_EDITOR_PROMPT_H + +#include +#include +#include + +#include "component.h" + +class CPrompt : public CEditorComponent +{ + bool m_ResetFilterResults = true; + CQuickAction *m_pLastAction = nullptr; + int m_PromptSelectedIndex = -1; + + std::vector m_vpFilteredPromptList; + std::vector m_vQuickActions; + CLineInputBuffered<512> m_PromptInput; + +public: + void OnInit(CEditor *pEditor) override; + bool OnInput(const IInput::CEvent &Event) override; + void OnRender(CUIRect _) override; + bool IsActive(); + void SetActive(); + void SetInactive(); +}; + +#endif diff --git a/src/game/editor/proof_mode.cpp b/src/game/editor/proof_mode.cpp index 773e356d12..769fee4989 100644 --- a/src/game/editor/proof_mode.cpp +++ b/src/game/editor/proof_mode.cpp @@ -4,9 +4,9 @@ #include "editor.h" -void CProofMode::Init(CEditor *pEditor) +void CProofMode::OnInit(CEditor *pEditor) { - CEditorComponent::Init(pEditor); + CEditorComponent::OnInit(pEditor); SetMenuBackgroundPositionNames(); OnReset(); OnMapLoad(); @@ -47,6 +47,7 @@ void CProofMode::SetMenuBackgroundPositionNames() m_vpMenuBackgroundPositionNames[CMenuBackground::POS_BROWSER_CUSTOM1] = "custom(2)"; m_vpMenuBackgroundPositionNames[CMenuBackground::POS_BROWSER_CUSTOM2] = "custom(3)"; m_vpMenuBackgroundPositionNames[CMenuBackground::POS_BROWSER_CUSTOM3] = "custom(4)"; + m_vpMenuBackgroundPositionNames[CMenuBackground::POS_BROWSER_CUSTOM4] = "custom(5)"; m_vpMenuBackgroundPositionNames[CMenuBackground::POS_SETTINGS_RESERVED0] = "reserved settings(1)"; m_vpMenuBackgroundPositionNames[CMenuBackground::POS_SETTINGS_RESERVED1] = "reserved settings(2)"; m_vpMenuBackgroundPositionNames[CMenuBackground::POS_RESERVED0] = "reserved(1)"; diff --git a/src/game/editor/proof_mode.h b/src/game/editor/proof_mode.h index 81b77f8eac..ffffd84732 100644 --- a/src/game/editor/proof_mode.h +++ b/src/game/editor/proof_mode.h @@ -6,7 +6,7 @@ class CProofMode : public CEditorComponent { public: - void Init(CEditor *pEditor) override; + void OnInit(CEditor *pEditor) override; void OnReset() override; void OnMapLoad() override; void RenderScreenSizes(); diff --git a/src/game/editor/quick_action.h b/src/game/editor/quick_action.h new file mode 100644 index 0000000000..b6bb686345 --- /dev/null +++ b/src/game/editor/quick_action.h @@ -0,0 +1,73 @@ +#ifndef GAME_EDITOR_QUICK_ACTION_H +#define GAME_EDITOR_QUICK_ACTION_H + +#include +#include + +typedef std::function FButtonClickCallback; +typedef std::function FButtonDisabledCallback; +typedef std::function FButtonActiveCallback; +typedef std::function FButtonColorCallback; + +class CQuickAction +{ +private: + const char *m_pLabel; + const char *m_pDescription; + + FButtonClickCallback m_pfnCallback; + FButtonDisabledCallback m_pfnDisabledCallback; + FButtonActiveCallback m_pfnActiveCallback; + FButtonColorCallback m_pfnColorCallback; + + const char m_ActionButtonId = 0; + +public: + CQuickAction( + const char *pLabel, + const char *pDescription, + FButtonClickCallback pfnCallback, + FButtonDisabledCallback pfnDisabledCallback, + FButtonActiveCallback pfnActiveCallback, + FButtonColorCallback pfnColorCallback) : + m_pLabel(pLabel), + m_pDescription(pDescription), + m_pfnCallback(std::move(pfnCallback)), + m_pfnDisabledCallback(std::move(pfnDisabledCallback)), + m_pfnActiveCallback(std::move(pfnActiveCallback)), + m_pfnColorCallback(std::move(pfnColorCallback)) + { + } + + // code to run when the action is triggered + void Call() const { m_pfnCallback(); } + + // bool that indicates if the action can be performed not or not + bool Disabled() { return m_pfnDisabledCallback(); } + + // bool that indicates if the action is currently running + // only applies to actions that can be turned on or off like proof borders + bool Active() { return m_pfnActiveCallback(); } + + // color "enum" that represents the state of the quick actions button + // used as Checked argument for DoButton_Editor() + int Color() { return m_pfnColorCallback(); } + + const char *Label() const { return m_pLabel; } + + // skips to the part of the label after the first colon + // useful for buttons that only show the state + const char *LabelShort() const + { + const char *pShort = str_find(m_pLabel, ": "); + if(!pShort) + return m_pLabel; + return pShort + 2; + } + + const char *Description() const { return m_pDescription; } + + const void *ActionButtonId() const { return &m_ActionButtonId; } +}; + +#endif diff --git a/src/game/editor/quick_actions.cpp b/src/game/editor/quick_actions.cpp new file mode 100644 index 0000000000..852ce48d61 --- /dev/null +++ b/src/game/editor/quick_actions.cpp @@ -0,0 +1,216 @@ +#include +#include + +#include "editor.h" + +#include "editor_actions.h" + +void CEditor::FillGameTiles(EGameTileOp FillTile) const +{ + std::shared_ptr pTileLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_TILES)); + if(pTileLayer) + pTileLayer->FillGameTiles(FillTile); +} + +bool CEditor::CanFillGameTiles() const +{ + std::shared_ptr pTileLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_TILES)); + if(pTileLayer) + return pTileLayer->CanFillGameTiles(); + return false; +} + +void CEditor::AddQuadOrSound() +{ + std::shared_ptr pLayer = GetSelectedLayer(0); + if(!pLayer) + return; + if(pLayer->m_Type != LAYERTYPE_QUADS && pLayer->m_Type != LAYERTYPE_SOUNDS) + return; + + std::shared_ptr pGroup = GetSelectedGroup(); + + float aMapping[4]; + pGroup->Mapping(aMapping); + int x = aMapping[0] + (aMapping[2] - aMapping[0]) / 2; + int y = aMapping[1] + (aMapping[3] - aMapping[1]) / 2; + if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr && Input()->KeyPress(KEY_Q) && Input()->ModifierIsPressed()) + { + x += Ui()->MouseWorldX() - (MapView()->GetWorldOffset().x * pGroup->m_ParallaxX / 100) - pGroup->m_OffsetX; + y += Ui()->MouseWorldY() - (MapView()->GetWorldOffset().y * pGroup->m_ParallaxY / 100) - pGroup->m_OffsetY; + } + + if(pLayer->m_Type == LAYERTYPE_QUADS) + m_EditorHistory.Execute(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0], x, y)); + else if(pLayer->m_Type == LAYERTYPE_SOUNDS) + m_EditorHistory.Execute(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0], x, y)); +} + +void CEditor::AddGroup() +{ + m_Map.NewGroup(); + m_SelectedGroup = m_Map.m_vpGroups.size() - 1; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, false)); +} + +void CEditor::AddSoundLayer() +{ + std::shared_ptr pSoundLayer = std::make_shared(this); + m_Map.m_vpGroups[m_SelectedGroup]->AddLayer(pSoundLayer); + int LayerIndex = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1; + SelectLayer(LayerIndex); + m_Map.m_vpGroups[m_SelectedGroup]->m_Collapse = false; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, LayerIndex)); +} + +void CEditor::AddTileLayer() +{ + std::shared_ptr pTileLayer = std::make_shared(this, m_Map.m_pGameLayer->m_Width, m_Map.m_pGameLayer->m_Height); + pTileLayer->m_pEditor = this; + m_Map.m_vpGroups[m_SelectedGroup]->AddLayer(pTileLayer); + int LayerIndex = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1; + SelectLayer(LayerIndex); + m_Map.m_vpGroups[m_SelectedGroup]->m_Collapse = false; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, LayerIndex)); +} + +void CEditor::AddQuadsLayer() +{ + std::shared_ptr pQuadLayer = std::make_shared(this); + m_Map.m_vpGroups[m_SelectedGroup]->AddLayer(pQuadLayer); + int LayerIndex = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1; + SelectLayer(LayerIndex); + m_Map.m_vpGroups[m_SelectedGroup]->m_Collapse = false; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, LayerIndex)); +} + +void CEditor::AddSwitchLayer() +{ + std::shared_ptr pSwitchLayer = std::make_shared(this, m_Map.m_pGameLayer->m_Width, m_Map.m_pGameLayer->m_Height); + m_Map.MakeSwitchLayer(pSwitchLayer); + m_Map.m_vpGroups[m_SelectedGroup]->AddLayer(pSwitchLayer); + int LayerIndex = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1; + SelectLayer(LayerIndex); + m_pBrush->Clear(); + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, LayerIndex)); +} + +void CEditor::AddFrontLayer() +{ + std::shared_ptr pFrontLayer = std::make_shared(this, m_Map.m_pGameLayer->m_Width, m_Map.m_pGameLayer->m_Height); + m_Map.MakeFrontLayer(pFrontLayer); + m_Map.m_vpGroups[m_SelectedGroup]->AddLayer(pFrontLayer); + int LayerIndex = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1; + SelectLayer(LayerIndex); + m_pBrush->Clear(); + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, LayerIndex)); +} + +void CEditor::AddTuneLayer() +{ + std::shared_ptr pTuneLayer = std::make_shared(this, m_Map.m_pGameLayer->m_Width, m_Map.m_pGameLayer->m_Height); + m_Map.MakeTuneLayer(pTuneLayer); + m_Map.m_vpGroups[m_SelectedGroup]->AddLayer(pTuneLayer); + int LayerIndex = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1; + SelectLayer(LayerIndex); + m_pBrush->Clear(); + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, LayerIndex)); +} + +void CEditor::AddSpeedupLayer() +{ + std::shared_ptr pSpeedupLayer = std::make_shared(this, m_Map.m_pGameLayer->m_Width, m_Map.m_pGameLayer->m_Height); + m_Map.MakeSpeedupLayer(pSpeedupLayer); + m_Map.m_vpGroups[m_SelectedGroup]->AddLayer(pSpeedupLayer); + int LayerIndex = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1; + SelectLayer(LayerIndex); + m_pBrush->Clear(); + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, LayerIndex)); +} + +void CEditor::AddTeleLayer() +{ + std::shared_ptr pTeleLayer = std::make_shared(this, m_Map.m_pGameLayer->m_Width, m_Map.m_pGameLayer->m_Height); + m_Map.MakeTeleLayer(pTeleLayer); + m_Map.m_vpGroups[m_SelectedGroup]->AddLayer(pTeleLayer); + int LayerIndex = m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers.size() - 1; + SelectLayer(LayerIndex); + m_pBrush->Clear(); + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, LayerIndex)); +} + +bool CEditor::IsNonGameTileLayerSelected() const +{ + std::shared_ptr pLayer = GetSelectedLayer(0); + if(!pLayer) + return false; + if(pLayer->m_Type != LAYERTYPE_TILES) + return false; + if( + pLayer == m_Map.m_pGameLayer || + pLayer == m_Map.m_pFrontLayer || + pLayer == m_Map.m_pSwitchLayer || + pLayer == m_Map.m_pTeleLayer || + pLayer == m_Map.m_pSpeedupLayer || + pLayer == m_Map.m_pTuneLayer) + return false; + + return true; +} + +void CEditor::LayerSelectImage() +{ + if(!IsNonGameTileLayerSelected()) + return; + + std::shared_ptr pLayer = GetSelectedLayer(0); + std::shared_ptr pTiles = std::static_pointer_cast(pLayer); + + static SLayerPopupContext s_LayerPopupContext = {}; + s_LayerPopupContext.m_pEditor = this; + Ui()->DoPopupMenu(&s_LayerPopupContext, Ui()->MouseX(), Ui()->MouseY(), 120, 270, &s_LayerPopupContext, PopupLayer); + PopupSelectImageInvoke(pTiles->m_Image, Ui()->MouseX(), Ui()->MouseY()); +} + +void CEditor::MapDetails() +{ + const CUIRect *pScreen = Ui()->Screen(); + m_Map.m_MapInfoTmp.Copy(m_Map.m_MapInfo); + static SPopupMenuId s_PopupMapInfoId; + constexpr float PopupWidth = 400.0f; + constexpr float PopupHeight = 170.0f; + Ui()->DoPopupMenu( + &s_PopupMapInfoId, + pScreen->w / 2.0f - PopupWidth / 2.0f, + pScreen->h / 2.0f - PopupHeight / 2.0f, + PopupWidth, + PopupHeight, + this, + PopupMapInfo); + Ui()->SetActiveItem(nullptr); +} + +void CEditor::DeleteSelectedLayer() +{ + std::shared_ptr pCurrentLayer = GetSelectedLayer(0); + if(!pCurrentLayer) + return; + if(m_Map.m_pGameLayer == pCurrentLayer) + return; + + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0])); + + if(pCurrentLayer == m_Map.m_pFrontLayer) + m_Map.m_pFrontLayer = nullptr; + if(pCurrentLayer == m_Map.m_pTeleLayer) + m_Map.m_pTeleLayer = nullptr; + if(pCurrentLayer == m_Map.m_pSpeedupLayer) + m_Map.m_pSpeedupLayer = nullptr; + if(pCurrentLayer == m_Map.m_pSwitchLayer) + m_Map.m_pSwitchLayer = nullptr; + if(pCurrentLayer == m_Map.m_pTuneLayer) + m_Map.m_pTuneLayer = nullptr; + m_Map.m_vpGroups[m_SelectedGroup]->DeleteLayer(m_vSelectedLayers[0]); + + SelectPreviousLayer(); +} diff --git a/src/game/editor/quick_actions.h b/src/game/editor/quick_actions.h new file mode 100644 index 0000000000..f1e1b282fb --- /dev/null +++ b/src/game/editor/quick_actions.h @@ -0,0 +1,372 @@ +// This file can be included several times. + +#ifndef REGISTER_QUICK_ACTION +#define REGISTER_QUICK_ACTION(name, text, callback, disabled, active, button_color, description) +#endif + +#define ALWAYS_FALSE []() -> bool { return false; } +#define DEFAULT_BTN []() -> int { return -1; } + +REGISTER_QUICK_ACTION( + ToggleGrid, + "Toggle Grid", + [&]() { MapView()->MapGrid()->Toggle(); }, + ALWAYS_FALSE, + [&]() -> bool { return MapView()->MapGrid()->IsEnabled(); }, + DEFAULT_BTN, + "[Ctrl+G] Toggle Grid.") +REGISTER_QUICK_ACTION( + GameTilesAir, + "Game tiles: Air", + [&]() { FillGameTiles(EGameTileOp::AIR); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesHookable, + "Game tiles: Hookable", + [&]() { FillGameTiles(EGameTileOp::HOOKABLE); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesDeath, + "Game tiles: Death", + [&]() { FillGameTiles(EGameTileOp::DEATH); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesUnhookable, + "Game tiles: Unhookable", + [&]() { FillGameTiles(EGameTileOp::UNHOOKABLE); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesHookthrough, + "Game tiles: Hookthrough", + [&]() { FillGameTiles(EGameTileOp::HOOKTHROUGH); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesFreeze, + "Game tiles: Freeze", + [&]() { FillGameTiles(EGameTileOp::FREEZE); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesUnfreeze, + "Game tiles: Unfreeze", + [&]() { FillGameTiles(EGameTileOp::UNFREEZE); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesDeepFreeze, + "Game tiles: Deep Freeze", + [&]() { FillGameTiles(EGameTileOp::DEEP_FREEZE); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesDeepUnfreeze, + "Game tiles: Deep Unfreeze", + [&]() { FillGameTiles(EGameTileOp::DEEP_UNFREEZE); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesBlueCheckTele, + "Game tiles: Blue Check Tele", + [&]() { FillGameTiles(EGameTileOp::BLUE_CHECK_TELE); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesRedCheckTele, + "Game tiles: Red Check Tele", + [&]() { FillGameTiles(EGameTileOp::RED_CHECK_TELE); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesLiveFreeze, + "Game tiles: Live Freeze", + [&]() { FillGameTiles(EGameTileOp::LIVE_FREEZE); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + GameTilesLiveUnfreeze, + "Game tiles: Live Unfreeze", + [&]() { FillGameTiles(EGameTileOp::LIVE_UNFREEZE); }, + [&]() -> bool { return !CanFillGameTiles(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Constructs game tiles from this layer.") +REGISTER_QUICK_ACTION( + AddGroup, + "Add group", + [&]() { AddGroup(); }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "Adds a new group.") +REGISTER_QUICK_ACTION( + ResetZoom, + "Reset Zoom", + [&]() { MapView()->ResetZoom(); }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "[Numpad*] Zoom to normal and remove editor offset.") +REGISTER_QUICK_ACTION( + ZoomOut, + "Zoom Out", + [&]() { MapView()->Zoom()->ChangeValue(50.0f); }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "[Numpad-] Zoom out.") +REGISTER_QUICK_ACTION( + ZoomIn, + "Zoom In", + [&]() { MapView()->Zoom()->ChangeValue(-50.0f); }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "[Numpad+] Zoom in.") +REGISTER_QUICK_ACTION( + Refocus, + "Refocus", + [&]() { MapView()->Focus(); }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "[Home] Restore map focus.") +REGISTER_QUICK_ACTION( + Proof, + "Proof", + [&]() { MapView()->ProofMode()->Toggle(); }, + ALWAYS_FALSE, + [&]() -> bool { return MapView()->ProofMode()->IsEnabled(); }, + DEFAULT_BTN, + "Toggles proof borders. These borders represent the area that a player can see with default zoom.") +REGISTER_QUICK_ACTION( + AddTileLayer, "Add tile layer", [&]() { AddTileLayer(); }, ALWAYS_FALSE, ALWAYS_FALSE, DEFAULT_BTN, "Creates a new tile layer.") +REGISTER_QUICK_ACTION( + AddSwitchLayer, + "Add switch layer", + [&]() { AddSwitchLayer(); }, + [&]() -> bool { return !GetSelectedGroup()->m_GameGroup || m_Map.m_pSwitchLayer; }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Creates a new switch layer.") +REGISTER_QUICK_ACTION( + AddTuneLayer, + "Add tune layer", + [&]() { AddTuneLayer(); }, + [&]() -> bool { return !GetSelectedGroup()->m_GameGroup || m_Map.m_pTuneLayer; }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Creates a new tuning layer.") +REGISTER_QUICK_ACTION( + AddSpeedupLayer, + "Add speedup layer", + [&]() { AddSpeedupLayer(); }, + [&]() -> bool { return !GetSelectedGroup()->m_GameGroup || m_Map.m_pSpeedupLayer; }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Creates a new speedup layer.") +REGISTER_QUICK_ACTION( + AddTeleLayer, + "Add tele layer", + [&]() { AddTeleLayer(); }, + [&]() -> bool { return !GetSelectedGroup()->m_GameGroup || m_Map.m_pTeleLayer; }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Creates a new tele layer.") +REGISTER_QUICK_ACTION( + AddFrontLayer, + "Add front layer", + [&]() { AddFrontLayer(); }, + [&]() -> bool { return !GetSelectedGroup()->m_GameGroup || m_Map.m_pFrontLayer; }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Creates a new item layer.") +REGISTER_QUICK_ACTION( + AddQuadsLayer, "Add quads layer", [&]() { AddQuadsLayer(); }, ALWAYS_FALSE, ALWAYS_FALSE, DEFAULT_BTN, "Creates a new quads layer.") +REGISTER_QUICK_ACTION( + AddSoundLayer, "Add sound layer", [&]() { AddSoundLayer(); }, ALWAYS_FALSE, ALWAYS_FALSE, DEFAULT_BTN, "Creates a new sound layer.") +REGISTER_QUICK_ACTION( + SaveAs, + "Save As", + [&]() { InvokeFileDialog(IStorage::TYPE_SAVE, FILETYPE_MAP, "Save map", "Save As", "maps", true, CEditor::CallbackSaveMap, this); }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "[Ctrl+Shift+S] Saves the current map under a new name.") +REGISTER_QUICK_ACTION( + LoadCurrentMap, + "Load Current Map", + [&]() { + if(HasUnsavedData()) + { + m_PopupEventType = POPEVENT_LOADCURRENT; + m_PopupEventActivated = true; + } + else + { + LoadCurrentMap(); + } + }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "[Ctrl+Alt+L] Opens the current in game map for editing.") +REGISTER_QUICK_ACTION( + Envelopes, + "Envelopes", + [&]() { m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES ? EXTRAEDITOR_NONE : EXTRAEDITOR_ENVELOPES; }, + ALWAYS_FALSE, + ALWAYS_FALSE, + [&]() -> int { return m_ShowPicker ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES; }, + "Toggles the envelope editor.") +REGISTER_QUICK_ACTION( + ServerSettings, + "Server settings", + [&]() { m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS ? EXTRAEDITOR_NONE : EXTRAEDITOR_SERVER_SETTINGS; }, + ALWAYS_FALSE, + ALWAYS_FALSE, + [&]() -> int { return m_ShowPicker ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS; }, + "Toggles the server settings editor.") +REGISTER_QUICK_ACTION( + History, + "History", + [&]() { m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_HISTORY ? EXTRAEDITOR_NONE : EXTRAEDITOR_HISTORY; }, + ALWAYS_FALSE, + ALWAYS_FALSE, + [&]() -> int { return m_ShowPicker ? -1 : m_ActiveExtraEditor == EXTRAEDITOR_HISTORY; }, + "Toggles the editor history view.") +REGISTER_QUICK_ACTION( + AddImage, + "Add Image", + [&]() { InvokeFileDialog(IStorage::TYPE_ALL, FILETYPE_IMG, "Add Image", "Add", "mapres", false, AddImage, this); }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "Load a new image to use in the map.") +REGISTER_QUICK_ACTION( + LayerPropAddImage, + "Layer: Add Image", + [&]() { LayerSelectImage(); }, + [&]() -> bool { return !IsNonGameTileLayerSelected(); }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Pick mapres image for currently selected layer.") +REGISTER_QUICK_ACTION( + ShowInfoOff, + "Show Info: Off", + [&]() { + m_ShowTileInfo = SHOW_TILE_OFF; + m_ShowEnvelopePreview = SHOWENV_NONE; + }, + ALWAYS_FALSE, + [&]() -> bool { return m_ShowTileInfo == SHOW_TILE_OFF; }, + DEFAULT_BTN, + "Do not show tile information.") +REGISTER_QUICK_ACTION( + ShowInfoDec, + "Show Info: Dec", + [&]() { + m_ShowTileInfo = SHOW_TILE_DECIMAL; + m_ShowEnvelopePreview = SHOWENV_NONE; + }, + ALWAYS_FALSE, + [&]() -> bool { return m_ShowTileInfo == SHOW_TILE_DECIMAL; }, + DEFAULT_BTN, + "[Ctrl+I] Show tile information.") +REGISTER_QUICK_ACTION( + ShowInfoHex, + "Show Info: Hex", + [&]() { + m_ShowTileInfo = SHOW_TILE_HEXADECIMAL; + m_ShowEnvelopePreview = SHOWENV_NONE; + }, + ALWAYS_FALSE, + [&]() -> bool { return m_ShowTileInfo == SHOW_TILE_HEXADECIMAL; }, + DEFAULT_BTN, + "[Ctrl+Shift+I] Show tile information in hexadecimal.") +REGISTER_QUICK_ACTION( + DeleteLayer, + "Delete layer", + [&]() { DeleteSelectedLayer(); }, + [&]() -> bool { + std::shared_ptr pCurrentLayer = GetSelectedLayer(0); + if(!pCurrentLayer) + return true; + return m_Map.m_pGameLayer == pCurrentLayer; + }, + ALWAYS_FALSE, + DEFAULT_BTN, + "Deletes the layer.") +REGISTER_QUICK_ACTION( + Pipette, + "Pipette", + [&]() { m_ColorPipetteActive = !m_ColorPipetteActive; }, + ALWAYS_FALSE, + [&]() -> bool { return m_ColorPipetteActive; }, + DEFAULT_BTN, + "[Ctrl+Shift+C] Color pipette. Pick a color from the screen by clicking on it.") +REGISTER_QUICK_ACTION( + MapDetails, + "Map details", + [&]() { MapDetails(); }, + ALWAYS_FALSE, + ALWAYS_FALSE, + DEFAULT_BTN, + "Adjust the map details of the current map.") +REGISTER_QUICK_ACTION( + AddQuad, + "Add Quad", + [&]() { AddQuadOrSound(); }, + [&]() -> bool { + std::shared_ptr pLayer = GetSelectedLayer(0); + if(!pLayer) + return false; + return pLayer->m_Type != LAYERTYPE_QUADS; + }, + ALWAYS_FALSE, + DEFAULT_BTN, + "[Ctrl+Q] Add a new quad.") +REGISTER_QUICK_ACTION( + AddSound, + "Add Sound", + [&]() { AddQuadOrSound(); }, + [&]() -> bool { + std::shared_ptr pLayer = GetSelectedLayer(0); + if(!pLayer) + return false; + return pLayer->m_Type != LAYERTYPE_SOUNDS; + }, + ALWAYS_FALSE, + DEFAULT_BTN, + "[Ctrl+Q] Add a new sound source.") + +#undef ALWAYS_FALSE +#undef DEFAULT_BTN diff --git a/src/game/editor/tileart.cpp b/src/game/editor/tileart.cpp index 5524dce026..102a75cc2c 100644 --- a/src/game/editor/tileart.cpp +++ b/src/game/editor/tileart.cpp @@ -17,60 +17,15 @@ bool operator<(const ColorRGBA &Left, const ColorRGBA &Right) return Left.a < Right.a; } -static ColorRGBA GetPixelColor(const CImageInfo &Image, size_t x, size_t y) -{ - uint8_t *pData = static_cast(Image.m_pData); - const size_t PixelSize = Image.PixelSize(); - const size_t PixelStartIndex = x * PixelSize + (Image.m_Width * PixelSize * y); - - ColorRGBA Color = {255, 255, 255, 255}; - if(PixelSize == 1) - { - Color.a = pData[PixelStartIndex]; - } - else - { - Color.r = pData[PixelStartIndex + 0]; - Color.g = pData[PixelStartIndex + 1]; - Color.b = pData[PixelStartIndex + 2]; - - if(PixelSize == 4) - Color.a = pData[PixelStartIndex + 3]; - } - - return Color; -} - -static void SetPixelColor(CImageInfo *pImage, size_t x, size_t y, ColorRGBA Color) -{ - uint8_t *pData = static_cast(pImage->m_pData); - const size_t PixelSize = pImage->PixelSize(); - const size_t PixelStartIndex = x * PixelSize + (pImage->m_Width * PixelSize * y); - - if(PixelSize == 1) - { - pData[PixelStartIndex] = Color.a; - } - else - { - pData[PixelStartIndex + 0] = Color.r; - pData[PixelStartIndex + 1] = Color.g; - pData[PixelStartIndex + 2] = Color.b; - - if(PixelSize == 4) - pData[PixelStartIndex + 3] = Color.a; - } -} - static std::vector GetUniqueColors(const CImageInfo &Image) { std::set ColorSet; std::vector vUniqueColors; - for(int x = 0; x < Image.m_Width; x++) + for(size_t x = 0; x < Image.m_Width; x++) { - for(int y = 0; y < Image.m_Height; y++) + for(size_t y = 0; y < Image.m_Height; y++) { - ColorRGBA Color = GetPixelColor(Image, x, y); + ColorRGBA Color = Image.PixelColor(x, y); if(Color.a > 0 && ColorSet.insert(Color).second) vUniqueColors.push_back(Color); } @@ -106,12 +61,12 @@ static std::vector> GroupColors(const std::vecto return vaColorGroups; } -static void SetColorTile(CImageInfo *pImage, int x, int y, ColorRGBA Color) +static void SetColorTile(CImageInfo &Image, int x, int y, ColorRGBA Color) { for(int i = 0; i < TileSize; i++) { for(int j = 0; j < TileSize; j++) - SetPixelColor(pImage, x * TileSize + i, y * TileSize + j, Color); + Image.SetPixelColor(x * TileSize + i, y * TileSize + j, Color); } } @@ -121,16 +76,14 @@ static CImageInfo ColorGroupToImage(const std::array &aColo Image.m_Width = NumTilesRow * TileSize; Image.m_Height = NumTilesColumn * TileSize; Image.m_Format = CImageInfo::FORMAT_RGBA; - - uint8_t *pData = static_cast(malloc(static_cast(Image.m_Width) * Image.m_Height * 4 * sizeof(uint8_t))); - Image.m_pData = pData; + Image.m_pData = static_cast(malloc(Image.DataSize())); for(int y = 0; y < NumTilesColumn; y++) { for(int x = 0; x < NumTilesRow; x++) { int ColorIndex = x + NumTilesRow * y; - SetColorTile(&Image, x, y, aColorGroup[ColorIndex]); + SetColorTile(Image, x, y, aColorGroup[ColorIndex]); } } @@ -156,7 +109,7 @@ static std::shared_ptr ImageInfoToEditorImage(CEditor *pEditor, co pEditorImage->m_pData = Image.m_pData; int TextureLoadFlag = pEditor->Graphics()->Uses2DTextureArrays() ? IGraphics::TEXLOAD_TO_2D_ARRAY_TEXTURE : IGraphics::TEXLOAD_TO_3D_TEXTURE; - pEditorImage->m_Texture = pEditor->Graphics()->LoadTextureRaw(Image.m_Width, Image.m_Height, Image.m_Format, Image.m_pData, TextureLoadFlag, pName); + pEditorImage->m_Texture = pEditor->Graphics()->LoadTextureRaw(Image, TextureLoadFlag, pName); pEditorImage->m_External = 0; str_copy(pEditorImage->m_aName, pName); @@ -181,7 +134,7 @@ static void SetTilelayerIndices(const std::shared_ptr &pLayer, cons for(int x = 0; x < pLayer->m_Width; x++) { for(int y = 0; y < pLayer->m_Height; y++) - pLayer->m_pTiles[x + y * pLayer->m_Width].m_Index = GetColorIndex(aColorGroup, GetPixelColor(Image, x, y)); + pLayer->m_pTiles[x + y * pLayer->m_Width].m_Index = GetColorIndex(aColorGroup, Image.PixelColor(x, y)); } } @@ -212,8 +165,7 @@ void CEditor::AddTileart(bool IgnoreHistory) m_EditorHistory.RecordAction(std::make_shared(this, ImageCount, m_aTileartFilename, IndexMap)); } - free(m_TileartImageInfo.m_pData); - m_TileartImageInfo.m_pData = nullptr; + m_TileartImageInfo.Free(); m_Map.OnModify(); m_Dialog = DIALOG_NONE; } @@ -226,8 +178,7 @@ void CEditor::TileartCheckColors() { m_PopupEventType = CEditor::POPEVENT_PIXELART_TOO_MANY_COLORS; m_PopupEventActivated = true; - free(m_TileartImageInfo.m_pData); - m_TileartImageInfo.m_pData = nullptr; + m_TileartImageInfo.Free(); } else if(NumColorGroups > 1) { @@ -242,7 +193,7 @@ bool CEditor::CallbackAddTileart(const char *pFilepath, int StorageType, void *p { CEditor *pEditor = (CEditor *)pUser; - if(!pEditor->Graphics()->LoadPNG(&pEditor->m_TileartImageInfo, pFilepath, StorageType)) + if(!pEditor->Graphics()->LoadPng(pEditor->m_TileartImageInfo, pFilepath, StorageType)) { pEditor->ShowFileDialogError("Failed to load image from file '%s'.", pFilepath); return false; diff --git a/src/game/gamecore.cpp b/src/game/gamecore.cpp index 3e0f921c66..042e64579f 100644 --- a/src/game/gamecore.cpp +++ b/src/game/gamecore.cpp @@ -9,6 +9,8 @@ #include #include +#include + const char *CTuningParams::ms_apNames[] = { #define MACRO_TUNING_PARAM(Name, ScriptName, Value, Description) #ScriptName, @@ -63,6 +65,59 @@ float CTuningParams::GetWeaponFireDelay(int Weapon) const } } +static_assert(std::numeric_limits::is_signed, "char must be signed for StrToInts to work correctly"); + +void StrToInts(int *pInts, size_t NumInts, const char *pStr) +{ + dbg_assert(NumInts > 0, "StrToInts: NumInts invalid"); + const size_t StrSize = str_length(pStr) + 1; + dbg_assert(StrSize <= NumInts * sizeof(int), "StrToInts: string truncated"); + + for(size_t i = 0; i < NumInts; i++) + { + // Copy to temporary buffer to ensure we don't read past the end of the input string + char aBuf[sizeof(int)] = {0, 0, 0, 0}; + for(size_t c = 0; c < sizeof(int) && i * sizeof(int) + c < StrSize; c++) + { + aBuf[c] = pStr[i * sizeof(int) + c]; + } + pInts[i] = ((aBuf[0] + 128) << 24) | ((aBuf[1] + 128) << 16) | ((aBuf[2] + 128) << 8) | (aBuf[3] + 128); + } + // Last byte is always zero and unused in this format + pInts[NumInts - 1] &= 0xFFFFFF00; +} + +bool IntsToStr(const int *pInts, size_t NumInts, char *pStr, size_t StrSize) +{ + dbg_assert(NumInts > 0, "IntsToStr: NumInts invalid"); + dbg_assert(StrSize >= NumInts * sizeof(int), "IntsToStr: StrSize invalid"); + + // Unpack string without validation + size_t StrIndex = 0; + for(size_t IntIndex = 0; IntIndex < NumInts; IntIndex++) + { + const int CurrentInt = pInts[IntIndex]; + pStr[StrIndex] = ((CurrentInt >> 24) & 0xff) - 128; + StrIndex++; + pStr[StrIndex] = ((CurrentInt >> 16) & 0xff) - 128; + StrIndex++; + pStr[StrIndex] = ((CurrentInt >> 8) & 0xff) - 128; + StrIndex++; + pStr[StrIndex] = (CurrentInt & 0xff) - 128; + StrIndex++; + } + // Ensure null-termination + pStr[StrIndex - 1] = '\0'; + + // Ensure valid UTF-8 + if(str_utf8_check(pStr)) + { + return true; + } + pStr[0] = '\0'; + return false; +} + float VelocityRamp(float Value, float Start, float Range, float Curvature) { if(Value < Start) @@ -70,11 +125,10 @@ float VelocityRamp(float Value, float Start, float Range, float Curvature) return 1.0f / std::pow(Curvature, (Value - Start) / Range); } -void CCharacterCore::Init(CWorldCore *pWorld, CCollision *pCollision, CTeamsCore *pTeams, std::map> *pTeleOuts) +void CCharacterCore::Init(CWorldCore *pWorld, CCollision *pCollision, CTeamsCore *pTeams) { m_pWorld = pWorld; m_pCollision = pCollision; - m_pTeleOuts = pTeleOuts; m_pTeams = pTeams; m_Id = -1; @@ -258,11 +312,16 @@ void CCharacterCore::Tick(bool UseInput, bool DoDeferredTick) } else if(m_HookState == HOOK_FLYING) { + vec2 HookBase = m_Pos; + if(m_NewHook) + { + HookBase = m_HookTeleBase; + } vec2 NewPos = m_HookPos + m_HookDir * m_Tuning.m_HookFireSpeed; - if((!m_NewHook && distance(m_Pos, NewPos) > m_Tuning.m_HookLength) || (m_NewHook && distance(m_HookTeleBase, NewPos) > m_Tuning.m_HookLength)) + if(distance(HookBase, NewPos) > m_Tuning.m_HookLength) { m_HookState = HOOK_RETRACT_START; - NewPos = m_Pos + normalize(NewPos - m_Pos) * m_Tuning.m_HookLength; + NewPos = HookBase + normalize(NewPos - HookBase) * m_Tuning.m_HookLength; m_Reset = true; } @@ -273,8 +332,6 @@ void CCharacterCore::Tick(bool UseInput, bool DoDeferredTick) int teleNr = 0; int Hit = m_pCollision->IntersectLineTeleHook(m_HookPos, NewPos, &NewPos, 0, &teleNr); - // m_NewHook = false; - if(Hit) { if(Hit == TILE_NOHOOK) @@ -287,7 +344,7 @@ void CCharacterCore::Tick(bool UseInput, bool DoDeferredTick) } // Check against other players first - if(!this->m_HookHitDisabled && m_pWorld && m_Tuning.m_PlayerHooking) + if(!m_HookHitDisabled && m_pWorld && m_Tuning.m_PlayerHooking && (m_HookState == HOOK_FLYING || !m_NewHook)) { float Distance = 0.0f; for(int i = 0; i < MAX_CLIENTS; i++) @@ -327,14 +384,14 @@ void CCharacterCore::Tick(bool UseInput, bool DoDeferredTick) m_HookState = HOOK_RETRACT_START; } - if(GoingThroughTele && m_pWorld && m_pTeleOuts && !m_pTeleOuts->empty() && !(*m_pTeleOuts)[teleNr - 1].empty()) + if(GoingThroughTele && m_pWorld && !m_pCollision->TeleOuts(teleNr - 1).empty()) { m_TriggeredEvents = 0; SetHookedPlayer(-1); m_NewHook = true; - int RandomOut = m_pWorld->RandomOr0((*m_pTeleOuts)[teleNr - 1].size()); - m_HookPos = (*m_pTeleOuts)[teleNr - 1][RandomOut] + TargetDirection * PhysicalSize() * 1.5f; + int RandomOut = m_pWorld->RandomOr0(m_pCollision->TeleOuts(teleNr - 1).size()); + m_HookPos = m_pCollision->TeleOuts(teleNr - 1)[RandomOut] + TargetDirection * PhysicalSize() * 1.5f; m_HookDir = TargetDirection; m_HookTeleBase = m_HookPos; } @@ -359,13 +416,9 @@ void CCharacterCore::Tick(bool UseInput, bool DoDeferredTick) m_HookState = HOOK_RETRACTED; m_HookPos = m_Pos; } - - // keep players hooked for a max of 1.5sec - // if(Server()->Tick() > hook_tick+(Server()->TickSpeed()*3)/2) - // release_hooked(); } - // don't do this hook rutine when we are hook to a player + // don't do this hook routine when we are already hooked to a player if(m_HookedPlayer == -1 && distance(m_HookPos, m_Pos) > 46.0f) { vec2 HookVel = normalize(m_HookPos - m_Pos) * m_Tuning.m_HookDragAccel; @@ -384,7 +437,8 @@ void CCharacterCore::Tick(bool UseInput, bool DoDeferredTick) vec2 NewVel = m_Vel + HookVel; // check if we are under the legal limit for the hook - if(length(NewVel) < m_Tuning.m_HookDragSpeed || length(NewVel) < length(m_Vel)) + const float NewVelLength = length(NewVel); + if(NewVelLength < m_Tuning.m_HookDragSpeed || NewVelLength < length(m_Vel)) m_Vel = NewVel; // no problem. apply } @@ -412,9 +466,6 @@ void CCharacterCore::TickDeferred() if(!pCharCore) continue; - // player *p = (player*)ent; - // if(pCharCore == this) // || !(p->flags&FLAG_ALIVE) - if(pCharCore == this || (m_Id != -1 && !m_pTeams->CanCollide(m_Id, i))) continue; // make sure that we don't nudge our self @@ -429,7 +480,7 @@ void CCharacterCore::TickDeferred() bool CanCollide = (m_Super || pCharCore->m_Super) || (!m_CollisionDisabled && !pCharCore->m_CollisionDisabled && m_Tuning.m_PlayerCollision); - if(CanCollide && Distance < PhysicalSize() * 1.25f && Distance > 0.0f) + if(CanCollide && Distance < PhysicalSize() * 1.25f) { float a = (PhysicalSize() * 1.45f - Distance); float Velocity = 0.5f; @@ -446,7 +497,7 @@ void CCharacterCore::TickDeferred() // handle hook influence if(!m_HookHitDisabled && m_HookedPlayer == i && m_Tuning.m_PlayerHooking) { - if(Distance > PhysicalSize() * 1.50f) // TODO: fix tweakable variable + if(Distance > PhysicalSize() * 1.50f) { float HookAccel = m_Tuning.m_HookDragAccel * (Distance / m_Tuning.m_HookLength); float DragSpeed = m_Tuning.m_HookDragSpeed; @@ -530,7 +581,7 @@ void CCharacterCore::Move() if((!(pCharCore->m_Super || m_Super) && (m_Solo || pCharCore->m_Solo || pCharCore->m_CollisionDisabled || (m_Id != -1 && !m_pTeams->CanCollide(m_Id, p))))) continue; float D = distance(Pos, pCharCore->m_Pos); - if(D < PhysicalSize() && D >= 0.0f) + if(D < PhysicalSize()) { if(a > 0.0f) m_Pos = LastPos; @@ -677,11 +728,6 @@ void CCharacterCore::SetTeamsCore(CTeamsCore *pTeams) m_pTeams = pTeams; } -void CCharacterCore::SetTeleOuts(std::map> *pTeleOuts) -{ - m_pTeleOuts = pTeleOuts; -} - bool CCharacterCore::IsSwitchActiveCb(int Number, void *pUser) { CCharacterCore *pThis = (CCharacterCore *)pUser; diff --git a/src/game/gamecore.h b/src/game/gamecore.h index 8b16cfcaae..c33b14e171 100644 --- a/src/game/gamecore.h +++ b/src/game/gamecore.h @@ -65,53 +65,9 @@ class CTuningParams float GetWeaponFireDelay(int Weapon) const; }; -inline void StrToInts(int *pInts, int Num, const char *pStr) -{ - int Index = 0; - while(Num) - { - char aBuf[4] = {0, 0, 0, 0}; -#ifdef __GNUC__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Warray-bounds" // false positive -#endif - for(int c = 0; c < 4 && pStr[Index]; c++, Index++) - aBuf[c] = pStr[Index]; -#ifdef __GNUC__ -#pragma GCC diagnostic pop -#endif - *pInts = ((aBuf[0] + 128) << 24) | ((aBuf[1] + 128) << 16) | ((aBuf[2] + 128) << 8) | (aBuf[3] + 128); - pInts++; - Num--; - } - - // null terminate - pInts[-1] &= 0xffffff00; -} - -inline void IntsToStr(const int *pInts, int Num, char *pStr) -{ - while(Num) - { - pStr[0] = (((*pInts) >> 24) & 0xff) - 128; - pStr[1] = (((*pInts) >> 16) & 0xff) - 128; - pStr[2] = (((*pInts) >> 8) & 0xff) - 128; - pStr[3] = ((*pInts) & 0xff) - 128; - pStr += 4; - pInts++; - Num--; - } - -#if defined(__GNUC__) && __GNUC__ >= 7 -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wstringop-overflow" // false positive -#endif - // null terminate - pStr[-1] = 0; -#if defined(__GNUC__) && __GNUC__ >= 7 -#pragma GCC diagnostic pop -#endif -} +// Do not use these function unless for legacy code! +void StrToInts(int *pInts, size_t NumInts, const char *pStr); +bool IntsToStr(const int *pInts, size_t NumInts, char *pStr, size_t StrSize); inline vec2 CalcPos(vec2 Pos, vec2 Velocity, float Curvature, float Speed, float Time) { @@ -164,7 +120,6 @@ enum COREEVENT_HOOK_ATTACH_GROUND = 0x10, COREEVENT_HOOK_HIT_NOHOOK = 0x20, COREEVENT_HOOK_RETRACT = 0x40, - // COREEVENT_HOOK_TELE=0x80, }; // show others values - do not change them @@ -221,7 +176,6 @@ class CCharacterCore { CWorldCore *m_pWorld = nullptr; CCollision *m_pCollision; - std::map> *m_pTeleOuts; public: static constexpr float PhysicalSize() { return 28.0f; }; @@ -269,7 +223,7 @@ class CCharacterCore int m_TriggeredEvents; - void Init(CWorldCore *pWorld, CCollision *pCollision, CTeamsCore *pTeams = nullptr, std::map> *pTeleOuts = nullptr); + void Init(CWorldCore *pWorld, CCollision *pCollision, CTeamsCore *pTeams = nullptr); void SetCoreWorld(CWorldCore *pWorld, CCollision *pCollision, CTeamsCore *pTeams); void Reset(); void TickDeferred(); @@ -290,7 +244,6 @@ class CCharacterCore // DDNet Character void SetTeamsCore(CTeamsCore *pTeams); - void SetTeleOuts(std::map> *pTeleOuts); void ReadDDNet(const CNetObj_DDNetCharacter *pObjDDNet); bool m_Solo; bool m_Jetpack; diff --git a/src/game/layers.cpp b/src/game/layers.cpp index b00d0038c3..52f97e0799 100644 --- a/src/game/layers.cpp +++ b/src/game/layers.cpp @@ -8,33 +8,17 @@ CLayers::CLayers() { - m_GroupsNum = 0; - m_GroupsStart = 0; - m_LayersNum = 0; - m_LayersStart = 0; - m_pGameGroup = 0; - m_pGameLayer = 0; - m_pMap = 0; - - m_pTeleLayer = 0; - m_pSpeedupLayer = 0; - m_pFrontLayer = 0; - m_pSwitchLayer = 0; - m_pTuneLayer = 0; + Unload(); } void CLayers::Init(class IKernel *pKernel) { + Unload(); + m_pMap = pKernel->RequestInterface(); m_pMap->GetType(MAPITEMTYPE_GROUP, &m_GroupsStart, &m_GroupsNum); m_pMap->GetType(MAPITEMTYPE_LAYER, &m_LayersStart, &m_LayersNum); - m_pTeleLayer = 0; - m_pSpeedupLayer = 0; - m_pFrontLayer = 0; - m_pSwitchLayer = 0; - m_pTuneLayer = 0; - for(int g = 0; g < NumGroups(); g++) { CMapItemGroup *pGroup = GetGroup(g); @@ -129,17 +113,12 @@ void CLayers::Init(class IKernel *pKernel) void CLayers::InitBackground(class IMap *pMap) { + Unload(); + m_pMap = pMap; m_pMap->GetType(MAPITEMTYPE_GROUP, &m_GroupsStart, &m_GroupsNum); m_pMap->GetType(MAPITEMTYPE_LAYER, &m_LayersStart, &m_LayersNum); - //following is here to prevent crash using standard map as background - m_pTeleLayer = 0; - m_pSpeedupLayer = 0; - m_pFrontLayer = 0; - m_pSwitchLayer = 0; - m_pTuneLayer = 0; - for(int g = 0; g < NumGroups(); g++) { CMapItemGroup *pGroup = GetGroup(g); @@ -179,6 +158,24 @@ void CLayers::InitBackground(class IMap *pMap) InitTilemapSkip(); } +void CLayers::Unload() +{ + m_GroupsNum = 0; + m_GroupsStart = 0; + m_LayersNum = 0; + m_LayersStart = 0; + + m_pGameGroup = nullptr; + m_pGameLayer = nullptr; + m_pMap = nullptr; + + m_pTeleLayer = nullptr; + m_pSpeedupLayer = nullptr; + m_pFrontLayer = nullptr; + m_pSwitchLayer = nullptr; + m_pTuneLayer = nullptr; +} + void CLayers::InitTilemapSkip() { for(int g = 0; g < NumGroups(); g++) diff --git a/src/game/layers.h b/src/game/layers.h index 1823dea9b4..2ce957cc15 100644 --- a/src/game/layers.h +++ b/src/game/layers.h @@ -12,20 +12,12 @@ struct CMapItemLayerTilemap; class CLayers { - int m_GroupsNum; - int m_GroupsStart; - int m_LayersNum; - int m_LayersStart; - CMapItemGroup *m_pGameGroup; - CMapItemLayerTilemap *m_pGameLayer; - IMap *m_pMap; - - void InitTilemapSkip(); - public: CLayers(); void Init(IKernel *pKernel); void InitBackground(IMap *pMap); + void Unload(); + int NumGroups() const { return m_GroupsNum; } int NumLayers() const { return m_LayersNum; } IMap *Map() const { return m_pMap; } @@ -43,11 +35,22 @@ class CLayers CMapItemLayerTilemap *TuneLayer() const { return m_pTuneLayer; } private: + int m_GroupsNum; + int m_GroupsStart; + int m_LayersNum; + int m_LayersStart; + + CMapItemGroup *m_pGameGroup; + CMapItemLayerTilemap *m_pGameLayer; + IMap *m_pMap; + CMapItemLayerTilemap *m_pTeleLayer; CMapItemLayerTilemap *m_pSpeedupLayer; CMapItemLayerTilemap *m_pFrontLayer; CMapItemLayerTilemap *m_pSwitchLayer; CMapItemLayerTilemap *m_pTuneLayer; + + void InitTilemapSkip(); }; #endif diff --git a/src/game/localization.cpp b/src/game/localization.cpp index 1b3feb33b7..5f29840f2d 100644 --- a/src/game/localization.cpp +++ b/src/game/localization.cpp @@ -3,6 +3,8 @@ #include "localization.h" +#include + #include #include #include @@ -21,20 +23,14 @@ void CLocalizationDatabase::LoadIndexfile(IStorage *pStorage, IConsole *pConsole m_vLanguages.emplace_back("English", "", 826, vEnglishLanguageCodes); const char *pFilename = "languages/index.txt"; - IOHANDLE File = pStorage->OpenFile(pFilename, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL); - if(!File) + CLineReader LineReader; + if(!LineReader.OpenFile(pStorage->OpenFile(pFilename, IOFLAG_READ, IStorage::TYPE_ALL))) { - char aBuf[64 + IO_MAX_PATH_LENGTH]; - str_format(aBuf, sizeof(aBuf), "Couldn't open index file '%s'", pFilename); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_error("localization", "Couldn't open index file '%s'", pFilename); return; } - CLineReader LineReader; - LineReader.Init(File); - - const char *pLine; - while((pLine = LineReader.Get())) + while(const char *pLine = LineReader.Get()) { if(!str_length(pLine) || pLine[0] == '#') // skip empty lines and comments continue; @@ -45,16 +41,12 @@ void CLocalizationDatabase::LoadIndexfile(IStorage *pStorage, IConsole *pConsole pLine = LineReader.Get(); if(!pLine) { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "Unexpected end of index file after language '%s'", aEnglishName); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_error("localization", "Unexpected end of index file after language '%s'", aEnglishName); break; } if(!str_startswith(pLine, "== ")) { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "Missing native name for language '%s'", aEnglishName); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_error("localization", "Missing native name for language '%s'", aEnglishName); (void)LineReader.Get(); (void)LineReader.Get(); continue; @@ -65,16 +57,12 @@ void CLocalizationDatabase::LoadIndexfile(IStorage *pStorage, IConsole *pConsole pLine = LineReader.Get(); if(!pLine) { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "Unexpected end of index file after language '%s'", aEnglishName); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_error("localization", "Unexpected end of index file after language '%s'", aEnglishName); break; } if(!str_startswith(pLine, "== ")) { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "Missing country code for language '%s'", aEnglishName); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_error("localization", "Missing country code for language '%s'", aEnglishName); (void)LineReader.Get(); continue; } @@ -84,16 +72,12 @@ void CLocalizationDatabase::LoadIndexfile(IStorage *pStorage, IConsole *pConsole pLine = LineReader.Get(); if(!pLine) { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "Unexpected end of index file after language '%s'", aEnglishName); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_error("localization", "Unexpected end of index file after language '%s'", aEnglishName); break; } if(!str_startswith(pLine, "== ")) { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "Missing language codes for language '%s'", aEnglishName); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_error("localization", "Missing language codes for language '%s'", aEnglishName); continue; } const char *pLanguageCodes = pLine + 3; @@ -108,9 +92,7 @@ void CLocalizationDatabase::LoadIndexfile(IStorage *pStorage, IConsole *pConsole } if(vLanguageCodes.empty()) { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "At least one language code required for language '%s'", aEnglishName); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_error("localization", "At least one language code required for language '%s'", aEnglishName); continue; } @@ -119,8 +101,6 @@ void CLocalizationDatabase::LoadIndexfile(IStorage *pStorage, IConsole *pConsole m_vLanguages.emplace_back(aNativeName, aFileName, str_toint(aCountryCode), vLanguageCodes); } - io_close(File); - std::sort(m_vLanguages.begin(), m_vLanguages.end()); } @@ -137,9 +117,7 @@ void CLocalizationDatabase::SelectDefaultLanguage(IConsole *pConsole, char *pFil char aLocaleStr[128]; os_locale_str(aLocaleStr, sizeof(aLocaleStr)); - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "Choosing default language based on user locale '%s'", aLocaleStr); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_info("localization", "Choosing default language based on user locale '%s'", aLocaleStr); while(true) { @@ -195,23 +173,18 @@ bool CLocalizationDatabase::Load(const char *pFilename, IStorage *pStorage, ICon return true; } - IOHANDLE IoHandle = pStorage->OpenFile(pFilename, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL); - if(!IoHandle) + CLineReader LineReader; + if(!LineReader.OpenFile(pStorage->OpenFile(pFilename, IOFLAG_READ, IStorage::TYPE_ALL))) return false; - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "loaded '%s'", pFilename); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_info("localization", "loaded '%s'", pFilename); m_vStrings.clear(); m_StringsHeap.Reset(); char aContext[512]; char aOrigin[512]; - CLineReader LineReader; - LineReader.Init(IoHandle); - char *pLine; int Line = 0; - while((pLine = LineReader.Get())) + while(const char *pLine = LineReader.Get()) { Line++; if(!str_length(pLine)) @@ -225,38 +198,41 @@ bool CLocalizationDatabase::Load(const char *pFilename, IStorage *pStorage, ICon size_t Len = str_length(pLine); if(Len < 1 || pLine[Len - 1] != ']') { - str_format(aBuf, sizeof(aBuf), "malformed context line (%d): %s", Line, pLine); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_error("localization", "malformed context '%s' on line %d", pLine, Line); continue; } - str_copy(aContext, pLine + 1, Len - 1); + str_truncate(aContext, sizeof(aContext), pLine + 1, Len - 2); pLine = LineReader.Get(); + if(!pLine) + { + log_error("localization", "unexpected end of file after context line '%s' on line %d", aContext, Line); + break; + } + Line++; } else { aContext[0] = '\0'; } - str_copy(aOrigin, pLine, sizeof(aOrigin)); - char *pReplacement = LineReader.Get(); - Line++; + str_copy(aOrigin, pLine); + const char *pReplacement = LineReader.Get(); if(!pReplacement) { - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", "unexpected end of file"); + log_error("localization", "unexpected end of file after original '%s' on line %d", aOrigin, Line); break; } + Line++; if(pReplacement[0] != '=' || pReplacement[1] != '=' || pReplacement[2] != ' ') { - str_format(aBuf, sizeof(aBuf), "malformed replacement line (%d) for '%s'", Line, aOrigin); - pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "localization", aBuf); + log_error("localization", "malformed replacement '%s' for original '%s' on line %d", pReplacement, aOrigin, Line); continue; } pReplacement += 3; AddString(aOrigin, pReplacement, aContext); } - io_close(IoHandle); std::sort(m_vStrings.begin(), m_vStrings.end()); return true; } diff --git a/src/game/mapitems.cpp b/src/game/mapitems.cpp index 2ccab30670..2abd7468e3 100644 --- a/src/game/mapitems.cpp +++ b/src/game/mapitems.cpp @@ -57,7 +57,20 @@ bool IsValidTeleTile(int Index) Index == TILE_TELECHECKINEVIL); } -bool IsTeleTileNumberUsed(int Index) +bool IsTeleTileCheckpoint(int Index) +{ + return Index == TILE_TELECHECK || Index == TILE_TELECHECKOUT; +} + +bool IsTeleTileNumberUsed(int Index, bool Checkpoint) +{ + if(Checkpoint) + return IsTeleTileCheckpoint(Index); + return !IsTeleTileCheckpoint(Index) && Index != TILE_TELECHECKIN && + Index != TILE_TELECHECKINEVIL; +} + +bool IsTeleTileNumberUsedAny(int Index) { return Index != TILE_TELECHECKIN && Index != TILE_TELECHECKINEVIL; diff --git a/src/game/mapitems.h b/src/game/mapitems.h index d89cde0cc2..0421e66e10 100644 --- a/src/game/mapitems.h +++ b/src/game/mapitems.h @@ -277,7 +277,7 @@ struct CMapItemImage_v2 : public CMapItemImage_v1 CURRENT_VERSION = 2, }; - int m_Format; // Default before this version is CImageInfo::FORMAT_RGBA + int m_MustBe1; }; typedef CMapItemImage_v1 CMapItemImage; @@ -520,6 +520,9 @@ struct CMapItemSound int m_SoundName; int m_SoundData; + // Deprecated. Do not read this value, it could be wrong. + // Use GetDataSize instead, which returns the de facto size. + // Value must still be written for compatibility. int m_SoundDataSize; }; @@ -568,7 +571,9 @@ class CTuneTile bool IsValidGameTile(int Index); bool IsValidFrontTile(int Index); bool IsValidTeleTile(int Index); -bool IsTeleTileNumberUsed(int Index); // Assumes that Index is a valid tele tile index +bool IsTeleTileCheckpoint(int Index); // Assumes that Index is a valid tele tile index +bool IsTeleTileNumberUsed(int Index, bool Checkpoint); // Assumes that Index is a valid tele tile index +bool IsTeleTileNumberUsedAny(int Index); // Does not check for checkpoint only bool IsValidSpeedupTile(int Index); bool IsValidSwitchTile(int Index); bool IsSwitchTileFlagsUsed(int Index); // Assumes that Index is a valid switch tile index diff --git a/src/game/server/ddracechat.cpp b/src/game/server/ddracechat.cpp index f422657ddb..2e027aa4c5 100644 --- a/src/game/server/ddracechat.cpp +++ b/src/game/server/ddracechat.cpp @@ -2,6 +2,7 @@ #include "gamecontext.h" #include #include +#include #include #include #include @@ -10,7 +11,9 @@ #include "player.h" #include "score.h" -bool CheckClientID(int ClientID); +#include + +bool CheckClientId(int ClientId); void CGameContext::ConCredits(IConsole::IResult *pResult, void *pUserData) { @@ -39,19 +42,21 @@ void CGameContext::ConCredits(IConsole::IResult *pResult, void *pUserData) pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "trafilaw, Zwelf, Patiga, Konsti, ElXreno, MikiGamer,"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "Fireball, Banana090, axblk, yangfl, Kaffeine,"); + "Fireball, Banana090, axblk, yangfl, Kaffeine, Zodiac,"); + pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", + "c0d3d3v, GiuCcc, Ravie, Robyt3, simpygirl, sjrc6, Cellegen,"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "Zodiac, c0d3d3v, GiuCcc, Ravie, Robyt3, simpygirl,"); + "srdante, Nouaa, Voxel, luk51, Vy0x2, Avolicious, louis,"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "sjrc6, Cellegen, srdante, Nouaa, Voxel, luk51,"); + "Marmare314, hus3h, ArijanJ, tarunsamanta2k20, Possseidon,"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "Vy0x2, Avolicious, louis, Marmare314, hus3h,"); + "M0REKZ, Teero, furo, dobrykafe, Moiman, JSaurusRex,"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "ArijanJ, tarunsamanta2k20, Possseidon, M0REKZ,"); + "Steinchen, ewancg, gerdoe-jr, BlaiZephyr, KebsCS, bencie,"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "Teero, furo, dobrykafe, Moiman, JSaurusRex,"); + "DynamoFox, MilkeeyCat, iMilchshake, SchrodingerZhu,"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "Steinchen & others"); + "catseyenebulous, Rei-Tw, Matodor, Emilcha, art0007i & others"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "Based on DDRace by the DDRace developers,"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", @@ -80,15 +85,15 @@ void CGameContext::ConInfo(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConList(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - int ClientID = pResult->m_ClientID; - if(!CheckClientID(ClientID)) + int ClientId = pResult->m_ClientId; + if(!CheckClientId(ClientId)) return; char zerochar = 0; if(pResult->NumArguments() > 0) - pSelf->List(ClientID, pResult->GetString(0)); + pSelf->List(ClientId, pResult->GetString(0)); else - pSelf->List(ClientID, &zerochar); + pSelf->List(ClientId, &zerochar); } void CGameContext::ConHelp(IConsole::IResult *pResult, void *pUserData) @@ -150,7 +155,7 @@ void CGameContext::ConSettings(IConsole::IResult *pResult, void *pUserData) float HookTemp; pSelf->m_Tuning.Get("player_collision", &ColTemp); pSelf->m_Tuning.Get("player_hooking", &HookTemp); - if(str_comp(pArg, "teams") == 0) + if(str_comp_nocase(pArg, "teams") == 0) { str_format(aBuf, sizeof(aBuf), "%s %s", g_Config.m_SvTeam == SV_TEAM_ALLOWED ? @@ -161,61 +166,61 @@ void CGameContext::ConSettings(IConsole::IResult *pResult, void *pUserData) "and all of your team will die if the team is locked"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", aBuf); } - else if(str_comp(pArg, "cheats") == 0) + else if(str_comp_nocase(pArg, "cheats") == 0) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", g_Config.m_SvTestingCommands ? "Cheats are enabled on this server" : "Cheats are disabled on this server"); } - else if(str_comp(pArg, "collision") == 0) + else if(str_comp_nocase(pArg, "collision") == 0) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", ColTemp ? "Players can collide on this server" : "Players can't collide on this server"); } - else if(str_comp(pArg, "hooking") == 0) + else if(str_comp_nocase(pArg, "hooking") == 0) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", HookTemp ? "Players can hook each other on this server" : "Players can't hook each other on this server"); } - else if(str_comp(pArg, "endlesshooking") == 0) + else if(str_comp_nocase(pArg, "endlesshooking") == 0) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", g_Config.m_SvEndlessDrag ? "Players hook time is unlimited" : "Players hook time is limited"); } - else if(str_comp(pArg, "hitting") == 0) + else if(str_comp_nocase(pArg, "hitting") == 0) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", g_Config.m_SvHit ? "Players weapons affect others" : "Players weapons has no affect on others"); } - else if(str_comp(pArg, "oldlaser") == 0) + else if(str_comp_nocase(pArg, "oldlaser") == 0) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", g_Config.m_SvOldLaser ? "Lasers can hit you if you shot them and they pull you towards the bounce origin (Like DDRace Beta)" : "Lasers can't hit you if you shot them, and they pull others towards the shooter"); } - else if(str_comp(pArg, "me") == 0) + else if(str_comp_nocase(pArg, "me") == 0) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", g_Config.m_SvSlashMe ? "Players can use /me commands the famous IRC Command" : "Players can't use the /me command"); } - else if(str_comp(pArg, "timeout") == 0) + else if(str_comp_nocase(pArg, "timeout") == 0) { str_format(aBuf, sizeof(aBuf), "The Server Timeout is currently set to %d seconds", g_Config.m_ConnTimeout); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", aBuf); } - else if(str_comp(pArg, "votes") == 0) + else if(str_comp_nocase(pArg, "votes") == 0) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", g_Config.m_SvVoteKick ? @@ -232,14 +237,14 @@ void CGameContext::ConSettings(IConsole::IResult *pResult, void *pUserData) "Players are just kicked and not banned if they get voted off"); } } - else if(str_comp(pArg, "pause") == 0) + else if(str_comp_nocase(pArg, "pause") == 0) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", g_Config.m_SvPauseable ? "/spec will pause you and your tee will vanish" : "/spec will pause you but your tee will not vanish"); } - else if(str_comp(pArg, "scores") == 0) + else if(str_comp_nocase(pArg, "scores") == 0) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", g_Config.m_SvHideScore ? @@ -294,12 +299,12 @@ void CGameContext::ConRules(IConsole::IResult *pResult, void *pUserData) void ToggleSpecPause(IConsole::IResult *pResult, void *pUserData, int PauseType) { - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; CGameContext *pSelf = (CGameContext *)pUserData; IServer *pServ = pSelf->Server(); - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; @@ -312,7 +317,7 @@ void ToggleSpecPause(IConsole::IResult *pResult, void *pUserData, int PauseType) } else if(pResult->NumArguments() > 0) { - if(-PauseState == PauseType && pPlayer->m_SpectatorID != pResult->m_ClientID && pServ->ClientIngame(pPlayer->m_SpectatorID) && !str_comp(pServ->ClientName(pPlayer->m_SpectatorID), pResult->GetString(0))) + if(-PauseState == PauseType && pPlayer->m_SpectatorId != pResult->m_ClientId && pServ->ClientIngame(pPlayer->m_SpectatorId) && !str_comp(pServ->ClientName(pPlayer->m_SpectatorId), pResult->GetString(0))) { pPlayer->Pause(CPlayer::PAUSE_NONE, false); } @@ -334,11 +339,11 @@ void ToggleSpecPause(IConsole::IResult *pResult, void *pUserData, int PauseType) void ToggleSpecPauseVoted(IConsole::IResult *pResult, void *pUserData, int PauseType) { - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; CGameContext *pSelf = (CGameContext *)pUserData; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; @@ -354,9 +359,9 @@ void ToggleSpecPauseVoted(IConsole::IResult *pResult, void *pUserData, int Pause bool IsPlayerBeingVoted = pSelf->m_VoteCloseTime && (pSelf->IsKickVote() || pSelf->IsSpecVote()) && - pResult->m_ClientID != pSelf->m_VoteVictim; + pResult->m_ClientId != pSelf->m_VoteVictim; if((!IsPlayerBeingVoted && -PauseState == PauseType) || - (IsPlayerBeingVoted && PauseState && pPlayer->m_SpectatorID == pSelf->m_VoteVictim)) + (IsPlayerBeingVoted && PauseState && pPlayer->m_SpectatorId == pSelf->m_VoteVictim)) { pPlayer->Pause(CPlayer::PAUSE_NONE, false); } @@ -364,7 +369,7 @@ void ToggleSpecPauseVoted(IConsole::IResult *pResult, void *pUserData, int Pause { pPlayer->Pause(PauseType, false); if(IsPlayerBeingVoted) - pPlayer->m_SpectatorID = pSelf->m_VoteVictim; + pPlayer->m_SpectatorId = pSelf->m_VoteVictim; } } @@ -391,7 +396,7 @@ void CGameContext::ConTogglePauseVoted(IConsole::IResult *pResult, void *pUserDa void CGameContext::ConTeamTop5(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(g_Config.m_SvHideScore) @@ -403,28 +408,28 @@ void CGameContext::ConTeamTop5(IConsole::IResult *pResult, void *pUserData) if(pResult->NumArguments() == 0) { - pSelf->Score()->ShowTeamTop5(pResult->m_ClientID, 1); + pSelf->Score()->ShowTeamTop5(pResult->m_ClientId, 1); } else if(pResult->NumArguments() == 1) { if(pResult->GetInteger(0) != 0) { - pSelf->Score()->ShowTeamTop5(pResult->m_ClientID, pResult->GetInteger(0)); + pSelf->Score()->ShowTeamTop5(pResult->m_ClientId, pResult->GetInteger(0)); } else { - const char *pRequestedName = (str_comp(pResult->GetString(0), "me") == 0) ? - pSelf->Server()->ClientName(pResult->m_ClientID) : + const char *pRequestedName = (str_comp_nocase(pResult->GetString(0), "me") == 0) ? + pSelf->Server()->ClientName(pResult->m_ClientId) : pResult->GetString(0); - pSelf->Score()->ShowPlayerTeamTop5(pResult->m_ClientID, pRequestedName, 0); + pSelf->Score()->ShowPlayerTeamTop5(pResult->m_ClientId, pRequestedName, 0); } } else if(pResult->NumArguments() == 2 && pResult->GetInteger(1) != 0) { - const char *pRequestedName = (str_comp(pResult->GetString(0), "me") == 0) ? - pSelf->Server()->ClientName(pResult->m_ClientID) : + const char *pRequestedName = (str_comp_nocase(pResult->GetString(0), "me") == 0) ? + pSelf->Server()->ClientName(pResult->m_ClientId) : pResult->GetString(0); - pSelf->Score()->ShowPlayerTeamTop5(pResult->m_ClientID, pRequestedName, pResult->GetInteger(1)); + pSelf->Score()->ShowPlayerTeamTop5(pResult->m_ClientId, pRequestedName, pResult->GetInteger(1)); } else { @@ -437,7 +442,7 @@ void CGameContext::ConTeamTop5(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConTop(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(g_Config.m_SvHideScore) @@ -448,41 +453,41 @@ void CGameContext::ConTop(IConsole::IResult *pResult, void *pUserData) } if(pResult->NumArguments() > 0) - pSelf->Score()->ShowTop(pResult->m_ClientID, pResult->GetInteger(0)); + pSelf->Score()->ShowTop(pResult->m_ClientId, pResult->GetInteger(0)); else - pSelf->Score()->ShowTop(pResult->m_ClientID); + pSelf->Score()->ShowTop(pResult->m_ClientId); } void CGameContext::ConTimes(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(pResult->NumArguments() == 0) { - pSelf->Score()->ShowTimes(pResult->m_ClientID, 1); + pSelf->Score()->ShowTimes(pResult->m_ClientId, 1); } else if(pResult->NumArguments() == 1) { if(pResult->GetInteger(0) != 0) { - pSelf->Score()->ShowTimes(pResult->m_ClientID, pResult->GetInteger(0)); + pSelf->Score()->ShowTimes(pResult->m_ClientId, pResult->GetInteger(0)); } else { - const char *pRequestedName = (str_comp(pResult->GetString(0), "me") == 0) ? - pSelf->Server()->ClientName(pResult->m_ClientID) : + const char *pRequestedName = (str_comp_nocase(pResult->GetString(0), "me") == 0) ? + pSelf->Server()->ClientName(pResult->m_ClientId) : pResult->GetString(0); - pSelf->Score()->ShowTimes(pResult->m_ClientID, pRequestedName, pResult->GetInteger(1)); + pSelf->Score()->ShowTimes(pResult->m_ClientId, pRequestedName, pResult->GetInteger(1)); } } else if(pResult->NumArguments() == 2 && pResult->GetInteger(1) != 0) { - const char *pRequestedName = (str_comp(pResult->GetString(0), "me") == 0) ? - pSelf->Server()->ClientName(pResult->m_ClientID) : + const char *pRequestedName = (str_comp_nocase(pResult->GetString(0), "me") == 0) ? + pSelf->Server()->ClientName(pResult->m_ClientId) : pResult->GetString(0); - pSelf->Score()->ShowTimes(pResult->m_ClientID, pRequestedName, pResult->GetInteger(1)); + pSelf->Score()->ShowTimes(pResult->m_ClientId, pRequestedName, pResult->GetInteger(1)); } else { @@ -495,29 +500,35 @@ void CGameContext::ConTimes(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConDND(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; - if(pPlayer->m_DND) - { - pPlayer->m_DND = false; - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "You will receive global chat and server messages"); - } - else - { - pPlayer->m_DND = true; - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "You will not receive any further global chat and server messages"); - } + pPlayer->m_DND = pResult->NumArguments() == 0 ? !pPlayer->m_DND : pResult->GetInteger(0); + pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", pPlayer->m_DND ? "You will not receive any further global chat and server messages" : "You will receive global chat and server messages"); +} + +void CGameContext::ConWhispers(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(!CheckClientId(pResult->m_ClientId)) + return; + + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; + if(!pPlayer) + return; + + pPlayer->m_Whispers = pResult->NumArguments() == 0 ? !pPlayer->m_Whispers : pResult->GetInteger(0); + pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", pPlayer->m_Whispers ? "You will receive whispers" : "You will not receive any further whispers"); } void CGameContext::ConMap(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(g_Config.m_SvMapVote == 0) @@ -533,55 +544,55 @@ void CGameContext::ConMap(IConsole::IResult *pResult, void *pUserData) return; } - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; - if(pSelf->RateLimitPlayerVote(pResult->m_ClientID) || pSelf->RateLimitPlayerMapVote(pResult->m_ClientID)) + if(pSelf->RateLimitPlayerVote(pResult->m_ClientId) || pSelf->RateLimitPlayerMapVote(pResult->m_ClientId)) return; - pSelf->Score()->MapVote(pResult->m_ClientID, pResult->GetString(0)); + pSelf->Score()->MapVote(pResult->m_ClientId, pResult->GetString(0)); } void CGameContext::ConMapInfo(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; if(pResult->NumArguments() > 0) - pSelf->Score()->MapInfo(pResult->m_ClientID, pResult->GetString(0)); + pSelf->Score()->MapInfo(pResult->m_ClientId, pResult->GetString(0)); else - pSelf->Score()->MapInfo(pResult->m_ClientID, g_Config.m_SvMap); + pSelf->Score()->MapInfo(pResult->m_ClientId, pSelf->Server()->GetMapName()); } void CGameContext::ConTimeout(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; const char *pTimeout = pResult->NumArguments() > 0 ? pResult->GetString(0) : pPlayer->m_aTimeoutCode; - if(!pSelf->Server()->IsSixup(pResult->m_ClientID)) + if(!pSelf->Server()->IsSixup(pResult->m_ClientId)) { for(int i = 0; i < pSelf->Server()->MaxClients(); i++) { - if(i == pResult->m_ClientID) + if(i == pResult->m_ClientId) continue; if(!pSelf->m_apPlayers[i]) continue; if(str_comp(pSelf->m_apPlayers[i]->m_aTimeoutCode, pTimeout)) continue; - if(pSelf->Server()->SetTimedOut(i, pResult->m_ClientID)) + if(pSelf->Server()->SetTimedOut(i, pResult->m_ClientId)) { if(pSelf->m_apPlayers[i]->GetCharacter()) pSelf->SendTuningParams(i, pSelf->m_apPlayers[i]->GetCharacter()->m_TuneZone); @@ -595,21 +606,21 @@ void CGameContext::ConTimeout(IConsole::IResult *pResult, void *pUserData) "Your timeout code has been set. 0.7 clients can not reclaim their tees on timeout; however, a 0.6 client can claim your tee "); } - pSelf->Server()->SetTimeoutProtected(pResult->m_ClientID); + pSelf->Server()->SetTimeoutProtected(pResult->m_ClientId); str_copy(pPlayer->m_aTimeoutCode, pResult->GetString(0), sizeof(pPlayer->m_aTimeoutCode)); } void CGameContext::ConPractice(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; - if(pSelf->ProcessSpamProtection(pResult->m_ClientID, false)) + if(pSelf->ProcessSpamProtection(pResult->m_ClientId, false)) return; if(!g_Config.m_SvPractice) @@ -623,7 +634,7 @@ void CGameContext::ConPractice(IConsole::IResult *pResult, void *pUserData) CGameTeams &Teams = pSelf->m_pController->Teams(); - int Team = Teams.m_Core.Team(pResult->m_ClientID); + int Team = Teams.m_Core.Team(pResult->m_ClientId); if(Team < TEAM_FLOCK || (Team == TEAM_FLOCK && g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO) || Team >= TEAM_SUPER) { @@ -634,6 +645,24 @@ void CGameContext::ConPractice(IConsole::IResult *pResult, void *pUserData) return; } + if(Teams.TeamFlock(Team)) + { + pSelf->Console()->Print( + IConsole::OUTPUT_LEVEL_STANDARD, + "chatresp", + "Practice mode can't be enabled in team 0 mode."); + return; + } + + if(Teams.GetSaving(Team)) + { + pSelf->Console()->Print( + IConsole::OUTPUT_LEVEL_STANDARD, + "chatresp", + "Practice mode can't be enabled while team save or load is in progress"); + return; + } + if(Teams.IsPractice(Team)) { pSelf->Console()->Print( @@ -667,25 +696,50 @@ void CGameContext::ConPractice(IConsole::IResult *pResult, void *pUserData) int NumRequiredVotes = TeamSize / 2 + 1; char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "'%s' voted to %s /practice mode for your team, which means you can use /r, but you can't earn a rank. Type /practice to vote (%d/%d required votes)", pSelf->Server()->ClientName(pResult->m_ClientID), VotedForPractice ? "enable" : "disable", NumCurrentVotes, NumRequiredVotes); + str_format(aBuf, sizeof(aBuf), "'%s' voted to %s /practice mode for your team, which means you can use practice commands, but you can't earn a rank. Type /practice to vote (%d/%d required votes)", pSelf->Server()->ClientName(pResult->m_ClientId), VotedForPractice ? "enable" : "disable", NumCurrentVotes, NumRequiredVotes); pSelf->SendChatTeam(Team, aBuf); if(NumCurrentVotes >= NumRequiredVotes) { Teams.SetPractice(Team, true); pSelf->SendChatTeam(Team, "Practice mode enabled for your team, happy practicing!"); + pSelf->SendChatTeam(Team, "See /practicecmdlist for a list of all avaliable practice commands. Most commonly used ones are /telecursor, /lasttp and /rescue"); } } +void CGameContext::ConPracticeCmdList(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + + char aPracticeCommands[256]; + mem_zero(aPracticeCommands, sizeof(aPracticeCommands)); + str_append(aPracticeCommands, "Available practice commands: "); + for(const IConsole::CCommandInfo *pCmd = pSelf->Console()->FirstCommandInfo(IConsole::ACCESS_LEVEL_USER, CMDFLAG_PRACTICE); + pCmd; pCmd = pCmd->NextCommandInfo(IConsole::ACCESS_LEVEL_USER, CMDFLAG_PRACTICE)) + { + char aCommand[64]; + + str_format(aCommand, sizeof(aCommand), "/%s%s", pCmd->m_pName, pCmd->NextCommandInfo(IConsole::ACCESS_LEVEL_USER, CMDFLAG_PRACTICE) ? ", " : ""); + + if(str_length(aCommand) + str_length(aPracticeCommands) > 255) + { + pSelf->SendChatTarget(pResult->m_ClientId, aPracticeCommands); + mem_zero(aPracticeCommands, sizeof(aPracticeCommands)); + } + str_append(aPracticeCommands, aCommand); + } + pSelf->SendChatTarget(pResult->m_ClientId, aPracticeCommands); +} + void CGameContext::ConSwap(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; const char *pName = pResult->GetString(0); - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; @@ -698,9 +752,18 @@ void CGameContext::ConSwap(IConsole::IResult *pResult, void *pUserData) return; } + if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) + { + pSelf->Console()->Print( + IConsole::OUTPUT_LEVEL_STANDARD, + "chatresp", + "Swap is not available on forced solo servers."); + return; + } + CGameTeams &Teams = pSelf->m_pController->Teams(); - int Team = Teams.m_Core.Team(pResult->m_ClientID); + int Team = Teams.m_Core.Team(pResult->m_ClientId); if(Team < TEAM_FLOCK || Team >= TEAM_SUPER) { @@ -728,7 +791,7 @@ void CGameContext::ConSwap(IConsole::IResult *pResult, void *pUserData) int TeamSize = 1; for(int i = 0; i < MAX_CLIENTS; i++) { - if(pSelf->m_apPlayers[i] && Teams.m_Core.Team(i) == Team && i != pResult->m_ClientID) + if(pSelf->m_apPlayers[i] && Teams.m_Core.Team(i) == Team && i != pResult->m_ClientId) { TargetClientId = i; TeamSize++; @@ -744,7 +807,7 @@ void CGameContext::ConSwap(IConsole::IResult *pResult, void *pUserData) return; } - if(TargetClientId == pResult->m_ClientID) + if(TargetClientId == pResult->m_ClientId) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "Can't swap with yourself"); return; @@ -758,7 +821,7 @@ void CGameContext::ConSwap(IConsole::IResult *pResult, void *pUserData) } CPlayer *pSwapPlayer = pSelf->m_apPlayers[TargetClientId]; - if(Team == TEAM_FLOCK && g_Config.m_SvTeam != 3) + if(Team == TEAM_FLOCK || Teams.TeamFlock(Team)) { CCharacter *pChr = pPlayer->GetCharacter(); CCharacter *pSwapChr = pSwapPlayer->GetCharacter(); @@ -768,21 +831,21 @@ void CGameContext::ConSwap(IConsole::IResult *pResult, void *pUserData) return; } } - else if(!Teams.IsStarted(Team)) + else if(!Teams.IsStarted(Team) && !Teams.TeamFlock(Team)) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "Need to have started the map to swap with a player."); return; } - if(pSelf->m_World.m_Core.m_apCharacters[pResult->m_ClientID] == nullptr || pSelf->m_World.m_Core.m_apCharacters[TargetClientId] == nullptr) + if(pSelf->m_World.m_Core.m_apCharacters[pResult->m_ClientId] == nullptr || pSelf->m_World.m_Core.m_apCharacters[TargetClientId] == nullptr) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "You and the other player must not be paused."); return; } - bool SwapPending = pSwapPlayer->m_SwapTargetsClientID != pResult->m_ClientID; + bool SwapPending = pSwapPlayer->m_SwapTargetsClientId != pResult->m_ClientId; if(SwapPending) { - if(pSelf->ProcessSpamProtection(pResult->m_ClientID)) + if(pSelf->ProcessSpamProtection(pResult->m_ClientId)) return; Teams.RequestTeamSwap(pPlayer, pSwapPlayer, Team); @@ -795,12 +858,12 @@ void CGameContext::ConSwap(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConSave(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(!g_Config.m_SvSaveGames) { - pSelf->SendChatTarget(pResult->m_ClientID, "Save-function is disabled on this server"); + pSelf->SendChatTarget(pResult->m_ClientId, "Save-function is disabled on this server"); return; } @@ -808,37 +871,37 @@ void CGameContext::ConSave(IConsole::IResult *pResult, void *pUserData) if(pResult->NumArguments() > 0) pCode = pResult->GetString(0); - pSelf->Score()->SaveTeam(pResult->m_ClientID, pCode, g_Config.m_SvSqlServerName); + pSelf->Score()->SaveTeam(pResult->m_ClientId, pCode, g_Config.m_SvSqlServerName); } void CGameContext::ConLoad(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(!g_Config.m_SvSaveGames) { - pSelf->SendChatTarget(pResult->m_ClientID, "Save-function is disabled on this server"); + pSelf->SendChatTarget(pResult->m_ClientId, "Save-function is disabled on this server"); return; } if(pResult->NumArguments() > 0) - pSelf->Score()->LoadTeam(pResult->GetString(0), pResult->m_ClientID); + pSelf->Score()->LoadTeam(pResult->GetString(0), pResult->m_ClientId); else - pSelf->Score()->GetSaves(pResult->m_ClientID); + pSelf->Score()->GetSaves(pResult->m_ClientId); } void CGameContext::ConTeamRank(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(pResult->NumArguments() > 0) { if(!g_Config.m_SvHideScore) - pSelf->Score()->ShowTeamRank(pResult->m_ClientID, pResult->GetString(0)); + pSelf->Score()->ShowTeamRank(pResult->m_ClientId, pResult->GetString(0)); else pSelf->Console()->Print( IConsole::OUTPUT_LEVEL_STANDARD, @@ -846,20 +909,20 @@ void CGameContext::ConTeamRank(IConsole::IResult *pResult, void *pUserData) "Showing the team rank of other players is not allowed on this server."); } else - pSelf->Score()->ShowTeamRank(pResult->m_ClientID, - pSelf->Server()->ClientName(pResult->m_ClientID)); + pSelf->Score()->ShowTeamRank(pResult->m_ClientId, + pSelf->Server()->ClientName(pResult->m_ClientId)); } void CGameContext::ConRank(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(pResult->NumArguments() > 0) { if(!g_Config.m_SvHideScore) - pSelf->Score()->ShowRank(pResult->m_ClientID, pResult->GetString(0)); + pSelf->Score()->ShowRank(pResult->m_ClientId, pResult->GetString(0)); else pSelf->Console()->Print( IConsole::OUTPUT_LEVEL_STANDARD, @@ -867,14 +930,14 @@ void CGameContext::ConRank(IConsole::IResult *pResult, void *pUserData) "Showing the rank of other players is not allowed on this server."); } else - pSelf->Score()->ShowRank(pResult->m_ClientID, - pSelf->Server()->ClientName(pResult->m_ClientID)); + pSelf->Score()->ShowRank(pResult->m_ClientId, + pSelf->Server()->ClientName(pResult->m_ClientId)); } void CGameContext::ConLock(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(g_Config.m_SvTeam == SV_TEAM_FORBIDDEN || g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) @@ -884,7 +947,7 @@ void CGameContext::ConLock(IConsole::IResult *pResult, void *pUserData) return; } - int Team = pSelf->GetDDRaceTeam(pResult->m_ClientID); + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); bool Lock = pSelf->m_pController->Teams().TeamLocked(Team); @@ -900,19 +963,22 @@ void CGameContext::ConLock(IConsole::IResult *pResult, void *pUserData) return; } - if(pSelf->ProcessSpamProtection(pResult->m_ClientID, false)) + if(pSelf->ProcessSpamProtection(pResult->m_ClientId, false)) return; char aBuf[512]; if(Lock) { - pSelf->UnlockTeam(pResult->m_ClientID, Team); + pSelf->UnlockTeam(pResult->m_ClientId, Team); } else { pSelf->m_pController->Teams().SetTeamLock(Team, true); - str_format(aBuf, sizeof(aBuf), "'%s' locked your team. After the race starts, killing will kill everyone in your team.", pSelf->Server()->ClientName(pResult->m_ClientID)); + if(pSelf->m_pController->Teams().TeamFlock(Team)) + str_format(aBuf, sizeof(aBuf), "'%s' locked your team.", pSelf->Server()->ClientName(pResult->m_ClientId)); + else + str_format(aBuf, sizeof(aBuf), "'%s' locked your team. After the race starts, killing will kill everyone in your team.", pSelf->Server()->ClientName(pResult->m_ClientId)); pSelf->SendChatTeam(Team, aBuf); } } @@ -920,7 +986,7 @@ void CGameContext::ConLock(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConUnlock(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(g_Config.m_SvTeam == SV_TEAM_FORBIDDEN || g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) @@ -930,43 +996,45 @@ void CGameContext::ConUnlock(IConsole::IResult *pResult, void *pUserData) return; } - int Team = pSelf->GetDDRaceTeam(pResult->m_ClientID); + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); if(Team <= TEAM_FLOCK || Team >= TEAM_SUPER) return; - if(pSelf->ProcessSpamProtection(pResult->m_ClientID, false)) + if(pSelf->ProcessSpamProtection(pResult->m_ClientId, false)) return; - pSelf->UnlockTeam(pResult->m_ClientID, Team); + pSelf->UnlockTeam(pResult->m_ClientId, Team); } -void CGameContext::UnlockTeam(int ClientID, int Team) const +void CGameContext::UnlockTeam(int ClientId, int Team) const { m_pController->Teams().SetTeamLock(Team, false); char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "'%s' unlocked your team.", Server()->ClientName(ClientID)); + str_format(aBuf, sizeof(aBuf), "'%s' unlocked your team.", Server()->ClientName(ClientId)); SendChatTeam(Team, aBuf); } -void CGameContext::AttemptJoinTeam(int ClientID, int Team) +void CGameContext::AttemptJoinTeam(int ClientId, int Team) { - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; if(!pPlayer) return; - if(m_VoteCloseTime && m_VoteCreator == ClientID && (IsKickVote() || IsSpecVote())) + if(m_VoteCloseTime && m_VoteCreator == ClientId && (IsKickVote() || IsSpecVote())) { Console()->Print( IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "You are running a vote please try again after the vote is done!"); + return; } else if(g_Config.m_SvTeam == SV_TEAM_FORBIDDEN || g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) { Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "Teams are disabled"); + return; } else if(g_Config.m_SvTeam == SV_TEAM_MANDATORY && Team == 0 && pPlayer->GetCharacter() && pPlayer->GetCharacter()->m_LastStartWarning < Server()->Tick() - 3 * Server()->TickSpeed()) { @@ -992,20 +1060,20 @@ void CGameContext::AttemptJoinTeam(int ClientID, int Team) Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "You can\'t change teams that fast!"); } - else if(Team > 0 && Team < MAX_CLIENTS && m_pController->Teams().TeamLocked(Team) && !m_pController->Teams().IsInvited(Team, ClientID)) + else if(Team > 0 && Team < MAX_CLIENTS && m_pController->Teams().TeamLocked(Team) && !m_pController->Teams().IsInvited(Team, ClientId)) { Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", g_Config.m_SvInvite ? "This team is locked using /lock. Only members of the team can unlock it using /lock." : "This team is locked using /lock. Only members of the team can invite you or unlock it using /lock."); } - else if(Team > 0 && Team < MAX_CLIENTS && m_pController->Teams().Count(Team) >= g_Config.m_SvMaxTeamSize) + else if(Team > 0 && Team < MAX_CLIENTS && m_pController->Teams().Count(Team) >= g_Config.m_SvMaxTeamSize && !m_pController->Teams().TeamFlock(Team) && !m_pController->Teams().IsPractice(Team)) { char aBuf[512]; str_format(aBuf, sizeof(aBuf), "This team already has the maximum allowed size of %d players", g_Config.m_SvMaxTeamSize); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", aBuf); } - else if(const char *pError = m_pController->Teams().SetCharacterTeam(pPlayer->GetCID(), Team)) + else if(const char *pError = m_pController->Teams().SetCharacterTeam(pPlayer->GetCid(), Team)) { Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", pError); } @@ -1013,13 +1081,16 @@ void CGameContext::AttemptJoinTeam(int ClientID, int Team) { char aBuf[512]; str_format(aBuf, sizeof(aBuf), "'%s' joined team %d", - Server()->ClientName(pPlayer->GetCID()), + Server()->ClientName(pPlayer->GetCid()), Team); - SendChat(-1, CGameContext::CHAT_ALL, aBuf); + SendChat(-1, TEAM_ALL, aBuf); pPlayer->m_Last_Team = Server()->Tick(); if(m_pController->Teams().IsPractice(Team)) - SendChatTarget(pPlayer->GetCID(), "Practice mode enabled for your team, happy practicing!"); + SendChatTarget(pPlayer->GetCid(), "Practice mode enabled for your team, happy practicing!"); + + if(m_pController->Teams().TeamFlock(Team)) + SendChatTarget(pPlayer->GetCid(), "Team 0 mode enabled for your team. This will make your team behave like team 0."); } } } @@ -1043,7 +1114,7 @@ void CGameContext::ConInvite(IConsole::IResult *pResult, void *pUserData) return; } - int Team = pController->Teams().m_Core.Team(pResult->m_ClientID); + int Team = pController->Teams().m_Core.Team(pResult->m_ClientId); if(Team > TEAM_FLOCK && Team < TEAM_SUPER) { int Target = -1; @@ -1068,57 +1139,144 @@ void CGameContext::ConInvite(IConsole::IResult *pResult, void *pUserData) return; } - if(pSelf->m_apPlayers[pResult->m_ClientID] && pSelf->m_apPlayers[pResult->m_ClientID]->m_LastInvited + g_Config.m_SvInviteFrequency * pSelf->Server()->TickSpeed() > pSelf->Server()->Tick()) + if(pSelf->m_apPlayers[pResult->m_ClientId] && pSelf->m_apPlayers[pResult->m_ClientId]->m_LastInvited + g_Config.m_SvInviteFrequency * pSelf->Server()->TickSpeed() > pSelf->Server()->Tick()) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "Can't invite this quickly"); return; } pController->Teams().SetClientInvited(Team, Target, true); - pSelf->m_apPlayers[pResult->m_ClientID]->m_LastInvited = pSelf->Server()->Tick(); + pSelf->m_apPlayers[pResult->m_ClientId]->m_LastInvited = pSelf->Server()->Tick(); char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "'%s' invited you to team %d. Use /team %d to join.", pSelf->Server()->ClientName(pResult->m_ClientID), Team, Team); + str_format(aBuf, sizeof(aBuf), "'%s' invited you to team %d. Use /team %d to join.", pSelf->Server()->ClientName(pResult->m_ClientId), Team, Team); pSelf->SendChatTarget(Target, aBuf); - str_format(aBuf, sizeof(aBuf), "'%s' invited '%s' to your team.", pSelf->Server()->ClientName(pResult->m_ClientID), pSelf->Server()->ClientName(Target)); + str_format(aBuf, sizeof(aBuf), "'%s' invited '%s' to your team.", pSelf->Server()->ClientName(pResult->m_ClientId), pSelf->Server()->ClientName(Target)); pSelf->SendChatTeam(Team, aBuf); } else pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "Can't invite players to this team"); } +void CGameContext::ConTeam0Mode(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + auto *pController = pSelf->m_pController; + + if(!CheckClientId(pResult->m_ClientId)) + return; + + if(g_Config.m_SvTeam == SV_TEAM_FORBIDDEN || g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || g_Config.m_SvTeam == SV_TEAM_MANDATORY) + { + pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", + "Team mode change disabled"); + return; + } + + if(!g_Config.m_SvTeam0Mode) + { + pSelf->Console()->Print( + IConsole::OUTPUT_LEVEL_STANDARD, + "chatresp", + "Team mode change is disabled on this server."); + return; + } + + int Team = pController->Teams().m_Core.Team(pResult->m_ClientId); + bool Mode = pController->Teams().TeamFlock(Team); + + if(Team <= TEAM_FLOCK || Team >= TEAM_SUPER) + { + pSelf->Console()->Print( + IConsole::OUTPUT_LEVEL_STANDARD, + "chatresp", + "This team can't have the mode changed"); + return; + } + + if(pController->Teams().GetTeamState(Team) != CGameTeams::TEAMSTATE_OPEN) + { + pSelf->SendChatTarget(pResult->m_ClientId, "Team mode can't be changed while racing"); + return; + } + + if(pResult->NumArguments() > 0) + Mode = !pResult->GetInteger(0); + + if(pSelf->ProcessSpamProtection(pResult->m_ClientId, false)) + return; + + char aBuf[512]; + if(Mode) + { + if(pController->Teams().Count(Team) > g_Config.m_SvMaxTeamSize) + { + str_format(aBuf, sizeof(aBuf), "Can't disable team 0 mode. This team exceeds the maximum allowed size of %d players for regular team", g_Config.m_SvMaxTeamSize); + pSelf->SendChatTarget(pResult->m_ClientId, aBuf); + } + else + { + pController->Teams().SetTeamFlock(Team, false); + + str_format(aBuf, sizeof(aBuf), "'%s' disabled team 0 mode.", pSelf->Server()->ClientName(pResult->m_ClientId)); + pSelf->SendChatTeam(Team, aBuf); + } + } + else + { + if(pController->Teams().IsPractice(Team)) + { + pSelf->SendChatTarget(pResult->m_ClientId, "Can't enable team 0 mode with practice mode on."); + } + else + { + pController->Teams().SetTeamFlock(Team, true); + + str_format(aBuf, sizeof(aBuf), "'%s' enabled team 0 mode. This will make your team behave like team 0.", pSelf->Server()->ClientName(pResult->m_ClientId)); + pSelf->SendChatTeam(Team, aBuf); + } + } +} + void CGameContext::ConTeam(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; if(pResult->NumArguments() > 0) { - pSelf->AttemptJoinTeam(pResult->m_ClientID, pResult->GetInteger(0)); + pSelf->AttemptJoinTeam(pResult->m_ClientId, pResult->GetInteger(0)); } else { char aBuf[512]; if(!pPlayer->IsPlaying()) { - pSelf->Console()->Print( - IConsole::OUTPUT_LEVEL_STANDARD, - "chatresp", - "You can't check your team while you are dead/a spectator."); + pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "You can't check your team while you are dead/a spectator."); } else { - str_format( - aBuf, - sizeof(aBuf), - "You are in team %d", - pSelf->GetDDRaceTeam(pResult->m_ClientID)); + int TeamSize = 0; + const int PlayerTeam = pSelf->GetDDRaceTeam(pResult->m_ClientId); + + // Count players in team + for(int ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) + { + const CPlayer *pOtherPlayer = pSelf->m_apPlayers[ClientId]; + if(!pOtherPlayer || !pOtherPlayer->IsPlaying()) + continue; + + if(pSelf->GetDDRaceTeam(ClientId) == PlayerTeam) + TeamSize++; + } + + str_format(aBuf, sizeof(aBuf), "You are in team %d having %d %s", PlayerTeam, TeamSize, TeamSize > 1 ? "players" : "player"); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", aBuf); } } @@ -1127,7 +1285,7 @@ void CGameContext::ConTeam(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConJoin(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; int Target = -1; @@ -1148,25 +1306,25 @@ void CGameContext::ConJoin(IConsole::IResult *pResult, void *pUserData) } int Team = pSelf->GetDDRaceTeam(Target); - if(pSelf->ProcessSpamProtection(pResult->m_ClientID, false)) + if(pSelf->ProcessSpamProtection(pResult->m_ClientId, false)) return; - pSelf->AttemptJoinTeam(pResult->m_ClientID, Team); + pSelf->AttemptJoinTeam(pResult->m_ClientId, Team); } void CGameContext::ConMe(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; char aBuf[256 + 24]; str_format(aBuf, 256 + 24, "'%s' %s", - pSelf->Server()->ClientName(pResult->m_ClientID), + pSelf->Server()->ClientName(pResult->m_ClientId), pResult->GetString(0)); if(g_Config.m_SvSlashMe) - pSelf->SendChat(-2, CGameContext::CHAT_ALL, aBuf, pResult->m_ClientID); + pSelf->SendChat(-2, TEAM_ALL, aBuf, pResult->m_ClientId); else pSelf->Console()->Print( IConsole::OUTPUT_LEVEL_STANDARD, @@ -1188,10 +1346,10 @@ void CGameContext::ConSetEyeEmote(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; if(pResult->NumArguments() == 0) @@ -1201,7 +1359,7 @@ void CGameContext::ConSetEyeEmote(IConsole::IResult *pResult, "chatresp", (pPlayer->m_EyeEmoteEnabled) ? "You can now use the preset eye emotes." : - "You don't have any eye emotes, remember to bind some. (until you die)"); + "You don't have any eye emotes, remember to bind some."); return; } else if(str_comp_nocase(pResult->GetString(0), "on") == 0) @@ -1215,7 +1373,7 @@ void CGameContext::ConSetEyeEmote(IConsole::IResult *pResult, "chatresp", (pPlayer->m_EyeEmoteEnabled) ? "You can now use the preset eye emotes." : - "You don't have any eye emotes, remember to bind some. (until you die)"); + "You don't have any eye emotes, remember to bind some."); } void CGameContext::ConEyeEmote(IConsole::IResult *pResult, void *pUserData) @@ -1228,10 +1386,10 @@ void CGameContext::ConEyeEmote(IConsole::IResult *pResult, void *pUserData) return; } - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; @@ -1252,19 +1410,19 @@ void CGameContext::ConEyeEmote(IConsole::IResult *pResult, void *pUserData) return; int EmoteType = 0; - if(!str_comp(pResult->GetString(0), "angry")) + if(!str_comp_nocase(pResult->GetString(0), "angry")) EmoteType = EMOTE_ANGRY; - else if(!str_comp(pResult->GetString(0), "blink")) + else if(!str_comp_nocase(pResult->GetString(0), "blink")) EmoteType = EMOTE_BLINK; - else if(!str_comp(pResult->GetString(0), "close")) + else if(!str_comp_nocase(pResult->GetString(0), "close")) EmoteType = EMOTE_BLINK; - else if(!str_comp(pResult->GetString(0), "happy")) + else if(!str_comp_nocase(pResult->GetString(0), "happy")) EmoteType = EMOTE_HAPPY; - else if(!str_comp(pResult->GetString(0), "pain")) + else if(!str_comp_nocase(pResult->GetString(0), "pain")) EmoteType = EMOTE_PAIN; - else if(!str_comp(pResult->GetString(0), "surprise")) + else if(!str_comp_nocase(pResult->GetString(0), "surprise")) EmoteType = EMOTE_SURPRISE; - else if(!str_comp(pResult->GetString(0), "normal")) + else if(!str_comp_nocase(pResult->GetString(0), "normal")) EmoteType = EMOTE_NORMAL; else { @@ -1284,10 +1442,10 @@ void CGameContext::ConEyeEmote(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConNinjaJetpack(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; if(pResult->NumArguments()) @@ -1299,10 +1457,10 @@ void CGameContext::ConNinjaJetpack(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConShowOthers(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; if(g_Config.m_SvShowOthers) @@ -1322,10 +1480,10 @@ void CGameContext::ConShowOthers(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConShowAll(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; @@ -1342,18 +1500,18 @@ void CGameContext::ConShowAll(IConsole::IResult *pResult, void *pUserData) } if(pPlayer->m_ShowAll) - pSelf->SendChatTarget(pResult->m_ClientID, "You will now see all tees on this server, no matter the distance"); + pSelf->SendChatTarget(pResult->m_ClientId, "You will now see all tees on this server, no matter the distance"); else - pSelf->SendChatTarget(pResult->m_ClientID, "You will no longer see all tees on this server"); + pSelf->SendChatTarget(pResult->m_ClientId, "You will no longer see all tees on this server"); } void CGameContext::ConSpecTeam(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; @@ -1363,38 +1521,38 @@ void CGameContext::ConSpecTeam(IConsole::IResult *pResult, void *pUserData) pPlayer->m_SpecTeam = !pPlayer->m_SpecTeam; } -bool CheckClientID(int ClientID) +bool CheckClientId(int ClientId) { - return ClientID >= 0 && ClientID < MAX_CLIENTS; + return ClientId >= 0 && ClientId < MAX_CLIENTS; } void CGameContext::ConSayTime(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - int ClientID; + int ClientId; char aBufName[MAX_NAME_LENGTH]; if(pResult->NumArguments() > 0) { - for(ClientID = 0; ClientID < MAX_CLIENTS; ClientID++) - if(str_comp(pResult->GetString(0), pSelf->Server()->ClientName(ClientID)) == 0) + for(ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) + if(str_comp(pResult->GetString(0), pSelf->Server()->ClientName(ClientId)) == 0) break; - if(ClientID == MAX_CLIENTS) + if(ClientId == MAX_CLIENTS) return; - str_format(aBufName, sizeof(aBufName), "%s's", pSelf->Server()->ClientName(ClientID)); + str_format(aBufName, sizeof(aBufName), "%s's", pSelf->Server()->ClientName(ClientId)); } else { str_copy(aBufName, "Your", sizeof(aBufName)); - ClientID = pResult->m_ClientID; + ClientId = pResult->m_ClientId; } - CPlayer *pPlayer = pSelf->m_apPlayers[ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[ClientId]; if(!pPlayer) return; CCharacter *pChr = pPlayer->GetCharacter(); @@ -1414,10 +1572,10 @@ void CGameContext::ConSayTime(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConSayTimeAll(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; CCharacter *pChr = pPlayer->GetCharacter(); @@ -1429,19 +1587,19 @@ void CGameContext::ConSayTimeAll(IConsole::IResult *pResult, void *pUserData) char aBufTime[32]; char aBuf[64]; int64_t Time = (int64_t)100 * (float)(pSelf->Server()->Tick() - pChr->m_StartTime) / ((float)pSelf->Server()->TickSpeed()); - const char *pName = pSelf->Server()->ClientName(pResult->m_ClientID); + const char *pName = pSelf->Server()->ClientName(pResult->m_ClientId); str_time(Time, TIME_HOURS, aBufTime, sizeof(aBufTime)); str_format(aBuf, sizeof(aBuf), "%s\'s current race time is %s", pName, aBufTime); - pSelf->SendChat(-1, CGameContext::CHAT_ALL, aBuf, pResult->m_ClientID); + pSelf->SendChat(-1, TEAM_ALL, aBuf, pResult->m_ClientId); } void CGameContext::ConTime(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; CCharacter *pChr = pPlayer->GetCharacter(); @@ -1453,7 +1611,7 @@ void CGameContext::ConTime(IConsole::IResult *pResult, void *pUserData) int64_t Time = (int64_t)100 * (float)(pSelf->Server()->Tick() - pChr->m_StartTime) / ((float)pSelf->Server()->TickSpeed()); str_time(Time, TIME_HOURS, aBufTime, sizeof(aBufTime)); str_format(aBuf, sizeof(aBuf), "Your time is %s", aBufTime); - pSelf->SendBroadcast(aBuf, pResult->m_ClientID); + pSelf->SendBroadcast(aBuf, pResult->m_ClientId); } static const char s_aaMsg[4][128] = {"game/round timer.", "broadcast.", "both game/round timer and broadcast.", "racetime."}; @@ -1462,10 +1620,10 @@ void CGameContext::ConSetTimerType(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; @@ -1499,7 +1657,7 @@ void CGameContext::ConSetTimerType(IConsole::IResult *pResult, void *pUserData) } if((OldType == CPlayer::TIMERTYPE_BROADCAST || OldType == CPlayer::TIMERTYPE_GAMETIMER_AND_BROADCAST) && (pPlayer->m_TimerType == CPlayer::TIMERTYPE_GAMETIMER || pPlayer->m_TimerType == CPlayer::TIMERTYPE_NONE)) - pSelf->SendBroadcast("", pResult->m_ClientID); + pSelf->SendBroadcast("", pResult->m_ClientId); } if(pPlayer->m_TimerType <= CPlayer::TIMERTYPE_SIXUP && pPlayer->m_TimerType >= CPlayer::TIMERTYPE_GAMETIMER) @@ -1513,9 +1671,9 @@ void CGameContext::ConSetTimerType(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConRescue(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; CCharacter *pChr = pPlayer->GetCharacter(); @@ -1523,71 +1681,230 @@ void CGameContext::ConRescue(IConsole::IResult *pResult, void *pUserData) return; CGameTeams &Teams = pSelf->m_pController->Teams(); - int Team = pSelf->GetDDRaceTeam(pResult->m_ClientID); + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); if(!g_Config.m_SvRescue && !Teams.IsPractice(Team)) { - pSelf->SendChatTarget(pPlayer->GetCID(), "Rescue is not enabled on this server and you're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); + pSelf->SendChatTarget(pPlayer->GetCid(), "Rescue is not enabled on this server and you're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); return; } - pChr->Rescue(); - pChr->UnFreeze(); + bool GoRescue = true; + + if(pPlayer->m_RescueMode == RESCUEMODE_MANUAL) + { + // if character can't set his rescue state then we should rescue him instead + GoRescue = !pChr->TrySetRescue(RESCUEMODE_MANUAL); + } + + if(GoRescue) + { + pChr->Rescue(); + pChr->UnFreeze(); + } } -void CGameContext::ConTele(IConsole::IResult *pResult, void *pUserData) +void CGameContext::ConRescueMode(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; - CCharacter *pChr = pPlayer->GetCharacter(); - if(!pChr) + + CGameTeams &Teams = pSelf->m_pController->Teams(); + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); + if(!g_Config.m_SvRescue && !Teams.IsPractice(Team)) + { + pSelf->SendChatTarget(pPlayer->GetCid(), "Rescue is not enabled on this server and you're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); + return; + } + + if(str_comp_nocase(pResult->GetString(0), "auto") == 0) + { + if(pPlayer->m_RescueMode != RESCUEMODE_AUTO) + { + pPlayer->m_RescueMode = RESCUEMODE_AUTO; + + pSelf->SendChatTarget(pPlayer->GetCid(), "Rescue mode changed to auto."); + } + + return; + } + + if(str_comp_nocase(pResult->GetString(0), "manual") == 0) + { + if(pPlayer->m_RescueMode != RESCUEMODE_MANUAL) + { + pPlayer->m_RescueMode = RESCUEMODE_MANUAL; + + pSelf->SendChatTarget(pPlayer->GetCid(), "Rescue mode changed to manual."); + } + + return; + } + + if(str_comp_nocase(pResult->GetString(0), "list") == 0) + { + pSelf->SendChatTarget(pPlayer->GetCid(), "Available rescue modes: auto, manual"); + } + else if(str_comp_nocase(pResult->GetString(0), "") == 0) + { + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "Current rescue mode: %s.", pPlayer->m_RescueMode == RESCUEMODE_MANUAL ? "manual" : "auto"); + pSelf->SendChatTarget(pPlayer->GetCid(), aBuf); + } + else + { + pSelf->SendChatTarget(pPlayer->GetCid(), "Unknown argument. Check '/rescuemode list'"); + } +} + +void CGameContext::ConTeleTo(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(!CheckClientId(pResult->m_ClientId)) + return; + CPlayer *pCallingPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; + if(!pCallingPlayer) + return; + CCharacter *pCallingCharacter = pCallingPlayer->GetCharacter(); + if(!pCallingCharacter) return; CGameTeams &Teams = pSelf->m_pController->Teams(); - int Team = pSelf->GetDDRaceTeam(pResult->m_ClientID); + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); if(!Teams.IsPractice(Team)) { - pSelf->SendChatTarget(pPlayer->GetCID(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); + pSelf->SendChatTarget(pCallingPlayer->GetCid(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); return; } - vec2 Pos = pPlayer->m_ViewPos; - if(pResult->NumArguments() > 0) + vec2 Pos = {}; + + if(pResult->NumArguments() == 0) + { + // Set calling tee's position to the origin of its spectating viewport + Pos = pCallingPlayer->m_ViewPos; + } + else { - int ClientID; - for(ClientID = 0; ClientID < MAX_CLIENTS; ClientID++) + // Search for player with this name + int ClientId; + for(ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) { - if(str_comp(pResult->GetString(0), pSelf->Server()->ClientName(ClientID)) == 0) + if(str_comp(pResult->GetString(0), pSelf->Server()->ClientName(ClientId)) == 0) break; } - if(ClientID == MAX_CLIENTS) + if(ClientId == MAX_CLIENTS) { - pSelf->SendChatTarget(pPlayer->GetCID(), "No player with this name found."); + pSelf->SendChatTarget(pCallingPlayer->GetCid(), "No player with this name found."); return; } - CPlayer *pPlayerTo = pSelf->m_apPlayers[ClientID]; - if(!pPlayerTo) + + CPlayer *pDestPlayer = pSelf->m_apPlayers[ClientId]; + if(!pDestPlayer) return; - CCharacter *pChrTo = pPlayerTo->GetCharacter(); - if(!pChrTo) + CCharacter *pDestCharacter = pDestPlayer->GetCharacter(); + if(!pDestCharacter) return; - Pos = pChrTo->m_Pos; + + // Set calling tee's position to that of the destination tee + Pos = pDestCharacter->m_Pos; } - pSelf->Teleport(pChr, Pos); - pChr->UnFreeze(); - pChr->Core()->m_Vel = vec2(0, 0); - pPlayer->m_LastTeleTee.Save(pChr); + + // Teleport tee + pSelf->Teleport(pCallingCharacter, Pos); + pCallingCharacter->ResetJumps(); + pCallingCharacter->UnFreeze(); + pCallingCharacter->ResetVelocity(); + pCallingPlayer->m_LastTeleTee.Save(pCallingCharacter); +} + +void CGameContext::ConTeleXY(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(!CheckClientId(pResult->m_ClientId)) + return; + CPlayer *pCallingPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; + if(!pCallingPlayer) + return; + CCharacter *pCallingCharacter = pCallingPlayer->GetCharacter(); + if(!pCallingCharacter) + return; + + CGameTeams &Teams = pSelf->m_pController->Teams(); + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); + if(!Teams.IsPractice(Team)) + { + pSelf->SendChatTarget(pCallingPlayer->GetCid(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); + return; + } + + vec2 Pos = {}; + + if(pResult->NumArguments() != 2) + { + pSelf->SendChatTarget(pCallingPlayer->GetCid(), "Can't recognize specified arguments. Usage: /tpxy x y, e.g. /tpxy 9 3."); + return; + } + else + { + float BaseX = 0.f, BaseY = 0.f; + + CMapItemLayerTilemap *pGameLayer = pSelf->m_Layers.GameLayer(); + constexpr float OuterKillTileBoundaryDistance = 201 * 32.f; + float MapWidth = (pGameLayer->m_Width * 32) + (OuterKillTileBoundaryDistance * 2.f), MapHeight = (pGameLayer->m_Height * 32) + (OuterKillTileBoundaryDistance * 2.f); + + const auto DetermineCoordinateRelativity = [](const char *pInString, const float AbsoluteDefaultValue, float &OutFloat) -> bool { + // mode 0 = abs, 1 = sub, 2 = add + + // Relative? + const char *pStrDelta = str_startswith(pInString, "~"); + + float d; + if(!str_tofloat(pStrDelta ? pStrDelta : pInString, &d)) + return false; + + // Is the number valid? + if(std::isnan(d) || std::isinf(d)) + return false; + + // Convert our gleaned 'display' coordinate to an actual map coordinate + d *= 32.f; + + OutFloat = (pStrDelta ? AbsoluteDefaultValue : 0) + d; + return true; + }; + + if(!DetermineCoordinateRelativity(pResult->GetString(0), pCallingPlayer->m_ViewPos.x, BaseX)) + { + pSelf->SendChatTarget(pCallingPlayer->GetCid(), "Invalid X coordinate."); + return; + } + if(!DetermineCoordinateRelativity(pResult->GetString(1), pCallingPlayer->m_ViewPos.y, BaseY)) + { + pSelf->SendChatTarget(pCallingPlayer->GetCid(), "Invalid Y coordinate."); + return; + } + + Pos = {std::clamp(BaseX, (-OuterKillTileBoundaryDistance) + 1.f, (-OuterKillTileBoundaryDistance) + MapWidth - 1.f), std::clamp(BaseY, (-OuterKillTileBoundaryDistance) + 1.f, (-OuterKillTileBoundaryDistance) + MapHeight - 1.f)}; + } + + // Teleport tee + pSelf->Teleport(pCallingCharacter, Pos); + pCallingCharacter->ResetJumps(); + pCallingCharacter->UnFreeze(); + pCallingCharacter->ResetVelocity(); + pCallingPlayer->m_LastTeleTee.Save(pCallingCharacter); } void CGameContext::ConTeleCursor(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; CCharacter *pChr = pPlayer->GetCharacter(); @@ -1595,10 +1912,10 @@ void CGameContext::ConTeleCursor(IConsole::IResult *pResult, void *pUserData) return; CGameTeams &Teams = pSelf->m_pController->Teams(); - int Team = pSelf->GetDDRaceTeam(pResult->m_ClientID); + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); if(!Teams.IsPractice(Team)) { - pSelf->SendChatTarget(pPlayer->GetCID(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); + pSelf->SendChatTarget(pPlayer->GetCid(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); return; } @@ -1609,18 +1926,18 @@ void CGameContext::ConTeleCursor(IConsole::IResult *pResult, void *pUserData) } else if(pResult->NumArguments() > 0) { - int ClientID; - for(ClientID = 0; ClientID < MAX_CLIENTS; ClientID++) + int ClientId; + for(ClientId = 0; ClientId < MAX_CLIENTS; ClientId++) { - if(str_comp(pResult->GetString(0), pSelf->Server()->ClientName(ClientID)) == 0) + if(str_comp(pResult->GetString(0), pSelf->Server()->ClientName(ClientId)) == 0) break; } - if(ClientID == MAX_CLIENTS) + if(ClientId == MAX_CLIENTS) { - pSelf->SendChatTarget(pPlayer->GetCID(), "No player with this name found."); + pSelf->SendChatTarget(pPlayer->GetCid(), "No player with this name found."); return; } - CPlayer *pPlayerTo = pSelf->m_apPlayers[ClientID]; + CPlayer *pPlayerTo = pSelf->m_apPlayers[ClientId]; if(!pPlayerTo) return; CCharacter *pChrTo = pPlayerTo->GetCharacter(); @@ -1629,17 +1946,18 @@ void CGameContext::ConTeleCursor(IConsole::IResult *pResult, void *pUserData) Pos = pChrTo->m_Pos; } pSelf->Teleport(pChr, Pos); + pChr->ResetJumps(); pChr->UnFreeze(); - pChr->Core()->m_Vel = vec2(0, 0); + pChr->ResetVelocity(); pPlayer->m_LastTeleTee.Save(pChr); } void CGameContext::ConLastTele(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; CCharacter *pChr = pPlayer->GetCharacter(); @@ -1647,27 +1965,48 @@ void CGameContext::ConLastTele(IConsole::IResult *pResult, void *pUserData) return; CGameTeams &Teams = pSelf->m_pController->Teams(); - int Team = pSelf->GetDDRaceTeam(pResult->m_ClientID); + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); if(!Teams.IsPractice(Team)) { - pSelf->SendChatTarget(pPlayer->GetCID(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); + pSelf->SendChatTarget(pPlayer->GetCid(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); return; } if(!pPlayer->m_LastTeleTee.GetPos().x) { - pSelf->SendChatTarget(pPlayer->GetCID(), "You haven't previously teleported. Use /tp before using this command."); + pSelf->SendChatTarget(pPlayer->GetCid(), "You haven't previously teleported. Use /tp before using this command."); return; } pPlayer->m_LastTeleTee.Load(pChr, pChr->Team(), true); pPlayer->Pause(CPlayer::PAUSE_NONE, true); } +CCharacter *CGameContext::GetPracticeCharacter(IConsole::IResult *pResult) +{ + if(!CheckClientId(pResult->m_ClientId)) + return nullptr; + CPlayer *pPlayer = m_apPlayers[pResult->m_ClientId]; + if(!pPlayer) + return nullptr; + CCharacter *pChr = pPlayer->GetCharacter(); + if(!pChr) + return nullptr; + + CGameTeams &Teams = m_pController->Teams(); + int Team = GetDDRaceTeam(pResult->m_ClientId); + if(!Teams.IsPractice(Team)) + { + SendChatTarget(pPlayer->GetCid(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); + return nullptr; + } + return pChr; +} + void CGameContext::ConPracticeUnSolo(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; CCharacter *pChr = pPlayer->GetCharacter(); @@ -1676,51 +2015,180 @@ void CGameContext::ConPracticeUnSolo(IConsole::IResult *pResult, void *pUserData if(g_Config.m_SvTeam == SV_TEAM_FORBIDDEN || g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) { - pSelf->SendChatTarget(pPlayer->GetCID(), "Command is not available on solo servers"); + pSelf->SendChatTarget(pPlayer->GetCid(), "Command is not available on solo servers"); return; } CGameTeams &Teams = pSelf->m_pController->Teams(); - int Team = pSelf->GetDDRaceTeam(pResult->m_ClientID); + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); if(!Teams.IsPractice(Team)) { - pSelf->SendChatTarget(pPlayer->GetCID(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); + pSelf->SendChatTarget(pPlayer->GetCid(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); return; } - pChr->SetSolo(false); } -void CGameContext::ConPracticeUnDeep(IConsole::IResult *pResult, void *pUserData) +void CGameContext::ConPracticeSolo(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; CCharacter *pChr = pPlayer->GetCharacter(); if(!pChr) return; + if(g_Config.m_SvTeam == SV_TEAM_FORBIDDEN || g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) + { + pSelf->SendChatTarget(pPlayer->GetCid(), "Command is not available on solo servers"); + return; + } + CGameTeams &Teams = pSelf->m_pController->Teams(); - int Team = pSelf->GetDDRaceTeam(pResult->m_ClientID); + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); if(!Teams.IsPractice(Team)) { - pSelf->SendChatTarget(pPlayer->GetCID(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); + pSelf->SendChatTarget(pPlayer->GetCid(), "You're not in a team with /practice turned on. Note that you can't earn a rank with practice enabled."); return; } + pChr->SetSolo(true); +} + +void CGameContext::ConPracticeUnDeep(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + auto *pChr = pSelf->GetPracticeCharacter(pResult); + if(!pChr) + return; pChr->SetDeepFrozen(false); pChr->UnFreeze(); } +void CGameContext::ConPracticeDeep(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + auto *pChr = pSelf->GetPracticeCharacter(pResult); + if(!pChr) + return; + + pChr->SetDeepFrozen(true); +} + +void CGameContext::ConPracticeShotgun(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConShotgun(pResult, pUserData); +} + +void CGameContext::ConPracticeGrenade(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConGrenade(pResult, pUserData); +} + +void CGameContext::ConPracticeLaser(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConLaser(pResult, pUserData); +} + +void CGameContext::ConPracticeJetpack(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConJetpack(pResult, pUserData); +} + +void CGameContext::ConPracticeSetJumps(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConSetJumps(pResult, pUserData); +} + +void CGameContext::ConPracticeWeapons(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConWeapons(pResult, pUserData); +} + +void CGameContext::ConPracticeUnShotgun(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConUnShotgun(pResult, pUserData); +} + +void CGameContext::ConPracticeUnGrenade(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConUnGrenade(pResult, pUserData); +} + +void CGameContext::ConPracticeUnLaser(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConUnLaser(pResult, pUserData); +} + +void CGameContext::ConPracticeUnJetpack(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConUnJetpack(pResult, pUserData); +} + +void CGameContext::ConPracticeUnWeapons(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConUnWeapons(pResult, pUserData); +} + +void CGameContext::ConPracticeNinja(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConNinja(pResult, pUserData); +} + +void CGameContext::ConPracticeUnNinja(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConUnNinja(pResult, pUserData); +} + +void CGameContext::ConPracticeAddWeapon(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConAddWeapon(pResult, pUserData); +} + +void CGameContext::ConPracticeRemoveWeapon(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(pSelf->GetPracticeCharacter(pResult)) + ConRemoveWeapon(pResult, pUserData); +} + void CGameContext::ConProtectedKill(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; CCharacter *pChr = pPlayer->GetCharacter(); @@ -1737,13 +2205,13 @@ void CGameContext::ConProtectedKill(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConPoints(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(pResult->NumArguments() > 0) { if(!g_Config.m_SvHideScore) - pSelf->Score()->ShowPoints(pResult->m_ClientID, pResult->GetString(0)); + pSelf->Score()->ShowPoints(pResult->m_ClientId, pResult->GetString(0)); else pSelf->Console()->Print( IConsole::OUTPUT_LEVEL_STANDARD, @@ -1751,14 +2219,14 @@ void CGameContext::ConPoints(IConsole::IResult *pResult, void *pUserData) "Showing the global points of other players is not allowed on this server."); } else - pSelf->Score()->ShowPoints(pResult->m_ClientID, - pSelf->Server()->ClientName(pResult->m_ClientID)); + pSelf->Score()->ShowPoints(pResult->m_ClientId, + pSelf->Server()->ClientName(pResult->m_ClientId)); } void CGameContext::ConTopPoints(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(g_Config.m_SvHideScore) @@ -1769,15 +2237,15 @@ void CGameContext::ConTopPoints(IConsole::IResult *pResult, void *pUserData) } if(pResult->NumArguments() > 0) - pSelf->Score()->ShowTopPoints(pResult->m_ClientID, pResult->GetInteger(0)); + pSelf->Score()->ShowTopPoints(pResult->m_ClientId, pResult->GetInteger(0)); else - pSelf->Score()->ShowTopPoints(pResult->m_ClientID); + pSelf->Score()->ShowTopPoints(pResult->m_ClientId); } void CGameContext::ConTimeCP(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; if(g_Config.m_SvHideScore) @@ -1787,10 +2255,10 @@ void CGameContext::ConTimeCP(IConsole::IResult *pResult, void *pUserData) return; } - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer) return; const char *pName = pResult->GetString(0); - pSelf->Score()->LoadPlayerData(pResult->m_ClientID, pName); + pSelf->Score()->LoadPlayerTimeCp(pResult->m_ClientId, pName); } diff --git a/src/game/server/ddracecommands.cpp b/src/game/server/ddracecommands.cpp index 27eacf94e5..e65cad482c 100644 --- a/src/game/server/ddracecommands.cpp +++ b/src/game/server/ddracecommands.cpp @@ -10,16 +10,16 @@ #include #include -bool CheckClientID(int ClientID); +bool CheckClientId(int ClientId); void CGameContext::ConGoLeft(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; int Tiles = pResult->NumArguments() == 1 ? pResult->GetInteger(0) : 1; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - pSelf->MoveCharacter(pResult->m_ClientID, -1 * Tiles, 0); + pSelf->MoveCharacter(pResult->m_ClientId, -1 * Tiles, 0); } void CGameContext::ConGoRight(IConsole::IResult *pResult, void *pUserData) @@ -27,9 +27,9 @@ void CGameContext::ConGoRight(IConsole::IResult *pResult, void *pUserData) CGameContext *pSelf = (CGameContext *)pUserData; int Tiles = pResult->NumArguments() == 1 ? pResult->GetInteger(0) : 1; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - pSelf->MoveCharacter(pResult->m_ClientID, Tiles, 0); + pSelf->MoveCharacter(pResult->m_ClientId, Tiles, 0); } void CGameContext::ConGoDown(IConsole::IResult *pResult, void *pUserData) @@ -37,9 +37,9 @@ void CGameContext::ConGoDown(IConsole::IResult *pResult, void *pUserData) CGameContext *pSelf = (CGameContext *)pUserData; int Tiles = pResult->NumArguments() == 1 ? pResult->GetInteger(0) : 1; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - pSelf->MoveCharacter(pResult->m_ClientID, 0, Tiles); + pSelf->MoveCharacter(pResult->m_ClientId, 0, Tiles); } void CGameContext::ConGoUp(IConsole::IResult *pResult, void *pUserData) @@ -47,45 +47,44 @@ void CGameContext::ConGoUp(IConsole::IResult *pResult, void *pUserData) CGameContext *pSelf = (CGameContext *)pUserData; int Tiles = pResult->NumArguments() == 1 ? pResult->GetInteger(0) : 1; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - pSelf->MoveCharacter(pResult->m_ClientID, 0, -1 * Tiles); + pSelf->MoveCharacter(pResult->m_ClientId, 0, -1 * Tiles); } void CGameContext::ConMove(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - pSelf->MoveCharacter(pResult->m_ClientID, pResult->GetInteger(0), + pSelf->MoveCharacter(pResult->m_ClientId, pResult->GetInteger(0), pResult->GetInteger(1)); } void CGameContext::ConMoveRaw(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - pSelf->MoveCharacter(pResult->m_ClientID, pResult->GetInteger(0), + pSelf->MoveCharacter(pResult->m_ClientId, pResult->GetInteger(0), pResult->GetInteger(1), true); } -void CGameContext::MoveCharacter(int ClientID, int X, int Y, bool Raw) +void CGameContext::MoveCharacter(int ClientId, int X, int Y, bool Raw) { - CCharacter *pChr = GetPlayerChar(ClientID); + CCharacter *pChr = GetPlayerChar(ClientId); if(!pChr) return; - pChr->Core()->m_Pos.x += ((Raw) ? 1 : 32) * X; - pChr->Core()->m_Pos.y += ((Raw) ? 1 : 32) * Y; + pChr->Move(vec2((Raw ? 1 : 32) * X, (Raw ? 1 : 32) * Y)); pChr->m_DDRaceState = DDRACE_CHEAT; } void CGameContext::ConKillPlayer(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; int Victim = pResult->GetVictim(); @@ -95,8 +94,8 @@ void CGameContext::ConKillPlayer(IConsole::IResult *pResult, void *pUserData) char aBuf[512]; str_format(aBuf, sizeof(aBuf), "%s was killed by %s", pSelf->Server()->ClientName(Victim), - pSelf->Server()->ClientName(pResult->m_ClientID)); - pSelf->SendChat(-1, CGameContext::CHAT_ALL, aBuf); + pSelf->Server()->ClientName(pResult->m_ClientId)); + pSelf->SendChat(-1, TEAM_ALL, aBuf); } } @@ -115,9 +114,9 @@ void CGameContext::ConUnNinja(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConEndlessHook(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) { pChr->SetEndlessHook(true); @@ -127,9 +126,9 @@ void CGameContext::ConEndlessHook(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConUnEndlessHook(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) { pChr->SetEndlessHook(false); @@ -139,9 +138,9 @@ void CGameContext::ConUnEndlessHook(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConSuper(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr && !pChr->IsSuper()) { pChr->SetSuper(true); @@ -152,9 +151,9 @@ void CGameContext::ConSuper(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConUnSuper(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr && pChr->IsSuper()) { pChr->SetSuper(false); @@ -164,9 +163,9 @@ void CGameContext::ConUnSuper(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConSolo(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) pChr->SetSolo(true); } @@ -174,9 +173,9 @@ void CGameContext::ConSolo(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConUnSolo(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) pChr->SetSolo(false); } @@ -184,9 +183,9 @@ void CGameContext::ConUnSolo(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConFreeze(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) pChr->Freeze(); } @@ -194,9 +193,9 @@ void CGameContext::ConFreeze(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConUnFreeze(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) pChr->UnFreeze(); } @@ -204,9 +203,9 @@ void CGameContext::ConUnFreeze(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConDeep(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) pChr->SetDeepFrozen(true); } @@ -214,19 +213,22 @@ void CGameContext::ConDeep(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConUnDeep(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) + { pChr->SetDeepFrozen(false); + pChr->UnFreeze(); + } } void CGameContext::ConLiveFreeze(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) pChr->SetLiveFrozen(true); } @@ -234,9 +236,9 @@ void CGameContext::ConLiveFreeze(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConUnLiveFreeze(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) pChr->SetLiveFrozen(false); } @@ -262,11 +264,19 @@ void CGameContext::ConLaser(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConJetpack(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) pChr->SetJetpack(true); } +void CGameContext::ConSetJumps(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); + if(pChr) + pChr->SetJumps(pResult->GetInteger(0)); +} + void CGameContext::ConWeapons(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; @@ -294,7 +304,7 @@ void CGameContext::ConUnLaser(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConUnJetpack(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) pChr->SetJetpack(false); } @@ -321,7 +331,7 @@ void CGameContext::ModifyWeapons(IConsole::IResult *pResult, void *pUserData, int Weapon, bool Remove) { CGameContext *pSelf = (CGameContext *)pUserData; - CCharacter *pChr = GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = GetPlayerChar(pResult->m_ClientId); if(!pChr) return; @@ -348,7 +358,7 @@ void CGameContext::ModifyWeapons(IConsole::IResult *pResult, void *pUserData, void CGameContext::Teleport(CCharacter *pChr, vec2 Pos) { - pChr->Core()->m_Pos = Pos; + pChr->SetPosition(Pos); pChr->m_Pos = Pos; pChr->m_PrevPos = Pos; pChr->m_DDRaceState = DDRACE_CHEAT; @@ -359,13 +369,13 @@ void CGameContext::ConToTeleporter(IConsole::IResult *pResult, void *pUserData) CGameContext *pSelf = (CGameContext *)pUserData; unsigned int TeleTo = pResult->GetInteger(0); - if(!pSelf->m_pController->m_TeleOuts[TeleTo - 1].empty()) + if(!pSelf->Collision()->TeleOuts(TeleTo - 1).empty()) { - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) { - int TeleOut = pSelf->m_World.m_Core.RandomOr0(pSelf->m_pController->m_TeleOuts[TeleTo - 1].size()); - pSelf->Teleport(pChr, pSelf->m_pController->m_TeleOuts[TeleTo - 1][TeleOut]); + int TeleOut = pSelf->m_World.m_Core.RandomOr0(pSelf->Collision()->TeleOuts(TeleTo - 1).size()); + pSelf->Teleport(pChr, pSelf->Collision()->TeleOuts(TeleTo - 1)[TeleOut]); } } } @@ -375,13 +385,13 @@ void CGameContext::ConToCheckTeleporter(IConsole::IResult *pResult, void *pUserD CGameContext *pSelf = (CGameContext *)pUserData; unsigned int TeleTo = pResult->GetInteger(0); - if(!pSelf->m_pController->m_TeleCheckOuts[TeleTo - 1].empty()) + if(!pSelf->Collision()->TeleCheckOuts(TeleTo - 1).empty()) { - CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientID); + CCharacter *pChr = pSelf->GetPlayerChar(pResult->m_ClientId); if(pChr) { - int TeleOut = pSelf->m_World.m_Core.RandomOr0(pSelf->m_pController->m_TeleCheckOuts[TeleTo - 1].size()); - pSelf->Teleport(pChr, pSelf->m_pController->m_TeleCheckOuts[TeleTo - 1][TeleOut]); + int TeleOut = pSelf->m_World.m_Core.RandomOr0(pSelf->Collision()->TeleCheckOuts(TeleTo - 1).size()); + pSelf->Teleport(pChr, pSelf->Collision()->TeleCheckOuts(TeleTo - 1)[TeleOut]); pChr->m_TeleCheckpoint = TeleTo; } } @@ -390,18 +400,18 @@ void CGameContext::ConToCheckTeleporter(IConsole::IResult *pResult, void *pUserD void CGameContext::ConTeleport(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - int Tele = pResult->NumArguments() == 2 ? pResult->GetInteger(0) : pResult->m_ClientID; - int TeleTo = pResult->NumArguments() ? pResult->GetInteger(pResult->NumArguments() - 1) : pResult->m_ClientID; - int AuthLevel = pSelf->Server()->GetAuthedState(pResult->m_ClientID); + int Tele = pResult->NumArguments() == 2 ? pResult->GetInteger(0) : pResult->m_ClientId; + int TeleTo = pResult->NumArguments() ? pResult->GetInteger(pResult->NumArguments() - 1) : pResult->m_ClientId; + int AuthLevel = pSelf->Server()->GetAuthedState(pResult->m_ClientId); - if(Tele != pResult->m_ClientID && AuthLevel < g_Config.m_SvTeleOthersAuthLevel) + if(Tele != pResult->m_ClientId && AuthLevel < g_Config.m_SvTeleOthersAuthLevel) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "tele", "you aren't allowed to tele others"); return; } CCharacter *pChr = pSelf->GetPlayerChar(Tele); - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(pChr && pPlayer && pSelf->GetPlayerChar(TeleTo)) { @@ -411,17 +421,18 @@ void CGameContext::ConTeleport(IConsole::IResult *pResult, void *pUserData) Pos += vec2(pChr->Core()->m_Input.m_TargetX, pChr->Core()->m_Input.m_TargetY); } pSelf->Teleport(pChr, Pos); + pChr->ResetJumps(); pChr->UnFreeze(); - pChr->Core()->m_Vel = vec2(0, 0); + pChr->SetVelocity(vec2(0, 0)); } } void CGameContext::ConKill(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; if(!pPlayer || (pPlayer->m_LastKill && pPlayer->m_LastKill + pSelf->Server()->TickSpeed() * g_Config.m_SvKillDelay > pSelf->Server()->Tick())) return; @@ -472,7 +483,7 @@ bool CGameContext::TryVoteMute(const NETADDR *pAddr, int Secs, const char *pReas return false; } -void CGameContext::VoteMute(const NETADDR *pAddr, int Secs, const char *pReason, const char *pDisplayName, int AuthedID) +void CGameContext::VoteMute(const NETADDR *pAddr, int Secs, const char *pReason, const char *pDisplayName, int AuthedId) { if(!TryVoteMute(pAddr, Secs, pReason)) return; @@ -482,14 +493,14 @@ void CGameContext::VoteMute(const NETADDR *pAddr, int Secs, const char *pReason, char aBuf[128]; if(pReason[0]) str_format(aBuf, sizeof(aBuf), "'%s' banned '%s' for %d seconds from voting (%s)", - Server()->ClientName(AuthedID), pDisplayName, Secs, pReason); + Server()->ClientName(AuthedId), pDisplayName, Secs, pReason); else str_format(aBuf, sizeof(aBuf), "'%s' banned '%s' for %d seconds from voting", - Server()->ClientName(AuthedID), pDisplayName, Secs); - SendChat(-1, CHAT_ALL, aBuf); + Server()->ClientName(AuthedId), pDisplayName, Secs); + SendChat(-1, TEAM_ALL, aBuf); } -bool CGameContext::VoteUnmute(const NETADDR *pAddr, const char *pDisplayName, int AuthedID) +bool CGameContext::VoteUnmute(const NETADDR *pAddr, const char *pDisplayName, int AuthedId) { for(int i = 0; i < m_NumVoteMutes; i++) { @@ -501,7 +512,7 @@ bool CGameContext::VoteUnmute(const NETADDR *pAddr, const char *pDisplayName, in { char aBuf[128]; str_format(aBuf, sizeof(aBuf), "'%s' unbanned '%s' from voting.", - Server()->ClientName(AuthedID), pDisplayName); + Server()->ClientName(AuthedId), pDisplayName); Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "voteunmute", aBuf); } return true; @@ -559,7 +570,7 @@ void CGameContext::Mute(const NETADDR *pAddr, int Secs, const char *pDisplayName str_format(aBuf, sizeof(aBuf), "'%s' has been muted for %d seconds (%s)", pDisplayName, Secs, pReason); else str_format(aBuf, sizeof(aBuf), "'%s' has been muted for %d seconds", pDisplayName, Secs); - SendChat(-1, CHAT_ALL, aBuf); + SendChat(-1, TEAM_ALL, aBuf); } void CGameContext::ConVoteMute(IConsole::IResult *pResult, void *pUserData) @@ -578,7 +589,7 @@ void CGameContext::ConVoteMute(IConsole::IResult *pResult, void *pUserData) int Seconds = clamp(pResult->GetInteger(1), 1, 86400); const char *pReason = pResult->NumArguments() > 2 ? pResult->GetString(2) : ""; - pSelf->VoteMute(&Addr, Seconds, pReason, pSelf->Server()->ClientName(Victim), pResult->m_ClientID); + pSelf->VoteMute(&Addr, Seconds, pReason, pSelf->Server()->ClientName(Victim), pResult->m_ClientId); } void CGameContext::ConVoteUnmute(IConsole::IResult *pResult, void *pUserData) @@ -595,12 +606,12 @@ void CGameContext::ConVoteUnmute(IConsole::IResult *pResult, void *pUserData) NETADDR Addr; pSelf->Server()->GetClientAddr(Victim, &Addr); - bool Found = pSelf->VoteUnmute(&Addr, pSelf->Server()->ClientName(Victim), pResult->m_ClientID); + bool Found = pSelf->VoteUnmute(&Addr, pSelf->Server()->ClientName(Victim), pResult->m_ClientId); if(Found) { char aBuf[128]; str_format(aBuf, sizeof(aBuf), "'%s' unbanned '%s' from voting.", - pSelf->Server()->ClientName(pResult->m_ClientID), pSelf->Server()->ClientName(Victim)); + pSelf->Server()->ClientName(pResult->m_ClientId), pSelf->Server()->ClientName(Victim)); pSelf->SendChat(-1, 0, aBuf); } } @@ -641,7 +652,7 @@ void CGameContext::ConMute(IConsole::IResult *pResult, void *pUserData) } // mute through client id -void CGameContext::ConMuteID(IConsole::IResult *pResult, void *pUserData) +void CGameContext::ConMuteId(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; int Victim = pResult->GetVictim(); @@ -662,7 +673,7 @@ void CGameContext::ConMuteID(IConsole::IResult *pResult, void *pUserData) } // mute through ip, arguments reversed to workaround parsing -void CGameContext::ConMuteIP(IConsole::IResult *pResult, void *pUserData) +void CGameContext::ConMuteIp(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; NETADDR Addr; @@ -695,7 +706,7 @@ void CGameContext::ConUnmute(IConsole::IResult *pResult, void *pUserData) } // unmute by player id -void CGameContext::ConUnmuteID(IConsole::IResult *pResult, void *pUserData) +void CGameContext::ConUnmuteId(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; int Victim = pResult->GetVictim(); @@ -752,24 +763,24 @@ void CGameContext::ConMutes(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConModerate(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - if(!CheckClientID(pResult->m_ClientID)) + if(!CheckClientId(pResult->m_ClientId)) return; bool HadModerator = pSelf->PlayerModerating(); - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; pPlayer->m_Moderating = !pPlayer->m_Moderating; if(!HadModerator && pPlayer->m_Moderating) - pSelf->SendChat(-1, CHAT_ALL, "Server kick/spec votes will now be actively moderated.", 0); + pSelf->SendChat(-1, TEAM_ALL, "Server kick/spec votes will now be actively moderated.", 0); if(!pSelf->PlayerModerating()) - pSelf->SendChat(-1, CHAT_ALL, "Server kick/spec votes are no longer actively moderated.", 0); + pSelf->SendChat(-1, TEAM_ALL, "Server kick/spec votes are no longer actively moderated.", 0); if(pPlayer->m_Moderating) - pSelf->SendChatTarget(pResult->m_ClientID, "Active moderator mode enabled for you."); + pSelf->SendChatTarget(pResult->m_ClientId, "Active moderator mode enabled for you."); else - pSelf->SendChatTarget(pResult->m_ClientID, "Active moderator mode disabled for you."); + pSelf->SendChatTarget(pResult->m_ClientId, "Active moderator mode disabled for you."); } void CGameContext::ConSetDDRTeam(IConsole::IResult *pResult, void *pUserData) @@ -819,7 +830,7 @@ void CGameContext::ConFreezeHammer(IConsole::IResult *pResult, void *pUserData) char aBuf[128]; str_format(aBuf, sizeof(aBuf), "'%s' got freeze hammer!", pSelf->Server()->ClientName(Victim)); - pSelf->SendChat(-1, CHAT_ALL, aBuf); + pSelf->SendChat(-1, TEAM_ALL, aBuf); pChr->m_FreezeHammer = true; } @@ -837,7 +848,7 @@ void CGameContext::ConUnFreezeHammer(IConsole::IResult *pResult, void *pUserData char aBuf[128]; str_format(aBuf, sizeof(aBuf), "'%s' lost freeze hammer!", pSelf->Server()->ClientName(Victim)); - pSelf->SendChat(-1, CHAT_ALL, aBuf); + pSelf->SendChat(-1, TEAM_ALL, aBuf); pChr->m_FreezeHammer = false; } @@ -845,28 +856,28 @@ void CGameContext::ConVoteNo(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - pSelf->ForceVote(pResult->m_ClientID, false); + pSelf->ForceVote(pResult->m_ClientId, false); } void CGameContext::ConDrySave(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientID]; + CPlayer *pPlayer = pSelf->m_apPlayers[pResult->m_ClientId]; - if(!pPlayer || pSelf->Server()->GetAuthedState(pResult->m_ClientID) != AUTHED_ADMIN) + if(!pPlayer || pSelf->Server()->GetAuthedState(pResult->m_ClientId) != AUTHED_ADMIN) return; CSaveTeam SavedTeam; - int Team = pSelf->GetDDRaceTeam(pResult->m_ClientID); - int Result = SavedTeam.Save(pSelf, Team, true); - if(CSaveTeam::HandleSaveError(Result, pResult->m_ClientID, pSelf)) + int Team = pSelf->GetDDRaceTeam(pResult->m_ClientId); + ESaveResult Result = SavedTeam.Save(pSelf, Team, true); + if(CSaveTeam::HandleSaveError(Result, pResult->m_ClientId, pSelf)) return; char aTimestamp[32]; str_timestamp(aTimestamp, sizeof(aTimestamp)); char aBuf[64]; - str_format(aBuf, sizeof(aBuf), "%s_%s_%s.save", pSelf->Server()->GetMapName(), aTimestamp, pSelf->Server()->GetAuthName(pResult->m_ClientID)); + str_format(aBuf, sizeof(aBuf), "%s_%s_%s.save", pSelf->Server()->GetMapName(), aTimestamp, pSelf->Server()->GetAuthName(pResult->m_ClientId)); IOHANDLE File = pSelf->Storage()->OpenFile(aBuf, IOFLAG_WRITE, IStorage::TYPE_ALL); if(!File) return; @@ -876,6 +887,18 @@ void CGameContext::ConDrySave(IConsole::IResult *pResult, void *pUserData) io_close(File); } +void CGameContext::ConReloadCensorlist(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + pSelf->ReadCensorList(); +} + +void CGameContext::ConReloadAnnouncement(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + pSelf->Server()->ReadAnnouncementsFile(g_Config.m_SvAnnouncementFileName); +} + void CGameContext::ConDumpAntibot(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; @@ -921,18 +944,18 @@ void CGameContext::ConDumpLog(IConsole::IResult *pResult, void *pUserData) } } -void CGameContext::LogEvent(const char *Description, int ClientID) +void CGameContext::LogEvent(const char *Description, int ClientId) { CLog *pNewEntry = &m_aLogs[m_LatestLog]; m_LatestLog = (m_LatestLog + 1) % MAX_LOGS; pNewEntry->m_Timestamp = time_get(); str_copy(pNewEntry->m_aDescription, Description); - pNewEntry->m_FromServer = ClientID < 0; + pNewEntry->m_FromServer = ClientId < 0; if(!pNewEntry->m_FromServer) { - pNewEntry->m_ClientVersion = Server()->GetClientVersion(ClientID); - Server()->GetClientAddr(ClientID, pNewEntry->m_aClientAddrStr, sizeof(pNewEntry->m_aClientAddrStr)); - str_copy(pNewEntry->m_aClientName, Server()->ClientName(ClientID)); + pNewEntry->m_ClientVersion = Server()->GetClientVersion(ClientId); + Server()->GetClientAddr(ClientId, pNewEntry->m_aClientAddrStr, sizeof(pNewEntry->m_aClientAddrStr)); + str_copy(pNewEntry->m_aClientName, Server()->ClientName(ClientId)); } } diff --git a/src/game/server/entities/character.cpp b/src/game/server/entities/character.cpp index 1c0c5a1ae4..44363e9153 100644 --- a/src/game/server/entities/character.cpp +++ b/src/game/server/entities/character.cpp @@ -28,7 +28,8 @@ CCharacter::CCharacter(CGameWorld *pWorld, CNetObj_PlayerInput LastInput) : { m_Health = 0; m_Armor = 0; - m_StrongWeakID = 0; + m_TriggeredEvents7 = 0; + m_StrongWeakId = 0; m_Input = LastInput; // never initialize both to zero @@ -47,6 +48,7 @@ CCharacter::CCharacter(CGameWorld *pWorld, CNetObj_PlayerInput LastInput) : void CCharacter::Reset() { + StopRecording(); Destroy(); } @@ -72,14 +74,14 @@ bool CCharacter::Spawn(CPlayer *pPlayer, vec2 Pos) m_NumInputs = 0; m_SpawnTick = Server()->Tick(); m_WeaponChangeTick = Server()->Tick(); - Antibot()->OnSpawn(m_pPlayer->GetCID()); + Antibot()->OnSpawn(m_pPlayer->GetCid()); m_Core.Reset(); m_Core.Init(&GameServer()->m_World.m_Core, Collision()); m_Core.m_ActiveWeapon = WEAPON_GUN; m_Core.m_Pos = m_Pos; - m_Core.m_Id = m_pPlayer->GetCID(); - GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCID()] = &m_Core; + m_Core.m_Id = m_pPlayer->GetCid(); + GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCid()] = &m_Core; m_ReckoningTick = 0; m_SendCore = CCharacterCore(); @@ -96,16 +98,46 @@ bool CCharacter::Spawn(CPlayer *pPlayer, vec2 Pos) m_TuneZoneOld = -1; // no zone leave msg on spawn m_NeededFaketuning = 0; // reset fake tunings on respawn and send the client SendZoneMsgs(); // we want a entermessage also on spawn - GameServer()->SendTuningParams(m_pPlayer->GetCID(), m_TuneZone); + GameServer()->SendTuningParams(m_pPlayer->GetCid(), m_TuneZone); - Server()->StartRecord(m_pPlayer->GetCID()); + TrySetRescue(RESCUEMODE_MANUAL); + Server()->StartRecord(m_pPlayer->GetCid()); + + int Team = GameServer()->m_aTeamMapping[m_pPlayer->GetCid()]; + + if(Team != -1) + { + GameServer()->m_pController->Teams().SetForceCharacterTeam(m_pPlayer->GetCid(), Team); + GameServer()->m_aTeamMapping[m_pPlayer->GetCid()] = -1; + + if(GameServer()->m_apSavedTeams[Team]) + { + GameServer()->m_apSavedTeams[Team]->Load(GameServer(), Team, true, true); + delete GameServer()->m_apSavedTeams[Team]; + GameServer()->m_apSavedTeams[Team] = nullptr; + } + + if(GameServer()->m_apSavedTees[m_pPlayer->GetCid()]) + { + GameServer()->m_apSavedTees[m_pPlayer->GetCid()]->Load(m_pPlayer->GetCharacter(), Team); + delete GameServer()->m_apSavedTees[m_pPlayer->GetCid()]; + GameServer()->m_apSavedTees[m_pPlayer->GetCid()] = nullptr; + } + + if(GameServer()->m_apSavedTeleTees[m_pPlayer->GetCid()]) + { + m_pPlayer->m_LastTeleTee = *GameServer()->m_apSavedTeleTees[m_pPlayer->GetCid()]; + delete GameServer()->m_apSavedTeleTees[m_pPlayer->GetCid()]; + GameServer()->m_apSavedTeleTees[m_pPlayer->GetCid()] = nullptr; + } + } return true; } void CCharacter::Destroy() { - GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCID()] = 0; + GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCid()] = 0; m_Alive = false; SetSolo(false); } @@ -129,10 +161,15 @@ void CCharacter::SetJetpack(bool Active) m_Core.m_Jetpack = Active; } +void CCharacter::SetJumps(int Jumps) +{ + m_Core.m_Jumps = Jumps; +} + void CCharacter::SetSolo(bool Solo) { m_Core.m_Solo = Solo; - Teams()->m_Core.SetSolo(m_pPlayer->GetCID(), Solo); + Teams()->m_Core.SetSolo(m_pPlayer->GetCid(), Solo); } void CCharacter::SetSuper(bool Super) @@ -141,12 +178,12 @@ void CCharacter::SetSuper(bool Super) if(Super) { m_TeamBeforeSuper = Team(); - Teams()->SetCharacterTeam(GetPlayer()->GetCID(), TEAM_SUPER); + Teams()->SetCharacterTeam(GetPlayer()->GetCid(), TEAM_SUPER); m_DDRaceState = DDRACE_CHEAT; } else { - Teams()->SetForceCharacterTeam(GetPlayer()->GetCID(), m_TeamBeforeSuper); + Teams()->SetForceCharacterTeam(GetPlayer()->GetCid(), m_TeamBeforeSuper); } } @@ -204,12 +241,8 @@ void CCharacter::HandleJetpack() { if(m_Core.m_Jetpack) { - float Strength; - if(!m_TuneZone) - Strength = Tuning()->m_JetpackStrength; - else - Strength = TuningList()[m_TuneZone].m_JetpackStrength; - TakeDamage(Direction * -1.0f * (Strength / 100.0f / 6.11f), 0, m_pPlayer->GetCID(), m_Core.m_ActiveWeapon); + float Strength = GetTuning(m_TuneZone)->m_JetpackStrength; + TakeDamage(Direction * -1.0f * (Strength / 100.0f / 6.11f), 0, m_pPlayer->GetCid(), m_Core.m_ActiveWeapon); } } } @@ -234,7 +267,7 @@ void CCharacter::HandleNinja() GameServer()->CreateDamageInd(m_Pos, 0, NinjaTime / Server()->TickSpeed(), TeamMask() & GameServer()->ClientsMaskExcludeClientVersionAndHigher(VERSION_DDNET_NEW_HUD)); } - m_Armor = clamp(10 - (NinjaTime / 15), 0, 10); + GameServer()->m_pController->SetArmorProgress(this, NinjaTime); // force ninja Weapon SetWeapon(WEAPON_NINJA); @@ -252,17 +285,14 @@ void CCharacter::HandleNinja() // Set velocity m_Core.m_Vel = m_Core.m_Ninja.m_ActivationDir * g_pData->m_Weapons.m_Ninja.m_Velocity; vec2 OldPos = m_Pos; - vec2 GroundElasticity; - - if(!m_TuneZone) - GroundElasticity = vec2(Tuning()->m_GroundElasticityX, Tuning()->m_GroundElasticityY); - else - GroundElasticity = vec2(TuningList()[m_TuneZone].m_GroundElasticityX, TuningList()[m_TuneZone].m_GroundElasticityY); + vec2 GroundElasticity = vec2( + GetTuning(m_TuneZone)->m_GroundElasticityX, + GetTuning(m_TuneZone)->m_GroundElasticityY); Collision()->MoveBox(&m_Core.m_Pos, &m_Core.m_Vel, vec2(GetProximityRadius(), GetProximityRadius()), GroundElasticity); // reset velocity so the client doesn't predict stuff - m_Core.m_Vel = vec2(0.f, 0.f); + ResetVelocity(); // check if we Hit anything along the way { @@ -271,7 +301,7 @@ void CCharacter::HandleNinja() int Num = GameServer()->m_World.FindEntities(OldPos, Radius, apEnts, MAX_CLIENTS, CGameWorld::ENTTYPE_CHARACTER); // check that we're not in solo part - if(Teams()->m_Core.GetSolo(m_pPlayer->GetCID())) + if(Teams()->m_Core.GetSolo(m_pPlayer->GetCid())) return; for(int i = 0; i < Num; ++i) @@ -285,17 +315,17 @@ void CCharacter::HandleNinja() continue; // Don't hit players in solo parts - if(Teams()->m_Core.GetSolo(pChr->m_pPlayer->GetCID())) + if(Teams()->m_Core.GetSolo(pChr->m_pPlayer->GetCid())) return; // make sure we haven't Hit this object before - bool bAlreadyHit = false; + bool AlreadyHit = false; for(int j = 0; j < m_NumObjectsHit; j++) { if(m_apHitObjects[j] == pChr) - bAlreadyHit = true; + AlreadyHit = true; } - if(bAlreadyHit) + if(AlreadyHit) continue; // check so we are sufficiently close @@ -308,7 +338,7 @@ void CCharacter::HandleNinja() if(m_NumObjectsHit < 10) m_apHitObjects[m_NumObjectsHit++] = pChr; - pChr->TakeDamage(vec2(0, -10.0f), g_pData->m_Weapons.m_Ninja.m_pBase->m_Damage, m_pPlayer->GetCID(), WEAPON_NINJA); + pChr->TakeDamage(vec2(0, -10.0f), g_pData->m_Weapons.m_Ninja.m_pBase->m_Damage, m_pPlayer->GetCid(), WEAPON_NINJA); } } @@ -379,7 +409,7 @@ void CCharacter::FireWeapon() { if(m_LatestInput.m_Fire & 1) { - Antibot()->OnHammerFireReloading(m_pPlayer->GetCID()); + Antibot()->OnHammerFireReloading(m_pPlayer->GetCid()); } return; } @@ -419,7 +449,7 @@ void CCharacter::FireWeapon() if(m_PainSoundTimer <= 0 && !(m_LatestPrevInput.m_Fire & 1)) { m_PainSoundTimer = 1 * Server()->TickSpeed(); - GameServer()->CreateSound(m_Pos, SOUND_PLAYER_PAIN_LONG, TeamMask()); + GameServer()->CreateSound(m_Pos, SOUND_PLAYER_PAIN_LONG, TeamMask()); // NOLINT(clang-analyzer-unix.Malloc) } return; } @@ -436,9 +466,9 @@ void CCharacter::FireWeapon() { // reset objects Hit m_NumObjectsHit = 0; - GameServer()->CreateSound(m_Pos, SOUND_HAMMER_FIRE, TeamMask()); + GameServer()->CreateSound(m_Pos, SOUND_HAMMER_FIRE, TeamMask()); // NOLINT(clang-analyzer-unix.Malloc) - Antibot()->OnHammerFire(m_pPlayer->GetCID()); + Antibot()->OnHammerFire(m_pPlayer->GetCid()); if(m_Core.m_HammerHitDisabled) break; @@ -453,7 +483,7 @@ void CCharacter::FireWeapon() auto *pTarget = static_cast(apEnts[i]); //if ((pTarget == this) || Collision()->IntersectLine(ProjStartPos, pTarget->m_Pos, NULL, NULL)) - if((pTarget == this || (pTarget->IsAlive() && !CanCollide(pTarget->GetPlayer()->GetCID())))) + if((pTarget == this || (pTarget->IsAlive() && !CanCollide(pTarget->GetPlayer()->GetCid())))) continue; // set his velocity to fast upward (for now) @@ -468,23 +498,19 @@ void CCharacter::FireWeapon() else Dir = vec2(0.f, -1.f); - float Strength; - if(!m_TuneZone) - Strength = Tuning()->m_HammerStrength; - else - Strength = TuningList()[m_TuneZone].m_HammerStrength; + float Strength = GetTuning(m_TuneZone)->m_HammerStrength; vec2 Temp = pTarget->m_Core.m_Vel + normalize(Dir + vec2(0.f, -1.1f)) * 10.0f; Temp = ClampVel(pTarget->m_MoveRestrictions, Temp); Temp -= pTarget->m_Core.m_Vel; pTarget->TakeDamage((vec2(0.f, -1.0f) + Temp) * Strength, g_pData->m_Weapons.m_Hammer.m_pBase->m_Damage, - m_pPlayer->GetCID(), m_Core.m_ActiveWeapon); + m_pPlayer->GetCid(), m_Core.m_ActiveWeapon); pTarget->UnFreeze(); if(m_FreezeHammer) pTarget->Freeze(); - Antibot()->OnHammerHit(m_pPlayer->GetCID(), pTarget->GetPlayer()->GetCID()); + Antibot()->OnHammerHit(m_pPlayer->GetCid(), pTarget->GetPlayer()->GetCid()); Hits++; } @@ -492,11 +518,7 @@ void CCharacter::FireWeapon() // if we Hit anything, we have to wait for the reload if(Hits) { - float FireDelay; - if(!m_TuneZone) - FireDelay = Tuning()->m_HammerHitFireDelay; - else - FireDelay = TuningList()[m_TuneZone].m_HammerHitFireDelay; + float FireDelay = GetTuning(m_TuneZone)->m_HammerHitFireDelay; m_ReloadTimer = FireDelay * Server()->TickSpeed() / 1000; } } @@ -506,16 +528,12 @@ void CCharacter::FireWeapon() { if(!m_Core.m_Jetpack || !m_pPlayer->m_NinjaJetpack || m_Core.m_HasTelegunGun) { - int Lifetime; - if(!m_TuneZone) - Lifetime = (int)(Server()->TickSpeed() * Tuning()->m_GunLifetime); - else - Lifetime = (int)(Server()->TickSpeed() * TuningList()[m_TuneZone].m_GunLifetime); + int Lifetime = (int)(Server()->TickSpeed() * GetTuning(m_TuneZone)->m_GunLifetime); new CProjectile( GameWorld(), WEAPON_GUN, //Type - m_pPlayer->GetCID(), //Owner + m_pPlayer->GetCid(), //Owner ProjStartPos, //Pos Direction, //Dir Lifetime, //Span @@ -532,29 +550,21 @@ void CCharacter::FireWeapon() case WEAPON_SHOTGUN: { - float LaserReach; - if(!m_TuneZone) - LaserReach = Tuning()->m_LaserReach; - else - LaserReach = TuningList()[m_TuneZone].m_LaserReach; + float LaserReach = GetTuning(m_TuneZone)->m_LaserReach; - new CLaser(&GameServer()->m_World, m_Pos, Direction, LaserReach, m_pPlayer->GetCID(), WEAPON_SHOTGUN); + new CLaser(&GameServer()->m_World, m_Pos, Direction, LaserReach, m_pPlayer->GetCid(), WEAPON_SHOTGUN); GameServer()->CreateSound(m_Pos, SOUND_SHOTGUN_FIRE, TeamMask()); // NOLINT(clang-analyzer-unix.Malloc) } break; case WEAPON_GRENADE: { - int Lifetime; - if(!m_TuneZone) - Lifetime = (int)(Server()->TickSpeed() * Tuning()->m_GrenadeLifetime); - else - Lifetime = (int)(Server()->TickSpeed() * TuningList()[m_TuneZone].m_GrenadeLifetime); + int Lifetime = (int)(Server()->TickSpeed() * GetTuning(m_TuneZone)->m_GrenadeLifetime); new CProjectile( GameWorld(), WEAPON_GRENADE, //Type - m_pPlayer->GetCID(), //Owner + m_pPlayer->GetCid(), //Owner ProjStartPos, //Pos Direction, //Dir Lifetime, //Span @@ -564,19 +574,15 @@ void CCharacter::FireWeapon() MouseTarget // MouseTarget ); - GameServer()->CreateSound(m_Pos, SOUND_GRENADE_FIRE, TeamMask()); + GameServer()->CreateSound(m_Pos, SOUND_GRENADE_FIRE, TeamMask()); // NOLINT(clang-analyzer-unix.Malloc) } break; case WEAPON_LASER: { - float LaserReach; - if(!m_TuneZone) - LaserReach = Tuning()->m_LaserReach; - else - LaserReach = TuningList()[m_TuneZone].m_LaserReach; + float LaserReach = GetTuning(m_TuneZone)->m_LaserReach; - new CLaser(GameWorld(), m_Pos, Direction, LaserReach, m_pPlayer->GetCID(), WEAPON_LASER); + new CLaser(GameWorld(), m_Pos, Direction, LaserReach, m_pPlayer->GetCid(), WEAPON_LASER); GameServer()->CreateSound(m_Pos, SOUND_LASER_FIRE, TeamMask()); // NOLINT(clang-analyzer-unix.Malloc) } break; @@ -590,7 +596,7 @@ void CCharacter::FireWeapon() m_Core.m_Ninja.m_CurrentMoveTime = g_pData->m_Weapons.m_Ninja.m_Movetime * Server()->TickSpeed() / 1000; m_Core.m_Ninja.m_OldVelAmount = length(m_Core.m_Vel); - GameServer()->CreateSound(m_Pos, SOUND_NINJA_FIRE, TeamMask()); + GameServer()->CreateSound(m_Pos, SOUND_NINJA_FIRE, TeamMask()); // NOLINT(clang-analyzer-unix.Malloc) } break; } @@ -600,10 +606,7 @@ void CCharacter::FireWeapon() if(!m_ReloadTimer) { float FireDelay; - if(!m_TuneZone) - Tuning()->Get(38 + m_Core.m_ActiveWeapon, &FireDelay); - else - TuningList()[m_TuneZone].Get(38 + m_Core.m_ActiveWeapon, &FireDelay); + GetTuning(m_TuneZone)->Get(38 + m_Core.m_ActiveWeapon, &FireDelay); m_ReloadTimer = FireDelay * Server()->TickSpeed() / 1000; } } @@ -686,7 +689,7 @@ void CCharacter::OnDirectInput(CNetObj_PlayerInput *pNewInput) if(m_LatestInput.m_TargetX == 0 && m_LatestInput.m_TargetY == 0) m_LatestInput.m_TargetY = -1; - Antibot()->OnDirectInput(m_pPlayer->GetCID()); + Antibot()->OnDirectInput(m_pPlayer->GetCid()); if(m_NumInputs > 1 && m_pPlayer->GetTeam() != TEAM_SPECTATORS) { @@ -698,11 +701,16 @@ void CCharacter::OnDirectInput(CNetObj_PlayerInput *pNewInput) mem_copy(&m_LatestPrevInput, &m_LatestInput, sizeof(m_LatestInput)); } -void CCharacter::ResetHook() +void CCharacter::ReleaseHook() { m_Core.SetHookedPlayer(-1); m_Core.m_HookState = HOOK_RETRACTED; m_Core.m_TriggeredEvents |= COREEVENT_HOOK_RETRACT; +} + +void CCharacter::ResetHook() +{ + ReleaseHook(); m_Core.m_HookPos = m_Core.m_Pos; } @@ -724,8 +732,8 @@ void CCharacter::PreTick() // Prevent the player from getting a negative time // The main reason why this can happen is because of time penalty tiles // However, other reasons are hereby also excluded - GameServer()->SendChatTarget(m_pPlayer->GetCID(), "You died of old age"); - Die(m_pPlayer->GetCID(), WEAPON_WORLD); + GameServer()->SendChatTarget(m_pPlayer->GetCid(), "You died of old age"); + Die(m_pPlayer->GetCid(), WEAPON_WORLD); } if(m_Paused) @@ -739,7 +747,7 @@ void CCharacter::PreTick() DDRaceTick(); - Antibot()->OnCharacterTick(m_pPlayer->GetCID()); + Antibot()->OnCharacterTick(m_pPlayer->GetCid()); m_Core.m_Input = m_Input; m_Core.Tick(true, !g_Config.m_SvNoWeakHook); @@ -761,7 +769,7 @@ void CCharacter::Tick() if(!m_PrevInput.m_Hook && m_Input.m_Hook && !(m_Core.m_TriggeredEvents & COREEVENT_HOOK_ATTACH_PLAYER)) { - Antibot()->OnHookAttach(m_pPlayer->GetCID(), false); + Antibot()->OnHookAttach(m_pPlayer->GetCid(), false); } // handle Weapons @@ -774,7 +782,7 @@ void CCharacter::Tick() const int HookedPlayer = m_Core.HookedPlayer(); if(HookedPlayer != -1 && GameServer()->m_apPlayers[HookedPlayer]->GetTeam() != TEAM_SPECTATORS) { - Antibot()->OnHookAttach(m_pPlayer->GetCID(), true); + Antibot()->OnHookAttach(m_pPlayer->GetCid(), true); } } @@ -789,8 +797,8 @@ void CCharacter::TickDeferred() // advance the dummy { CWorldCore TempWorld; - m_ReckoningCore.Init(&TempWorld, Collision(), &Teams()->m_Core, m_pTeleOuts); - m_ReckoningCore.m_Id = m_pPlayer->GetCID(); + m_ReckoningCore.Init(&TempWorld, Collision(), &Teams()->m_Core); + m_ReckoningCore.m_Id = m_pPlayer->GetCid(); m_ReckoningCore.Tick(false); m_ReckoningCore.Move(); m_ReckoningCore.Quantize(); @@ -801,7 +809,7 @@ void CCharacter::TickDeferred() vec2 StartVel = m_Core.m_Vel; bool StuckBefore = Collision()->TestBox(m_Core.m_Pos, CCharacterCore::PhysicalSizeVec2()); - m_Core.m_Id = m_pPlayer->GetCID(); + m_Core.m_Id = m_pPlayer->GetCid(); m_Core.Move(); bool StuckAfterMove = Collision()->TestBox(m_Core.m_Pos, CCharacterCore::PhysicalSizeVec2()); m_Core.Quantize(); @@ -836,26 +844,36 @@ void CCharacter::TickDeferred() { int Events = m_Core.m_TriggeredEvents; - int CID = m_pPlayer->GetCID(); + int CID = m_pPlayer->GetCid(); - CClientMask TeamMask = Teams()->TeamMask(Team(), -1, CID); - // Some sounds are triggered client-side for the acting player + // Some sounds are triggered client-side for the acting player (or for all players on Sixup) // so we need to avoid duplicating them - CClientMask TeamMaskExceptSelf = Teams()->TeamMask(Team(), CID, CID); + CClientMask TeamMaskExceptSelfAndSixup = Teams()->TeamMask(Team(), CID, CID, CGameContext::FLAG_SIX); // Some are triggered client-side but only on Sixup - CClientMask TeamMaskExceptSelfIfSixup = Server()->IsSixup(CID) ? TeamMaskExceptSelf : TeamMask; + CClientMask TeamMaskExceptSixup = Teams()->TeamMask(Team(), -1, CID, CGameContext::FLAG_SIX); if(Events & COREEVENT_GROUND_JUMP) - GameServer()->CreateSound(m_Pos, SOUND_PLAYER_JUMP, TeamMaskExceptSelf); + GameServer()->CreateSound(m_Pos, SOUND_PLAYER_JUMP, TeamMaskExceptSelfAndSixup); if(Events & COREEVENT_HOOK_ATTACH_PLAYER) - GameServer()->CreateSound(m_Pos, SOUND_HOOK_ATTACH_PLAYER, TeamMaskExceptSelfIfSixup); + GameServer()->CreateSound(m_Pos, SOUND_HOOK_ATTACH_PLAYER, TeamMaskExceptSixup); if(Events & COREEVENT_HOOK_ATTACH_GROUND) - GameServer()->CreateSound(m_Pos, SOUND_HOOK_ATTACH_GROUND, TeamMaskExceptSelf); + GameServer()->CreateSound(m_Pos, SOUND_HOOK_ATTACH_GROUND, TeamMaskExceptSelfAndSixup); + + if(Events & COREEVENT_HOOK_HIT_NOHOOK) + GameServer()->CreateSound(m_Pos, SOUND_HOOK_NOATTACH, TeamMaskExceptSelfAndSixup); + if(Events & COREEVENT_GROUND_JUMP) + m_TriggeredEvents7 |= protocol7::COREEVENTFLAG_GROUND_JUMP; + if(Events & COREEVENT_AIR_JUMP) + m_TriggeredEvents7 |= protocol7::COREEVENTFLAG_AIR_JUMP; + if(Events & COREEVENT_HOOK_ATTACH_PLAYER) + m_TriggeredEvents7 |= protocol7::COREEVENTFLAG_HOOK_ATTACH_PLAYER; + if(Events & COREEVENT_HOOK_ATTACH_GROUND) + m_TriggeredEvents7 |= protocol7::COREEVENTFLAG_HOOK_ATTACH_GROUND; if(Events & COREEVENT_HOOK_HIT_NOHOOK) - GameServer()->CreateSound(m_Pos, SOUND_HOOK_NOATTACH, TeamMaskExceptSelf); + m_TriggeredEvents7 |= protocol7::COREEVENTFLAG_HOOK_HIT_NOHOOK; } if(m_pPlayer->GetTeam() == TEAM_SPECTATORS) @@ -873,7 +891,7 @@ void CCharacter::TickDeferred() m_ReckoningCore.Write(&Predicted); m_Core.Write(&Current); - // only allow dead reackoning for a top of 3 seconds + // only allow dead reckoning for a top of 3 seconds if(m_Core.m_Reset || m_ReckoningTick + Server()->TickSpeed() * 3 < Server()->Tick() || mem_comp(&Predicted, &Current, sizeof(CNetObj_Character)) != 0) { m_ReckoningTick = Server()->Tick(); @@ -914,34 +932,38 @@ bool CCharacter::IncreaseArmor(int Amount) return true; } -void CCharacter::Die(int Killer, int Weapon, bool SendKillMsg) +void CCharacter::StopRecording() { - if(Server()->IsRecording(m_pPlayer->GetCID())) + if(Server()->IsRecording(m_pPlayer->GetCid())) { - CPlayerData *pData = GameServer()->Score()->PlayerData(m_pPlayer->GetCID()); + CPlayerData *pData = GameServer()->Score()->PlayerData(m_pPlayer->GetCid()); if(pData->m_RecordStopTick - Server()->Tick() <= Server()->TickSpeed() && pData->m_RecordStopTick != -1) - Server()->SaveDemo(m_pPlayer->GetCID(), pData->m_RecordFinishTime); + Server()->SaveDemo(m_pPlayer->GetCid(), pData->m_RecordFinishTime); else - Server()->StopRecord(m_pPlayer->GetCID()); + Server()->StopRecord(m_pPlayer->GetCid()); pData->m_RecordStopTick = -1; } +} +void CCharacter::Die(int Killer, int Weapon, bool SendKillMsg) +{ + StopRecording(); int ModeSpecial = GameServer()->m_pController->OnCharacterDeath(this, GameServer()->m_apPlayers[Killer], Weapon); char aBuf[256]; str_format(aBuf, sizeof(aBuf), "kill killer='%d:%s' victim='%d:%s' weapon=%d special=%d", Killer, Server()->ClientName(Killer), - m_pPlayer->GetCID(), Server()->ClientName(m_pPlayer->GetCID()), Weapon, ModeSpecial); + m_pPlayer->GetCid(), Server()->ClientName(m_pPlayer->GetCid()), Weapon, ModeSpecial); GameServer()->Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "game", aBuf); // send the kill message - if(SendKillMsg && (Team() == TEAM_FLOCK || Teams()->Count(Team()) == 1 || Teams()->GetTeamState(Team()) == CGameTeams::TEAMSTATE_OPEN || Teams()->TeamLocked(Team()) == false)) + if(SendKillMsg && (Team() == TEAM_FLOCK || Teams()->TeamFlock(Team()) || Teams()->Count(Team()) == 1 || Teams()->GetTeamState(Team()) == CGameTeams::TEAMSTATE_OPEN || !Teams()->TeamLocked(Team()))) { CNetMsg_Sv_KillMsg Msg; Msg.m_Killer = Killer; - Msg.m_Victim = m_pPlayer->GetCID(); + Msg.m_Victim = m_pPlayer->GetCid(); Msg.m_Weapon = Weapon; Msg.m_ModeSpecial = ModeSpecial; Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, -1); @@ -958,9 +980,9 @@ void CCharacter::Die(int Killer, int Weapon, bool SendKillMsg) SetSolo(false); GameServer()->m_World.RemoveEntity(this); - GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCID()] = 0; - GameServer()->CreateDeath(m_Pos, m_pPlayer->GetCID(), TeamMask()); - Teams()->OnCharacterDeath(GetPlayer()->GetCID(), Weapon); + GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCid()] = 0; + GameServer()->CreateDeath(m_Pos, m_pPlayer->GetCid(), TeamMask()); + Teams()->OnCharacterDeath(GetPlayer()->GetCid(), Weapon); } bool CCharacter::TakeDamage(vec2 Force, int Dmg, int From, int Weapon) @@ -977,7 +999,7 @@ bool CCharacter::TakeDamage(vec2 Force, int Dmg, int From, int Weapon) } //TODO: Move the emote stuff to a function -void CCharacter::SnapCharacter(int SnappingClient, int ID) +void CCharacter::SnapCharacter(int SnappingClient, int Id) { int SnappingClientVersion = GameServer()->GetClientVersion(SnappingClient); CCharacterCore *pCore; @@ -1005,7 +1027,7 @@ void CCharacter::SnapCharacter(int SnappingClient, int ID) } // solo, collision, jetpack and ninjajetpack prediction - if(m_pPlayer->GetCID() == SnappingClient) + if(m_pPlayer->GetCid() == SnappingClient) { int Faketuning = 0; if(m_pPlayer->GetClientVersion() < VERSION_DDNET_NEW_HUD) @@ -1026,7 +1048,7 @@ void CCharacter::SnapCharacter(int SnappingClient, int ID) if(Faketuning != m_NeededFaketuning) { m_NeededFaketuning = Faketuning; - GameServer()->SendTuningParams(m_pPlayer->GetCID(), m_TuneZone); // update tunings + GameServer()->SendTuningParams(m_pPlayer->GetCid(), m_TuneZone); // update tunings } } @@ -1039,8 +1061,8 @@ void CCharacter::SnapCharacter(int SnappingClient, int ID) AmmoCount = 10; } - if(m_pPlayer->GetCID() == SnappingClient || SnappingClient == SERVER_DEMO_CLIENT || - (!g_Config.m_SvStrictSpectateMode && m_pPlayer->GetCID() == GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID)) + if(m_pPlayer->GetCid() == SnappingClient || SnappingClient == SERVER_DEMO_CLIENT || + (!g_Config.m_SvStrictSpectateMode && m_pPlayer->GetCid() == GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId)) { Health = m_Health; Armor = m_Armor; @@ -1064,7 +1086,7 @@ void CCharacter::SnapCharacter(int SnappingClient, int ID) if(!Server()->IsSixup(SnappingClient)) { - CNetObj_Character *pCharacter = Server()->SnapNewItem(ID); + CNetObj_Character *pCharacter = Server()->SnapNewItem(Id); if(!pCharacter) return; @@ -1089,7 +1111,7 @@ void CCharacter::SnapCharacter(int SnappingClient, int ID) } else { - protocol7::CNetObj_Character *pCharacter = Server()->SnapNewItem(ID); + protocol7::CNetObj_Character *pCharacter = Server()->SnapNewItem(Id); if(!pCharacter) return; @@ -1099,6 +1121,10 @@ void CCharacter::SnapCharacter(int SnappingClient, int ID) pCharacter->m_Angle -= (int)(2.0f * pi * 256.0f); } + // m_HookTick can be negative when using the hook_duration tune, which 0.7 clients + // will consider invalid. https://github.com/ddnet/ddnet/issues/3915 + pCharacter->m_HookTick = maximum(0, pCharacter->m_HookTick); + pCharacter->m_Tick = Tick; pCharacter->m_Emote = Emote; pCharacter->m_AttackTick = m_AttackTick; @@ -1113,18 +1139,7 @@ void CCharacter::SnapCharacter(int SnappingClient, int ID) pCharacter->m_Health = Health; pCharacter->m_Armor = Armor; - int TriggeredEvents7 = 0; - if(m_Core.m_TriggeredEvents & COREEVENT_GROUND_JUMP) - TriggeredEvents7 |= protocol7::COREEVENTFLAG_GROUND_JUMP; - if(m_Core.m_TriggeredEvents & COREEVENT_AIR_JUMP) - TriggeredEvents7 |= protocol7::COREEVENTFLAG_AIR_JUMP; - if(m_Core.m_TriggeredEvents & COREEVENT_HOOK_ATTACH_PLAYER) - TriggeredEvents7 |= protocol7::COREEVENTFLAG_HOOK_ATTACH_PLAYER; - if(m_Core.m_TriggeredEvents & COREEVENT_HOOK_ATTACH_GROUND) - TriggeredEvents7 |= protocol7::COREEVENTFLAG_HOOK_ATTACH_GROUND; - if(m_Core.m_TriggeredEvents & COREEVENT_HOOK_HIT_NOHOOK) - TriggeredEvents7 |= protocol7::COREEVENTFLAG_HOOK_HIT_NOHOOK; - pCharacter->m_TriggeredEvents = TriggeredEvents7; + pCharacter->m_TriggeredEvents = m_TriggeredEvents7; } } @@ -1138,9 +1153,9 @@ bool CCharacter::CanSnapCharacter(int SnappingClient) if(pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused()) { - if(pSnapPlayer->m_SpectatorID != SPEC_FREEVIEW && !CanCollide(pSnapPlayer->m_SpectatorID) && (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_OFF || (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM && !SameTeam(pSnapPlayer->m_SpectatorID)))) + if(pSnapPlayer->m_SpectatorId != SPEC_FREEVIEW && !CanCollide(pSnapPlayer->m_SpectatorId) && (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_OFF || (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM && !SameTeam(pSnapPlayer->m_SpectatorId)))) return false; - else if(pSnapPlayer->m_SpectatorID == SPEC_FREEVIEW && !CanCollide(SnappingClient) && pSnapPlayer->m_SpecTeam && !SameTeam(SnappingClient)) + else if(pSnapPlayer->m_SpectatorId == SPEC_FREEVIEW && !CanCollide(SnappingClient) && pSnapPlayer->m_SpecTeam && !SameTeam(SnappingClient)) return false; } else if(pSnapChar && !pSnapChar->m_Core.m_Super && !CanCollide(SnappingClient) && (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_OFF || (pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM && !SameTeam(SnappingClient)))) @@ -1149,21 +1164,21 @@ bool CCharacter::CanSnapCharacter(int SnappingClient) return true; } -bool CCharacter::IsSnappingCharacterInView(int SnappingClientID) +bool CCharacter::IsSnappingCharacterInView(int SnappingClientId) { - int ID = m_pPlayer->GetCID(); + int Id = m_pPlayer->GetCid(); // A player may not be clipped away if his hook or a hook attached to him is in the field of view - bool PlayerAndHookNotInView = NetworkClippedLine(SnappingClientID, m_Pos, m_Core.m_HookPos); + bool PlayerAndHookNotInView = NetworkClippedLine(SnappingClientId, m_Pos, m_Core.m_HookPos); bool AttachedHookInView = false; if(PlayerAndHookNotInView) { - for(const auto &AttachedPlayerID : m_Core.m_AttachedPlayers) + for(const auto &AttachedPlayerId : m_Core.m_AttachedPlayers) { - const CCharacter *pOtherPlayer = GameServer()->GetPlayerChar(AttachedPlayerID); - if(pOtherPlayer && pOtherPlayer->m_Core.HookedPlayer() == ID) + const CCharacter *pOtherPlayer = GameServer()->GetPlayerChar(AttachedPlayerId); + if(pOtherPlayer && pOtherPlayer->m_Core.HookedPlayer() == Id) { - if(!NetworkClippedLine(SnappingClientID, m_Pos, pOtherPlayer->m_Pos)) + if(!NetworkClippedLine(SnappingClientId, m_Pos, pOtherPlayer->m_Pos)) { AttachedHookInView = true; break; @@ -1180,9 +1195,9 @@ bool CCharacter::IsSnappingCharacterInView(int SnappingClientID) void CCharacter::Snap(int SnappingClient) { - int ID = m_pPlayer->GetCID(); + int Id = m_pPlayer->GetCid(); - if(!Server()->Translate(ID, SnappingClient)) + if(!Server()->Translate(Id, SnappingClient)) return; if(!CanSnapCharacter(SnappingClient)) @@ -1193,9 +1208,9 @@ void CCharacter::Snap(int SnappingClient) if(!IsSnappingCharacterInView(SnappingClient)) return; - SnapCharacter(SnappingClient, ID); + SnapCharacter(SnappingClient, Id); - CNetObj_DDNetCharacter *pDDNetCharacter = Server()->SnapNewItem(ID); + CNetObj_DDNetCharacter *pDDNetCharacter = Server()->SnapNewItem(Id); if(!pDDNetCharacter) return; @@ -1246,7 +1261,7 @@ void CCharacter::Snap(int SnappingClient) pDDNetCharacter->m_FreezeEnd = m_Core.m_DeepFrozen ? -1 : m_FreezeTime == 0 ? 0 : Server()->Tick() + m_FreezeTime; pDDNetCharacter->m_Jumps = m_Core.m_Jumps; pDDNetCharacter->m_TeleCheckpoint = m_TeleCheckpoint; - pDDNetCharacter->m_StrongWeakID = m_StrongWeakID; + pDDNetCharacter->m_StrongWeakId = m_StrongWeakId; // Display Information pDDNetCharacter->m_JumpedTotal = m_Core.m_JumpedTotal; @@ -1260,31 +1275,37 @@ void CCharacter::Snap(int SnappingClient) { pDDNetCharacter->m_Flags |= CHARACTERFLAG_PRACTICE_MODE; } + if(Teams()->TeamLocked(Team())) + { + pDDNetCharacter->m_Flags |= CHARACTERFLAG_LOCK_MODE; + } + if(Teams()->TeamFlock(Team())) + { + pDDNetCharacter->m_Flags |= CHARACTERFLAG_TEAM0_MODE; + } pDDNetCharacter->m_TargetX = m_Core.m_Input.m_TargetX; pDDNetCharacter->m_TargetY = m_Core.m_Input.m_TargetY; } +void CCharacter::PostSnap() +{ + m_TriggeredEvents7 = 0; +} + // DDRace -bool CCharacter::CanCollide(int ClientID) +bool CCharacter::CanCollide(int ClientId) { - return Teams()->m_Core.CanCollide(GetPlayer()->GetCID(), ClientID); + return Teams()->m_Core.CanCollide(GetPlayer()->GetCid(), ClientId); } -bool CCharacter::SameTeam(int ClientID) +bool CCharacter::SameTeam(int ClientId) { - return Teams()->m_Core.SameTeam(GetPlayer()->GetCID(), ClientID); + return Teams()->m_Core.SameTeam(GetPlayer()->GetCid(), ClientId); } int CCharacter::Team() { - return Teams()->m_Core.Team(m_pPlayer->GetCID()); -} - -void CCharacter::SetTeleports(std::map> *pTeleOuts, std::map> *pTeleCheckOuts) -{ - m_pTeleOuts = pTeleOuts; - m_pTeleCheckOuts = pTeleCheckOuts; - m_Core.SetTeleOuts(pTeleOuts); + return Teams()->m_Core.Team(m_pPlayer->GetCid()); } void CCharacter::FillAntibot(CAntibotCharacterData *pData) @@ -1305,7 +1326,7 @@ void CCharacter::FillAntibot(CAntibotCharacterData *pData) void CCharacter::HandleBroadcast() { - CPlayerData *pData = GameServer()->Score()->PlayerData(m_pPlayer->GetCID()); + CPlayerData *pData = GameServer()->Score()->PlayerData(m_pPlayer->GetCid()); if(m_DDRaceState == DDRACE_STARTED && m_pPlayer->GetClientVersion() == VERSION_VANILLA && m_LastTimeCpBroadcasted != m_LastTimeCp && m_LastTimeCp > -1 && @@ -1314,7 +1335,7 @@ void CCharacter::HandleBroadcast() char aBroadcast[128]; float Diff = m_aCurrentTimeCp[m_LastTimeCp] - pData->m_aBestTimeCp[m_LastTimeCp]; str_format(aBroadcast, sizeof(aBroadcast), "Checkpoint | Diff : %+5.2f", Diff); - GameServer()->SendBroadcast(aBroadcast, m_pPlayer->GetCID()); + GameServer()->SendBroadcast(aBroadcast, m_pPlayer->GetCid()); m_LastTimeCpBroadcasted = m_LastTimeCp; m_LastBroadcast = Server()->Tick(); } @@ -1323,7 +1344,7 @@ void CCharacter::HandleBroadcast() char aBuf[32]; int Time = (int64_t)100 * ((float)(Server()->Tick() - m_StartTime) / ((float)Server()->TickSpeed())); str_time(Time, TIME_HOURS, aBuf, sizeof(aBuf)); - GameServer()->SendBroadcast(aBuf, m_pPlayer->GetCID(), false); + GameServer()->SendBroadcast(aBuf, m_pPlayer->GetCid(), false); m_LastTimeCpBroadcasted = m_LastTimeCp; m_LastBroadcast = Server()->Tick(); } @@ -1340,15 +1361,15 @@ void CCharacter::HandleSkippableTiles(int Index) Collision()->GetFCollisionAt(m_Pos.x + GetProximityRadius() / 3.f, m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH || Collision()->GetFCollisionAt(m_Pos.x - GetProximityRadius() / 3.f, m_Pos.y - GetProximityRadius() / 3.f) == TILE_DEATH || Collision()->GetFCollisionAt(m_Pos.x - GetProximityRadius() / 3.f, m_Pos.y + GetProximityRadius() / 3.f) == TILE_DEATH) && - !m_Core.m_Super && !(Team() && Teams()->TeeFinished(m_pPlayer->GetCID()))) + !m_Core.m_Super && !(Team() && Teams()->TeeFinished(m_pPlayer->GetCid()))) { - Die(m_pPlayer->GetCID(), WEAPON_WORLD); + Die(m_pPlayer->GetCid(), WEAPON_WORLD); return; } if(GameLayerClipped(m_Pos)) { - Die(m_pPlayer->GetCID(), WEAPON_WORLD); + Die(m_pPlayer->GetCid(), WEAPON_WORLD); return; } @@ -1431,15 +1452,15 @@ void CCharacter::SetTimeCheckpoint(int TimeCheckpoint) m_TimeCpBroadcastEndTick = Server()->Tick() + Server()->TickSpeed() * 2; if(m_pPlayer->GetClientVersion() >= VERSION_DDRACE) { - CPlayerData *pData = GameServer()->Score()->PlayerData(m_pPlayer->GetCID()); - if(pData->m_BestTime && pData->m_aBestTimeCp[m_LastTimeCp] != 0.0f) + CPlayerData *pData = GameServer()->Score()->PlayerData(m_pPlayer->GetCid()); + if(pData->m_aBestTimeCp[m_LastTimeCp] != 0.0f) { CNetMsg_Sv_DDRaceTime Msg; Msg.m_Time = (int)(m_Time * 100.0f); Msg.m_Finish = 0; float Diff = (m_aCurrentTimeCp[m_LastTimeCp] - pData->m_aBestTimeCp[m_LastTimeCp]) * 100; Msg.m_Check = (int)Diff; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, m_pPlayer->GetCID()); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, m_pPlayer->GetCid()); } } } @@ -1466,6 +1487,8 @@ void CCharacter::HandleTiles(int Index) m_TeleCheckpoint = TeleCheckpoint; GameServer()->m_pController->HandleCharacterTiles(this, Index); + if(!m_Alive) + return; // freeze if(((m_TileIndex == TILE_FREEZE) || (m_TileFIndex == TILE_FREEZE)) && !m_Core.m_Super && !m_Core.m_DeepFrozen) @@ -1504,7 +1527,7 @@ void CCharacter::HandleTiles(int Index) // hit others if(((m_TileIndex == TILE_HIT_DISABLE) || (m_TileFIndex == TILE_HIT_DISABLE)) && (!m_Core.m_HammerHitDisabled || !m_Core.m_ShotgunHitDisabled || !m_Core.m_GrenadeHitDisabled || !m_Core.m_LaserHitDisabled)) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can't hit others"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can't hit others"); m_Core.m_HammerHitDisabled = true; m_Core.m_ShotgunHitDisabled = true; m_Core.m_GrenadeHitDisabled = true; @@ -1512,7 +1535,7 @@ void CCharacter::HandleTiles(int Index) } else if(((m_TileIndex == TILE_HIT_ENABLE) || (m_TileFIndex == TILE_HIT_ENABLE)) && (m_Core.m_HammerHitDisabled || m_Core.m_ShotgunHitDisabled || m_Core.m_GrenadeHitDisabled || m_Core.m_LaserHitDisabled)) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can hit others"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can hit others"); m_Core.m_ShotgunHitDisabled = false; m_Core.m_GrenadeHitDisabled = false; m_Core.m_HammerHitDisabled = false; @@ -1522,36 +1545,36 @@ void CCharacter::HandleTiles(int Index) // collide with others if(((m_TileIndex == TILE_NPC_DISABLE) || (m_TileFIndex == TILE_NPC_DISABLE)) && !m_Core.m_CollisionDisabled) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can't collide with others"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can't collide with others"); m_Core.m_CollisionDisabled = true; } else if(((m_TileIndex == TILE_NPC_ENABLE) || (m_TileFIndex == TILE_NPC_ENABLE)) && m_Core.m_CollisionDisabled) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can collide with others"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can collide with others"); m_Core.m_CollisionDisabled = false; } // hook others if(((m_TileIndex == TILE_NPH_DISABLE) || (m_TileFIndex == TILE_NPH_DISABLE)) && !m_Core.m_HookHitDisabled) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can't hook others"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can't hook others"); m_Core.m_HookHitDisabled = true; } else if(((m_TileIndex == TILE_NPH_ENABLE) || (m_TileFIndex == TILE_NPH_ENABLE)) && m_Core.m_HookHitDisabled) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can hook others"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can hook others"); m_Core.m_HookHitDisabled = false; } // unlimited air jumps if(((m_TileIndex == TILE_UNLIMITED_JUMPS_ENABLE) || (m_TileFIndex == TILE_UNLIMITED_JUMPS_ENABLE)) && !m_Core.m_EndlessJump) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You have unlimited air jumps"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You have unlimited air jumps"); m_Core.m_EndlessJump = true; } else if(((m_TileIndex == TILE_UNLIMITED_JUMPS_DISABLE) || (m_TileFIndex == TILE_UNLIMITED_JUMPS_DISABLE)) && m_Core.m_EndlessJump) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You don't have unlimited air jumps"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You don't have unlimited air jumps"); m_Core.m_EndlessJump = false; } @@ -1569,12 +1592,12 @@ void CCharacter::HandleTiles(int Index) // jetpack gun if(((m_TileIndex == TILE_JETPACK_ENABLE) || (m_TileFIndex == TILE_JETPACK_ENABLE)) && !m_Core.m_Jetpack) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You have a jetpack gun"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You have a jetpack gun"); m_Core.m_Jetpack = true; } else if(((m_TileIndex == TILE_JETPACK_DISABLE) || (m_TileFIndex == TILE_JETPACK_DISABLE)) && m_Core.m_Jetpack) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You lost your jetpack gun"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You lost your jetpack gun"); m_Core.m_Jetpack = false; } @@ -1595,39 +1618,39 @@ void CCharacter::HandleTiles(int Index) { m_Core.m_HasTelegunGun = true; - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "Teleport gun enabled"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "Teleport gun enabled"); } else if(((m_TileIndex == TILE_TELE_GUN_DISABLE) || (m_TileFIndex == TILE_TELE_GUN_DISABLE)) && m_Core.m_HasTelegunGun) { m_Core.m_HasTelegunGun = false; - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "Teleport gun disabled"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "Teleport gun disabled"); } if(((m_TileIndex == TILE_TELE_GRENADE_ENABLE) || (m_TileFIndex == TILE_TELE_GRENADE_ENABLE)) && !m_Core.m_HasTelegunGrenade) { m_Core.m_HasTelegunGrenade = true; - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "Teleport grenade enabled"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "Teleport grenade enabled"); } else if(((m_TileIndex == TILE_TELE_GRENADE_DISABLE) || (m_TileFIndex == TILE_TELE_GRENADE_DISABLE)) && m_Core.m_HasTelegunGrenade) { m_Core.m_HasTelegunGrenade = false; - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "Teleport grenade disabled"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "Teleport grenade disabled"); } if(((m_TileIndex == TILE_TELE_LASER_ENABLE) || (m_TileFIndex == TILE_TELE_LASER_ENABLE)) && !m_Core.m_HasTelegunLaser) { m_Core.m_HasTelegunLaser = true; - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "Teleport laser enabled"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "Teleport laser enabled"); } else if(((m_TileIndex == TILE_TELE_LASER_DISABLE) || (m_TileFIndex == TILE_TELE_LASER_DISABLE)) && m_Core.m_HasTelegunLaser) { m_Core.m_HasTelegunLaser = false; - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "Teleport laser disabled"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "Teleport laser disabled"); } // stopper @@ -1636,7 +1659,7 @@ void CCharacter::HandleTiles(int Index) m_Core.m_Jumped = 0; m_Core.m_JumpedTotal = 0; } - m_Core.m_Vel = ClampVel(m_MoveRestrictions, m_Core.m_Vel); + ApplyMoveRestrictions(); // handle switch tiles if(Collision()->GetSwitchType(MapIndex) == TILE_SWITCHOPEN && Team() != TEAM_SUPER && Collision()->GetSwitchNumber(MapIndex) > 0) @@ -1700,42 +1723,42 @@ void CCharacter::HandleTiles(int Index) } else if(Collision()->GetSwitchType(MapIndex) == TILE_HIT_ENABLE && m_Core.m_HammerHitDisabled && Collision()->GetSwitchDelay(MapIndex) == WEAPON_HAMMER) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can hammer hit others"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can hammer hit others"); m_Core.m_HammerHitDisabled = false; } else if(Collision()->GetSwitchType(MapIndex) == TILE_HIT_DISABLE && !(m_Core.m_HammerHitDisabled) && Collision()->GetSwitchDelay(MapIndex) == WEAPON_HAMMER) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can't hammer hit others"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can't hammer hit others"); m_Core.m_HammerHitDisabled = true; } else if(Collision()->GetSwitchType(MapIndex) == TILE_HIT_ENABLE && m_Core.m_ShotgunHitDisabled && Collision()->GetSwitchDelay(MapIndex) == WEAPON_SHOTGUN) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can shoot others with shotgun"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can shoot others with shotgun"); m_Core.m_ShotgunHitDisabled = false; } else if(Collision()->GetSwitchType(MapIndex) == TILE_HIT_DISABLE && !(m_Core.m_ShotgunHitDisabled) && Collision()->GetSwitchDelay(MapIndex) == WEAPON_SHOTGUN) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can't shoot others with shotgun"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can't shoot others with shotgun"); m_Core.m_ShotgunHitDisabled = true; } else if(Collision()->GetSwitchType(MapIndex) == TILE_HIT_ENABLE && m_Core.m_GrenadeHitDisabled && Collision()->GetSwitchDelay(MapIndex) == WEAPON_GRENADE) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can shoot others with grenade"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can shoot others with grenade"); m_Core.m_GrenadeHitDisabled = false; } else if(Collision()->GetSwitchType(MapIndex) == TILE_HIT_DISABLE && !(m_Core.m_GrenadeHitDisabled) && Collision()->GetSwitchDelay(MapIndex) == WEAPON_GRENADE) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can't shoot others with grenade"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can't shoot others with grenade"); m_Core.m_GrenadeHitDisabled = true; } else if(Collision()->GetSwitchType(MapIndex) == TILE_HIT_ENABLE && m_Core.m_LaserHitDisabled && Collision()->GetSwitchDelay(MapIndex) == WEAPON_LASER) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can shoot others with laser"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can shoot others with laser"); m_Core.m_LaserHitDisabled = false; } else if(Collision()->GetSwitchType(MapIndex) == TILE_HIT_DISABLE && !(m_Core.m_LaserHitDisabled) && Collision()->GetSwitchDelay(MapIndex) == WEAPON_LASER) { - GameServer()->SendChatTarget(GetPlayer()->GetCID(), "You can't shoot others with laser"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), "You can't shoot others with laser"); m_Core.m_LaserHitDisabled = true; } else if(Collision()->GetSwitchType(MapIndex) == TILE_JUMP) @@ -1755,7 +1778,7 @@ void CCharacter::HandleTiles(int Index) str_format(aBuf, sizeof(aBuf), "You can jump %d time", NewJumps); else str_format(aBuf, sizeof(aBuf), "You can jump %d times", NewJumps); - GameServer()->SendChatTarget(GetPlayer()->GetCID(), aBuf); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), aBuf); m_Core.m_Jumps = NewJumps; } } @@ -1767,7 +1790,7 @@ void CCharacter::HandleTiles(int Index) m_StartTime -= (min * 60 + sec) * Server()->TickSpeed(); - if((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || Team != TEAM_FLOCK) && Team != TEAM_SUPER) + if((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || (Team != TEAM_FLOCK && !Teams()->TeamFlock(Team))) && Team != TEAM_SUPER) { for(int i = 0; i < MAX_CLIENTS; i++) { @@ -1793,7 +1816,7 @@ void CCharacter::HandleTiles(int Index) if(m_StartTime > Server()->Tick()) m_StartTime = Server()->Tick(); - if((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || Team != TEAM_FLOCK) && Team != TEAM_SUPER) + if((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || (Team != TEAM_FLOCK && !Teams()->TeamFlock(Team))) && Team != TEAM_SUPER) { for(int i = 0; i < MAX_CLIENTS; i++) { @@ -1821,12 +1844,12 @@ void CCharacter::HandleTiles(int Index) } int z = Collision()->IsTeleport(MapIndex); - if(!g_Config.m_SvOldTeleportHook && !g_Config.m_SvOldTeleportWeapons && z && !(*m_pTeleOuts)[z - 1].empty()) + if(!g_Config.m_SvOldTeleportHook && !g_Config.m_SvOldTeleportWeapons && z && !Collision()->TeleOuts(z - 1).empty()) { if(m_Core.m_Super) return; - int TeleOut = GameWorld()->m_Core.RandomOr0((*m_pTeleOuts)[z - 1].size()); - m_Core.m_Pos = (*m_pTeleOuts)[z - 1][TeleOut]; + int TeleOut = GameWorld()->m_Core.RandomOr0(Collision()->TeleOuts(z - 1).size()); + m_Core.m_Pos = Collision()->TeleOuts(z - 1)[TeleOut]; if(!g_Config.m_SvTeleportHoldHook) { ResetHook(); @@ -1836,12 +1859,12 @@ void CCharacter::HandleTiles(int Index) return; } int evilz = Collision()->IsEvilTeleport(MapIndex); - if(evilz && !(*m_pTeleOuts)[evilz - 1].empty()) + if(evilz && !Collision()->TeleOuts(evilz - 1).empty()) { if(m_Core.m_Super) return; - int TeleOut = GameWorld()->m_Core.RandomOr0((*m_pTeleOuts)[evilz - 1].size()); - m_Core.m_Pos = (*m_pTeleOuts)[evilz - 1][TeleOut]; + int TeleOut = GameWorld()->m_Core.RandomOr0(Collision()->TeleOuts(evilz - 1).size()); + m_Core.m_Pos = Collision()->TeleOuts(evilz - 1)[TeleOut]; if(!g_Config.m_SvOldTeleportHook && !g_Config.m_SvOldTeleportWeapons) { m_Core.m_Vel = vec2(0, 0); @@ -1849,7 +1872,7 @@ void CCharacter::HandleTiles(int Index) if(!g_Config.m_SvTeleportHoldHook) { ResetHook(); - GameWorld()->ReleaseHooked(GetPlayer()->GetCID()); + GameWorld()->ReleaseHooked(GetPlayer()->GetCid()); } if(g_Config.m_SvTeleportLoseWeapons) { @@ -1865,16 +1888,16 @@ void CCharacter::HandleTiles(int Index) // first check if there is a TeleCheckOut for the current recorded checkpoint, if not check previous checkpoints for(int k = m_TeleCheckpoint - 1; k >= 0; k--) { - if(!(*m_pTeleCheckOuts)[k].empty()) + if(!Collision()->TeleCheckOuts(k).empty()) { - int TeleOut = GameWorld()->m_Core.RandomOr0((*m_pTeleCheckOuts)[k].size()); - m_Core.m_Pos = (*m_pTeleCheckOuts)[k][TeleOut]; + int TeleOut = GameWorld()->m_Core.RandomOr0(Collision()->TeleCheckOuts(k).size()); + m_Core.m_Pos = Collision()->TeleCheckOuts(k)[TeleOut]; m_Core.m_Vel = vec2(0, 0); if(!g_Config.m_SvTeleportHoldHook) { ResetHook(); - GameWorld()->ReleaseHooked(GetPlayer()->GetCID()); + GameWorld()->ReleaseHooked(GetPlayer()->GetCid()); } return; @@ -1882,7 +1905,7 @@ void CCharacter::HandleTiles(int Index) } // if no checkpointout have been found (or if there no recorded checkpoint), teleport to start vec2 SpawnPos; - if(GameServer()->m_pController->CanSpawn(m_pPlayer->GetTeam(), &SpawnPos, GameServer()->GetDDRaceTeam(GetPlayer()->GetCID()))) + if(GameServer()->m_pController->CanSpawn(m_pPlayer->GetTeam(), &SpawnPos, GameServer()->GetDDRaceTeam(GetPlayer()->GetCid()))) { m_Core.m_Pos = SpawnPos; m_Core.m_Vel = vec2(0, 0); @@ -1890,7 +1913,7 @@ void CCharacter::HandleTiles(int Index) if(!g_Config.m_SvTeleportHoldHook) { ResetHook(); - GameWorld()->ReleaseHooked(GetPlayer()->GetCID()); + GameWorld()->ReleaseHooked(GetPlayer()->GetCid()); } } return; @@ -1902,10 +1925,10 @@ void CCharacter::HandleTiles(int Index) // first check if there is a TeleCheckOut for the current recorded checkpoint, if not check previous checkpoints for(int k = m_TeleCheckpoint - 1; k >= 0; k--) { - if(!(*m_pTeleCheckOuts)[k].empty()) + if(!Collision()->TeleCheckOuts(k).empty()) { - int TeleOut = GameWorld()->m_Core.RandomOr0((*m_pTeleCheckOuts)[k].size()); - m_Core.m_Pos = (*m_pTeleCheckOuts)[k][TeleOut]; + int TeleOut = GameWorld()->m_Core.RandomOr0(Collision()->TeleCheckOuts(k).size()); + m_Core.m_Pos = Collision()->TeleCheckOuts(k)[TeleOut]; if(!g_Config.m_SvTeleportHoldHook) { @@ -1917,7 +1940,7 @@ void CCharacter::HandleTiles(int Index) } // if no checkpointout have been found (or if there no recorded checkpoint), teleport to start vec2 SpawnPos; - if(GameServer()->m_pController->CanSpawn(m_pPlayer->GetTeam(), &SpawnPos, GameServer()->GetDDRaceTeam(GetPlayer()->GetCID()))) + if(GameServer()->m_pController->CanSpawn(m_pPlayer->GetTeam(), &SpawnPos, GameServer()->GetDDRaceTeam(GetPlayer()->GetCid()))) { m_Core.m_Pos = SpawnPos; @@ -1962,9 +1985,9 @@ void CCharacter::SendZoneMsgs() str_copy(aBuf, pCur, pPos - pCur + 1); aBuf[pPos - pCur + 1] = '\0'; pCur = pPos + 2; - GameServer()->SendChatTarget(m_pPlayer->GetCID(), aBuf); + GameServer()->SendChatTarget(m_pPlayer->GetCid(), aBuf); } - GameServer()->SendChatTarget(m_pPlayer->GetCID(), pCur); + GameServer()->SendChatTarget(m_pPlayer->GetCid(), pCur); } // send zone enter msg if(GameServer()->m_aaZoneEnterMsg[m_TuneZone][0]) @@ -1977,9 +2000,9 @@ void CCharacter::SendZoneMsgs() str_copy(aBuf, pCur, pPos - pCur + 1); aBuf[pPos - pCur + 1] = '\0'; pCur = pPos + 2; - GameServer()->SendChatTarget(m_pPlayer->GetCID(), aBuf); + GameServer()->SendChatTarget(m_pPlayer->GetCid(), aBuf); } - GameServer()->SendChatTarget(m_pPlayer->GetCID(), pCur); + GameServer()->SendChatTarget(m_pPlayer->GetCid(), pCur); } } @@ -1994,16 +2017,52 @@ void CCharacter::SetTeams(CGameTeams *pTeams) m_Core.SetTeamsCore(&m_pTeams->m_Core); } -void CCharacter::SetRescue() +bool CCharacter::TrySetRescue(int RescueMode) +{ + bool Set = false; + if(g_Config.m_SvRescue || ((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || Team() > TEAM_FLOCK) && Team() >= TEAM_FLOCK && Team() < TEAM_SUPER)) + { + // check for nearby health pickups (also freeze) + bool InHealthPickup = false; + if(!m_Core.m_IsInFreeze) + { + CEntity *apEnts[9]; + int Num = GameWorld()->FindEntities(m_Pos, GetProximityRadius() + CPickup::ms_CollisionExtraSize, apEnts, std::size(apEnts), CGameWorld::ENTTYPE_PICKUP); + for(int i = 0; i < Num; ++i) + { + CPickup *pPickup = static_cast(apEnts[i]); + if(pPickup->Type() == POWERUP_HEALTH) + { + // This uses a separate variable InHealthPickup instead of setting m_Core.m_IsInFreeze + // as the latter causes freezebars to flicker when standing in the freeze range of a + // health pickup. When the same code for client prediction is added, the freezebars + // still flicker, but only when standing at the edge of the health pickup's freeze range. + InHealthPickup = true; + break; + } + } + } + + if(!m_Core.m_IsInFreeze && IsGrounded() && !m_Core.m_DeepFrozen && !InHealthPickup) + { + ForceSetRescue(RescueMode); + Set = true; + } + } + + return Set; +} + +void CCharacter::ForceSetRescue(int RescueMode) { - m_RescueTee.Save(this); - m_SetSavePos = true; + m_RescueTee[RescueMode].Save(this); + m_SetSavePos[RescueMode] = true; } void CCharacter::DDRaceTick() { mem_copy(&m_Input, &m_SavedInput, sizeof(m_Input)); - m_Armor = clamp(10 - (m_FreezeTime / 15), 0, 10); + GameServer()->m_pController->SetArmorProgress(this, m_FreezeTime); if(m_Input.m_Direction != 0 || m_Input.m_Jump != 0) m_LastMove = Server()->Tick(); @@ -2045,37 +2104,11 @@ void CCharacter::DDRaceTick() } } - // check for nearby health pickups (also freeze) - bool InHealthPickup = false; - if(!m_Core.m_IsInFreeze) - { - CEntity *apEnts[9]; - int Num = GameWorld()->FindEntities(m_Pos, GetProximityRadius() + CPickup::ms_CollisionExtraSize, apEnts, std::size(apEnts), CGameWorld::ENTTYPE_PICKUP); - for(int i = 0; i < Num; ++i) - { - CPickup *pPickup = static_cast(apEnts[i]); - if(pPickup->Type() == POWERUP_HEALTH) - { - // This uses a separate variable InHealthPickup instead of setting m_Core.m_IsInFreeze - // as the latter causes freezebars to flicker when standing in the freeze range of a - // health pickup. When the same code for client prediction is added, the freezebars - // still flicker, but only when standing at the edge of the health pickup's freeze range. - InHealthPickup = true; - break; - } - } - } - // look for save position for rescue feature - if(g_Config.m_SvRescue || ((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || Team() > TEAM_FLOCK) && Team() >= TEAM_FLOCK && Team() < TEAM_SUPER)) - { - if(!m_Core.m_IsInFreeze && IsGrounded() && !m_Core.m_DeepFrozen && !InHealthPickup) - { - SetRescue(); - } - } + // always update auto rescue + TrySetRescue(RESCUEMODE_AUTO); - m_Core.m_Id = GetPlayer()->GetCID(); + m_Core.m_Id = GetPlayer()->GetCid(); } void CCharacter::DDRacePostCoreTick() @@ -2144,11 +2177,11 @@ void CCharacter::DDRacePostCoreTick() // teleport gun if(m_TeleGunTeleport) { - GameServer()->CreateDeath(m_Pos, m_pPlayer->GetCID(), TeamMask()); + GameServer()->CreateDeath(m_Pos, m_pPlayer->GetCid(), TeamMask()); m_Core.m_Pos = m_TeleGunPos; if(!m_IsBlueTeleGunTeleport) m_Core.m_Vel = vec2(0, 0); - GameServer()->CreateDeath(m_TeleGunPos, m_pPlayer->GetCID(), TeamMask()); + GameServer()->CreateDeath(m_TeleGunPos, m_pPlayer->GetCid(), TeamMask()); GameServer()->CreateSound(m_TeleGunPos, SOUND_WEAPON_SPAWN, TeamMask()); m_TeleGunTeleport = false; m_IsBlueTeleGunTeleport = false; @@ -2161,7 +2194,7 @@ bool CCharacter::Freeze(int Seconds) { if(Seconds <= 0 || m_Core.m_Super || m_FreezeTime > Seconds * Server()->TickSpeed()) return false; - if(m_Core.m_FreezeStart < Server()->Tick() - Server()->TickSpeed()) + if(m_FreezeTime == 0 || m_Core.m_FreezeStart < Server()->Tick() - Server()->TickSpeed()) { m_Armor = 0; m_FreezeTime = Seconds * Server()->TickSpeed(); @@ -2191,6 +2224,12 @@ bool CCharacter::UnFreeze() return false; } +void CCharacter::ResetJumps() +{ + m_Core.m_JumpedTotal = 0; + m_Core.m_Jumped = 0; +} + void CCharacter::GiveWeapon(int Weapon, bool Remove) { if(Weapon == WEAPON_NINJA) @@ -2239,7 +2278,7 @@ void CCharacter::SetEndlessHook(bool Enable) { return; } - GameServer()->SendChatTarget(GetPlayer()->GetCID(), Enable ? "Endless hook has been activated" : "Endless hook has been deactivated"); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), Enable ? "Endless hook has been activated" : "Endless hook has been deactivated"); m_Core.m_EndlessHook = Enable; } @@ -2249,20 +2288,25 @@ void CCharacter::Pause(bool Pause) m_Paused = Pause; if(Pause) { - GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCID()] = 0; + GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCid()] = 0; GameServer()->m_World.RemoveEntity(this); if(m_Core.HookedPlayer() != -1) // Keeping hook would allow cheats { ResetHook(); - GameWorld()->ReleaseHooked(GetPlayer()->GetCID()); + GameWorld()->ReleaseHooked(GetPlayer()->GetCid()); } + m_PausedTick = Server()->Tick(); } else { m_Core.m_Vel = vec2(0, 0); - GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCID()] = &m_Core; + GameServer()->m_World.m_Core.m_apCharacters[m_pPlayer->GetCid()] = &m_Core; GameServer()->m_World.InsertEntity(this); + if(m_Core.m_FreezeStart > 0 && m_PausedTick >= 0) + { + m_Core.m_FreezeStart += Server()->Tick() - m_PausedTick; + } } } @@ -2271,10 +2315,11 @@ void CCharacter::DDRaceInit() m_Paused = false; m_DDRaceState = DDRACE_NONE; m_PrevPos = m_Pos; - m_SetSavePos = false; + for(bool &Set : m_SetSavePos) + Set = false; m_LastBroadcast = 0; m_TeamBeforeSuper = 0; - m_Core.m_Id = GetPlayer()->GetCID(); + m_Core.m_Id = GetPlayer()->GetCid(); m_TeleCheckpoint = 0; m_Core.m_EndlessHook = g_Config.m_SvEndlessDrag; if(g_Config.m_SvHit) @@ -2296,7 +2341,7 @@ void CCharacter::DDRaceInit() int Team = Teams()->m_Core.Team(m_Core.m_Id); - if(Teams()->TeamLocked(Team)) + if(Teams()->TeamLocked(Team) && !Teams()->TeamFlock(Team)) { for(int i = 0; i < MAX_CLIENTS; i++) { @@ -2315,24 +2360,25 @@ void CCharacter::DDRaceInit() if(g_Config.m_SvTeam == SV_TEAM_MANDATORY && Team == TEAM_FLOCK) { - GameServer()->SendStartWarning(GetPlayer()->GetCID(), "Please join a team before you start"); + GameServer()->SendStartWarning(GetPlayer()->GetCid(), "Please join a team before you start"); } } void CCharacter::Rescue() { - if(m_SetSavePos && !m_Core.m_Super) + if(m_SetSavePos[GetPlayer()->m_RescueMode] && !m_Core.m_Super) { if(m_LastRescue + (int64_t)g_Config.m_SvRescueDelay * Server()->TickSpeed() > Server()->Tick()) { char aBuf[256]; str_format(aBuf, sizeof(aBuf), "You have to wait %d seconds until you can rescue yourself", (int)((m_LastRescue + (int64_t)g_Config.m_SvRescueDelay * Server()->TickSpeed() - Server()->Tick()) / Server()->TickSpeed())); - GameServer()->SendChatTarget(GetPlayer()->GetCID(), aBuf); + GameServer()->SendChatTarget(GetPlayer()->GetCid(), aBuf); return; } + m_LastRescue = Server()->Tick(); float StartTime = m_StartTime; - m_RescueTee.Load(this, Team()); + m_RescueTee[GetPlayer()->m_RescueMode].Load(this, Team()); // Don't load these from saved tee: m_Core.m_Vel = vec2(0, 0); m_Core.m_HookState = HOOK_IDLE; @@ -2350,7 +2396,44 @@ void CCharacter::Rescue() CClientMask CCharacter::TeamMask() { - return Teams()->TeamMask(Team(), -1, GetPlayer()->GetCID()); + return Teams()->TeamMask(Team(), -1, GetPlayer()->GetCid()); +} + +void CCharacter::SetPosition(const vec2 &Position) +{ + m_Core.m_Pos = Position; +} + +void CCharacter::Move(vec2 RelPos) +{ + m_Core.m_Pos += RelPos; +} + +void CCharacter::ResetVelocity() +{ + m_Core.m_Vel = vec2(0, 0); +} + +void CCharacter::SetVelocity(vec2 NewVelocity) +{ + m_Core.m_Vel = ClampVel(m_MoveRestrictions, NewVelocity); +} + +// The method is needed only to reproduce 'shotgun bug' ddnet#5258 +// Use SetVelocity() instead. +void CCharacter::SetRawVelocity(vec2 NewVelocity) +{ + m_Core.m_Vel = NewVelocity; +} + +void CCharacter::AddVelocity(vec2 Addition) +{ + SetVelocity(m_Core.m_Vel + Addition); +} + +void CCharacter::ApplyMoveRestrictions() +{ + m_Core.m_Vel = ClampVel(m_MoveRestrictions, m_Core.m_Vel); } void CCharacter::SwapClients(int Client1, int Client2) diff --git a/src/game/server/entities/character.h b/src/game/server/entities/character.h index 8f984b7701..df569dc2c6 100644 --- a/src/game/server/entities/character.h +++ b/src/game/server/entities/character.h @@ -38,15 +38,17 @@ class CCharacter : public CEntity void TickDeferred() override; void TickPaused() override; void Snap(int SnappingClient) override; + void PostSnap() override; void SwapClients(int Client1, int Client2) override; bool CanSnapCharacter(int SnappingClient); - bool IsSnappingCharacterInView(int SnappingClientID); + bool IsSnappingCharacterInView(int SnappingClientId); bool IsGrounded(); void SetWeapon(int W); void SetJetpack(bool Active); + void SetJumps(int Jumps); void SetSolo(bool Solo); void SetSuper(bool Super); void SetLiveFrozen(bool Active); @@ -60,6 +62,7 @@ class CCharacter : public CEntity void OnPredictedInput(CNetObj_PlayerInput *pNewInput); void OnDirectInput(CNetObj_PlayerInput *pNewInput); + void ReleaseHook(); void ResetHook(); void ResetInput(); void FireWeapon(); @@ -82,18 +85,28 @@ class CCharacter : public CEntity void Rescue(); - int NeededFaketuning() { return m_NeededFaketuning; } + int NeededFaketuning() const { return m_NeededFaketuning; } bool IsAlive() const { return m_Alive; } bool IsPaused() const { return m_Paused; } class CPlayer *GetPlayer() { return m_pPlayer; } CClientMask TeamMask(); + void SetPosition(const vec2 &Position); + void Move(vec2 RelPos); + + void ResetVelocity(); + void SetVelocity(vec2 NewVelocity); + void SetRawVelocity(vec2 NewVelocity); + void AddVelocity(vec2 Addition); + void ApplyMoveRestrictions(); + private: // player controlling this character class CPlayer *m_pPlayer; bool m_Alive; bool m_Paused; + int m_PausedTick; int m_NeededFaketuning; // weapon info @@ -106,6 +119,8 @@ class CCharacter : public CEntity int m_ReloadTimer; int m_AttackTick; + int m_MoveRestrictions; + int m_DamageTaken; int m_EmoteType; @@ -131,13 +146,12 @@ class CCharacter : public CEntity int m_Health; int m_Armor; + int m_TriggeredEvents7; + // the player core for the physics CCharacterCore m_Core; CGameTeams *m_pTeams = nullptr; - std::map> *m_pTeleOuts = nullptr; - std::map> *m_pTeleCheckOuts = nullptr; - // info for dead reckoning int m_ReckoningTick; // tick that we are performing dead reckoning From CCharacterCore m_SendCore; // core that we should send @@ -145,7 +159,7 @@ class CCharacter : public CEntity // DDRace - void SnapCharacter(int SnappingClient, int ID); + void SnapCharacter(int SnappingClient, int Id); static bool IsSwitchActiveCb(int Number, void *pUser); void SetTimeCheckpoint(int TimeCheckpoint); void HandleTiles(int Index); @@ -153,7 +167,7 @@ class CCharacter : public CEntity int m_LastBroadcast; void DDRaceInit(); void HandleSkippableTiles(int Index); - void SetRescue(); + void ForceSetRescue(int RescueMode); void DDRaceTick(); void DDRacePostCoreTick(); void HandleBroadcast(); @@ -161,13 +175,13 @@ class CCharacter : public CEntity void SendZoneMsgs(); IAntibot *Antibot(); - bool m_SetSavePos; - CSaveTee m_RescueTee; + bool m_SetSavePos[NUM_RESCUEMODES]; + CSaveTee m_RescueTee[NUM_RESCUEMODES]; public: CGameTeams *Teams() { return m_pTeams; } void SetTeams(CGameTeams *pTeams); - void SetTeleports(std::map> *pTeleOuts, std::map> *pTeleCheckOuts); + bool TrySetRescue(int RescueMode); void FillAntibot(CAntibotCharacterData *pData); void Pause(bool Pause); @@ -176,10 +190,12 @@ class CCharacter : public CEntity bool UnFreeze(); void GiveAllWeapons(); void ResetPickups(); + void ResetJumps(); int m_DDRaceState; int Team(); - bool CanCollide(int ClientID); - bool SameTeam(int ClientID); + bool CanCollide(int ClientId); + bool SameTeam(int ClientId); + void StopRecording(); bool m_NinjaJetpack; int m_TeamBeforeSuper; int m_FreezeTime; @@ -201,8 +217,6 @@ class CCharacter : public CEntity int m_TileIndex; int m_TileFIndex; - int m_MoveRestrictions; - int64_t m_LastStartWarning; int64_t m_LastRescue; bool m_LastRefillJumps; @@ -211,22 +225,22 @@ class CCharacter : public CEntity vec2 m_TeleGunPos; bool m_TeleGunTeleport; bool m_IsBlueTeleGunTeleport; - int m_StrongWeakID; + int m_StrongWeakId; int m_SpawnTick; int m_WeaponChangeTick; // Setters/Getters because i don't want to modify vanilla vars access modifiers - int GetLastWeapon() { return m_LastWeapon; } + int GetLastWeapon() const { return m_LastWeapon; } void SetLastWeapon(int LastWeap) { m_LastWeapon = LastWeap; } - int GetActiveWeapon() { return m_Core.m_ActiveWeapon; } + int GetActiveWeapon() const { return m_Core.m_ActiveWeapon; } void SetActiveWeapon(int ActiveWeap) { m_Core.m_ActiveWeapon = ActiveWeap; } void SetLastAction(int LastAction) { m_LastAction = LastAction; } - int GetArmor() { return m_Armor; } + int GetArmor() const { return m_Armor; } void SetArmor(int Armor) { m_Armor = Armor; } CCharacterCore GetCore() { return m_Core; } void SetCore(CCharacterCore Core) { m_Core = Core; } - CCharacterCore *Core() { return &m_Core; } + const CCharacterCore *Core() const { return &m_Core; } bool GetWeaponGot(int Type) { return m_Core.m_aWeapons[Type].m_Got; } void SetWeaponGot(int Type, bool Value) { m_Core.m_aWeapons[Type].m_Got = Value; } int GetWeaponAmmo(int Type) { return m_Core.m_aWeapons[Type].m_Ammo; } @@ -237,18 +251,19 @@ class CCharacter : public CEntity int GetLastAction() const { return m_LastAction; } - bool HasTelegunGun() { return m_Core.m_HasTelegunGun; } - bool HasTelegunGrenade() { return m_Core.m_HasTelegunGrenade; } - bool HasTelegunLaser() { return m_Core.m_HasTelegunLaser; } + bool HasTelegunGun() const { return m_Core.m_HasTelegunGun; } + bool HasTelegunGrenade() const { return m_Core.m_HasTelegunGrenade; } + bool HasTelegunLaser() const { return m_Core.m_HasTelegunLaser; } - bool HammerHitDisabled() { return m_Core.m_HammerHitDisabled; } - bool ShotgunHitDisabled() { return m_Core.m_ShotgunHitDisabled; } - bool LaserHitDisabled() { return m_Core.m_LaserHitDisabled; } - bool GrenadeHitDisabled() { return m_Core.m_GrenadeHitDisabled; } + bool HammerHitDisabled() const { return m_Core.m_HammerHitDisabled; } + bool ShotgunHitDisabled() const { return m_Core.m_ShotgunHitDisabled; } + bool LaserHitDisabled() const { return m_Core.m_LaserHitDisabled; } + bool GrenadeHitDisabled() const { return m_Core.m_GrenadeHitDisabled; } - bool IsSuper() { return m_Core.m_Super; } + bool IsSuper() const { return m_Core.m_Super; } - CSaveTee &GetRescueTeeRef() { return m_RescueTee; } + CSaveTee &GetLastRescueTeeRef(int Mode = RESCUEMODE_AUTO) { return m_RescueTee[Mode]; } + CTuningParams *GetTuning(int Zone) { return Zone ? &TuningList()[Zone] : Tuning(); } }; enum diff --git a/src/game/server/entities/door.cpp b/src/game/server/entities/door.cpp index 14e8670b73..c0ad7b0f8b 100644 --- a/src/game/server/entities/door.cpp +++ b/src/game/server/entities/door.cpp @@ -64,8 +64,8 @@ void CDoor::Snap(int SnappingClient) { CCharacter *pChr = GameServer()->GetPlayerChar(SnappingClient); - if(SnappingClient != SERVER_DEMO_CLIENT && (GameServer()->m_apPlayers[SnappingClient]->GetTeam() == TEAM_SPECTATORS || GameServer()->m_apPlayers[SnappingClient]->IsPaused()) && GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID != SPEC_FREEVIEW) - pChr = GameServer()->GetPlayerChar(GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID); + if(SnappingClient != SERVER_DEMO_CLIENT && (GameServer()->m_apPlayers[SnappingClient]->GetTeam() == TEAM_SPECTATORS || GameServer()->m_apPlayers[SnappingClient]->IsPaused()) && GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId != SPEC_FREEVIEW) + pChr = GameServer()->GetPlayerChar(GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId); if(pChr && pChr->Team() != TEAM_SUPER && pChr->IsAlive() && !Switchers().empty() && Switchers()[m_Number].m_aStatus[pChr->Team()]) { @@ -78,6 +78,6 @@ void CDoor::Snap(int SnappingClient) StartTick = Server()->Tick(); } - GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetID(), + GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetId(), m_Pos, From, StartTick, -1, LASERTYPE_DOOR, 0, m_Number); } diff --git a/src/game/server/entities/dragger.cpp b/src/game/server/entities/dragger.cpp index e22800187c..4b9a96bd90 100644 --- a/src/game/server/entities/dragger.cpp +++ b/src/game/server/entities/dragger.cpp @@ -105,7 +105,7 @@ void CDragger::LookForPlayersToDrag() !GameServer()->Collision()->IntersectNoLaser(m_Pos, pTarget->m_Pos, 0, 0); if(IsReachable && pTarget->IsAlive()) { - const int &TargetClientId = pTarget->GetPlayer()->GetCID(); + const int &TargetClientId = pTarget->GetPlayer()->GetCid(); // Solo players are dragged independently from the rest of the team if(pTarget->Teams()->m_Core.GetSolo(TargetClientId)) { @@ -156,30 +156,30 @@ void CDragger::LookForPlayersToDrag() } } -void CDragger::RemoveDraggerBeam(int ClientID) +void CDragger::RemoveDraggerBeam(int ClientId) { - m_apDraggerBeam[ClientID] = nullptr; + m_apDraggerBeam[ClientId] = nullptr; } -bool CDragger::WillDraggerBeamUseDraggerID(int TargetClientID, int SnappingClientID) +bool CDragger::WillDraggerBeamUseDraggerId(int TargetClientId, int SnappingClientId) { // For each snapping client, this must return true for at most one target (i.e. only one of the dragger beams), // in which case the dragger itself must not be snapped - CCharacter *pTargetChar = GameServer()->GetPlayerChar(TargetClientID); - CCharacter *pSnapChar = GameServer()->GetPlayerChar(SnappingClientID); - if(pTargetChar && pSnapChar && m_apDraggerBeam[TargetClientID] != nullptr) + CCharacter *pTargetChar = GameServer()->GetPlayerChar(TargetClientId); + CCharacter *pSnapChar = GameServer()->GetPlayerChar(SnappingClientId); + if(pTargetChar && pSnapChar && m_apDraggerBeam[TargetClientId] != nullptr) { const int SnapTeam = pSnapChar->Team(); const int TargetTeam = pTargetChar->Team(); if(SnapTeam == TargetTeam && SnapTeam < MAX_CLIENTS) { - if(pSnapChar->Teams()->m_Core.GetSolo(SnappingClientID) || m_aTargetIdInTeam[SnapTeam] < 0) + if(pSnapChar->Teams()->m_Core.GetSolo(SnappingClientId) || m_aTargetIdInTeam[SnapTeam] < 0) { - return SnappingClientID == TargetClientID; + return SnappingClientId == TargetClientId; } else { - return m_aTargetIdInTeam[SnapTeam] == TargetClientID; + return m_aTargetIdInTeam[SnapTeam] == TargetClientId; } } } @@ -200,7 +200,7 @@ void CDragger::Snap(int SnappingClient) // Send the dragger in its resting position if the player would not otherwise see a dragger beam within its own team for(int i = 0; i < MAX_CLIENTS; i++) { - if(WillDraggerBeamUseDraggerID(i, SnappingClient)) + if(WillDraggerBeamUseDraggerId(i, SnappingClient)) { return; } @@ -222,8 +222,8 @@ void CDragger::Snap(int SnappingClient) if(SnappingClient != SERVER_DEMO_CLIENT && (GameServer()->m_apPlayers[SnappingClient]->GetTeam() == TEAM_SPECTATORS || GameServer()->m_apPlayers[SnappingClient]->IsPaused()) && - GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID != SPEC_FREEVIEW) - pChar = GameServer()->GetPlayerChar(GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID); + GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId != SPEC_FREEVIEW) + pChar = GameServer()->GetPlayerChar(GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId); int Tick = (Server()->Tick() % Server()->TickSpeed()) % 11; if(pChar && m_Layer == LAYER_SWITCH && m_Number > 0 && @@ -237,7 +237,7 @@ void CDragger::Snap(int SnappingClient) StartTick = Server()->Tick(); } - GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetID(), + GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetId(), m_Pos, m_Pos, StartTick, -1, LASERTYPE_DRAGGER, Subtype, m_Number); } diff --git a/src/game/server/entities/dragger.h b/src/game/server/entities/dragger.h index de8abe5a35..77b29703c4 100644 --- a/src/game/server/entities/dragger.h +++ b/src/game/server/entities/dragger.h @@ -35,8 +35,8 @@ class CDragger : public CEntity public: CDragger(CGameWorld *pGameWorld, vec2 Pos, float Strength, bool IgnoreWalls, int Layer = 0, int Number = 0); - void RemoveDraggerBeam(int ClientID); - bool WillDraggerBeamUseDraggerID(int TargetClientID, int SnappingClientID); + void RemoveDraggerBeam(int ClientId); + bool WillDraggerBeamUseDraggerId(int TargetClientId, int SnappingClientId); void Reset() override; void Tick() override; diff --git a/src/game/server/entities/dragger_beam.cpp b/src/game/server/entities/dragger_beam.cpp index 26f03dab73..5dace37f67 100644 --- a/src/game/server/entities/dragger_beam.cpp +++ b/src/game/server/entities/dragger_beam.cpp @@ -10,16 +10,17 @@ #include #include +#include CDraggerBeam::CDraggerBeam(CGameWorld *pGameWorld, CDragger *pDragger, vec2 Pos, float Strength, bool IgnoreWalls, - int ForClientID, int Layer, int Number) : + int ForClientId, int Layer, int Number) : CEntity(pGameWorld, CGameWorld::ENTTYPE_LASER) { m_pDragger = pDragger; m_Pos = Pos; m_Strength = Strength; m_IgnoreWalls = IgnoreWalls; - m_ForClientID = ForClientID; + m_ForClientId = ForClientId; m_Active = true; m_Layer = Layer; m_Number = Number; @@ -36,7 +37,7 @@ void CDraggerBeam::Tick() } // Drag only if the player is reachable and alive - CCharacter *pTarget = GameServer()->GetPlayerChar(m_ForClientID); + CCharacter *pTarget = GameServer()->GetPlayerChar(m_ForClientId); if(!pTarget) { Reset(); @@ -71,8 +72,7 @@ void CDraggerBeam::Tick() // In the center of the dragger a tee does not experience speed-up else if(distance(pTarget->m_Pos, m_Pos) > 28) { - vec2 Temp = pTarget->Core()->m_Vel + (normalize(m_Pos - pTarget->m_Pos) * m_Strength); - pTarget->Core()->m_Vel = ClampVel(pTarget->m_MoveRestrictions, Temp); + pTarget->AddVelocity(normalize(m_Pos - pTarget->m_Pos) * m_Strength); } } @@ -86,7 +86,7 @@ void CDraggerBeam::Reset() m_MarkedForDestroy = true; m_Active = false; - m_pDragger->RemoveDraggerBeam(m_ForClientID); + m_pDragger->RemoveDraggerBeam(m_ForClientId); } void CDraggerBeam::Snap(int SnappingClient) @@ -97,7 +97,7 @@ void CDraggerBeam::Snap(int SnappingClient) } // Only players who can see the player attached to the dragger can see the dragger beam - CCharacter *pTarget = GameServer()->GetPlayerChar(m_ForClientID); + CCharacter *pTarget = GameServer()->GetPlayerChar(m_ForClientId); if(!pTarget || !pTarget->CanSnapCharacter(SnappingClient)) { return; @@ -127,17 +127,22 @@ void CDraggerBeam::Snap(int SnappingClient) StartTick = -1; } - int SnapObjID = GetID(); - if(m_pDragger->WillDraggerBeamUseDraggerID(m_ForClientID, SnappingClient)) + int SnapObjId = GetId(); + if(m_pDragger->WillDraggerBeamUseDraggerId(m_ForClientId, SnappingClient)) { - SnapObjID = m_pDragger->GetID(); + SnapObjId = m_pDragger->GetId(); } - GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), SnapObjID, - TargetPos, m_Pos, StartTick, m_ForClientID, LASERTYPE_DRAGGER, Subtype, m_Number); + GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), SnapObjId, + TargetPos, m_Pos, StartTick, m_ForClientId, LASERTYPE_DRAGGER, Subtype, m_Number); } void CDraggerBeam::SwapClients(int Client1, int Client2) { - m_ForClientID = m_ForClientID == Client1 ? Client2 : m_ForClientID == Client2 ? Client1 : m_ForClientID; + m_ForClientId = m_ForClientId == Client1 ? Client2 : m_ForClientId == Client2 ? Client1 : m_ForClientId; +} + +ESaveResult CDraggerBeam::BlocksSave(int ClientId) +{ + return m_ForClientId == ClientId ? ESaveResult::DRAGGER_ACTIVE : ESaveResult::SUCCESS; } diff --git a/src/game/server/entities/dragger_beam.h b/src/game/server/entities/dragger_beam.h index ce3f65d983..70249d1e84 100644 --- a/src/game/server/entities/dragger_beam.h +++ b/src/game/server/entities/dragger_beam.h @@ -26,12 +26,12 @@ class CDraggerBeam : public CEntity CDragger *m_pDragger; float m_Strength; bool m_IgnoreWalls; - int m_ForClientID; + int m_ForClientId; int m_EvalTick; bool m_Active; public: - CDraggerBeam(CGameWorld *pGameWorld, CDragger *pDragger, vec2 Pos, float Strength, bool IgnoreWalls, int ForClientID, int Layer, int Number); + CDraggerBeam(CGameWorld *pGameWorld, CDragger *pDragger, vec2 Pos, float Strength, bool IgnoreWalls, int ForClientId, int Layer, int Number); void SetPos(vec2 Pos); @@ -39,6 +39,7 @@ class CDraggerBeam : public CEntity void Tick() override; void Snap(int SnappingClient) override; void SwapClients(int Client1, int Client2) override; + ESaveResult BlocksSave(int ClientId) override; }; #endif // GAME_SERVER_ENTITIES_DRAGGER_BEAM_H diff --git a/src/game/server/entities/gun.cpp b/src/game/server/entities/gun.cpp index 5c03604d63..993407f45e 100644 --- a/src/game/server/entities/gun.cpp +++ b/src/game/server/entities/gun.cpp @@ -85,7 +85,7 @@ void CGun::Fire() } // Turrets can only shoot at a speed of sv_plasma_per_sec - const int &TargetClientId = pTarget->GetPlayer()->GetCID(); + const int &TargetClientId = pTarget->GetPlayer()->GetCid(); const bool &TargetIsSolo = pTarget->Teams()->m_Core.GetSolo(TargetClientId); if((TargetIsSolo && m_aLastFireSolo[TargetClientId] + Server()->TickSpeed() / g_Config.m_SvPlasmaPerSec > Server()->Tick()) || @@ -165,8 +165,8 @@ void CGun::Snap(int SnappingClient) if(SnappingClient != SERVER_DEMO_CLIENT && (GameServer()->m_apPlayers[SnappingClient]->GetTeam() == TEAM_SPECTATORS || GameServer()->m_apPlayers[SnappingClient]->IsPaused()) && - GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID != SPEC_FREEVIEW) - pChar = GameServer()->GetPlayerChar(GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID); + GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId != SPEC_FREEVIEW) + pChar = GameServer()->GetPlayerChar(GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId); int Tick = (Server()->Tick() % Server()->TickSpeed()) % 11; if(pChar && m_Layer == LAYER_SWITCH && m_Number > 0 && @@ -176,6 +176,6 @@ void CGun::Snap(int SnappingClient) StartTick = m_EvalTick; } - GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetID(), + GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetId(), m_Pos, m_Pos, StartTick, -1, LASERTYPE_GUN, Subtype, m_Number); } diff --git a/src/game/server/entities/laser.cpp b/src/game/server/entities/laser.cpp index 2907d5b958..ba15d94f5a 100644 --- a/src/game/server/entities/laser.cpp +++ b/src/game/server/entities/laser.cpp @@ -44,9 +44,9 @@ bool CLaser::HitCharacter(vec2 From, vec2 To) bool pDontHitSelf = g_Config.m_SvOldLaser || (m_Bounces == 0 && !m_WasTele); if(pOwnerChar ? (!pOwnerChar->LaserHitDisabled() && m_Type == WEAPON_LASER) || (!pOwnerChar->ShotgunHitDisabled() && m_Type == WEAPON_SHOTGUN) : g_Config.m_SvHit) - pHit = GameServer()->m_World.IntersectCharacter(m_Pos, To, 0.f, At, pDontHitSelf ? pOwnerChar : 0, m_Owner); + pHit = GameWorld()->IntersectCharacter(m_Pos, To, 0.f, At, pDontHitSelf ? pOwnerChar : 0, m_Owner); else - pHit = GameServer()->m_World.IntersectCharacter(m_Pos, To, 0.f, At, pDontHitSelf ? pOwnerChar : 0, m_Owner, pOwnerChar); + pHit = GameWorld()->IntersectCharacter(m_Pos, To, 0.f, At, pDontHitSelf ? pOwnerChar : 0, m_Owner, pOwnerChar); if(!pHit || (pHit == pOwnerChar && g_Config.m_SvOldLaser) || (pHit != pOwnerChar && pOwnerChar ? (pOwnerChar->LaserHitDisabled() && m_Type == WEAPON_LASER) || (pOwnerChar->ShotgunHitDisabled() && m_Type == WEAPON_SHOTGUN) : !g_Config.m_SvHit)) return false; @@ -55,48 +55,46 @@ bool CLaser::HitCharacter(vec2 From, vec2 To) m_Energy = -1; if(m_Type == WEAPON_SHOTGUN) { - vec2 Temp; - float Strength; if(!m_TuneZone) Strength = Tuning()->m_ShotgunStrength; else Strength = TuningList()[m_TuneZone].m_ShotgunStrength; - vec2 &HitPos = pHit->Core()->m_Pos; + const vec2 &HitPos = pHit->Core()->m_Pos; if(!g_Config.m_SvOldLaser) { if(m_PrevPos != HitPos) { - Temp = pHit->Core()->m_Vel + normalize(m_PrevPos - HitPos) * Strength; - pHit->Core()->m_Vel = ClampVel(pHit->m_MoveRestrictions, Temp); + pHit->AddVelocity(normalize(m_PrevPos - HitPos) * Strength); } else { - pHit->Core()->m_Vel = StackedLaserShotgunBugSpeed; + pHit->SetRawVelocity(StackedLaserShotgunBugSpeed); } } else if(g_Config.m_SvOldLaser && pOwnerChar) { if(pOwnerChar->Core()->m_Pos != HitPos) { - Temp = pHit->Core()->m_Vel + normalize(pOwnerChar->Core()->m_Pos - HitPos) * Strength; - pHit->Core()->m_Vel = ClampVel(pHit->m_MoveRestrictions, Temp); + pHit->AddVelocity(normalize(pOwnerChar->Core()->m_Pos - HitPos) * Strength); } else { - pHit->Core()->m_Vel = StackedLaserShotgunBugSpeed; + pHit->SetRawVelocity(StackedLaserShotgunBugSpeed); } } else { - pHit->Core()->m_Vel = ClampVel(pHit->m_MoveRestrictions, pHit->Core()->m_Vel); + // Re-apply move restrictions as a part of 'shotgun bug' reproduction + pHit->ApplyMoveRestrictions(); } } else if(m_Type == WEAPON_LASER) { pHit->UnFreeze(); } + pHit->TakeDamage(vec2(0, 0), 0, m_Owner, m_Type); return true; } @@ -167,10 +165,10 @@ void CLaser::DoBounce() } m_ZeroEnergyBounceInLastTick = Distance == 0.0f; - if(Res == TILE_TELEINWEAPON && !GameServer()->m_pController->m_TeleOuts[z - 1].empty()) + if(Res == TILE_TELEINWEAPON && !GameServer()->Collision()->TeleOuts(z - 1).empty()) { - int TeleOut = GameServer()->m_World.m_Core.RandomOr0(GameServer()->m_pController->m_TeleOuts[z - 1].size()); - m_TelePos = GameServer()->m_pController->m_TeleOuts[z - 1][TeleOut]; + int TeleOut = GameServer()->m_World.m_Core.RandomOr0(GameServer()->Collision()->TeleOuts(z - 1).size()); + m_TelePos = GameServer()->Collision()->TeleOuts(z - 1)[TeleOut]; m_WasTele = true; } else @@ -317,7 +315,7 @@ void CLaser::Snap(int SnappingClient) int SnappingClientVersion = GameServer()->GetClientVersion(SnappingClient); int LaserType = m_Type == WEAPON_LASER ? LASERTYPE_RIFLE : m_Type == WEAPON_SHOTGUN ? LASERTYPE_SHOTGUN : -1; - GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetID(), + GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetId(), m_Pos, m_From, m_EvalTick, m_Owner, LaserType, 0, m_Number); } diff --git a/src/game/server/entities/laser.h b/src/game/server/entities/laser.h index 00654c0ade..736789c0c4 100644 --- a/src/game/server/entities/laser.h +++ b/src/game/server/entities/laser.h @@ -16,7 +16,7 @@ class CLaser : public CEntity virtual void Snap(int SnappingClient) override; virtual void SwapClients(int Client1, int Client2) override; - virtual int GetOwnerID() const override { return m_Owner; } + virtual int GetOwnerId() const override { return m_Owner; } protected: bool HitCharacter(vec2 From, vec2 To); diff --git a/src/game/server/entities/light.cpp b/src/game/server/entities/light.cpp index 42aea3f952..bc862caa0f 100644 --- a/src/game/server/entities/light.cpp +++ b/src/game/server/entities/light.cpp @@ -110,8 +110,8 @@ void CLight::Snap(int SnappingClient) CCharacter *pChr = GameServer()->GetPlayerChar(SnappingClient); - if(SnappingClient != SERVER_DEMO_CLIENT && (GameServer()->m_apPlayers[SnappingClient]->GetTeam() == TEAM_SPECTATORS || GameServer()->m_apPlayers[SnappingClient]->IsPaused()) && GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID != SPEC_FREEVIEW) - pChr = GameServer()->GetPlayerChar(GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID); + if(SnappingClient != SERVER_DEMO_CLIENT && (GameServer()->m_apPlayers[SnappingClient]->GetTeam() == TEAM_SPECTATORS || GameServer()->m_apPlayers[SnappingClient]->IsPaused()) && GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId != SPEC_FREEVIEW) + pChr = GameServer()->GetPlayerChar(GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId); vec2 From = m_Pos; int StartTick = -1; @@ -142,6 +142,6 @@ void CLight::Snap(int SnappingClient) StartTick = Server()->Tick(); } - GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetID(), + GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetId(), m_Pos, From, StartTick, -1, LASERTYPE_FREEZE, 0, m_Number); } diff --git a/src/game/server/entities/pickup.cpp b/src/game/server/entities/pickup.cpp index 6b599a08e5..a622c02bef 100644 --- a/src/game/server/entities/pickup.cpp +++ b/src/game/server/entities/pickup.cpp @@ -142,7 +142,7 @@ void CPickup::Tick() GameServer()->CreateSound(m_Pos, SOUND_PICKUP_SHOTGUN, pChr->TeamMask()); if(pChr->GetPlayer()) - GameServer()->SendWeaponPickup(pChr->GetPlayer()->GetCID(), m_Subtype); + GameServer()->SendWeaponPickup(pChr->GetPlayer()->GetCid(), m_Subtype); } break; @@ -175,15 +175,15 @@ void CPickup::Snap(int SnappingClient) { CCharacter *pChar = GameServer()->GetPlayerChar(SnappingClient); - if(SnappingClient != SERVER_DEMO_CLIENT && (GameServer()->m_apPlayers[SnappingClient]->GetTeam() == TEAM_SPECTATORS || GameServer()->m_apPlayers[SnappingClient]->IsPaused()) && GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID != SPEC_FREEVIEW) - pChar = GameServer()->GetPlayerChar(GameServer()->m_apPlayers[SnappingClient]->m_SpectatorID); + if(SnappingClient != SERVER_DEMO_CLIENT && (GameServer()->m_apPlayers[SnappingClient]->GetTeam() == TEAM_SPECTATORS || GameServer()->m_apPlayers[SnappingClient]->IsPaused()) && GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId != SPEC_FREEVIEW) + pChar = GameServer()->GetPlayerChar(GameServer()->m_apPlayers[SnappingClient]->m_SpectatorId); int Tick = (Server()->Tick() % Server()->TickSpeed()) % 11; if(pChar && pChar->IsAlive() && m_Layer == LAYER_SWITCH && m_Number > 0 && !Switchers()[m_Number].m_aStatus[pChar->Team()] && !Tick) return; } - GameServer()->SnapPickup(CSnapContext(SnappingClientVersion, Sixup), GetID(), m_Pos, m_Type, m_Subtype, m_Number); + GameServer()->SnapPickup(CSnapContext(SnappingClientVersion, Sixup), GetId(), m_Pos, m_Type, m_Subtype, m_Number); } void CPickup::Move() diff --git a/src/game/server/entities/plasma.cpp b/src/game/server/entities/plasma.cpp index f89d5ea38a..9d8978338f 100644 --- a/src/game/server/entities/plasma.cpp +++ b/src/game/server/entities/plasma.cpp @@ -12,14 +12,14 @@ const float PLASMA_ACCEL = 1.1f; CPlasma::CPlasma(CGameWorld *pGameWorld, vec2 Pos, vec2 Dir, bool Freeze, - bool Explosive, int ForClientID) : + bool Explosive, int ForClientId) : CEntity(pGameWorld, CGameWorld::ENTTYPE_LASER) { m_Pos = Pos; m_Core = Dir; m_Freeze = Freeze; m_Explosive = Explosive; - m_ForClientID = ForClientID; + m_ForClientId = ForClientId; m_EvalTick = Server()->Tick(); m_LifeTime = Server()->TickSpeed() * 1.5f; @@ -34,7 +34,7 @@ void CPlasma::Tick() Reset(); return; } - CCharacter *pTarget = GameServer()->GetPlayerChar(m_ForClientID); + CCharacter *pTarget = GameServer()->GetPlayerChar(m_ForClientId); // Without a target, a plasma bullet has no reason to live if(!pTarget) { @@ -58,7 +58,7 @@ bool CPlasma::HitCharacter(CCharacter *pTarget) { vec2 IntersectPos; CCharacter *pHitPlayer = GameServer()->m_World.IntersectCharacter( - m_Pos, m_Pos + m_Core, 0.0f, IntersectPos, 0, m_ForClientID); + m_Pos, m_Pos + m_Core, 0.0f, IntersectPos, 0, m_ForClientId); if(!pHitPlayer) { return false; @@ -76,7 +76,7 @@ bool CPlasma::HitCharacter(CCharacter *pTarget) // Plasma Turrets are very precise weapons only one tee gets speed from it, // other tees near the explosion remain unaffected GameServer()->CreateExplosion( - m_Pos, m_ForClientID, WEAPON_GRENADE, true, pTarget->Team(), pTarget->TeamMask()); + m_Pos, m_ForClientId, WEAPON_GRENADE, true, pTarget->Team(), pTarget->TeamMask()); } Reset(); return true; @@ -92,7 +92,7 @@ bool CPlasma::HitObstacle(CCharacter *pTarget) { // Even in the case of an explosion due to a collision with obstacles, only one player is affected GameServer()->CreateExplosion( - m_Pos, m_ForClientID, WEAPON_GRENADE, true, pTarget->Team(), pTarget->TeamMask()); + m_Pos, m_ForClientId, WEAPON_GRENADE, true, pTarget->Team(), pTarget->TeamMask()); } Reset(); return true; @@ -108,7 +108,7 @@ void CPlasma::Reset() void CPlasma::Snap(int SnappingClient) { // Only players who can see the targeted player can see the plasma bullet - CCharacter *pTarget = GameServer()->GetPlayerChar(m_ForClientID); + CCharacter *pTarget = GameServer()->GetPlayerChar(m_ForClientId); if(!pTarget || !pTarget->CanSnapCharacter(SnappingClient)) { return; @@ -121,11 +121,11 @@ void CPlasma::Snap(int SnappingClient) int SnappingClientVersion = GameServer()->GetClientVersion(SnappingClient); int Subtype = (m_Explosive ? 1 : 0) | (m_Freeze ? 2 : 0); - GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetID(), + GameServer()->SnapLaserObject(CSnapContext(SnappingClientVersion), GetId(), m_Pos, m_Pos, m_EvalTick, -1, LASERTYPE_PLASMA, Subtype, m_Number); } void CPlasma::SwapClients(int Client1, int Client2) { - m_ForClientID = m_ForClientID == Client1 ? Client2 : m_ForClientID == Client2 ? Client1 : m_ForClientID; + m_ForClientId = m_ForClientId == Client1 ? Client2 : m_ForClientId == Client2 ? Client1 : m_ForClientId; } diff --git a/src/game/server/entities/plasma.h b/src/game/server/entities/plasma.h index b961b961ed..f9d94d3e7a 100644 --- a/src/game/server/entities/plasma.h +++ b/src/game/server/entities/plasma.h @@ -25,7 +25,7 @@ class CPlasma : public CEntity vec2 m_Core; int m_Freeze; bool m_Explosive; - int m_ForClientID; + int m_ForClientId; int m_EvalTick; int m_LifeTime; diff --git a/src/game/server/entities/projectile.cpp b/src/game/server/entities/projectile.cpp index c829f6be34..c2bdac75fd 100644 --- a/src/game/server/entities/projectile.cpp +++ b/src/game/server/entities/projectile.cpp @@ -177,6 +177,8 @@ void CProjectile::Tick() pChr->Freeze(); } } + else if(pTargetChr) + pTargetChr->TakeDamage(vec2(0, 0), 0, m_Owner, m_Type); if(pOwnerChar && !GameLayerClipped(ColPos) && ((m_Type == WEAPON_GRENADE && pOwnerChar->HasTelegunGrenade()) || (m_Type == WEAPON_GUN && pOwnerChar->HasTelegunGun()))) @@ -276,10 +278,10 @@ void CProjectile::Tick() z = GameServer()->Collision()->IsTeleport(x); else z = GameServer()->Collision()->IsTeleportWeapon(x); - if(z && !GameServer()->m_pController->m_TeleOuts[z - 1].empty()) + if(z && !GameServer()->Collision()->TeleOuts(z - 1).empty()) { - int TeleOut = GameServer()->m_World.m_Core.RandomOr0(GameServer()->m_pController->m_TeleOuts[z - 1].size()); - m_Pos = GameServer()->m_pController->m_TeleOuts[z - 1][TeleOut]; + int TeleOut = GameServer()->m_World.m_Core.RandomOr0(GameServer()->Collision()->TeleOuts(z - 1).size()); + m_Pos = GameServer()->Collision()->TeleOuts(z - 1)[TeleOut]; m_StartTick = Server()->Tick(); } } @@ -331,7 +333,7 @@ void CProjectile::Snap(int SnappingClient) if(SnappingClientVersion >= VERSION_DDNET_ENTITY_NETOBJS) { - CNetObj_DDNetProjectile *pDDNetProjectile = static_cast(Server()->SnapNewItem(NETOBJTYPE_DDNETPROJECTILE, GetID(), sizeof(CNetObj_DDNetProjectile))); + CNetObj_DDNetProjectile *pDDNetProjectile = static_cast(Server()->SnapNewItem(NETOBJTYPE_DDNETPROJECTILE, GetId(), sizeof(CNetObj_DDNetProjectile))); if(!pDDNetProjectile) { return; @@ -341,7 +343,7 @@ void CProjectile::Snap(int SnappingClient) else if(SnappingClientVersion >= VERSION_DDNET_ANTIPING_PROJECTILE && FillExtraInfoLegacy(&DDRaceProjectile)) { int Type = SnappingClientVersion < VERSION_DDNET_MSG_LEGACY ? (int)NETOBJTYPE_PROJECTILE : NETOBJTYPE_DDRACEPROJECTILE; - void *pProj = Server()->SnapNewItem(Type, GetID(), sizeof(DDRaceProjectile)); + void *pProj = Server()->SnapNewItem(Type, GetId(), sizeof(DDRaceProjectile)); if(!pProj) { return; @@ -350,7 +352,7 @@ void CProjectile::Snap(int SnappingClient) } else { - CNetObj_Projectile *pProj = Server()->SnapNewItem(GetID()); + CNetObj_Projectile *pProj = Server()->SnapNewItem(GetId()); if(!pProj) { return; diff --git a/src/game/server/entities/projectile.h b/src/game/server/entities/projectile.h index 85fae7f18d..1e5133b3de 100644 --- a/src/game/server/entities/projectile.h +++ b/src/game/server/entities/projectile.h @@ -54,7 +54,7 @@ class CProjectile : public CEntity bool FillExtraInfoLegacy(CNetObj_DDRaceProjectile *pProj); void FillExtraInfo(CNetObj_DDNetProjectile *pProj); - virtual int GetOwnerID() const override { return m_Owner; } + virtual int GetOwnerId() const override { return m_Owner; } }; #endif diff --git a/src/game/server/entity.cpp b/src/game/server/entity.cpp index 658302ea94..93390dcafd 100644 --- a/src/game/server/entity.cpp +++ b/src/game/server/entity.cpp @@ -18,7 +18,7 @@ CEntity::CEntity(CGameWorld *pGameWorld, int ObjType, vec2 Pos, int ProximityRad m_ProximityRadius = ProximityRadius; m_MarkedForDestroy = false; - m_ID = Server()->SnapNewID(); + m_Id = Server()->SnapNewId(); m_pPrevTypeEntity = 0; m_pNextTypeEntity = 0; @@ -27,7 +27,7 @@ CEntity::CEntity(CGameWorld *pGameWorld, int ObjType, vec2 Pos, int ProximityRad CEntity::~CEntity() { GameWorld()->RemoveEntity(this); - Server()->SnapFreeID(m_ID); + Server()->SnapFreeId(m_Id); } bool CEntity::NetworkClipped(int SnappingClient) const diff --git a/src/game/server/entity.h b/src/game/server/entity.h index 69e9a43856..b3cf409bc2 100644 --- a/src/game/server/entity.h +++ b/src/game/server/entity.h @@ -8,6 +8,7 @@ #include #include "gameworld.h" +#include "save.h" class CCollision; class CGameContext; @@ -29,7 +30,7 @@ class CEntity CGameWorld *m_pGameWorld; CCollision *m_pCCollision; - int m_ID; + int m_Id; int m_ObjType; /* @@ -50,7 +51,7 @@ class CEntity vec2 m_Pos; /* Getters */ - int GetID() const { return m_ID; } + int GetId() const { return m_Id; } /* Constructor */ CEntity(CGameWorld *pGameWorld, int Objtype, vec2 Pos = vec2(0, 0), int ProximityRadius = 0); @@ -122,6 +123,12 @@ class CEntity */ virtual void Snap(int SnappingClient) {} + /* + Function: PostSnap + Called after all clients received their snapshot. + */ + virtual void PostSnap() {} + /* Function: SwapClients Called when two players have swapped their client ids. @@ -133,14 +140,23 @@ class CEntity virtual void SwapClients(int Client1, int Client2) {} /* - Function GetOwnerID + Function: BlocksSave + Called to check if a team can be saved + + Arguments: + ClientId - Client ID + */ + virtual ESaveResult BlocksSave(int ClientId) { return ESaveResult::SUCCESS; } + + /* + Function GetOwnerId Returns: - ClientID of the initiator from this entity. -1 created by map. + ClientId of the initiator from this entity. -1 created by map. This is used by save/load to remove related entities to the tee. CCharacter should not return the PlayerId, because they get handled separately in save/load code. */ - virtual int GetOwnerID() const { return -1; } + virtual int GetOwnerId() const { return -1; } /* Function: NetworkClipped diff --git a/src/game/server/eventhandler.cpp b/src/game/server/eventhandler.cpp index bff14e5a85..ddee723277 100644 --- a/src/game/server/eventhandler.cpp +++ b/src/game/server/eventhandler.cpp @@ -81,11 +81,17 @@ void CEventHandler::EventToSixup(int *pType, int *pSize, const char **ppData) pEvent7->m_X = pEvent->m_X; pEvent7->m_Y = pEvent->m_Y; + pEvent7->m_ClientId = 0; + pEvent7->m_Angle = 0; + // This will need some work, perhaps an event wrapper for damageind, // a scan of the event array to merge multiple damageinds // or a separate array of "damage ind" events that's added in while snapping pEvent7->m_HealthAmount = 1; + pEvent7->m_ArmorAmount = 0; + pEvent7->m_Self = 0; + *ppData = s_aEventStore; } else if(*pType == NETEVENTTYPE_SOUNDGLOBAL) // No more global sounds for the server @@ -96,7 +102,7 @@ void CEventHandler::EventToSixup(int *pType, int *pSize, const char **ppData) *pType = -protocol7::NETEVENTTYPE_SOUNDWORLD; *pSize = sizeof(*pEvent7); - pEvent7->m_SoundID = pEvent->m_SoundID; + pEvent7->m_SoundId = pEvent->m_SoundId; pEvent7->m_X = pEvent->m_X; pEvent7->m_Y = pEvent->m_Y; diff --git a/src/game/server/eventhandler.h b/src/game/server/eventhandler.h index 4390109b2f..25b05f9820 100644 --- a/src/game/server/eventhandler.h +++ b/src/game/server/eventhandler.h @@ -36,7 +36,7 @@ class CEventHandler template T *Create(CClientMask Mask = CClientMask().set()) { - return static_cast(Create(T::ms_MsgID, sizeof(T), Mask)); + return static_cast(Create(T::ms_MsgId, sizeof(T), Mask)); } void Clear(); diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index e55cfd4db9..3b0356a8e7 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -38,13 +39,13 @@ class CClientChatLogger : public ILogger { CGameContext *m_pGameServer; - int m_ClientID; + int m_ClientId; ILogger *m_pOuterLogger; public: - CClientChatLogger(CGameContext *pGameServer, int ClientID, ILogger *pOuterLogger) : + CClientChatLogger(CGameContext *pGameServer, int ClientId, ILogger *pOuterLogger) : m_pGameServer(pGameServer), - m_ClientID(ClientID), + m_ClientId(ClientId), m_pOuterLogger(pOuterLogger) { } @@ -59,7 +60,7 @@ void CClientChatLogger::Log(const CLogMessage *pMessage) { return; } - m_pGameServer->SendChatTarget(m_ClientID, pMessage->Message()); + m_pGameServer->SendChatTarget(m_ClientId, pMessage->Message()); } else { @@ -104,6 +105,17 @@ void CGameContext::Construct(int Resetting) if(Resetting == NO_RESET) { + for(auto &pSavedTee : m_apSavedTees) + pSavedTee = nullptr; + + for(auto &pSavedTeleTee : m_apSavedTeleTees) + pSavedTeleTee = nullptr; + + for(auto &pSavedTeam : m_apSavedTeams) + pSavedTeam = nullptr; + + std::fill(std::begin(m_aTeamMapping), std::end(m_aTeamMapping), -1); + m_NonEmptySince = 0; m_pVoteOptionHeap = new CHeap(); } @@ -118,7 +130,18 @@ void CGameContext::Destruct(int Resetting) delete pPlayer; if(Resetting == NO_RESET) + { + for(auto &pSavedTee : m_apSavedTees) + delete pSavedTee; + + for(auto &pSavedTeleTee : m_apSavedTeleTees) + delete pSavedTeleTee; + + for(auto &pSavedTeam : m_apSavedTeams) + delete pSavedTeam; + delete m_pVoteOptionHeap; + } if(m_pScore) { @@ -167,26 +190,26 @@ void CGameContext::TeeHistorianWrite(const void *pData, int DataSize, void *pUse aio_write(pSelf->m_pTeeHistorianFile, pData, DataSize); } -void CGameContext::CommandCallback(int ClientID, int FlagMask, const char *pCmd, IConsole::IResult *pResult, void *pUser) +void CGameContext::CommandCallback(int ClientId, int FlagMask, const char *pCmd, IConsole::IResult *pResult, void *pUser) { CGameContext *pSelf = (CGameContext *)pUser; if(pSelf->m_TeeHistorianActive) { - pSelf->m_TeeHistorian.RecordConsoleCommand(ClientID, FlagMask, pCmd, pResult); + pSelf->m_TeeHistorian.RecordConsoleCommand(ClientId, FlagMask, pCmd, pResult); } } -CNetObj_PlayerInput CGameContext::GetLastPlayerInput(int ClientID) const +CNetObj_PlayerInput CGameContext::GetLastPlayerInput(int ClientId) const { - dbg_assert(0 <= ClientID && ClientID < MAX_CLIENTS, "invalid ClientID"); - return m_aLastPlayerInput[ClientID]; + dbg_assert(0 <= ClientId && ClientId < MAX_CLIENTS, "invalid ClientId"); + return m_aLastPlayerInput[ClientId]; } -class CCharacter *CGameContext::GetPlayerChar(int ClientID) +class CCharacter *CGameContext::GetPlayerChar(int ClientId) { - if(ClientID < 0 || ClientID >= MAX_CLIENTS || !m_apPlayers[ClientID]) + if(ClientId < 0 || ClientId >= MAX_CLIENTS || !m_apPlayers[ClientId]) return 0; - return m_apPlayers[ClientID]->GetCharacter(); + return m_apPlayers[ClientId]->GetCharacter(); } bool CGameContext::EmulateBug(int Bug) @@ -257,7 +280,6 @@ void CGameContext::CreateDamageInd(vec2 Pos, float Angle, int Amount, CClientMas void CGameContext::CreateHammerHit(vec2 Pos, CClientMask Mask) { - // create the event CNetEvent_HammerHit *pEvent = m_Events.Create(Mask); if(pEvent) { @@ -301,7 +323,7 @@ void CGameContext::CreateExplosion(vec2 Pos, int Owner, int Weapon, bool NoDamag if(!(int)Dmg) continue; - if((GetPlayerChar(Owner) ? !GetPlayerChar(Owner)->GrenadeHitDisabled() : g_Config.m_SvHit) || NoDamage || Owner == pChr->GetPlayer()->GetCID()) + if((GetPlayerChar(Owner) ? !GetPlayerChar(Owner)->GrenadeHitDisabled() : g_Config.m_SvHit) || NoDamage || Owner == pChr->GetPlayer()->GetCid()) { if(Owner != -1 && pChr->IsAlive() && !pChr->CanCollide(Owner)) continue; @@ -326,7 +348,6 @@ void CGameContext::CreateExplosion(vec2 Pos, int Owner, int Weapon, bool NoDamag void CGameContext::CreatePlayerSpawn(vec2 Pos, CClientMask Mask) { - // create the event CNetEvent_Spawn *pEvent = m_Events.Create(Mask); if(pEvent) { @@ -335,15 +356,34 @@ void CGameContext::CreatePlayerSpawn(vec2 Pos, CClientMask Mask) } } -void CGameContext::CreateDeath(vec2 Pos, int ClientID, CClientMask Mask) +void CGameContext::CreateDeath(vec2 Pos, int ClientId, CClientMask Mask) { - // create the event CNetEvent_Death *pEvent = m_Events.Create(Mask); if(pEvent) { pEvent->m_X = (int)Pos.x; pEvent->m_Y = (int)Pos.y; - pEvent->m_ClientID = ClientID; + pEvent->m_ClientId = ClientId; + } +} + +void CGameContext::CreateBirthdayEffect(vec2 Pos, CClientMask Mask) +{ + CNetEvent_Birthday *pEvent = m_Events.Create(Mask); + if(pEvent) + { + pEvent->m_X = (int)Pos.x; + pEvent->m_Y = (int)Pos.y; + } +} + +void CGameContext::CreateFinishEffect(vec2 Pos, CClientMask Mask) +{ + CNetEvent_Finish *pEvent = m_Events.Create(Mask); + if(pEvent) + { + pEvent->m_X = (int)Pos.x; + pEvent->m_Y = (int)Pos.y; } } @@ -358,7 +398,7 @@ void CGameContext::CreateSound(vec2 Pos, int Sound, CClientMask Mask) { pEvent->m_X = (int)Pos.x; pEvent->m_Y = (int)Pos.y; - pEvent->m_SoundID = Sound; + pEvent->m_SoundId = Sound; } } @@ -368,7 +408,7 @@ void CGameContext::CreateSoundGlobal(int Sound, int Target) const return; CNetMsg_Sv_SoundGlobal Msg; - Msg.m_SoundID = Sound; + Msg.m_SoundId = Sound; if(Target == -2) Server()->SendPackMsg(&Msg, MSGFLAG_NOSEND, -1); else @@ -388,8 +428,8 @@ void CGameContext::SnapSwitchers(int SnappingClient) CPlayer *pPlayer = SnappingClient != SERVER_DEMO_CLIENT ? m_apPlayers[SnappingClient] : 0; int Team = pPlayer && pPlayer->GetCharacter() ? pPlayer->GetCharacter()->Team() : 0; - if(pPlayer && (pPlayer->GetTeam() == TEAM_SPECTATORS || pPlayer->IsPaused()) && pPlayer->m_SpectatorID != SPEC_FREEVIEW && m_apPlayers[pPlayer->m_SpectatorID] && m_apPlayers[pPlayer->m_SpectatorID]->GetCharacter()) - Team = m_apPlayers[pPlayer->m_SpectatorID]->GetCharacter()->Team(); + if(pPlayer && (pPlayer->GetTeam() == TEAM_SPECTATORS || pPlayer->IsPaused()) && pPlayer->m_SpectatorId != SPEC_FREEVIEW && m_apPlayers[pPlayer->m_SpectatorId] && m_apPlayers[pPlayer->m_SpectatorId]->GetCharacter()) + Team = m_apPlayers[pPlayer->m_SpectatorId]->GetCharacter()->Team(); if(Team == TEAM_SUPER) return; @@ -434,11 +474,11 @@ void CGameContext::SnapSwitchers(int SnappingClient) } } -bool CGameContext::SnapLaserObject(const CSnapContext &Context, int SnapID, const vec2 &To, const vec2 &From, int StartTick, int Owner, int LaserType, int Subtype, int SwitchNumber) const +bool CGameContext::SnapLaserObject(const CSnapContext &Context, int SnapId, const vec2 &To, const vec2 &From, int StartTick, int Owner, int LaserType, int Subtype, int SwitchNumber) const { if(Context.GetClientVersion() >= VERSION_DDNET_MULTI_LASER) { - CNetObj_DDNetLaser *pObj = Server()->SnapNewItem(SnapID); + CNetObj_DDNetLaser *pObj = Server()->SnapNewItem(SnapId); if(!pObj) return false; @@ -455,7 +495,7 @@ bool CGameContext::SnapLaserObject(const CSnapContext &Context, int SnapID, cons } else { - CNetObj_Laser *pObj = Server()->SnapNewItem(SnapID); + CNetObj_Laser *pObj = Server()->SnapNewItem(SnapId); if(!pObj) return false; @@ -469,25 +509,21 @@ bool CGameContext::SnapLaserObject(const CSnapContext &Context, int SnapID, cons return true; } -bool CGameContext::SnapPickup(const CSnapContext &Context, int SnapID, const vec2 &Pos, int Type, int SubType, int SwitchNumber) const +bool CGameContext::SnapPickup(const CSnapContext &Context, int SnapId, const vec2 &Pos, int Type, int SubType, int SwitchNumber) const { if(Context.IsSixup()) { - protocol7::CNetObj_Pickup *pPickup = Server()->SnapNewItem(SnapID); + protocol7::CNetObj_Pickup *pPickup = Server()->SnapNewItem(SnapId); if(!pPickup) return false; pPickup->m_X = (int)Pos.x; pPickup->m_Y = (int)Pos.y; - - if(Type == POWERUP_WEAPON) - pPickup->m_Type = SubType == WEAPON_SHOTGUN ? protocol7::PICKUP_SHOTGUN : SubType == WEAPON_GRENADE ? protocol7::PICKUP_GRENADE : protocol7::PICKUP_LASER; - else if(Type == POWERUP_NINJA) - pPickup->m_Type = protocol7::PICKUP_NINJA; + pPickup->m_Type = PickupType_SixToSeven(Type, SubType); } else if(Context.GetClientVersion() >= VERSION_DDNET_ENTITY_NETOBJS) { - CNetObj_DDNetPickup *pPickup = Server()->SnapNewItem(SnapID); + CNetObj_DDNetPickup *pPickup = Server()->SnapNewItem(SnapId); if(!pPickup) return false; @@ -499,7 +535,7 @@ bool CGameContext::SnapPickup(const CSnapContext &Context, int SnapID, const vec } else { - CNetObj_Pickup *pPickup = Server()->SnapNewItem(SnapID); + CNetObj_Pickup *pPickup = Server()->SnapNewItem(SnapId); if(!pPickup) return false; @@ -520,34 +556,34 @@ bool CGameContext::SnapPickup(const CSnapContext &Context, int SnapID, const vec return true; } -void CGameContext::CallVote(int ClientID, const char *pDesc, const char *pCmd, const char *pReason, const char *pChatmsg, const char *pSixupDesc) +void CGameContext::CallVote(int ClientId, const char *pDesc, const char *pCmd, const char *pReason, const char *pChatmsg, const char *pSixupDesc) { // check if a vote is already running if(m_VoteCloseTime) return; int64_t Now = Server()->Tick(); - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; if(!pPlayer) return; - SendChat(-1, CGameContext::CHAT_ALL, pChatmsg, -1, CHAT_SIX); + SendChat(-1, TEAM_ALL, pChatmsg, -1, FLAG_SIX); if(!pSixupDesc) pSixupDesc = pDesc; - m_VoteCreator = ClientID; + m_VoteCreator = ClientId; StartVote(pDesc, pCmd, pReason, pSixupDesc); pPlayer->m_Vote = 1; pPlayer->m_VotePos = m_VotePos = 1; pPlayer->m_LastVoteCall = Now; } -void CGameContext::SendChatTarget(int To, const char *pText, int Flags) const +void CGameContext::SendChatTarget(int To, const char *pText, int VersionFlags) const { CNetMsg_Sv_Chat Msg; Msg.m_Team = 0; - Msg.m_ClientID = -1; + Msg.m_ClientId = -1; Msg.m_pMessage = pText; if(g_Config.m_SvDemoChat) @@ -557,8 +593,8 @@ void CGameContext::SendChatTarget(int To, const char *pText, int Flags) const { for(int i = 0; i < Server()->MaxClients(); i++) { - if(!((Server()->IsSixup(i) && (Flags & CHAT_SIXUP)) || - (!Server()->IsSixup(i) && (Flags & CHAT_SIX)))) + if(!((Server()->IsSixup(i) && (VersionFlags & FLAG_SIXUP)) || + (!Server()->IsSixup(i) && (VersionFlags & FLAG_SIX)))) continue; Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i); @@ -566,8 +602,8 @@ void CGameContext::SendChatTarget(int To, const char *pText, int Flags) const } else { - if(!((Server()->IsSixup(To) && (Flags & CHAT_SIXUP)) || - (!Server()->IsSixup(To) && (Flags & CHAT_SIX)))) + if(!((Server()->IsSixup(To) && (VersionFlags & FLAG_SIXUP)) || + (!Server()->IsSixup(To) && (VersionFlags & FLAG_SIX)))) return; Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, To); @@ -581,31 +617,31 @@ void CGameContext::SendChatTeam(int Team, const char *pText) const SendChatTarget(i, pText); } -void CGameContext::SendChat(int ChatterClientID, int Team, const char *pText, int SpamProtectionClientID, int Flags) +void CGameContext::SendChat(int ChatterClientId, int Team, const char *pText, int SpamProtectionClientId, int VersionFlags) { - if(SpamProtectionClientID >= 0 && SpamProtectionClientID < MAX_CLIENTS) - if(ProcessSpamProtection(SpamProtectionClientID)) + if(SpamProtectionClientId >= 0 && SpamProtectionClientId < MAX_CLIENTS) + if(ProcessSpamProtection(SpamProtectionClientId)) return; char aBuf[256], aText[256]; str_copy(aText, pText, sizeof(aText)); - if(ChatterClientID >= 0 && ChatterClientID < MAX_CLIENTS) - str_format(aBuf, sizeof(aBuf), "%d:%d:%s: %s", ChatterClientID, Team, Server()->ClientName(ChatterClientID), aText); - else if(ChatterClientID == -2) + if(ChatterClientId >= 0 && ChatterClientId < MAX_CLIENTS) + str_format(aBuf, sizeof(aBuf), "%d:%d:%s: %s", ChatterClientId, Team, Server()->ClientName(ChatterClientId), aText); + else if(ChatterClientId == -2) { str_format(aBuf, sizeof(aBuf), "### %s", aText); str_copy(aText, aBuf, sizeof(aText)); - ChatterClientID = -1; + ChatterClientId = -1; } else str_format(aBuf, sizeof(aBuf), "*** %s", aText); - Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, Team != CHAT_ALL ? "teamchat" : "chat", aBuf); + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, Team != TEAM_ALL ? "teamchat" : "chat", aBuf); - if(Team == CHAT_ALL) + if(Team == TEAM_ALL) { CNetMsg_Sv_Chat Msg; Msg.m_Team = 0; - Msg.m_ClientID = ChatterClientID; + Msg.m_ClientId = ChatterClientId; Msg.m_pMessage = aText; // pack one for the recording only @@ -617,22 +653,22 @@ void CGameContext::SendChat(int ChatterClientID, int Team, const char *pText, in { if(!m_apPlayers[i]) continue; - bool Send = (Server()->IsSixup(i) && (Flags & CHAT_SIXUP)) || - (!Server()->IsSixup(i) && (Flags & CHAT_SIX)); + bool Send = (Server()->IsSixup(i) && (VersionFlags & FLAG_SIXUP)) || + (!Server()->IsSixup(i) && (VersionFlags & FLAG_SIX)); if(!m_apPlayers[i]->m_DND && Send) Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i); } str_format(aBuf, sizeof(aBuf), "Chat: %s", aText); - LogEvent(aBuf, ChatterClientID); + LogEvent(aBuf, ChatterClientId); } else { CTeamsCore *pTeams = &m_pController->Teams().m_Core; CNetMsg_Sv_Chat Msg; Msg.m_Team = 1; - Msg.m_ClientID = ChatterClientID; + Msg.m_ClientId = ChatterClientId; Msg.m_pMessage = aText; // pack one for the recording only @@ -644,16 +680,16 @@ void CGameContext::SendChat(int ChatterClientID, int Team, const char *pText, in { if(m_apPlayers[i] != 0) { - if(Team == CHAT_SPEC) + if(Team == TEAM_SPECTATORS) { - if(m_apPlayers[i]->GetTeam() == CHAT_SPEC) + if(m_apPlayers[i]->GetTeam() == TEAM_SPECTATORS) { Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i); } } else { - if(pTeams->Team(i) == Team && m_apPlayers[i]->GetTeam() != CHAT_SPEC) + if(pTeams->Team(i) == Team && m_apPlayers[i]->GetTeam() != TEAM_SPECTATORS) { Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i); } @@ -663,39 +699,39 @@ void CGameContext::SendChat(int ChatterClientID, int Team, const char *pText, in } } -void CGameContext::SendStartWarning(int ClientID, const char *pMessage) +void CGameContext::SendStartWarning(int ClientId, const char *pMessage) { - CCharacter *pChr = GetPlayerChar(ClientID); + CCharacter *pChr = GetPlayerChar(ClientId); if(pChr && pChr->m_LastStartWarning < Server()->Tick() - 3 * Server()->TickSpeed()) { - SendChatTarget(ClientID, pMessage); + SendChatTarget(ClientId, pMessage); pChr->m_LastStartWarning = Server()->Tick(); } } -void CGameContext::SendEmoticon(int ClientID, int Emoticon, int TargetClientID) const +void CGameContext::SendEmoticon(int ClientId, int Emoticon, int TargetClientId) const { CNetMsg_Sv_Emoticon Msg; - Msg.m_ClientID = ClientID; + Msg.m_ClientId = ClientId; Msg.m_Emoticon = Emoticon; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, TargetClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, TargetClientId); } -void CGameContext::SendWeaponPickup(int ClientID, int Weapon) const +void CGameContext::SendWeaponPickup(int ClientId, int Weapon) const { CNetMsg_Sv_WeaponPickup Msg; Msg.m_Weapon = Weapon; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId); } -void CGameContext::SendMotd(int ClientID) const +void CGameContext::SendMotd(int ClientId) const { CNetMsg_Sv_Motd Msg; Msg.m_pMessage = g_Config.m_SvMotd; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId); } -void CGameContext::SendSettings(int ClientID) const +void CGameContext::SendSettings(int ClientId) const { protocol7::CNetMsg_Sv_ServerSettings Msg; Msg.m_KickVote = g_Config.m_SvVoteKick; @@ -703,19 +739,19 @@ void CGameContext::SendSettings(int ClientID) const Msg.m_SpecVote = g_Config.m_SvVoteSpectate; Msg.m_TeamLock = 0; Msg.m_TeamBalance = 0; - Msg.m_PlayerSlots = g_Config.m_SvMaxClients - g_Config.m_SvSpectatorSlots; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); + Msg.m_PlayerSlots = Server()->MaxClients() - g_Config.m_SvSpectatorSlots; + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); } -void CGameContext::SendBroadcast(const char *pText, int ClientID, bool IsImportant) +void CGameContext::SendBroadcast(const char *pText, int ClientId, bool IsImportant) { CNetMsg_Sv_Broadcast Msg; Msg.m_pMessage = pText; - if(ClientID == -1) + if(ClientId == -1) { dbg_assert(IsImportant, "broadcast messages to all players must be important"); - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId); for(auto &pPlayer : m_apPlayers) { @@ -728,22 +764,21 @@ void CGameContext::SendBroadcast(const char *pText, int ClientID, bool IsImporta return; } - if(!m_apPlayers[ClientID]) + if(!m_apPlayers[ClientId]) return; - if(!IsImportant && m_apPlayers[ClientID]->m_LastBroadcastImportance && m_apPlayers[ClientID]->m_LastBroadcast > Server()->Tick() - Server()->TickSpeed() * 10) + if(!IsImportant && m_apPlayers[ClientId]->m_LastBroadcastImportance && m_apPlayers[ClientId]->m_LastBroadcast > Server()->Tick() - Server()->TickSpeed() * 10) return; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); - m_apPlayers[ClientID]->m_LastBroadcast = Server()->Tick(); - m_apPlayers[ClientID]->m_LastBroadcastImportance = IsImportant; + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId); + m_apPlayers[ClientId]->m_LastBroadcast = Server()->Tick(); + m_apPlayers[ClientId]->m_LastBroadcastImportance = IsImportant; } void CGameContext::StartVote(const char *pDesc, const char *pCommand, const char *pReason, const char *pSixupDesc) { // reset votes m_VoteEnforce = VOTE_ENFORCE_UNKNOWN; - m_VoteEnforcer = -1; for(auto &pPlayer : m_apPlayers) { if(pPlayer) @@ -769,12 +804,12 @@ void CGameContext::EndVote() SendVoteSet(-1); } -void CGameContext::SendVoteSet(int ClientID) +void CGameContext::SendVoteSet(int ClientId) { ::CNetMsg_Sv_VoteSet Msg6; protocol7::CNetMsg_Sv_VoteSet Msg7; - Msg7.m_ClientID = m_VoteCreator; + Msg7.m_ClientId = m_VoteCreator; if(m_VoteCloseTime) { Msg6.m_Timeout = Msg7.m_Timeout = (m_VoteCloseTime - time_get()) / time_freq(); @@ -801,14 +836,14 @@ void CGameContext::SendVoteSet(int ClientID) Type = protocol7::VOTE_END_FAIL; else if(m_VoteEnforce == VOTE_ENFORCE_YES || m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN) Type = protocol7::VOTE_END_PASS; - else if(m_VoteEnforce == VOTE_ENFORCE_ABORT) + else if(m_VoteEnforce == VOTE_ENFORCE_ABORT || m_VoteEnforce == VOTE_ENFORCE_CANCEL) Type = protocol7::VOTE_END_ABORT; if(m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN || m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN) - Msg7.m_ClientID = -1; + Msg7.m_ClientId = -1; } - if(ClientID == -1) + if(ClientId == -1) { for(int i = 0; i < Server()->MaxClients(); i++) { @@ -822,16 +857,16 @@ void CGameContext::SendVoteSet(int ClientID) } else { - if(!Server()->IsSixup(ClientID)) - Server()->SendPackMsg(&Msg6, MSGFLAG_VITAL, ClientID); + if(!Server()->IsSixup(ClientId)) + Server()->SendPackMsg(&Msg6, MSGFLAG_VITAL, ClientId); else - Server()->SendPackMsg(&Msg7, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&Msg7, MSGFLAG_VITAL, ClientId); } } -void CGameContext::SendVoteStatus(int ClientID, int Total, int Yes, int No) +void CGameContext::SendVoteStatus(int ClientId, int Total, int Yes, int No) { - if(ClientID == -1) + if(ClientId == -1) { for(int i = 0; i < MAX_CLIENTS; ++i) if(Server()->ClientIngame(i)) @@ -839,7 +874,7 @@ void CGameContext::SendVoteStatus(int ClientID, int Total, int Yes, int No) return; } - if(Total > VANILLA_MAX_CLIENTS && m_apPlayers[ClientID] && m_apPlayers[ClientID]->GetClientVersion() <= VERSION_DDRACE) + if(Total > VANILLA_MAX_CLIENTS && m_apPlayers[ClientId] && m_apPlayers[ClientId]->GetClientVersion() <= VERSION_DDRACE) { Yes = (Yes * VANILLA_MAX_CLIENTS) / (float)Total; No = (No * VANILLA_MAX_CLIENTS) / (float)Total; @@ -852,13 +887,13 @@ void CGameContext::SendVoteStatus(int ClientID, int Total, int Yes, int No) Msg.m_No = No; Msg.m_Pass = Total - (Yes + No); - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId); } -void CGameContext::AbortVoteKickOnDisconnect(int ClientID) +void CGameContext::AbortVoteKickOnDisconnect(int ClientId) { - if(m_VoteCloseTime && ((str_startswith(m_aVoteCommand, "kick ") && str_toint(&m_aVoteCommand[5]) == ClientID) || - (str_startswith(m_aVoteCommand, "set_team ") && str_toint(&m_aVoteCommand[9]) == ClientID))) + if(m_VoteCloseTime && ((str_startswith(m_aVoteCommand, "kick ") && str_toint(&m_aVoteCommand[5]) == ClientId) || + (str_startswith(m_aVoteCommand, "set_team ") && str_toint(&m_aVoteCommand[9]) == ClientId))) m_VoteEnforce = VOTE_ENFORCE_ABORT; } @@ -881,9 +916,9 @@ void CGameContext::CheckPureTuning() } } -void CGameContext::SendTuningParams(int ClientID, int Zone) +void CGameContext::SendTuningParams(int ClientId, int Zone) { - if(ClientID == -1) + if(ClientId == -1) { for(int i = 0; i < MAX_CLIENTS; ++i) { @@ -914,35 +949,35 @@ void CGameContext::SendTuningParams(int ClientID, int Zone) for(unsigned i = 0; i < sizeof(m_Tuning) / sizeof(int); i++) { - if(m_apPlayers[ClientID] && m_apPlayers[ClientID]->GetCharacter()) + if(m_apPlayers[ClientId] && m_apPlayers[ClientId]->GetCharacter()) { if((i == 30) // laser_damage is removed from 0.7 - && (Server()->IsSixup(ClientID))) + && (Server()->IsSixup(ClientId))) { continue; } else if((i == 31) // collision - && (m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_SOLO || m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOCOLL)) + && (m_apPlayers[ClientId]->GetCharacter()->NeededFaketuning() & FAKETUNE_SOLO || m_apPlayers[ClientId]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOCOLL)) { Msg.AddInt(0); } else if((i == 32) // hooking - && (m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_SOLO || m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOHOOK)) + && (m_apPlayers[ClientId]->GetCharacter()->NeededFaketuning() & FAKETUNE_SOLO || m_apPlayers[ClientId]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOHOOK)) { Msg.AddInt(0); } else if((i == 3) // ground jump impulse - && m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOJUMP) + && m_apPlayers[ClientId]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOJUMP) { Msg.AddInt(0); } else if((i == 33) // jetpack - && !(m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_JETPACK)) + && !(m_apPlayers[ClientId]->GetCharacter()->NeededFaketuning() & FAKETUNE_JETPACK)) { Msg.AddInt(0); } else if((i == 36) // hammer hit - && m_apPlayers[ClientID]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOHAMMER) + && m_apPlayers[ClientId]->GetCharacter()->NeededFaketuning() & FAKETUNE_NOHAMMER) { Msg.AddInt(0); } @@ -954,7 +989,7 @@ void CGameContext::SendTuningParams(int ClientID, int Zone) else Msg.AddInt(pParams[i]); // if everything is normal just send true tunings } - Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientID); + Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientId); } void CGameContext::OnPreTickTeehistorian() @@ -1031,7 +1066,14 @@ void CGameContext::OnTick() // abort the kick-vote on player-leave if(m_VoteEnforce == VOTE_ENFORCE_ABORT) { - SendChat(-1, CGameContext::CHAT_ALL, "Vote aborted"); + SendChat(-1, TEAM_ALL, "Vote aborted"); + EndVote(); + } + else if(m_VoteEnforce == VOTE_ENFORCE_CANCEL) + { + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "'%s' canceled their vote", Server()->ClientName(m_VoteCreator)); + SendChat(-1, TEAM_ALL, aBuf); EndVote(); } else @@ -1041,16 +1083,16 @@ void CGameContext::OnTick() if(m_VoteUpdate) { // count votes - char aaBuf[MAX_CLIENTS][NETADDR_MAXSTRSIZE] = {{0}}, *pIP = NULL; + char aaBuf[MAX_CLIENTS][NETADDR_MAXSTRSIZE] = {{0}}, *pIp = NULL; bool SinglePlayer = true; for(int i = 0; i < MAX_CLIENTS; i++) { if(m_apPlayers[i]) { Server()->GetClientAddr(i, aaBuf[i], NETADDR_MAXSTRSIZE); - if(!pIP) - pIP = aaBuf[i]; - else if(SinglePlayer && str_comp(pIP, aaBuf[i])) + if(!pIp) + pIp = aaBuf[i]; + else if(SinglePlayer && str_comp(pIp, aaBuf[i])) SinglePlayer = false; } } @@ -1156,34 +1198,34 @@ void CGameContext::OnTick() if(m_VoteEnforce == VOTE_ENFORCE_YES && !(PlayerModerating() && (IsKickVote() || IsSpecVote()) && time_get() < m_VoteCloseTime)) { - Server()->SetRconCID(IServer::RCON_CID_VOTE); + Server()->SetRconCid(IServer::RCON_CID_VOTE); Console()->ExecuteLine(m_aVoteCommand); - Server()->SetRconCID(IServer::RCON_CID_SERV); + Server()->SetRconCid(IServer::RCON_CID_SERV); EndVote(); - SendChat(-1, CGameContext::CHAT_ALL, "Vote passed", -1, CHAT_SIX); + SendChat(-1, TEAM_ALL, "Vote passed", -1, FLAG_SIX); if(m_apPlayers[m_VoteCreator] && !IsKickVote() && !IsSpecVote()) m_apPlayers[m_VoteCreator]->m_LastVoteCall = 0; } else if(m_VoteEnforce == VOTE_ENFORCE_YES_ADMIN) { - Console()->ExecuteLine(m_aVoteCommand, m_VoteEnforcer); - SendChat(-1, CGameContext::CHAT_ALL, "Vote passed enforced by authorized player", -1, CHAT_SIX); + Console()->ExecuteLine(m_aVoteCommand, m_VoteCreator); + SendChat(-1, TEAM_ALL, "Vote passed enforced by authorized player", -1, FLAG_SIX); EndVote(); } else if(m_VoteEnforce == VOTE_ENFORCE_NO_ADMIN) { EndVote(); - SendChat(-1, CGameContext::CHAT_ALL, "Vote failed enforced by authorized player", -1, CHAT_SIX); + SendChat(-1, TEAM_ALL, "Vote failed enforced by authorized player", -1, FLAG_SIX); } //else if(m_VoteEnforce == VOTE_ENFORCE_NO || time_get() > m_VoteCloseTime) else if(m_VoteEnforce == VOTE_ENFORCE_NO || (time_get() > m_VoteCloseTime && g_Config.m_SvVoteMajority)) { EndVote(); if(VetoStop || (m_VoteWillPass && Veto)) - SendChat(-1, CGameContext::CHAT_ALL, "Vote failed because of veto. Find an empty server instead", -1, CHAT_SIX); + SendChat(-1, TEAM_ALL, "Vote failed because of veto. Find an empty server instead", -1, FLAG_SIX); else - SendChat(-1, CGameContext::CHAT_ALL, "Vote failed", -1, CHAT_SIX); + SendChat(-1, TEAM_ALL, "Vote failed", -1, FLAG_SIX); } else if(m_VoteUpdate) { @@ -1211,9 +1253,9 @@ void CGameContext::OnTick() if(Server()->Tick() % (g_Config.m_SvAnnouncementInterval * Server()->TickSpeed() * 60) == 0) { - const char *pLine = Server()->GetAnnouncementLine(g_Config.m_SvAnnouncementFileName); + const char *pLine = Server()->GetAnnouncementLine(); if(pLine) - SendChat(-1, CGameContext::CHAT_ALL, pLine); + SendChat(-1, TEAM_ALL, pLine); } for(auto &Switcher : Switchers()) @@ -1239,8 +1281,8 @@ void CGameContext::OnTick() { if(m_SqlRandomMapResult->m_Success) { - if(PlayerExists(m_SqlRandomMapResult->m_ClientID) && m_SqlRandomMapResult->m_aMessage[0] != '\0') - SendChatTarget(m_SqlRandomMapResult->m_ClientID, m_SqlRandomMapResult->m_aMessage); + if(PlayerExists(m_SqlRandomMapResult->m_ClientId) && m_SqlRandomMapResult->m_aMessage[0] != '\0') + SendChat(-1, TEAM_ALL, m_SqlRandomMapResult->m_aMessage); if(m_SqlRandomMapResult->m_aMap[0] != '\0') Server()->ChangeMap(m_SqlRandomMapResult->m_aMap); else @@ -1271,66 +1313,54 @@ void CGameContext::OnTick() // Warning: do not put code in this function directly above or below this comment } -static int PlayerFlags_SevenToSix(int Flags) -{ - int Six = 0; - if(Flags & protocol7::PLAYERFLAG_CHATTING) - Six |= PLAYERFLAG_CHATTING; - if(Flags & protocol7::PLAYERFLAG_SCOREBOARD) - Six |= PLAYERFLAG_SCOREBOARD; - if(Flags & protocol7::PLAYERFLAG_AIM) - Six |= PLAYERFLAG_AIM; - return Six; -} - // Server hooks -void CGameContext::OnClientPrepareInput(int ClientID, void *pInput) +void CGameContext::OnClientPrepareInput(int ClientId, void *pInput) { auto *pPlayerInput = (CNetObj_PlayerInput *)pInput; - if(Server()->IsSixup(ClientID)) + if(Server()->IsSixup(ClientId)) pPlayerInput->m_PlayerFlags = PlayerFlags_SevenToSix(pPlayerInput->m_PlayerFlags); } -void CGameContext::OnClientDirectInput(int ClientID, void *pInput) +void CGameContext::OnClientDirectInput(int ClientId, void *pInput) { if(!m_World.m_Paused) - m_apPlayers[ClientID]->OnDirectInput((CNetObj_PlayerInput *)pInput); + m_apPlayers[ClientId]->OnDirectInput((CNetObj_PlayerInput *)pInput); int Flags = ((CNetObj_PlayerInput *)pInput)->m_PlayerFlags; if((Flags & 256) || (Flags & 512)) { - Server()->Kick(ClientID, "please update your client or use DDNet client"); + Server()->Kick(ClientId, "please update your client or use DDNet client"); } } -void CGameContext::OnClientPredictedInput(int ClientID, void *pInput) +void CGameContext::OnClientPredictedInput(int ClientId, void *pInput) { // early return if no input at all has been sent by a player - if(pInput == nullptr && !m_aPlayerHasInput[ClientID]) + if(pInput == nullptr && !m_aPlayerHasInput[ClientId]) return; // set to last sent input when no new input has been sent CNetObj_PlayerInput *pApplyInput = (CNetObj_PlayerInput *)pInput; if(pApplyInput == nullptr) { - pApplyInput = &m_aLastPlayerInput[ClientID]; + pApplyInput = &m_aLastPlayerInput[ClientId]; } if(!m_World.m_Paused) - m_apPlayers[ClientID]->OnPredictedInput(pApplyInput); + m_apPlayers[ClientId]->OnPredictedInput(pApplyInput); } -void CGameContext::OnClientPredictedEarlyInput(int ClientID, void *pInput) +void CGameContext::OnClientPredictedEarlyInput(int ClientId, void *pInput) { // early return if no input at all has been sent by a player - if(pInput == nullptr && !m_aPlayerHasInput[ClientID]) + if(pInput == nullptr && !m_aPlayerHasInput[ClientId]) return; // set to last sent input when no new input has been sent CNetObj_PlayerInput *pApplyInput = (CNetObj_PlayerInput *)pInput; if(pApplyInput == nullptr) { - pApplyInput = &m_aLastPlayerInput[ClientID]; + pApplyInput = &m_aLastPlayerInput[ClientId]; } else { @@ -1338,16 +1368,16 @@ void CGameContext::OnClientPredictedEarlyInput(int ClientID, void *pInput) // because this function is called on all inputs, while // `OnClientPredictedInput` is only called on the first input of each // tick. - mem_copy(&m_aLastPlayerInput[ClientID], pApplyInput, sizeof(m_aLastPlayerInput[ClientID])); - m_aPlayerHasInput[ClientID] = true; + mem_copy(&m_aLastPlayerInput[ClientId], pApplyInput, sizeof(m_aLastPlayerInput[ClientId])); + m_aPlayerHasInput[ClientId] = true; } if(!m_World.m_Paused) - m_apPlayers[ClientID]->OnPredictedEarlyInput(pApplyInput); + m_apPlayers[ClientId]->OnPredictedEarlyInput(pApplyInput); if(m_TeeHistorianActive) { - m_TeeHistorian.RecordPlayerInput(ClientID, m_apPlayers[ClientID]->GetUniqueCID(), pApplyInput); + m_TeeHistorian.RecordPlayerInput(ClientId, m_apPlayers[ClientId]->GetUniqueCid(), pApplyInput); } } @@ -1364,9 +1394,9 @@ const CVoteOptionServer *CGameContext::GetVoteOption(int Index) const return pCurrent; } -void CGameContext::ProgressVoteOptions(int ClientID) +void CGameContext::ProgressVoteOptions(int ClientId) { - CPlayer *pPl = m_apPlayers[ClientID]; + CPlayer *pPl = m_apPlayers[ClientId]; if(pPl->m_SendVoteIndex == -1) return; // we didn't start sending options yet @@ -1432,46 +1462,42 @@ void CGameContext::ProgressVoteOptions(int ClientID) } // send msg + if(pPl->m_SendVoteIndex == 0) + { + CNetMsg_Sv_VoteOptionGroupStart StartMsg; + Server()->SendPackMsg(&StartMsg, MSGFLAG_VITAL, ClientId); + } + OptionMsg.m_NumOptions = NumVotesToSend; - Server()->SendPackMsg(&OptionMsg, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&OptionMsg, MSGFLAG_VITAL, ClientId); pPl->m_SendVoteIndex += NumVotesToSend; + + if(pPl->m_SendVoteIndex == m_NumVoteOptions) + { + CNetMsg_Sv_VoteOptionGroupEnd EndMsg; + Server()->SendPackMsg(&EndMsg, MSGFLAG_VITAL, ClientId); + } } -void CGameContext::OnClientEnter(int ClientID) +void CGameContext::OnClientEnter(int ClientId) { if(m_TeeHistorianActive) { - m_TeeHistorian.RecordPlayerReady(ClientID); + m_TeeHistorian.RecordPlayerReady(ClientId); } - m_pController->OnPlayerConnect(m_apPlayers[ClientID]); + m_pController->OnPlayerConnect(m_apPlayers[ClientId]); - if(Server()->IsSixup(ClientID)) { - { - protocol7::CNetMsg_Sv_GameInfo Msg; - Msg.m_GameFlags = protocol7::GAMEFLAG_RACE; - Msg.m_MatchCurrent = 1; - Msg.m_MatchNum = 0; - Msg.m_ScoreLimit = 0; - Msg.m_TimeLimit = 0; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); - } - - // /team is essential - { - protocol7::CNetMsg_Sv_CommandInfoRemove Msg; - Msg.m_pName = "team"; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); - } + CNetMsg_Sv_CommandInfoGroupStart Msg; + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); } - for(const IConsole::CCommandInfo *pCmd = Console()->FirstCommandInfo(IConsole::ACCESS_LEVEL_USER, CFGFLAG_CHAT); pCmd; pCmd = pCmd->NextCommandInfo(IConsole::ACCESS_LEVEL_USER, CFGFLAG_CHAT)) { const char *pName = pCmd->m_pName; - if(Server()->IsSixup(ClientID)) + if(Server()->IsSixup(ClientId)) { if(!str_comp_nocase(pName, "w") || !str_comp_nocase(pName, "whisper")) continue; @@ -1483,7 +1509,7 @@ void CGameContext::OnClientEnter(int ClientID) Msg.m_pName = pName; Msg.m_pArgsFormat = pCmd->m_pParams; Msg.m_pHelpText = pCmd->m_pHelp; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); } else { @@ -1491,15 +1517,19 @@ void CGameContext::OnClientEnter(int ClientID) Msg.m_pName = pName; Msg.m_pArgsFormat = pCmd->m_pParams; Msg.m_pHelpText = pCmd->m_pHelp; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); } } + { + CNetMsg_Sv_CommandInfoGroupEnd Msg; + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); + } { int Empty = -1; for(int i = 0; i < MAX_CLIENTS; i++) { - if(!Server()->ClientIngame(i)) + if(Server()->ClientSlotEmpty(i)) { Empty = i; break; @@ -1507,55 +1537,55 @@ void CGameContext::OnClientEnter(int ClientID) } CNetMsg_Sv_Chat Msg; Msg.m_Team = 0; - Msg.m_ClientID = Empty; + Msg.m_ClientId = Empty; Msg.m_pMessage = "Do you know someone who uses a bot? Please report them to the moderators."; - m_apPlayers[ClientID]->m_EligibleForFinishCheck = time_get(); - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); + m_apPlayers[ClientId]->m_EligibleForFinishCheck = time_get(); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); } IServer::CClientInfo Info; - if(Server()->GetClientInfo(ClientID, &Info) && Info.m_GotDDNetVersion) + if(Server()->GetClientInfo(ClientId, &Info) && Info.m_GotDDNetVersion) { - if(OnClientDDNetVersionKnown(ClientID)) + if(OnClientDDNetVersionKnown(ClientId)) return; // kicked } - if(!Server()->ClientPrevIngame(ClientID)) + if(!Server()->ClientPrevIngame(ClientId)) { if(g_Config.m_SvWelcome[0] != 0) - SendChatTarget(ClientID, g_Config.m_SvWelcome); + SendChatTarget(ClientId, g_Config.m_SvWelcome); if(g_Config.m_SvShowOthersDefault > SHOW_OTHERS_OFF) { if(g_Config.m_SvShowOthers) - SendChatTarget(ClientID, "You can see other players. To disable this use DDNet client and type /showothers"); + SendChatTarget(ClientId, "You can see other players. To disable this use DDNet client and type /showothers"); - m_apPlayers[ClientID]->m_ShowOthers = g_Config.m_SvShowOthersDefault; + m_apPlayers[ClientId]->m_ShowOthers = g_Config.m_SvShowOthersDefault; } } m_VoteUpdate = true; // send active vote if(m_VoteCloseTime) - SendVoteSet(ClientID); + SendVoteSet(ClientId); Server()->ExpireServerInfo(); - CPlayer *pNewPlayer = m_apPlayers[ClientID]; - mem_zero(&m_aLastPlayerInput[ClientID], sizeof(m_aLastPlayerInput[ClientID])); - m_aPlayerHasInput[ClientID] = false; + CPlayer *pNewPlayer = m_apPlayers[ClientId]; + mem_zero(&m_aLastPlayerInput[ClientId], sizeof(m_aLastPlayerInput[ClientId])); + m_aPlayerHasInput[ClientId] = false; // new info for others protocol7::CNetMsg_Sv_ClientInfo NewClientInfoMsg; - NewClientInfoMsg.m_ClientID = ClientID; + NewClientInfoMsg.m_ClientId = ClientId; NewClientInfoMsg.m_Local = 0; NewClientInfoMsg.m_Team = pNewPlayer->GetTeam(); - NewClientInfoMsg.m_pName = Server()->ClientName(ClientID); - NewClientInfoMsg.m_pClan = Server()->ClientClan(ClientID); - NewClientInfoMsg.m_Country = Server()->ClientCountry(ClientID); + NewClientInfoMsg.m_pName = Server()->ClientName(ClientId); + NewClientInfoMsg.m_pClan = Server()->ClientClan(ClientId); + NewClientInfoMsg.m_Country = Server()->ClientCountry(ClientId); NewClientInfoMsg.m_Silent = false; - for(int p = 0; p < 6; p++) + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) { NewClientInfoMsg.m_apSkinPartNames[p] = pNewPlayer->m_TeeInfos.m_apSkinPartNames[p]; NewClientInfoMsg.m_aUseCustomColors[p] = pNewPlayer->m_TeeInfos.m_aUseCustomColors[p]; @@ -1565,7 +1595,7 @@ void CGameContext::OnClientEnter(int ClientID) // update client infos (others before local) for(int i = 0; i < Server()->MaxClients(); ++i) { - if(i == ClientID || !m_apPlayers[i] || !Server()->ClientIngame(i)) + if(i == ClientId || !m_apPlayers[i] || !Server()->ClientIngame(i)) continue; CPlayer *pPlayer = m_apPlayers[i]; @@ -1573,11 +1603,11 @@ void CGameContext::OnClientEnter(int ClientID) if(Server()->IsSixup(i)) Server()->SendPackMsg(&NewClientInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, i); - if(Server()->IsSixup(ClientID)) + if(Server()->IsSixup(ClientId)) { // existing infos for new player protocol7::CNetMsg_Sv_ClientInfo ClientInfoMsg; - ClientInfoMsg.m_ClientID = i; + ClientInfoMsg.m_ClientId = i; ClientInfoMsg.m_Local = 0; ClientInfoMsg.m_Team = pPlayer->GetTeam(); ClientInfoMsg.m_pName = Server()->ClientName(i); @@ -1585,51 +1615,51 @@ void CGameContext::OnClientEnter(int ClientID) ClientInfoMsg.m_Country = Server()->ClientCountry(i); ClientInfoMsg.m_Silent = 0; - for(int p = 0; p < 6; p++) + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) { ClientInfoMsg.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_apSkinPartNames[p]; ClientInfoMsg.m_aUseCustomColors[p] = pPlayer->m_TeeInfos.m_aUseCustomColors[p]; ClientInfoMsg.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p]; } - Server()->SendPackMsg(&ClientInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); + Server()->SendPackMsg(&ClientInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); } } // local info - if(Server()->IsSixup(ClientID)) + if(Server()->IsSixup(ClientId)) { NewClientInfoMsg.m_Local = 1; - Server()->SendPackMsg(&NewClientInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); + Server()->SendPackMsg(&NewClientInfoMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); } // initial chat delay - if(g_Config.m_SvChatInitialDelay != 0 && m_apPlayers[ClientID]->m_JoinTick > m_NonEmptySince + 10 * Server()->TickSpeed()) + if(g_Config.m_SvChatInitialDelay != 0 && m_apPlayers[ClientId]->m_JoinTick > m_NonEmptySince + 10 * Server()->TickSpeed()) { char aBuf[128]; NETADDR Addr; - Server()->GetClientAddr(ClientID, &Addr); + Server()->GetClientAddr(ClientId, &Addr); str_format(aBuf, sizeof(aBuf), "This server has an initial chat delay, you will need to wait %d seconds before talking.", g_Config.m_SvChatInitialDelay); - SendChatTarget(ClientID, aBuf); - Mute(&Addr, g_Config.m_SvChatInitialDelay, Server()->ClientName(ClientID), "Initial chat delay", true); + SendChatTarget(ClientId, aBuf); + Mute(&Addr, g_Config.m_SvChatInitialDelay, Server()->ClientName(ClientId), "Initial chat delay", true); } - LogEvent("Connect", ClientID); + LogEvent("Connect", ClientId); } -bool CGameContext::OnClientDataPersist(int ClientID, void *pData) +bool CGameContext::OnClientDataPersist(int ClientId, void *pData) { CPersistentClientData *pPersistent = (CPersistentClientData *)pData; - if(!m_apPlayers[ClientID]) + if(!m_apPlayers[ClientId]) { return false; } - pPersistent->m_IsSpectator = m_apPlayers[ClientID]->GetTeam() == TEAM_SPECTATORS; - pPersistent->m_IsAfk = m_apPlayers[ClientID]->IsAfk(); + pPersistent->m_IsSpectator = m_apPlayers[ClientId]->GetTeam() == TEAM_SPECTATORS; + pPersistent->m_IsAfk = m_apPlayers[ClientId]->IsAfk(); return true; } -void CGameContext::OnClientConnected(int ClientID, void *pData) +void CGameContext::OnClientConnected(int ClientId, void *pData) { CPersistentClientData *pPersistentData = (CPersistentClientData *)pData; bool Spec = false; @@ -1645,7 +1675,7 @@ void CGameContext::OnClientConnected(int ClientID, void *pData) for(auto &pPlayer : m_apPlayers) { // connecting clients with spoofed ips can clog slots without being ingame - if(pPlayer && Server()->ClientIngame(pPlayer->GetCID())) + if(pPlayer && Server()->ClientIngame(pPlayer->GetCid())) { Empty = false; break; @@ -1658,47 +1688,58 @@ void CGameContext::OnClientConnected(int ClientID, void *pData) } // Check which team the player should be on - const int StartTeam = (Spec || g_Config.m_SvTournamentMode) ? TEAM_SPECTATORS : m_pController->GetAutoTeam(ClientID); + const int StartTeam = (Spec || g_Config.m_SvTournamentMode) ? TEAM_SPECTATORS : m_pController->GetAutoTeam(ClientId); - if(m_apPlayers[ClientID]) - delete m_apPlayers[ClientID]; - m_apPlayers[ClientID] = new(ClientID) CPlayer(this, NextUniqueClientID, ClientID, StartTeam); - m_apPlayers[ClientID]->SetInitialAfk(Afk); - NextUniqueClientID += 1; + if(m_apPlayers[ClientId]) + delete m_apPlayers[ClientId]; + m_apPlayers[ClientId] = new(ClientId) CPlayer(this, NextUniqueClientId, ClientId, StartTeam); + m_apPlayers[ClientId]->SetInitialAfk(Afk); + NextUniqueClientId += 1; - SendMotd(ClientID); - SendSettings(ClientID); + SendMotd(ClientId); + SendSettings(ClientId); Server()->ExpireServerInfo(); } -void CGameContext::OnClientDrop(int ClientID, const char *pReason) +void CGameContext::OnClientDrop(int ClientId, const char *pReason) { - LogEvent("Disconnect", ClientID); + LogEvent("Disconnect", ClientId); + + AbortVoteKickOnDisconnect(ClientId); + m_pController->OnPlayerDisconnect(m_apPlayers[ClientId], pReason); + delete m_apPlayers[ClientId]; + m_apPlayers[ClientId] = 0; + + delete m_apSavedTeams[ClientId]; + m_apSavedTeams[ClientId] = nullptr; + + delete m_apSavedTees[ClientId]; + m_apSavedTees[ClientId] = nullptr; - AbortVoteKickOnDisconnect(ClientID); - m_pController->OnPlayerDisconnect(m_apPlayers[ClientID], pReason); - delete m_apPlayers[ClientID]; - m_apPlayers[ClientID] = 0; + delete m_apSavedTeleTees[ClientId]; + m_apSavedTeleTees[ClientId] = nullptr; + + m_aTeamMapping[ClientId] = -1; m_VoteUpdate = true; // update spectator modes for(auto &pPlayer : m_apPlayers) { - if(pPlayer && pPlayer->m_SpectatorID == ClientID) - pPlayer->m_SpectatorID = SPEC_FREEVIEW; + if(pPlayer && pPlayer->m_SpectatorId == ClientId) + pPlayer->m_SpectatorId = SPEC_FREEVIEW; } // update conversation targets for(auto &pPlayer : m_apPlayers) { - if(pPlayer && pPlayer->m_LastWhisperTo == ClientID) + if(pPlayer && pPlayer->m_LastWhisperTo == ClientId) pPlayer->m_LastWhisperTo = -1; } protocol7::CNetMsg_Sv_ClientDrop Msg; - Msg.m_ClientID = ClientID; + Msg.m_ClientId = ClientId; Msg.m_pReason = pReason; Msg.m_Silent = false; Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1); @@ -1714,92 +1755,116 @@ void CGameContext::TeehistorianRecordAntibot(const void *pData, int DataSize) } } -void CGameContext::TeehistorianRecordPlayerJoin(int ClientID, bool Sixup) +void CGameContext::TeehistorianRecordPlayerJoin(int ClientId, bool Sixup) { if(m_TeeHistorianActive) { - m_TeeHistorian.RecordPlayerJoin(ClientID, !Sixup ? CTeeHistorian::PROTOCOL_6 : CTeeHistorian::PROTOCOL_7); + m_TeeHistorian.RecordPlayerJoin(ClientId, !Sixup ? CTeeHistorian::PROTOCOL_6 : CTeeHistorian::PROTOCOL_7); } } -void CGameContext::TeehistorianRecordPlayerDrop(int ClientID, const char *pReason) +void CGameContext::TeehistorianRecordPlayerDrop(int ClientId, const char *pReason) { if(m_TeeHistorianActive) { - m_TeeHistorian.RecordPlayerDrop(ClientID, pReason); + m_TeeHistorian.RecordPlayerDrop(ClientId, pReason); } } -void CGameContext::TeehistorianRecordPlayerRejoin(int ClientID) +void CGameContext::TeehistorianRecordPlayerRejoin(int ClientId) { if(m_TeeHistorianActive) { - m_TeeHistorian.RecordPlayerRejoin(ClientID); + m_TeeHistorian.RecordPlayerRejoin(ClientId); } } -bool CGameContext::OnClientDDNetVersionKnown(int ClientID) +void CGameContext::TeehistorianRecordPlayerName(int ClientId, const char *pName) +{ + if(m_TeeHistorianActive) + { + m_TeeHistorian.RecordPlayerName(ClientId, pName); + } +} + +void CGameContext::TeehistorianRecordPlayerFinish(int ClientId, int TimeTicks) +{ + if(m_TeeHistorianActive) + { + m_TeeHistorian.RecordPlayerFinish(ClientId, TimeTicks); + } +} + +void CGameContext::TeehistorianRecordTeamFinish(int TeamId, int TimeTicks) +{ + if(m_TeeHistorianActive) + { + m_TeeHistorian.RecordTeamFinish(TeamId, TimeTicks); + } +} + +bool CGameContext::OnClientDDNetVersionKnown(int ClientId) { IServer::CClientInfo Info; - dbg_assert(Server()->GetClientInfo(ClientID, &Info), "failed to get client info"); + dbg_assert(Server()->GetClientInfo(ClientId, &Info), "failed to get client info"); int ClientVersion = Info.m_DDNetVersion; - dbg_msg("ddnet", "cid=%d version=%d", ClientID, ClientVersion); + dbg_msg("ddnet", "cid=%d version=%d", ClientId, ClientVersion); if(m_TeeHistorianActive) { - if(Info.m_pConnectionID && Info.m_pDDNetVersionStr) + if(Info.m_pConnectionId && Info.m_pDDNetVersionStr) { - m_TeeHistorian.RecordDDNetVersion(ClientID, *Info.m_pConnectionID, ClientVersion, Info.m_pDDNetVersionStr); + m_TeeHistorian.RecordDDNetVersion(ClientId, *Info.m_pConnectionId, ClientVersion, Info.m_pDDNetVersionStr); } else { - m_TeeHistorian.RecordDDNetVersionOld(ClientID, ClientVersion); + m_TeeHistorian.RecordDDNetVersionOld(ClientId, ClientVersion); } } // Autoban known bot versions. if(g_Config.m_SvBannedVersions[0] != '\0' && IsVersionBanned(ClientVersion)) { - Server()->Kick(ClientID, "unsupported client"); + Server()->Kick(ClientId, "unsupported client"); return true; } - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; if(ClientVersion >= VERSION_DDNET_GAMETICK) pPlayer->m_TimerType = g_Config.m_SvDefaultTimerType; // First update the teams state. - m_pController->Teams().SendTeamsState(ClientID); + m_pController->Teams().SendTeamsState(ClientId); // Then send records. - SendRecord(ClientID); + SendRecord(ClientId); // And report correct tunings. if(ClientVersion < VERSION_DDNET_EARLY_VERSION) - SendTuningParams(ClientID, pPlayer->m_TuneZone); + SendTuningParams(ClientId, pPlayer->m_TuneZone); // Tell old clients to update. if(ClientVersion < VERSION_DDNET_UPDATER_FIXED && g_Config.m_SvClientSuggestionOld[0] != '\0') - SendBroadcast(g_Config.m_SvClientSuggestionOld, ClientID); + SendBroadcast(g_Config.m_SvClientSuggestionOld, ClientId); // Tell known bot clients that they're botting and we know it. if(((ClientVersion >= 15 && ClientVersion < 100) || ClientVersion == 502) && g_Config.m_SvClientSuggestionBot[0] != '\0') - SendBroadcast(g_Config.m_SvClientSuggestionBot, ClientID); + SendBroadcast(g_Config.m_SvClientSuggestionBot, ClientId); return false; } -void *CGameContext::PreProcessMsg(int *pMsgID, CUnpacker *pUnpacker, int ClientID) +void *CGameContext::PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientId) { - if(Server()->IsSixup(ClientID) && *pMsgID < OFFSET_UUID) + if(Server()->IsSixup(ClientId) && *pMsgId < OFFSET_UUID) { - void *pRawMsg = m_NetObjHandler7.SecureUnpackMsg(*pMsgID, pUnpacker); + void *pRawMsg = m_NetObjHandler7.SecureUnpackMsg(*pMsgId, pUnpacker); if(!pRawMsg) return 0; - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; static char s_aRawMsg[1024]; - if(*pMsgID == protocol7::NETMSGTYPE_CL_SAY) + if(*pMsgId == protocol7::NETMSGTYPE_CL_SAY) { protocol7::CNetMsg_Cl_Say *pMsg7 = (protocol7::CNetMsg_Cl_Say *)pRawMsg; // Should probably use a placement new to start the lifetime of the object to avoid future weirdness @@ -1807,18 +1872,18 @@ void *CGameContext::PreProcessMsg(int *pMsgID, CUnpacker *pUnpacker, int ClientI if(pMsg7->m_Target >= 0) { - if(ProcessSpamProtection(ClientID)) + if(ProcessSpamProtection(ClientId)) return 0; // Should we maybe recraft the message so that it can go through the usual path? - WhisperID(ClientID, pMsg7->m_Target, pMsg7->m_pMessage); + WhisperId(ClientId, pMsg7->m_Target, pMsg7->m_pMessage); return 0; } pMsg->m_Team = pMsg7->m_Mode == protocol7::CHAT_TEAM; pMsg->m_pMessage = pMsg7->m_pMessage; } - else if(*pMsgID == protocol7::NETMSGTYPE_CL_STARTINFO) + else if(*pMsgId == protocol7::NETMSGTYPE_CL_STARTINFO) { protocol7::CNetMsg_Cl_StartInfo *pMsg7 = (protocol7::CNetMsg_Cl_StartInfo *)pRawMsg; ::CNetMsg_Cl_StartInfo *pMsg = (::CNetMsg_Cl_StartInfo *)s_aRawMsg; @@ -1838,7 +1903,7 @@ void *CGameContext::PreProcessMsg(int *pMsgID, CUnpacker *pUnpacker, int ClientI pMsg->m_ColorBody = pPlayer->m_TeeInfos.m_ColorBody; pMsg->m_ColorFeet = pPlayer->m_TeeInfos.m_ColorFeet; } - else if(*pMsgID == protocol7::NETMSGTYPE_CL_SKINCHANGE) + else if(*pMsgId == protocol7::NETMSGTYPE_CL_SKINCHANGE) { protocol7::CNetMsg_Cl_SkinChange *pMsg = (protocol7::CNetMsg_Cl_SkinChange *)pRawMsg; if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo && @@ -1852,8 +1917,8 @@ void *CGameContext::PreProcessMsg(int *pMsgID, CUnpacker *pUnpacker, int ClientI pPlayer->m_TeeInfos = Info; protocol7::CNetMsg_Sv_SkinChange Msg; - Msg.m_ClientID = ClientID; - for(int p = 0; p < 6; p++) + Msg.m_ClientId = ClientId; + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) { Msg.m_apSkinPartNames[p] = pMsg->m_apSkinPartNames[p]; Msg.m_aSkinPartColors[p] = pMsg->m_aSkinPartColors[p]; @@ -1864,26 +1929,26 @@ void *CGameContext::PreProcessMsg(int *pMsgID, CUnpacker *pUnpacker, int ClientI return 0; } - else if(*pMsgID == protocol7::NETMSGTYPE_CL_SETSPECTATORMODE) + else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETSPECTATORMODE) { protocol7::CNetMsg_Cl_SetSpectatorMode *pMsg7 = (protocol7::CNetMsg_Cl_SetSpectatorMode *)pRawMsg; ::CNetMsg_Cl_SetSpectatorMode *pMsg = (::CNetMsg_Cl_SetSpectatorMode *)s_aRawMsg; if(pMsg7->m_SpecMode == protocol7::SPEC_FREEVIEW) - pMsg->m_SpectatorID = SPEC_FREEVIEW; + pMsg->m_SpectatorId = SPEC_FREEVIEW; else if(pMsg7->m_SpecMode == protocol7::SPEC_PLAYER) - pMsg->m_SpectatorID = pMsg7->m_SpectatorID; + pMsg->m_SpectatorId = pMsg7->m_SpectatorId; else - pMsg->m_SpectatorID = SPEC_FREEVIEW; // Probably not needed + pMsg->m_SpectatorId = SPEC_FREEVIEW; // Probably not needed } - else if(*pMsgID == protocol7::NETMSGTYPE_CL_SETTEAM) + else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETTEAM) { protocol7::CNetMsg_Cl_SetTeam *pMsg7 = (protocol7::CNetMsg_Cl_SetTeam *)pRawMsg; ::CNetMsg_Cl_SetTeam *pMsg = (::CNetMsg_Cl_SetTeam *)s_aRawMsg; pMsg->m_Team = pMsg7->m_Team; } - else if(*pMsgID == protocol7::NETMSGTYPE_CL_COMMAND) + else if(*pMsgId == protocol7::NETMSGTYPE_CL_COMMAND) { protocol7::CNetMsg_Cl_Command *pMsg7 = (protocol7::CNetMsg_Cl_Command *)pRawMsg; ::CNetMsg_Cl_Say *pMsg = (::CNetMsg_Cl_Say *)s_aRawMsg; @@ -1893,20 +1958,20 @@ void *CGameContext::PreProcessMsg(int *pMsgID, CUnpacker *pUnpacker, int ClientI dbg_msg("debug", "line='%s'", s_aRawMsg + sizeof(*pMsg)); pMsg->m_Team = 0; - *pMsgID = NETMSGTYPE_CL_SAY; + *pMsgId = NETMSGTYPE_CL_SAY; return s_aRawMsg; } - else if(*pMsgID == protocol7::NETMSGTYPE_CL_CALLVOTE) + else if(*pMsgId == protocol7::NETMSGTYPE_CL_CALLVOTE) { protocol7::CNetMsg_Cl_CallVote *pMsg7 = (protocol7::CNetMsg_Cl_CallVote *)pRawMsg; ::CNetMsg_Cl_CallVote *pMsg = (::CNetMsg_Cl_CallVote *)s_aRawMsg; - int Authed = Server()->GetAuthedState(ClientID); + int Authed = Server()->GetAuthedState(ClientId); if(pMsg7->m_Force) { str_format(s_aRawMsg, sizeof(s_aRawMsg), "force_vote \"%s\" \"%s\" \"%s\"", pMsg7->m_pType, pMsg7->m_pValue, pMsg7->m_pReason); Console()->SetAccessLevel(Authed == AUTHED_ADMIN ? IConsole::ACCESS_LEVEL_ADMIN : Authed == AUTHED_MOD ? IConsole::ACCESS_LEVEL_MOD : IConsole::ACCESS_LEVEL_HELPER); - Console()->ExecuteLine(s_aRawMsg, ClientID, false); + Console()->ExecuteLine(s_aRawMsg, ClientId, false); Console()->SetAccessLevel(IConsole::ACCESS_LEVEL_ADMIN); return 0; } @@ -1915,14 +1980,14 @@ void *CGameContext::PreProcessMsg(int *pMsgID, CUnpacker *pUnpacker, int ClientI pMsg->m_pReason = pMsg7->m_pReason; pMsg->m_pType = pMsg7->m_pType; } - else if(*pMsgID == protocol7::NETMSGTYPE_CL_EMOTICON) + else if(*pMsgId == protocol7::NETMSGTYPE_CL_EMOTICON) { protocol7::CNetMsg_Cl_Emoticon *pMsg7 = (protocol7::CNetMsg_Cl_Emoticon *)pRawMsg; ::CNetMsg_Cl_Emoticon *pMsg = (::CNetMsg_Cl_Emoticon *)s_aRawMsg; pMsg->m_Emoticon = pMsg7->m_Emoticon; } - else if(*pMsgID == protocol7::NETMSGTYPE_CL_VOTE) + else if(*pMsgId == protocol7::NETMSGTYPE_CL_VOTE) { protocol7::CNetMsg_Cl_Vote *pMsg7 = (protocol7::CNetMsg_Cl_Vote *)pRawMsg; ::CNetMsg_Cl_Vote *pMsg = (::CNetMsg_Cl_Vote *)s_aRawMsg; @@ -1930,12 +1995,12 @@ void *CGameContext::PreProcessMsg(int *pMsgID, CUnpacker *pUnpacker, int ClientI pMsg->m_Vote = pMsg7->m_Vote; } - *pMsgID = Msg_SevenToSix(*pMsgID); + *pMsgId = Msg_SevenToSix(*pMsgId); return s_aRawMsg; } else - return m_NetObjHandler.SecureUnpackMsg(*pMsgID, pUnpacker); + return m_NetObjHandler.SecureUnpackMsg(*pMsgId, pUnpacker); } void CGameContext::CensorMessage(char *pCensoredMessage, const char *pMessage, int Size) @@ -1960,88 +2025,84 @@ void CGameContext::CensorMessage(char *pCensoredMessage, const char *pMessage, i } } -void CGameContext::OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID) +void CGameContext::OnMessage(int MsgId, CUnpacker *pUnpacker, int ClientId) { if(m_TeeHistorianActive) { - if(m_NetObjHandler.TeeHistorianRecordMsg(MsgID)) + if(m_NetObjHandler.TeeHistorianRecordMsg(MsgId)) { - m_TeeHistorian.RecordPlayerMessage(ClientID, pUnpacker->CompleteData(), pUnpacker->CompleteSize()); + m_TeeHistorian.RecordPlayerMessage(ClientId, pUnpacker->CompleteData(), pUnpacker->CompleteSize()); } } - void *pRawMsg = PreProcessMsg(&MsgID, pUnpacker, ClientID); + void *pRawMsg = PreProcessMsg(&MsgId, pUnpacker, ClientId); if(!pRawMsg) return; - if(Server()->ClientIngame(ClientID)) + if(Server()->ClientIngame(ClientId)) { - switch(MsgID) + switch(MsgId) { case NETMSGTYPE_CL_SAY: - OnSayNetMessage(static_cast(pRawMsg), ClientID, pUnpacker); + OnSayNetMessage(static_cast(pRawMsg), ClientId, pUnpacker); break; case NETMSGTYPE_CL_CALLVOTE: - OnCallVoteNetMessage(static_cast(pRawMsg), ClientID); + OnCallVoteNetMessage(static_cast(pRawMsg), ClientId); break; case NETMSGTYPE_CL_VOTE: - OnVoteNetMessage(static_cast(pRawMsg), ClientID); + OnVoteNetMessage(static_cast(pRawMsg), ClientId); break; case NETMSGTYPE_CL_SETTEAM: - OnSetTeamNetMessage(static_cast(pRawMsg), ClientID); + OnSetTeamNetMessage(static_cast(pRawMsg), ClientId); break; case NETMSGTYPE_CL_ISDDNETLEGACY: - OnIsDDNetLegacyNetMessage(static_cast(pRawMsg), ClientID, pUnpacker); + OnIsDDNetLegacyNetMessage(static_cast(pRawMsg), ClientId, pUnpacker); break; case NETMSGTYPE_CL_SHOWOTHERSLEGACY: - OnShowOthersLegacyNetMessage(static_cast(pRawMsg), ClientID); + OnShowOthersLegacyNetMessage(static_cast(pRawMsg), ClientId); break; case NETMSGTYPE_CL_SHOWOTHERS: - OnShowOthersNetMessage(static_cast(pRawMsg), ClientID); + OnShowOthersNetMessage(static_cast(pRawMsg), ClientId); break; case NETMSGTYPE_CL_SHOWDISTANCE: - OnShowDistanceNetMessage(static_cast(pRawMsg), ClientID); + OnShowDistanceNetMessage(static_cast(pRawMsg), ClientId); break; case NETMSGTYPE_CL_SETSPECTATORMODE: - OnSetSpectatorModeNetMessage(static_cast(pRawMsg), ClientID); + OnSetSpectatorModeNetMessage(static_cast(pRawMsg), ClientId); break; case NETMSGTYPE_CL_CHANGEINFO: - OnChangeInfoNetMessage(static_cast(pRawMsg), ClientID); + OnChangeInfoNetMessage(static_cast(pRawMsg), ClientId); break; case NETMSGTYPE_CL_EMOTICON: - OnEmoticonNetMessage(static_cast(pRawMsg), ClientID); + OnEmoticonNetMessage(static_cast(pRawMsg), ClientId); break; case NETMSGTYPE_CL_KILL: - OnKillNetMessage(static_cast(pRawMsg), ClientID); + OnKillNetMessage(static_cast(pRawMsg), ClientId); break; default: break; } } - if(MsgID == NETMSGTYPE_CL_STARTINFO) + if(MsgId == NETMSGTYPE_CL_STARTINFO) { - OnStartInfoNetMessage(static_cast(pRawMsg), ClientID); + OnStartInfoNetMessage(static_cast(pRawMsg), ClientId); } } -void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientID, const CUnpacker *pUnpacker) +void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientId, const CUnpacker *pUnpacker) { - if(!str_utf8_check(pMsg->m_pMessage)) - { - return; - } - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; bool Check = !pPlayer->m_NotEligibleForFinish && pPlayer->m_EligibleForFinishCheck + 10 * time_freq() >= time_get(); if(Check && str_comp(pMsg->m_pMessage, "xd sure chillerbot.png is lyfe") == 0 && pMsg->m_Team == 0) { if(m_TeeHistorianActive) { - m_TeeHistorian.RecordPlayerMessage(ClientID, pUnpacker->CompleteData(), pUnpacker->CompleteSize()); + m_TeeHistorian.RecordPlayerMessage(ClientId, pUnpacker->CompleteData(), pUnpacker->CompleteSize()); } pPlayer->m_NotEligibleForFinish = true; - dbg_msg("hack", "bot detected, cid=%d", ClientID); + dbg_msg("hack", "bot detected, cid=%d", ClientId); return; } int Team = pMsg->m_Team; @@ -2076,11 +2137,11 @@ void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientID, con if(Length == 0 || (pMsg->m_pMessage[0] != '/' && (g_Config.m_SvSpamprotection && pPlayer->m_LastChat && pPlayer->m_LastChat + Server()->TickSpeed() * ((31 + Length) / 32) > Server()->Tick()))) return; - int GameTeam = GetDDRaceTeam(pPlayer->GetCID()); + int GameTeam = GetDDRaceTeam(pPlayer->GetCid()); if(Team) - Team = ((pPlayer->GetTeam() == TEAM_SPECTATORS) ? CHAT_SPEC : GameTeam); + Team = ((pPlayer->GetTeam() == TEAM_SPECTATORS) ? TEAM_SPECTATORS : GameTeam); else - Team = CHAT_ALL; + Team = TEAM_ALL; if(pMsg->m_pMessage[0] == '/') { @@ -2088,25 +2149,25 @@ void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientID, con { char aWhisperMsg[256]; str_copy(aWhisperMsg, pMsg->m_pMessage + 3, 256); - Whisper(pPlayer->GetCID(), aWhisperMsg); + Whisper(pPlayer->GetCid(), aWhisperMsg); } else if(str_startswith_nocase(pMsg->m_pMessage + 1, "whisper ")) { char aWhisperMsg[256]; str_copy(aWhisperMsg, pMsg->m_pMessage + 9, 256); - Whisper(pPlayer->GetCID(), aWhisperMsg); + Whisper(pPlayer->GetCid(), aWhisperMsg); } else if(str_startswith_nocase(pMsg->m_pMessage + 1, "c ")) { char aWhisperMsg[256]; str_copy(aWhisperMsg, pMsg->m_pMessage + 3, 256); - Converse(pPlayer->GetCID(), aWhisperMsg); + Converse(pPlayer->GetCid(), aWhisperMsg); } else if(str_startswith_nocase(pMsg->m_pMessage + 1, "converse ")) { char aWhisperMsg[256]; str_copy(aWhisperMsg, pMsg->m_pMessage + 10, 256); - Converse(pPlayer->GetCID(), aWhisperMsg); + Converse(pPlayer->GetCid(), aWhisperMsg); } else { @@ -2118,21 +2179,21 @@ void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientID, con pPlayer->m_LastCommandPos = (pPlayer->m_LastCommandPos + 1) % 4; Console()->SetFlagMask(CFGFLAG_CHAT); - int Authed = Server()->GetAuthedState(ClientID); + int Authed = Server()->GetAuthedState(ClientId); if(Authed) Console()->SetAccessLevel(Authed == AUTHED_ADMIN ? IConsole::ACCESS_LEVEL_ADMIN : Authed == AUTHED_MOD ? IConsole::ACCESS_LEVEL_MOD : IConsole::ACCESS_LEVEL_HELPER); else Console()->SetAccessLevel(IConsole::ACCESS_LEVEL_USER); { - CClientChatLogger Logger(this, ClientID, log_get_scope_logger()); + CClientChatLogger Logger(this, ClientId, log_get_scope_logger()); CLogScope Scope(&Logger); - Console()->ExecuteLine(pMsg->m_pMessage + 1, ClientID, false); + Console()->ExecuteLine(pMsg->m_pMessage + 1, ClientId, false); } - // m_apPlayers[ClientID] can be NULL, if the player used a + // m_apPlayers[ClientId] can be NULL, if the player used a // timeout code and replaced another client. char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "%d used %s", ClientID, pMsg->m_pMessage); + str_format(aBuf, sizeof(aBuf), "%d used %s", ClientId, pMsg->m_pMessage); Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "chat-command", aBuf); Console()->SetAccessLevel(IConsole::ACCESS_LEVEL_ADMIN); @@ -2144,16 +2205,16 @@ void CGameContext::OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientID, con pPlayer->UpdatePlaytime(); char aCensoredMessage[256]; CensorMessage(aCensoredMessage, pMsg->m_pMessage, sizeof(aCensoredMessage)); - SendChat(ClientID, Team, aCensoredMessage, ClientID); + SendChat(ClientId, Team, aCensoredMessage, ClientId); } } -void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int ClientID) +void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int ClientId) { - if(RateLimitPlayerVote(ClientID) || m_VoteCloseTime) + if(RateLimitPlayerVote(ClientId) || m_VoteCloseTime) return; - m_apPlayers[ClientID]->UpdatePlaytime(); + m_apPlayers[ClientId]->UpdatePlaytime(); m_VoteType = VOTE_TYPE_UNKNOWN; char aChatmsg[512] = {0}; @@ -2161,18 +2222,14 @@ void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int Cli char aSixupDesc[VOTE_DESC_LENGTH] = {0}; char aCmd[VOTE_CMD_LENGTH] = {0}; char aReason[VOTE_REASON_LENGTH] = "No reason given"; - if(!str_utf8_check(pMsg->m_pType) || !str_utf8_check(pMsg->m_pReason) || !str_utf8_check(pMsg->m_pValue)) - { - return; - } if(pMsg->m_pReason[0]) { str_copy(aReason, pMsg->m_pReason, sizeof(aReason)); } + int Authed = Server()->GetAuthedState(ClientId); if(str_comp_nocase(pMsg->m_pType, "option") == 0) { - int Authed = Server()->GetAuthedState(ClientID); CVoteOptionServer *pOption = m_pVoteOptionFirst; while(pOption) { @@ -2180,15 +2237,15 @@ void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int Cli { if(!Console()->LineIsValid(pOption->m_aCommand)) { - SendChatTarget(ClientID, "Invalid option"); + SendChatTarget(ClientId, "Invalid option"); return; } - if((str_find(pOption->m_aCommand, "sv_map ") != 0 || str_find(pOption->m_aCommand, "change_map ") != 0 || str_find(pOption->m_aCommand, "random_map") != 0 || str_find(pOption->m_aCommand, "random_unfinished_map") != 0) && RateLimitPlayerMapVote(ClientID)) + if((str_find(pOption->m_aCommand, "sv_map ") != 0 || str_find(pOption->m_aCommand, "change_map ") != 0 || str_find(pOption->m_aCommand, "random_map") != 0 || str_find(pOption->m_aCommand, "random_unfinished_map") != 0) && RateLimitPlayerMapVote(ClientId)) { return; } - str_format(aChatmsg, sizeof(aChatmsg), "'%s' called vote to change server option '%s' (%s)", Server()->ClientName(ClientID), + str_format(aChatmsg, sizeof(aChatmsg), "'%s' called vote to change server option '%s' (%s)", Server()->ClientName(ClientId), pOption->m_aDescription, aReason); str_copy(aDesc, pOption->m_aDescription); @@ -2214,12 +2271,12 @@ void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int Cli if(Authed != AUTHED_ADMIN) // allow admins to call any vote they want { str_format(aChatmsg, sizeof(aChatmsg), "'%s' isn't an option on this server", pMsg->m_pValue); - SendChatTarget(ClientID, aChatmsg); + SendChatTarget(ClientId, aChatmsg); return; } else { - str_format(aChatmsg, sizeof(aChatmsg), "'%s' called vote to change server option '%s'", Server()->ClientName(ClientID), pMsg->m_pValue); + str_format(aChatmsg, sizeof(aChatmsg), "'%s' called vote to change server option '%s'", Server()->ClientName(ClientId), pMsg->m_pValue); str_copy(aDesc, pMsg->m_pValue); str_copy(aCmd, pMsg->m_pValue); } @@ -2229,23 +2286,21 @@ void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int Cli } else if(str_comp_nocase(pMsg->m_pType, "kick") == 0) { - int Authed = Server()->GetAuthedState(ClientID); - if(!g_Config.m_SvVoteKick && !Authed) // allow admins to call kick votes even if they are forbidden { - SendChatTarget(ClientID, "Server does not allow voting to kick players"); + SendChatTarget(ClientId, "Server does not allow voting to kick players"); return; } - if(!Authed && time_get() < m_apPlayers[ClientID]->m_Last_KickVote + (time_freq() * g_Config.m_SvVoteKickDelay)) + if(!Authed && time_get() < m_apPlayers[ClientId]->m_Last_KickVote + (time_freq() * g_Config.m_SvVoteKickDelay)) { str_format(aChatmsg, sizeof(aChatmsg), "There's a %d second wait time between kick votes for each player please wait %d second(s)", g_Config.m_SvVoteKickDelay, - (int)((m_apPlayers[ClientID]->m_Last_KickVote + g_Config.m_SvVoteKickDelay * time_freq() - time_get()) / time_freq())); - SendChatTarget(ClientID, aChatmsg); + (int)((m_apPlayers[ClientId]->m_Last_KickVote + g_Config.m_SvVoteKickDelay * time_freq() - time_get()) / time_freq())); + SendChatTarget(ClientId, aChatmsg); return; } - if(g_Config.m_SvVoteKickMin && !GetDDRaceTeam(ClientID)) + if(g_Config.m_SvVoteKickMin && !GetDDRaceTeam(ClientId)) { char aaAddresses[MAX_CLIENTS][NETADDR_MAXSTRSIZE] = {{0}}; for(int i = 0; i < MAX_CLIENTS; i++) @@ -2278,128 +2333,137 @@ void CGameContext::OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int Cli if(NumPlayers < g_Config.m_SvVoteKickMin) { str_format(aChatmsg, sizeof(aChatmsg), "Kick voting requires %d players", g_Config.m_SvVoteKickMin); - SendChatTarget(ClientID, aChatmsg); + SendChatTarget(ClientId, aChatmsg); return; } } - int KickID = str_toint(pMsg->m_pValue); + int KickId = str_toint(pMsg->m_pValue); - if(KickID < 0 || KickID >= MAX_CLIENTS || !m_apPlayers[KickID]) + if(KickId < 0 || KickId >= MAX_CLIENTS || !m_apPlayers[KickId]) { - SendChatTarget(ClientID, "Invalid client id to kick"); + SendChatTarget(ClientId, "Invalid client id to kick"); return; } - if(KickID == ClientID) + if(KickId == ClientId) { - SendChatTarget(ClientID, "You can't kick yourself"); + SendChatTarget(ClientId, "You can't kick yourself"); return; } - if(!Server()->ReverseTranslate(KickID, ClientID)) + if(!Server()->ReverseTranslate(KickId, ClientId)) { return; } - int KickedAuthed = Server()->GetAuthedState(KickID); + int KickedAuthed = Server()->GetAuthedState(KickId); if(KickedAuthed > Authed) { - SendChatTarget(ClientID, "You can't kick authorized players"); + SendChatTarget(ClientId, "You can't kick authorized players"); char aBufKick[128]; - str_format(aBufKick, sizeof(aBufKick), "'%s' called for vote to kick you", Server()->ClientName(ClientID)); - SendChatTarget(KickID, aBufKick); + str_format(aBufKick, sizeof(aBufKick), "'%s' called for vote to kick you", Server()->ClientName(ClientId)); + SendChatTarget(KickId, aBufKick); return; } // Don't allow kicking if a player has no character - if(!GetPlayerChar(ClientID) || !GetPlayerChar(KickID) || GetDDRaceTeam(ClientID) != GetDDRaceTeam(KickID)) + if(!GetPlayerChar(ClientId) || !GetPlayerChar(KickId) || GetDDRaceTeam(ClientId) != GetDDRaceTeam(KickId)) { - SendChatTarget(ClientID, "You can kick only your team member"); + SendChatTarget(ClientId, "You can kick only your team member"); return; } - str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to kick '%s' (%s)", Server()->ClientName(ClientID), Server()->ClientName(KickID), aReason); - str_format(aSixupDesc, sizeof(aSixupDesc), "%2d: %s", KickID, Server()->ClientName(KickID)); - if(!GetDDRaceTeam(ClientID)) + str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to kick '%s' (%s)", Server()->ClientName(ClientId), Server()->ClientName(KickId), aReason); + str_format(aSixupDesc, sizeof(aSixupDesc), "%2d: %s", KickId, Server()->ClientName(KickId)); + if(!GetDDRaceTeam(ClientId)) { if(!g_Config.m_SvVoteKickBantime) { - str_format(aCmd, sizeof(aCmd), "kick %d Kicked by vote", KickID); - str_format(aDesc, sizeof(aDesc), "Kick '%s'", Server()->ClientName(KickID)); + str_format(aCmd, sizeof(aCmd), "kick %d Kicked by vote", KickId); + str_format(aDesc, sizeof(aDesc), "Kick '%s'", Server()->ClientName(KickId)); } else { char aAddrStr[NETADDR_MAXSTRSIZE] = {0}; - Server()->GetClientAddr(KickID, aAddrStr, sizeof(aAddrStr)); + Server()->GetClientAddr(KickId, aAddrStr, sizeof(aAddrStr)); str_format(aCmd, sizeof(aCmd), "ban %s %d Banned by vote", aAddrStr, g_Config.m_SvVoteKickBantime); - str_format(aDesc, sizeof(aDesc), "Ban '%s'", Server()->ClientName(KickID)); + str_format(aDesc, sizeof(aDesc), "Ban '%s'", Server()->ClientName(KickId)); } } else { - str_format(aCmd, sizeof(aCmd), "uninvite %d %d; set_team_ddr %d 0", KickID, GetDDRaceTeam(KickID), KickID); - str_format(aDesc, sizeof(aDesc), "Move '%s' to team 0", Server()->ClientName(KickID)); + str_format(aCmd, sizeof(aCmd), "uninvite %d %d; set_team_ddr %d 0", KickId, GetDDRaceTeam(KickId), KickId); + str_format(aDesc, sizeof(aDesc), "Move '%s' to team 0", Server()->ClientName(KickId)); } - m_apPlayers[ClientID]->m_Last_KickVote = time_get(); + m_apPlayers[ClientId]->m_Last_KickVote = time_get(); m_VoteType = VOTE_TYPE_KICK; - m_VoteVictim = KickID; + m_VoteVictim = KickId; } else if(str_comp_nocase(pMsg->m_pType, "spectate") == 0) { if(!g_Config.m_SvVoteSpectate) { - SendChatTarget(ClientID, "Server does not allow voting to move players to spectators"); + SendChatTarget(ClientId, "Server does not allow voting to move players to spectators"); return; } - int SpectateID = str_toint(pMsg->m_pValue); + int SpectateId = str_toint(pMsg->m_pValue); - if(SpectateID < 0 || SpectateID >= MAX_CLIENTS || !m_apPlayers[SpectateID] || m_apPlayers[SpectateID]->GetTeam() == TEAM_SPECTATORS) + if(SpectateId < 0 || SpectateId >= MAX_CLIENTS || !m_apPlayers[SpectateId] || m_apPlayers[SpectateId]->GetTeam() == TEAM_SPECTATORS) + { + SendChatTarget(ClientId, "Invalid client id to move to spectators"); + return; + } + if(SpectateId == ClientId) { - SendChatTarget(ClientID, "Invalid client id to move"); + SendChatTarget(ClientId, "You can't move yourself to spectators"); return; } - if(SpectateID == ClientID) + int SpectateAuthed = Server()->GetAuthedState(SpectateId); + if(SpectateAuthed > Authed) { - SendChatTarget(ClientID, "You can't move yourself"); + SendChatTarget(ClientId, "You can't move authorized players to spectators"); + char aBufSpectate[128]; + str_format(aBufSpectate, sizeof(aBufSpectate), "'%s' called for vote to move you to spectators", Server()->ClientName(ClientId)); + SendChatTarget(SpectateId, aBufSpectate); return; } - if(!Server()->ReverseTranslate(SpectateID, ClientID)) + if(!Server()->ReverseTranslate(SpectateId, ClientId)) { return; } - if(!GetPlayerChar(ClientID) || !GetPlayerChar(SpectateID) || GetDDRaceTeam(ClientID) != GetDDRaceTeam(SpectateID)) + if(!GetPlayerChar(ClientId) || !GetPlayerChar(SpectateId) || GetDDRaceTeam(ClientId) != GetDDRaceTeam(SpectateId)) { - SendChatTarget(ClientID, "You can only move your team member to spectators"); + SendChatTarget(ClientId, "You can only move your team member to spectators"); return; } - str_format(aSixupDesc, sizeof(aSixupDesc), "%2d: %s", SpectateID, Server()->ClientName(SpectateID)); + str_format(aSixupDesc, sizeof(aSixupDesc), "%2d: %s", SpectateId, Server()->ClientName(SpectateId)); if(g_Config.m_SvPauseable && g_Config.m_SvVotePause) { - str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to pause '%s' for %d seconds (%s)", Server()->ClientName(ClientID), Server()->ClientName(SpectateID), g_Config.m_SvVotePauseTime, aReason); - str_format(aDesc, sizeof(aDesc), "Pause '%s' (%ds)", Server()->ClientName(SpectateID), g_Config.m_SvVotePauseTime); - str_format(aCmd, sizeof(aCmd), "uninvite %d %d; force_pause %d %d", SpectateID, GetDDRaceTeam(SpectateID), SpectateID, g_Config.m_SvVotePauseTime); + str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to pause '%s' for %d seconds (%s)", Server()->ClientName(ClientId), Server()->ClientName(SpectateId), g_Config.m_SvVotePauseTime, aReason); + str_format(aDesc, sizeof(aDesc), "Pause '%s' (%ds)", Server()->ClientName(SpectateId), g_Config.m_SvVotePauseTime); + str_format(aCmd, sizeof(aCmd), "uninvite %d %d; force_pause %d %d", SpectateId, GetDDRaceTeam(SpectateId), SpectateId, g_Config.m_SvVotePauseTime); } else { - str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to move '%s' to spectators (%s)", Server()->ClientName(ClientID), Server()->ClientName(SpectateID), aReason); - str_format(aDesc, sizeof(aDesc), "Move '%s' to spectators", Server()->ClientName(SpectateID)); - str_format(aCmd, sizeof(aCmd), "uninvite %d %d; set_team %d -1 %d", SpectateID, GetDDRaceTeam(SpectateID), SpectateID, g_Config.m_SvVoteSpectateRejoindelay); + str_format(aChatmsg, sizeof(aChatmsg), "'%s' called for vote to move '%s' to spectators (%s)", Server()->ClientName(ClientId), Server()->ClientName(SpectateId), aReason); + str_format(aDesc, sizeof(aDesc), "Move '%s' to spectators", Server()->ClientName(SpectateId)); + str_format(aCmd, sizeof(aCmd), "uninvite %d %d; set_team %d -1 %d", SpectateId, GetDDRaceTeam(SpectateId), SpectateId, g_Config.m_SvVoteSpectateRejoindelay); } m_VoteType = VOTE_TYPE_SPECTATE; - m_VoteVictim = SpectateID; + m_VoteVictim = SpectateId; } if(aCmd[0] && str_comp_nocase(aCmd, "info") != 0) - CallVote(ClientID, aDesc, aCmd, aReason, aChatmsg, aSixupDesc[0] ? aSixupDesc : 0); + CallVote(ClientId, aDesc, aCmd, aReason, aChatmsg, aSixupDesc[0] ? aSixupDesc : 0); } -void CGameContext::OnVoteNetMessage(const CNetMsg_Cl_Vote *pMsg, int ClientID) +void CGameContext::OnVoteNetMessage(const CNetMsg_Cl_Vote *pMsg, int ClientId) { if(!m_VoteCloseTime) return; - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; if(g_Config.m_SvSpamprotection && pPlayer->m_LastVoteTry && pPlayer->m_LastVoteTry + Server()->TickSpeed() * 3 > Server()->Tick()) return; @@ -2412,20 +2476,27 @@ void CGameContext::OnVoteNetMessage(const CNetMsg_Cl_Vote *pMsg, int ClientID) if(!pMsg->m_Vote) return; + // Allow the vote creator to cancel the vote + if(pPlayer->GetCid() == m_VoteCreator && pMsg->m_Vote == -1) + { + m_VoteEnforce = VOTE_ENFORCE_CANCEL; + return; + } + pPlayer->m_Vote = pMsg->m_Vote; pPlayer->m_VotePos = ++m_VotePos; m_VoteUpdate = true; CNetMsg_Sv_YourVote Msg = {pMsg->m_Vote}; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId); } -void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int ClientID) +void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int ClientId) { if(m_World.m_Paused) return; - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; if(pPlayer->GetTeam() == pMsg->m_Team || (g_Config.m_SvSpamprotection && pPlayer->m_LastSetTeam && pPlayer->m_LastSetTeam + Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay > Server()->Tick())) return; @@ -2437,7 +2508,7 @@ void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int Clien int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed(); if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == DDRACE_STARTED) { - SendChatTarget(ClientID, "Kill Protection enabled. If you really want to join the spectators, first type /kill"); + SendChatTarget(ClientId, "Kill Protection enabled. If you really want to join the spectators, first type /kill"); return; } } @@ -2450,13 +2521,13 @@ void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int Clien str_time((int64_t)TimeLeft * 100, TIME_HOURS, aTime, sizeof(aTime)); char aBuf[128]; str_format(aBuf, sizeof(aBuf), "Time to wait before changing team: %s", aTime); - SendBroadcast(aBuf, ClientID); + SendBroadcast(aBuf, ClientId); return; } // Switch team on given client and kill/respawn them char aTeamJoinError[512]; - if(m_pController->CanJoinTeam(pMsg->m_Team, ClientID, aTeamJoinError, sizeof(aTeamJoinError))) + if(m_pController->CanJoinTeam(pMsg->m_Team, ClientId, aTeamJoinError, sizeof(aTeamJoinError))) { if(pPlayer->GetTeam() == TEAM_SPECTATORS || pMsg->m_Team == TEAM_SPECTATORS) m_VoteUpdate = true; @@ -2464,13 +2535,13 @@ void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int Clien pPlayer->m_TeamChangeTick = Server()->Tick(); } else - SendBroadcast(aTeamJoinError, ClientID); + SendBroadcast(aTeamJoinError, ClientId); } -void CGameContext::OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMsg, int ClientID, CUnpacker *pUnpacker) +void CGameContext::OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMsg, int ClientId, CUnpacker *pUnpacker) { IServer::CClientInfo Info; - if(Server()->GetClientInfo(ClientID, &Info) && Info.m_GotDDNetVersion) + if(Server()->GetClientInfo(ClientId, &Info) && Info.m_GotDDNetVersion) { return; } @@ -2479,127 +2550,130 @@ void CGameContext::OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMs { DDNetVersion = VERSION_DDRACE; } - Server()->SetClientDDNetVersion(ClientID, DDNetVersion); - OnClientDDNetVersionKnown(ClientID); + Server()->SetClientDDNetVersion(ClientId, DDNetVersion); + OnClientDDNetVersionKnown(ClientId); } -void CGameContext::OnShowOthersLegacyNetMessage(const CNetMsg_Cl_ShowOthersLegacy *pMsg, int ClientID) +void CGameContext::OnShowOthersLegacyNetMessage(const CNetMsg_Cl_ShowOthersLegacy *pMsg, int ClientId) { if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault) { - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; pPlayer->m_ShowOthers = pMsg->m_Show; } } -void CGameContext::OnShowOthersNetMessage(const CNetMsg_Cl_ShowOthers *pMsg, int ClientID) +void CGameContext::OnShowOthersNetMessage(const CNetMsg_Cl_ShowOthers *pMsg, int ClientId) { if(g_Config.m_SvShowOthers && !g_Config.m_SvShowOthersDefault) { - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; pPlayer->m_ShowOthers = pMsg->m_Show; } } -void CGameContext::OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, int ClientID) +void CGameContext::OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, int ClientId) { - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; pPlayer->m_ShowDistance = vec2(pMsg->m_X, pMsg->m_Y); } -void CGameContext::OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientID) +void CGameContext::OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientId) { if(m_World.m_Paused) return; - int SpectatorID = clamp(pMsg->m_SpectatorID, (int)SPEC_FOLLOW, MAX_CLIENTS - 1); - if(SpectatorID >= 0) - if(!Server()->ReverseTranslate(SpectatorID, ClientID)) + int SpectatorId = clamp(pMsg->m_SpectatorId, (int)SPEC_FOLLOW, MAX_CLIENTS - 1); + if(SpectatorId >= 0) + if(!Server()->ReverseTranslate(SpectatorId, ClientId)) return; - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; if((g_Config.m_SvSpamprotection && pPlayer->m_LastSetSpectatorMode && pPlayer->m_LastSetSpectatorMode + Server()->TickSpeed() / 4 > Server()->Tick())) return; pPlayer->m_LastSetSpectatorMode = Server()->Tick(); pPlayer->UpdatePlaytime(); - if(SpectatorID >= 0 && (!m_apPlayers[SpectatorID] || m_apPlayers[SpectatorID]->GetTeam() == TEAM_SPECTATORS)) - SendChatTarget(ClientID, "Invalid spectator id used"); + if(SpectatorId >= 0 && (!m_apPlayers[SpectatorId] || m_apPlayers[SpectatorId]->GetTeam() == TEAM_SPECTATORS)) + SendChatTarget(ClientId, "Invalid spectator id used"); else - pPlayer->m_SpectatorID = SpectatorID; + pPlayer->m_SpectatorId = SpectatorId; } -void CGameContext::OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int ClientID) +void CGameContext::OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int ClientId) { - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; if(g_Config.m_SvSpamprotection && pPlayer->m_LastChangeInfo && pPlayer->m_LastChangeInfo + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay > Server()->Tick()) return; bool SixupNeedsUpdate = false; - if(!str_utf8_check(pMsg->m_pName) || !str_utf8_check(pMsg->m_pClan) || !str_utf8_check(pMsg->m_pSkin)) - { - return; - } pPlayer->m_LastChangeInfo = Server()->Tick(); pPlayer->UpdatePlaytime(); + if(g_Config.m_SvSpamprotection) + { + CNetMsg_Sv_ChangeInfoCooldown ChangeInfoCooldownMsg; + ChangeInfoCooldownMsg.m_WaitUntil = Server()->Tick() + Server()->TickSpeed() * g_Config.m_SvInfoChangeDelay; + Server()->SendPackMsg(&ChangeInfoCooldownMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); + } + // set infos - if(Server()->WouldClientNameChange(ClientID, pMsg->m_pName) && !ProcessSpamProtection(ClientID)) + if(Server()->WouldClientNameChange(ClientId, pMsg->m_pName) && !ProcessSpamProtection(ClientId)) { char aOldName[MAX_NAME_LENGTH]; - str_copy(aOldName, Server()->ClientName(ClientID), sizeof(aOldName)); + str_copy(aOldName, Server()->ClientName(ClientId), sizeof(aOldName)); - Server()->SetClientName(ClientID, pMsg->m_pName); + Server()->SetClientName(ClientId, pMsg->m_pName); char aChatText[256]; - str_format(aChatText, sizeof(aChatText), "'%s' changed name to '%s'", aOldName, Server()->ClientName(ClientID)); - SendChat(-1, CGameContext::CHAT_ALL, aChatText); + str_format(aChatText, sizeof(aChatText), "'%s' changed name to '%s'", aOldName, Server()->ClientName(ClientId)); + SendChat(-1, TEAM_ALL, aChatText); // reload scores - Score()->PlayerData(ClientID)->Reset(); - m_apPlayers[ClientID]->m_Score.reset(); - Score()->LoadPlayerData(ClientID); + Score()->PlayerData(ClientId)->Reset(); + m_apPlayers[ClientId]->m_Score.reset(); + Score()->LoadPlayerData(ClientId); SixupNeedsUpdate = true; - LogEvent("Name change", ClientID); + LogEvent("Name change", ClientId); } - if(Server()->WouldClientClanChange(ClientID, pMsg->m_pClan)) + if(Server()->WouldClientClanChange(ClientId, pMsg->m_pClan)) { SixupNeedsUpdate = true; - Server()->SetClientClan(ClientID, pMsg->m_pClan); + Server()->SetClientClan(ClientId, pMsg->m_pClan); } - if(Server()->ClientCountry(ClientID) != pMsg->m_Country) + if(Server()->ClientCountry(ClientId) != pMsg->m_Country) SixupNeedsUpdate = true; - Server()->SetClientCountry(ClientID, pMsg->m_Country); + Server()->SetClientCountry(ClientId, pMsg->m_Country); str_copy(pPlayer->m_TeeInfos.m_aSkinName, pMsg->m_pSkin, sizeof(pPlayer->m_TeeInfos.m_aSkinName)); pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor; pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody; pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet; - if(!Server()->IsSixup(ClientID)) + if(!Server()->IsSixup(ClientId)) pPlayer->m_TeeInfos.ToSixup(); if(SixupNeedsUpdate) { protocol7::CNetMsg_Sv_ClientDrop Drop; - Drop.m_ClientID = ClientID; + Drop.m_ClientId = ClientId; Drop.m_pReason = ""; Drop.m_Silent = true; protocol7::CNetMsg_Sv_ClientInfo Info; - Info.m_ClientID = ClientID; - Info.m_pName = Server()->ClientName(ClientID); + Info.m_ClientId = ClientId; + Info.m_pName = Server()->ClientName(ClientId); Info.m_Country = pMsg->m_Country; Info.m_pClan = pMsg->m_pClan; Info.m_Local = 0; Info.m_Silent = true; Info.m_Team = pPlayer->GetTeam(); - for(int p = 0; p < 6; p++) + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) { Info.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_apSkinPartNames[p]; Info.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p]; @@ -2608,7 +2682,7 @@ void CGameContext::OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int for(int i = 0; i < Server()->MaxClients(); i++) { - if(i != ClientID) + if(i != ClientId) { Server()->SendPackMsg(&Drop, MSGFLAG_VITAL | MSGFLAG_NORECORD, i); Server()->SendPackMsg(&Info, MSGFLAG_VITAL | MSGFLAG_NORECORD, i); @@ -2618,8 +2692,8 @@ void CGameContext::OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int else { protocol7::CNetMsg_Sv_SkinChange Msg; - Msg.m_ClientID = ClientID; - for(int p = 0; p < 6; p++) + Msg.m_ClientId = ClientId; + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) { Msg.m_apSkinPartNames[p] = pPlayer->m_TeeInfos.m_apSkinPartNames[p]; Msg.m_aSkinPartColors[p] = pPlayer->m_TeeInfos.m_aSkinPartColors[p]; @@ -2632,12 +2706,12 @@ void CGameContext::OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int Server()->ExpireServerInfo(); } -void CGameContext::OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int ClientID) +void CGameContext::OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int ClientId) { if(m_World.m_Paused) return; - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; auto &&CheckPreventEmote = [&](int64_t LastEmote, int64_t DelayInMs) { return (LastEmote * (int64_t)1000) + (int64_t)Server()->TickSpeed() * DelayInMs > ((int64_t)Server()->Tick() * (int64_t)1000); @@ -2662,7 +2736,7 @@ void CGameContext::OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int Cli { if(m_apPlayers[i] && pChr->CanSnapCharacter(i) && pChr->IsSnappingCharacterInView(i)) { - SendEmoticon(ClientID, pMsg->m_Emoticon, i); + SendEmoticon(ClientId, pMsg->m_Emoticon, i); } } } @@ -2670,10 +2744,10 @@ void CGameContext::OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int Cli { // else send emoticons to all players pPlayer->m_LastEmoteGlobal = Server()->Tick(); - SendEmoticon(ClientID, pMsg->m_Emoticon, -1); + SendEmoticon(ClientId, pMsg->m_Emoticon, -1); } - if(g_Config.m_SvEmotionalTees && pPlayer->m_EyeEmoteEnabled) + if(g_Config.m_SvEmotionalTees == 1 && pPlayer->m_EyeEmoteEnabled) { int EmoteType = EMOTE_NORMAL; switch(pMsg->m_Emoticon) @@ -2711,17 +2785,17 @@ void CGameContext::OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int Cli } } -void CGameContext::OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientID) +void CGameContext::OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientId) { if(m_World.m_Paused) return; - if(m_VoteCloseTime && m_VoteCreator == ClientID && GetDDRaceTeam(ClientID) && (IsKickVote() || IsSpecVote())) + if(m_VoteCloseTime && m_VoteCreator == ClientId && GetDDRaceTeam(ClientId) && (IsKickVote() || IsSpecVote())) { - SendChatTarget(ClientID, "You are running a vote please try again after the vote is done!"); + SendChatTarget(ClientId, "You are running a vote please try again after the vote is done!"); return; } - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; if(pPlayer->m_LastKill && pPlayer->m_LastKill + Server()->TickSpeed() * g_Config.m_SvKillDelay > Server()->Tick()) return; if(pPlayer->IsPaused()) @@ -2735,7 +2809,7 @@ void CGameContext::OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientID) int CurrTime = (Server()->Tick() - pChr->m_StartTime) / Server()->TickSpeed(); if(g_Config.m_SvKillProtection != 0 && CurrTime >= (60 * g_Config.m_SvKillProtection) && pChr->m_DDRaceState == DDRACE_STARTED) { - SendChatTarget(ClientID, "Kill Protection enabled. If you really want to kill, type /kill"); + SendChatTarget(ClientId, "Kill Protection enabled. If you really want to kill, type /kill"); return; } @@ -2744,66 +2818,50 @@ void CGameContext::OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientID) pPlayer->Respawn(); } -void CGameContext::OnStartInfoNetMessage(const CNetMsg_Cl_StartInfo *pMsg, int ClientID) +void CGameContext::OnStartInfoNetMessage(const CNetMsg_Cl_StartInfo *pMsg, int ClientId) { - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; if(pPlayer->m_IsReady) return; - if(!str_utf8_check(pMsg->m_pName)) - { - Server()->Kick(ClientID, "name is not valid utf8"); - return; - } - if(!str_utf8_check(pMsg->m_pClan)) - { - Server()->Kick(ClientID, "clan is not valid utf8"); - return; - } - if(!str_utf8_check(pMsg->m_pSkin)) - { - Server()->Kick(ClientID, "skin is not valid utf8"); - return; - } - pPlayer->m_LastChangeInfo = Server()->Tick(); // set start infos - Server()->SetClientName(ClientID, pMsg->m_pName); + Server()->SetClientName(ClientId, pMsg->m_pName); // trying to set client name can delete the player object, check if it still exists - if(!m_apPlayers[ClientID]) + if(!m_apPlayers[ClientId]) { return; } - Server()->SetClientClan(ClientID, pMsg->m_pClan); + Server()->SetClientClan(ClientId, pMsg->m_pClan); // trying to set client clan can delete the player object, check if it still exists - if(!m_apPlayers[ClientID]) + if(!m_apPlayers[ClientId]) { return; } - Server()->SetClientCountry(ClientID, pMsg->m_Country); + Server()->SetClientCountry(ClientId, pMsg->m_Country); str_copy(pPlayer->m_TeeInfos.m_aSkinName, pMsg->m_pSkin, sizeof(pPlayer->m_TeeInfos.m_aSkinName)); pPlayer->m_TeeInfos.m_UseCustomColor = pMsg->m_UseCustomColor; pPlayer->m_TeeInfos.m_ColorBody = pMsg->m_ColorBody; pPlayer->m_TeeInfos.m_ColorFeet = pMsg->m_ColorFeet; - if(!Server()->IsSixup(ClientID)) + if(!Server()->IsSixup(ClientId)) pPlayer->m_TeeInfos.ToSixup(); // send clear vote options CNetMsg_Sv_VoteClearOptions ClearMsg; - Server()->SendPackMsg(&ClearMsg, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&ClearMsg, MSGFLAG_VITAL, ClientId); // begin sending vote options pPlayer->m_SendVoteIndex = 0; // send tuning parameters to client - SendTuningParams(ClientID, pPlayer->m_TuneZone); + SendTuningParams(ClientId, pPlayer->m_TuneZone); // client is ready to enter pPlayer->m_IsReady = true; CNetMsg_Sv_ReadyToEnter m; - Server()->SendPackMsg(&m, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientID); + Server()->SendPackMsg(&m, MSGFLAG_VITAL | MSGFLAG_FLUSH, ClientId); Server()->ExpireServerInfo(); } @@ -3053,7 +3111,7 @@ void CGameContext::ConPause(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConChangeMap(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - pSelf->m_pController->ChangeMap(pResult->NumArguments() ? pResult->GetString(0) : ""); + pSelf->m_pController->ChangeMap(pResult->GetString(0)); } void CGameContext::ConRandomMap(IConsole::IResult *pResult, void *pUserData) @@ -3070,6 +3128,8 @@ void CGameContext::ConRandomUnfinishedMap(IConsole::IResult *pResult, void *pUse CGameContext *pSelf = (CGameContext *)pUserData; int Stars = pResult->NumArguments() ? pResult->GetInteger(0) : -1; + if(pResult->m_ClientId != -1) + pSelf->m_VoteCreator = pResult->m_ClientId; pSelf->m_pScore->RandomUnfinishedMap(pSelf->m_VoteCreator, Stars); } @@ -3111,27 +3171,27 @@ void CGameContext::ConBroadcast(IConsole::IResult *pResult, void *pUserData) void CGameContext::ConSay(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - pSelf->SendChat(-1, CGameContext::CHAT_ALL, pResult->GetString(0)); + pSelf->SendChat(-1, TEAM_ALL, pResult->GetString(0)); } void CGameContext::ConSetTeam(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - int ClientID = clamp(pResult->GetInteger(0), 0, (int)MAX_CLIENTS - 1); + int ClientId = clamp(pResult->GetInteger(0), 0, (int)MAX_CLIENTS - 1); int Team = clamp(pResult->GetInteger(1), -1, 1); int Delay = pResult->NumArguments() > 2 ? pResult->GetInteger(2) : 0; - if(!pSelf->m_apPlayers[ClientID]) + if(!pSelf->m_apPlayers[ClientId]) return; char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "moved client %d to team %d", ClientID, Team); + str_format(aBuf, sizeof(aBuf), "moved client %d to team %d", ClientId, Team); pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); - pSelf->m_apPlayers[ClientID]->Pause(CPlayer::PAUSE_NONE, false); // reset /spec and /pause to allow rejoin - pSelf->m_apPlayers[ClientID]->m_TeamChangeTick = pSelf->Server()->Tick() + pSelf->Server()->TickSpeed() * Delay * 60; - pSelf->m_pController->DoTeamChange(pSelf->m_apPlayers[ClientID], Team); + pSelf->m_apPlayers[ClientId]->Pause(CPlayer::PAUSE_NONE, false); // reset /spec and /pause to allow rejoin + pSelf->m_apPlayers[ClientId]->m_TeamChangeTick = pSelf->Server()->Tick() + pSelf->Server()->TickSpeed() * Delay * 60; + pSelf->m_pController->DoTeamChange(pSelf->m_apPlayers[ClientId], Team); if(Team == TEAM_SPECTATORS) - pSelf->m_apPlayers[ClientID]->Pause(CPlayer::PAUSE_NONE, true); + pSelf->m_apPlayers[ClientId]->Pause(CPlayer::PAUSE_NONE, true); } void CGameContext::ConSetTeamAll(IConsole::IResult *pResult, void *pUserData) @@ -3141,13 +3201,44 @@ void CGameContext::ConSetTeamAll(IConsole::IResult *pResult, void *pUserData) char aBuf[256]; str_format(aBuf, sizeof(aBuf), "All players were moved to the %s", pSelf->m_pController->GetTeamName(Team)); - pSelf->SendChat(-1, CGameContext::CHAT_ALL, aBuf); + pSelf->SendChat(-1, TEAM_ALL, aBuf); for(auto &pPlayer : pSelf->m_apPlayers) if(pPlayer) pSelf->m_pController->DoTeamChange(pPlayer, Team, false); } +void CGameContext::ConHotReload(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(!pSelf->GetPlayerChar(i)) + continue; + + CCharacter *pChar = pSelf->GetPlayerChar(i); + + // Save the tee individually + pSelf->m_apSavedTees[i] = new CSaveTee(); + pSelf->m_apSavedTees[i]->Save(pChar, false); + + if(pSelf->m_apPlayers[i]) + pSelf->m_apSavedTeleTees[i] = new CSaveTee(pSelf->m_apPlayers[i]->m_LastTeleTee); + + // Save the team state + pSelf->m_aTeamMapping[i] = pSelf->GetDDRaceTeam(i); + if(pSelf->m_aTeamMapping[i] == TEAM_SUPER) + pSelf->m_aTeamMapping[i] = pChar->m_TeamBeforeSuper; + + if(pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]]) + continue; + + pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]] = new CSaveTeam(); + pSelf->m_apSavedTeams[pSelf->m_aTeamMapping[i]]->Save(pSelf, pSelf->m_aTeamMapping[i], true, true); + } + pSelf->Server()->ReloadMap(); +} + void CGameContext::ConAddVote(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; @@ -3211,7 +3302,7 @@ void CGameContext::AddVote(const char *pDescription, const char *pCommand) m_pVoteOptionFirst = pOption; str_copy(pOption->m_aDescription, pDescription, sizeof(pOption->m_aDescription)); - mem_copy(pOption->m_aCommand, pCommand, Len + 1); + str_copy(pOption->m_aCommand, pCommand, Len + 1); } void CGameContext::ConRemoveVote(IConsole::IResult *pResult, void *pUserData) @@ -3262,7 +3353,7 @@ void CGameContext::ConRemoveVote(IConsole::IResult *pResult, void *pUserData) // copy option int Len = str_length(pSrc->m_aCommand); - CVoteOptionServer *pDst = (CVoteOptionServer *)pVoteOptionHeap->Allocate(sizeof(CVoteOptionServer) + Len); + CVoteOptionServer *pDst = (CVoteOptionServer *)pVoteOptionHeap->Allocate(sizeof(CVoteOptionServer) + Len, alignof(CVoteOptionServer)); pDst->m_pNext = 0; pDst->m_pPrev = pVoteOptionLast; if(pDst->m_pPrev) @@ -3272,7 +3363,7 @@ void CGameContext::ConRemoveVote(IConsole::IResult *pResult, void *pUserData) pVoteOptionFirst = pDst; str_copy(pDst->m_aDescription, pSrc->m_aDescription, sizeof(pDst->m_aDescription)); - mem_copy(pDst->m_aCommand, pSrc->m_aCommand, Len + 1); + str_copy(pDst->m_aCommand, pSrc->m_aCommand, Len + 1); } // clean up @@ -3299,7 +3390,8 @@ void CGameContext::ConForceVote(IConsole::IResult *pResult, void *pUserData) if(str_comp_nocase(pValue, pOption->m_aDescription) == 0) { str_format(aBuf, sizeof(aBuf), "authorized player forced server option '%s' (%s)", pValue, pReason); - pSelf->SendChatTarget(-1, aBuf, CHAT_SIX); + pSelf->SendChatTarget(-1, aBuf, FLAG_SIX); + pSelf->m_VoteCreator = pResult->m_ClientId; pSelf->Console()->ExecuteLine(pOption->m_aCommand); break; } @@ -3316,8 +3408,8 @@ void CGameContext::ConForceVote(IConsole::IResult *pResult, void *pUserData) } else if(str_comp_nocase(pType, "kick") == 0) { - int KickID = str_toint(pValue); - if(KickID < 0 || KickID >= MAX_CLIENTS || !pSelf->m_apPlayers[KickID]) + int KickId = str_toint(pValue); + if(KickId < 0 || KickId >= MAX_CLIENTS || !pSelf->m_apPlayers[KickId]) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "Invalid client id to kick"); return; @@ -3325,29 +3417,29 @@ void CGameContext::ConForceVote(IConsole::IResult *pResult, void *pUserData) if(!g_Config.m_SvVoteKickBantime) { - str_format(aBuf, sizeof(aBuf), "kick %d %s", KickID, pReason); + str_format(aBuf, sizeof(aBuf), "kick %d %s", KickId, pReason); pSelf->Console()->ExecuteLine(aBuf); } else { char aAddrStr[NETADDR_MAXSTRSIZE] = {0}; - pSelf->Server()->GetClientAddr(KickID, aAddrStr, sizeof(aAddrStr)); + pSelf->Server()->GetClientAddr(KickId, aAddrStr, sizeof(aAddrStr)); str_format(aBuf, sizeof(aBuf), "ban %s %d %s", aAddrStr, g_Config.m_SvVoteKickBantime, pReason); pSelf->Console()->ExecuteLine(aBuf); } } else if(str_comp_nocase(pType, "spectate") == 0) { - int SpectateID = str_toint(pValue); - if(SpectateID < 0 || SpectateID >= MAX_CLIENTS || !pSelf->m_apPlayers[SpectateID] || pSelf->m_apPlayers[SpectateID]->GetTeam() == TEAM_SPECTATORS) + int SpectateId = str_toint(pValue); + if(SpectateId < 0 || SpectateId >= MAX_CLIENTS || !pSelf->m_apPlayers[SpectateId] || pSelf->m_apPlayers[SpectateId]->GetTeam() == TEAM_SPECTATORS) { pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "Invalid client id to move"); return; } - str_format(aBuf, sizeof(aBuf), "'%s' was moved to spectator (%s)", pSelf->Server()->ClientName(SpectateID), pReason); + str_format(aBuf, sizeof(aBuf), "'%s' was moved to spectator (%s)", pSelf->Server()->ClientName(SpectateId), pReason); pSelf->SendChatTarget(-1, aBuf); - str_format(aBuf, sizeof(aBuf), "set_team %d -1 %d", SpectateID, g_Config.m_SvVoteSpectateRejoindelay); + str_format(aBuf, sizeof(aBuf), "set_team %d -1 %d", SpectateId, g_Config.m_SvVoteSpectateRejoindelay); pSelf->Console()->ExecuteLine(aBuf); } } @@ -3374,8 +3466,18 @@ void CGameContext::ConClearVotes(IConsole::IResult *pResult, void *pUserData) struct CMapNameItem { char m_aName[IO_MAX_PATH_LENGTH - 4]; + bool m_IsDirectory; - bool operator<(const CMapNameItem &Other) const { return str_comp_nocase(m_aName, Other.m_aName) < 0; } + static bool CompareFilenameAscending(const CMapNameItem Lhs, const CMapNameItem Rhs) + { + if(str_comp(Lhs.m_aName, "..") == 0) + return true; + if(str_comp(Rhs.m_aName, "..") == 0) + return false; + if(Lhs.m_IsDirectory != Rhs.m_IsDirectory) + return Lhs.m_IsDirectory; + return str_comp_filenames(Lhs.m_aName, Rhs.m_aName) < 0; + } }; void CGameContext::ConAddMapVotes(IConsole::IResult *pResult, void *pUserData) @@ -3383,19 +3485,48 @@ void CGameContext::ConAddMapVotes(IConsole::IResult *pResult, void *pUserData) CGameContext *pSelf = (CGameContext *)pUserData; std::vector vMapList; - pSelf->Storage()->ListDirectory(IStorage::TYPE_ALL, "maps", MapScan, &vMapList); - std::sort(vMapList.begin(), vMapList.end()); + const char *pDirectory = pResult->GetString(0); + + // Don't allow moving to parent directories + if(str_find_nocase(pDirectory, "..")) + return; + + char aPath[IO_MAX_PATH_LENGTH] = "maps/"; + str_append(aPath, pDirectory, sizeof(aPath)); + pSelf->Storage()->ListDirectory(IStorage::TYPE_ALL, aPath, MapScan, &vMapList); + std::sort(vMapList.begin(), vMapList.end(), CMapNameItem::CompareFilenameAscending); for(auto &Item : vMapList) { - char aDescription[64]; - str_format(aDescription, sizeof(aDescription), "Map: %s", Item.m_aName); + if(!str_comp(Item.m_aName, "..") && (!str_comp(aPath, "maps/"))) + continue; + + char aDescription[VOTE_DESC_LENGTH]; + str_format(aDescription, sizeof(aDescription), "%s: %s%s", Item.m_IsDirectory ? "Directory" : "Map", Item.m_aName, Item.m_IsDirectory ? "/" : ""); - char aCommand[IO_MAX_PATH_LENGTH * 2 + 10]; - char aMapEscaped[IO_MAX_PATH_LENGTH * 2]; - char *pDst = aMapEscaped; - str_escape(&pDst, Item.m_aName, aMapEscaped + sizeof(aMapEscaped)); - str_format(aCommand, sizeof(aCommand), "change_map \"%s\"", aMapEscaped); + char aCommand[VOTE_CMD_LENGTH]; + char aOptionEscaped[IO_MAX_PATH_LENGTH * 2]; + char *pDst = aOptionEscaped; + str_escape(&pDst, Item.m_aName, aOptionEscaped + sizeof(aOptionEscaped)); + + char aDirectory[IO_MAX_PATH_LENGTH] = ""; + if(pResult->NumArguments()) + str_copy(aDirectory, pDirectory); + + if(!str_comp(Item.m_aName, "..")) + { + fs_parent_dir(aDirectory); + str_format(aCommand, sizeof(aCommand), "clear_votes; add_map_votes \"%s\"", aDirectory); + } + else if(Item.m_IsDirectory) + { + str_append(aDirectory, "/", sizeof(aDirectory)); + str_append(aDirectory, aOptionEscaped, sizeof(aDirectory)); + + str_format(aCommand, sizeof(aCommand), "clear_votes; add_map_votes \"%s\"", aDirectory); + } + else + str_format(aCommand, sizeof(aCommand), "change_map \"%s%s%s\"", pDirectory, pDirectory[0] == '\0' ? "" : "/", aOptionEscaped); pSelf->AddVote(aDescription, aCommand); } @@ -3405,11 +3536,15 @@ void CGameContext::ConAddMapVotes(IConsole::IResult *pResult, void *pUserData) int CGameContext::MapScan(const char *pName, int IsDir, int DirType, void *pUserData) { - if(IsDir || !str_endswith(pName, ".map")) + if((!IsDir && !str_endswith(pName, ".map")) || !str_comp(pName, ".")) return 0; CMapNameItem Item; - str_truncate(Item.m_aName, sizeof(Item.m_aName), pName, str_length(pName) - str_length(".map")); + Item.m_IsDirectory = IsDir; + if(!IsDir) + str_truncate(Item.m_aName, sizeof(Item.m_aName), pName, str_length(pName) - str_length(".map")); + else + str_copy(Item.m_aName, pName); static_cast *>(pUserData)->push_back(Item); return 0; @@ -3420,9 +3555,9 @@ void CGameContext::ConVote(IConsole::IResult *pResult, void *pUserData) CGameContext *pSelf = (CGameContext *)pUserData; if(str_comp_nocase(pResult->GetString(0), "yes") == 0) - pSelf->ForceVote(pResult->m_ClientID, true); + pSelf->ForceVote(pResult->m_ClientId, true); else if(str_comp_nocase(pResult->GetString(0), "no") == 0) - pSelf->ForceVote(pResult->m_ClientID, false); + pSelf->ForceVote(pResult->m_ClientId, false); } void CGameContext::ConVotes(IConsole::IResult *pResult, void *pUserData) @@ -3486,32 +3621,35 @@ void CGameContext::OnConsoleInit() m_pEngine = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); - Console()->Register("tune", "s[tuning] ?i[value]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneParam, this, "Tune variable to value or show current value"); - Console()->Register("toggle_tune", "s[tuning] i[value 1] i[value 2]", CFGFLAG_SERVER | CFGFLAG_GAME, ConToggleTuneParam, this, "Toggle tune variable"); + Console()->Register("tune", "s[tuning] ?f[value]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneParam, this, "Tune variable to value or show current value"); + Console()->Register("toggle_tune", "s[tuning] f[value 1] f[value 2]", CFGFLAG_SERVER, ConToggleTuneParam, this, "Toggle tune variable"); Console()->Register("tune_reset", "?s[tuning]", CFGFLAG_SERVER, ConTuneReset, this, "Reset all or one tuning variable to default"); Console()->Register("tunes", "", CFGFLAG_SERVER, ConTunes, this, "List all tuning variables and their values"); - Console()->Register("tune_zone", "i[zone] s[tuning] i[value]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneZone, this, "Tune in zone a variable to value"); + Console()->Register("tune_zone", "i[zone] s[tuning] f[value]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneZone, this, "Tune in zone a variable to value"); Console()->Register("tune_zone_dump", "i[zone]", CFGFLAG_SERVER, ConTuneDumpZone, this, "Dump zone tuning in zone x"); - Console()->Register("tune_zone_reset", "?i[zone]", CFGFLAG_SERVER, ConTuneResetZone, this, "reset zone tuning in zone x or in all zones"); - Console()->Register("tune_zone_enter", "i[zone] r[message]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneSetZoneMsgEnter, this, "which message to display on zone enter; use 0 for normal area"); - Console()->Register("tune_zone_leave", "i[zone] r[message]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneSetZoneMsgLeave, this, "which message to display on zone leave; use 0 for normal area"); + Console()->Register("tune_zone_reset", "?i[zone]", CFGFLAG_SERVER, ConTuneResetZone, this, "Reset zone tuning in zone x or in all zones"); + Console()->Register("tune_zone_enter", "i[zone] r[message]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneSetZoneMsgEnter, this, "Which message to display on zone enter; use 0 for normal area"); + Console()->Register("tune_zone_leave", "i[zone] r[message]", CFGFLAG_SERVER | CFGFLAG_GAME, ConTuneSetZoneMsgLeave, this, "Which message to display on zone leave; use 0 for normal area"); Console()->Register("mapbug", "s[mapbug]", CFGFLAG_SERVER | CFGFLAG_GAME, ConMapbug, this, "Enable map compatibility mode using the specified bug (example: grenade-doubleexplosion@ddnet.tw)"); Console()->Register("switch_open", "i[switch]", CFGFLAG_SERVER | CFGFLAG_GAME, ConSwitchOpen, this, "Whether a switch is deactivated by default (otherwise activated)"); Console()->Register("pause_game", "", CFGFLAG_SERVER, ConPause, this, "Pause/unpause game"); - Console()->Register("change_map", "?r[map]", CFGFLAG_SERVER | CFGFLAG_STORE, ConChangeMap, this, "Change map"); - Console()->Register("random_map", "?i[stars]", CFGFLAG_SERVER, ConRandomMap, this, "Random map"); - Console()->Register("random_unfinished_map", "?i[stars]", CFGFLAG_SERVER, ConRandomUnfinishedMap, this, "Random unfinished map"); + Console()->Register("change_map", "r[map]", CFGFLAG_SERVER | CFGFLAG_STORE, ConChangeMap, this, "Change map"); + Console()->Register("random_map", "?i[stars]", CFGFLAG_SERVER | CFGFLAG_STORE, ConRandomMap, this, "Random map"); + Console()->Register("random_unfinished_map", "?i[stars]", CFGFLAG_SERVER | CFGFLAG_STORE, ConRandomUnfinishedMap, this, "Random unfinished map"); Console()->Register("restart", "?i[seconds]", CFGFLAG_SERVER | CFGFLAG_STORE, ConRestart, this, "Restart in x seconds (0 = abort)"); Console()->Register("broadcast", "r[message]", CFGFLAG_SERVER, ConBroadcast, this, "Broadcast message"); Console()->Register("say", "r[message]", CFGFLAG_SERVER, ConSay, this, "Say in chat"); Console()->Register("set_team", "i[id] i[team-id] ?i[delay in minutes]", CFGFLAG_SERVER, ConSetTeam, this, "Set team of player to team"); Console()->Register("set_team_all", "i[team-id]", CFGFLAG_SERVER, ConSetTeamAll, this, "Set team of all players to team"); + Console()->Register("hot_reload", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConHotReload, this, "Reload the map while preserving the state of tees and teams"); + Console()->Register("reload_censorlist", "", CFGFLAG_SERVER, ConReloadCensorlist, this, "Reload the censorlist"); + Console()->Register("reload_announcement", "", CFGFLAG_SERVER, ConReloadAnnouncement, this, "Reload the announcements"); Console()->Register("add_vote", "s[name] r[command]", CFGFLAG_SERVER, ConAddVote, this, "Add a voting option"); Console()->Register("remove_vote", "r[name]", CFGFLAG_SERVER, ConRemoveVote, this, "remove a voting option"); Console()->Register("force_vote", "s[name] s[command] ?r[reason]", CFGFLAG_SERVER, ConForceVote, this, "Force a voting option"); Console()->Register("clear_votes", "", CFGFLAG_SERVER, ConClearVotes, this, "Clears the voting options"); - Console()->Register("add_map_votes", "", CFGFLAG_SERVER, ConAddMapVotes, this, "Automatically adds voting options for all maps"); + Console()->Register("add_map_votes", "?s[directory]", CFGFLAG_SERVER, ConAddMapVotes, this, "Automatically adds voting options for all maps"); Console()->Register("vote", "r['yes'|'no']", CFGFLAG_SERVER, ConVote, this, "Force a vote to yes/no"); Console()->Register("votes", "?i[page]", CFGFLAG_SERVER, ConVotes, this, "Show all votes (page 0 by default, 20 entries per page)"); Console()->Register("dump_antibot", "", CFGFLAG_SERVER, ConDumpAntibot, this, "Dumps the antibot status"); @@ -3523,12 +3661,166 @@ void CGameContext::OnConsoleInit() Console()->Chain("sv_vote_kick_min", ConchainSettingUpdate, this); Console()->Chain("sv_vote_spectate", ConchainSettingUpdate, this); Console()->Chain("sv_spectator_slots", ConchainSettingUpdate, this); - Console()->Chain("sv_max_clients", ConchainSettingUpdate, this); -#define CONSOLE_COMMAND(name, params, flags, callback, userdata, help) m_pConsole->Register(name, params, flags, callback, userdata, help); -#include -#define CHAT_COMMAND(name, params, flags, callback, userdata, help) m_pConsole->Register(name, params, flags, callback, userdata, help); -#include + RegisterDDRaceCommands(); + RegisterChatCommands(); +} + +void CGameContext::RegisterDDRaceCommands() +{ + Console()->Register("kill_pl", "v[id]", CFGFLAG_SERVER, ConKillPlayer, this, "Kills player v and announces the kill"); + Console()->Register("totele", "i[number]", CFGFLAG_SERVER | CMDFLAG_TEST, ConToTeleporter, this, "Teleports you to teleporter v"); + Console()->Register("totelecp", "i[number]", CFGFLAG_SERVER | CMDFLAG_TEST, ConToCheckTeleporter, this, "Teleports you to checkpoint teleporter v"); + Console()->Register("tele", "?i[id] ?i[id]", CFGFLAG_SERVER | CMDFLAG_TEST, ConTeleport, this, "Teleports player i (or you) to player i (or you to where you look at)"); + Console()->Register("addweapon", "i[weapon-id]", CFGFLAG_SERVER | CMDFLAG_TEST, ConAddWeapon, this, "Gives weapon with id i to you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)"); + Console()->Register("removeweapon", "i[weapon-id]", CFGFLAG_SERVER | CMDFLAG_TEST, ConRemoveWeapon, this, "removes weapon with id i from you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)"); + Console()->Register("shotgun", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConShotgun, this, "Gives a shotgun to you"); + Console()->Register("grenade", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConGrenade, this, "Gives a grenade launcher to you"); + Console()->Register("laser", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConLaser, this, "Gives a laser to you"); + Console()->Register("rifle", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConLaser, this, "Gives a laser to you"); + Console()->Register("jetpack", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConJetpack, this, "Gives jetpack to you"); + Console()->Register("setjumps", "i[jumps]", CFGFLAG_SERVER | CMDFLAG_TEST, ConSetJumps, this, "Gives you as many jumps as you specify"); + Console()->Register("weapons", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConWeapons, this, "Gives all weapons to you"); + Console()->Register("unshotgun", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnShotgun, this, "Removes the shotgun from you"); + Console()->Register("ungrenade", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnGrenade, this, "Removes the grenade launcher from you"); + Console()->Register("unlaser", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnLaser, this, "Removes the laser from you"); + Console()->Register("unrifle", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnLaser, this, "Removes the laser from you"); + Console()->Register("unjetpack", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnJetpack, this, "Removes the jetpack from you"); + Console()->Register("unweapons", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnWeapons, this, "Removes all weapons from you"); + Console()->Register("ninja", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConNinja, this, "Makes you a ninja"); + Console()->Register("unninja", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnNinja, this, "Removes ninja from you"); + Console()->Register("super", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConSuper, this, "Makes you super"); + Console()->Register("unsuper", "", CFGFLAG_SERVER, ConUnSuper, this, "Removes super from you"); + Console()->Register("endless_hook", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConEndlessHook, this, "Gives you endless hook"); + Console()->Register("unendless_hook", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnEndlessHook, this, "Removes endless hook from you"); + Console()->Register("solo", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConSolo, this, "Puts you into solo part"); + Console()->Register("unsolo", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnSolo, this, "Puts you out of solo part"); + Console()->Register("freeze", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConFreeze, this, "Puts you into freeze"); + Console()->Register("unfreeze", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnFreeze, this, "Puts you out of freeze"); + Console()->Register("deep", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConDeep, this, "Puts you into deep freeze"); + Console()->Register("undeep", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnDeep, this, "Puts you out of deep freeze"); + Console()->Register("livefreeze", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConLiveFreeze, this, "Makes you live frozen"); + Console()->Register("unlivefreeze", "", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnLiveFreeze, this, "Puts you out of live freeze"); + Console()->Register("left", "?i[tiles]", CFGFLAG_SERVER | CMDFLAG_TEST, ConGoLeft, this, "Makes you move 1 tile left"); + Console()->Register("right", "?i[tiles]", CFGFLAG_SERVER | CMDFLAG_TEST, ConGoRight, this, "Makes you move 1 tile right"); + Console()->Register("up", "?i[tiles]", CFGFLAG_SERVER | CMDFLAG_TEST, ConGoUp, this, "Makes you move 1 tile up"); + Console()->Register("down", "?i[tiles]", CFGFLAG_SERVER | CMDFLAG_TEST, ConGoDown, this, "Makes you move 1 tile down"); + + Console()->Register("move", "i[x] i[y]", CFGFLAG_SERVER | CMDFLAG_TEST, ConMove, this, "Moves to the tile with x/y-number ii"); + Console()->Register("move_raw", "i[x] i[y]", CFGFLAG_SERVER | CMDFLAG_TEST, ConMoveRaw, this, "Moves to the point with x/y-coordinates ii"); + Console()->Register("force_pause", "v[id] i[seconds]", CFGFLAG_SERVER, ConForcePause, this, "Force i to pause for i seconds"); + Console()->Register("force_unpause", "v[id]", CFGFLAG_SERVER, ConForcePause, this, "Set force-pause timer of i to 0."); + + Console()->Register("set_team_ddr", "v[id] i[team]", CFGFLAG_SERVER, ConSetDDRTeam, this, "Set ddrace team of a player"); + Console()->Register("uninvite", "v[id] i[team]", CFGFLAG_SERVER, ConUninvite, this, "Uninvite player from team"); + + Console()->Register("vote_mute", "v[id] i[seconds] ?r[reason]", CFGFLAG_SERVER, ConVoteMute, this, "Remove v's right to vote for i seconds"); + Console()->Register("vote_unmute", "v[id]", CFGFLAG_SERVER, ConVoteUnmute, this, "Give back v's right to vote."); + Console()->Register("vote_mutes", "", CFGFLAG_SERVER, ConVoteMutes, this, "List the current active vote mutes."); + Console()->Register("mute", "", CFGFLAG_SERVER, ConMute, this, "Use either 'muteid ' or 'muteip '"); + Console()->Register("muteid", "v[id] i[seconds] ?r[reason]", CFGFLAG_SERVER, ConMuteId, this, "Mute player with id"); + Console()->Register("muteip", "s[ip] i[seconds] ?r[reason]", CFGFLAG_SERVER, ConMuteIp, this, "Mute player with IP address"); + Console()->Register("unmute", "i[muteid]", CFGFLAG_SERVER, ConUnmute, this, "Unmute mute with number from \"mutes\""); + Console()->Register("unmuteid", "v[id]", CFGFLAG_SERVER, ConUnmuteId, this, "Unmute player with id"); + Console()->Register("mutes", "", CFGFLAG_SERVER, ConMutes, this, "Show all active mutes"); + Console()->Register("moderate", "", CFGFLAG_SERVER, ConModerate, this, "Enables/disables active moderator mode for the player"); + Console()->Register("vote_no", "", CFGFLAG_SERVER, ConVoteNo, this, "Same as \"vote no\""); + Console()->Register("save_dry", "", CFGFLAG_SERVER, ConDrySave, this, "Dump the current savestring"); + Console()->Register("dump_log", "?i[seconds]", CFGFLAG_SERVER, ConDumpLog, this, "Show logs of the last i seconds"); + + Console()->Register("freezehammer", "v[id]", CFGFLAG_SERVER | CMDFLAG_TEST, ConFreezeHammer, this, "Gives a player Freeze Hammer"); + Console()->Register("unfreezehammer", "v[id]", CFGFLAG_SERVER | CMDFLAG_TEST, ConUnFreezeHammer, this, "Removes Freeze Hammer from a player"); +} + +void CGameContext::RegisterChatCommands() +{ + Console()->Register("credits", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConCredits, this, "Shows the credits of the DDNet mod"); + Console()->Register("rules", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConRules, this, "Shows the server rules"); + Console()->Register("emote", "?s[emote name] i[duration in seconds]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConEyeEmote, this, "Sets your tee's eye emote"); + Console()->Register("eyeemote", "?s['on'|'off'|'toggle']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSetEyeEmote, this, "Toggles use of standard eye-emotes on/off, eyeemote s, where s = on for on, off for off, toggle for toggle and nothing to show current status"); + Console()->Register("settings", "?s[configname]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSettings, this, "Shows gameplay information for this server"); + Console()->Register("help", "?r[command]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConHelp, this, "Shows help to command r, general help if left blank"); + Console()->Register("info", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConInfo, this, "Shows info about this server"); + Console()->Register("list", "?s[filter]", CFGFLAG_CHAT, ConList, this, "List connected players with optional case-insensitive substring matching filter"); + Console()->Register("me", "r[message]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConMe, this, "Like the famous irc command '/me says hi' will display ' says hi'"); + Console()->Register("w", "s[player name] r[message]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConWhisper, this, "Whisper something to someone (private message)"); + Console()->Register("whisper", "s[player name] r[message]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConWhisper, this, "Whisper something to someone (private message)"); + Console()->Register("c", "r[message]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConConverse, this, "Converse with the last person you whispered to (private message)"); + Console()->Register("converse", "r[message]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConConverse, this, "Converse with the last person you whispered to (private message)"); + Console()->Register("pause", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTogglePause, this, "Toggles pause"); + Console()->Register("spec", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConToggleSpec, this, "Toggles spec (if not available behaves as /pause)"); + Console()->Register("pausevoted", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTogglePauseVoted, this, "Toggles pause on the currently voted player"); + Console()->Register("specvoted", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConToggleSpecVoted, this, "Toggles spec on the currently voted player"); + Console()->Register("dnd", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConDND, this, "Toggle Do Not Disturb (no chat and server messages)"); + Console()->Register("whispers", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConWhispers, this, "Toggle receiving whispers"); + Console()->Register("mapinfo", "?r[map]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConMapInfo, this, "Show info about the map with name r gives (current map by default)"); + Console()->Register("timeout", "?s[code]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTimeout, this, "Set timeout protection code s"); + Console()->Register("practice", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConPractice, this, "Enable cheats for your current team's run, but you can't earn a rank"); + Console()->Register("practicecmdlist", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConPracticeCmdList, this, "List all commands that are avaliable in practice mode"); + Console()->Register("swap", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSwap, this, "Request to swap your tee with another team member"); + Console()->Register("save", "?r[code]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSave, this, "Save team with code r."); + Console()->Register("load", "?r[code]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConLoad, this, "Load with code r. /load to check your existing saves"); + Console()->Register("map", "?r[map]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConMap, this, "Vote a map by name"); + + Console()->Register("rankteam", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeamRank, this, "Shows the team rank of player with name r (your team rank by default)"); + Console()->Register("teamrank", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeamRank, this, "Shows the team rank of player with name r (your team rank by default)"); + + Console()->Register("rank", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConRank, this, "Shows the rank of player with name r (your rank by default)"); + Console()->Register("top5team", "?s[player name] ?i[rank to start with]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeamTop5, this, "Shows five team ranks of the ladder or of a player beginning with rank i (1 by default, -1 for worst)"); + Console()->Register("teamtop5", "?s[player name] ?i[rank to start with]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeamTop5, this, "Shows five team ranks of the ladder or of a player beginning with rank i (1 by default, -1 for worst)"); + Console()->Register("top", "?i[rank to start with]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTop, this, "Shows the top ranks of the global and regional ladder beginning with rank i (1 by default, -1 for worst)"); + Console()->Register("top5", "?i[rank to start with]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTop, this, "Shows the top ranks of the global and regional ladder beginning with rank i (1 by default, -1 for worst)"); + Console()->Register("times", "?s[player name] ?i[number of times to skip]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTimes, this, "/times ?s?i shows last 5 times of the server or of a player beginning with name s starting with time i (i = 1 by default, -1 for first)"); + Console()->Register("points", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConPoints, this, "Shows the global points of a player beginning with name r (your rank by default)"); + Console()->Register("top5points", "?i[number]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTopPoints, this, "Shows five points of the global point ladder beginning with rank i (1 by default)"); + Console()->Register("timecp", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTimeCP, this, "Set your checkpoints based on another player"); + + Console()->Register("team", "?i[id]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeam, this, "Lets you join team i (shows your team if left blank)"); + Console()->Register("lock", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConLock, this, "Toggle team lock so no one else can join and so the team restarts when a player dies. /lock 0 to unlock, /lock 1 to lock"); + Console()->Register("unlock", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConUnlock, this, "Unlock a team"); + Console()->Register("invite", "r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConInvite, this, "Invite a person to a locked team"); + Console()->Register("join", "r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConJoin, this, "Join the team of the specified player"); + Console()->Register("team0mode", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeam0Mode, this, "Toggle team between team 0 and team mode. This mode will make your team behave like team 0."); + + Console()->Register("showothers", "?i['0'|'1'|'2']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConShowOthers, this, "Whether to show players from other teams or not (off by default), optional i = 0 for off, i = 1 for on, i = 2 for own team only"); + Console()->Register("showall", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConShowAll, this, "Whether to show players at any distance (off by default), optional i = 0 for off else for on"); + Console()->Register("specteam", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSpecTeam, this, "Whether to show players from other teams when spectating (on by default), optional i = 0 for off else for on"); + Console()->Register("ninjajetpack", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConNinjaJetpack, this, "Whether to use ninja jetpack or not. Makes jetpack look more awesome"); + Console()->Register("saytime", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConSayTime, this, "Privately messages someone's current time in this current running race (your time by default)"); + Console()->Register("saytimeall", "", CFGFLAG_CHAT | CFGFLAG_SERVER | CFGFLAG_NONTEEHISTORIC, ConSayTimeAll, this, "Publicly messages everyone your current time in this current running race"); + Console()->Register("time", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTime, this, "Privately shows you your current time in this current running race in the broadcast message"); + Console()->Register("timer", "?s['gametimer'|'broadcast'|'both'|'none'|'cycle']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConSetTimerType, this, "Personal Setting of showing time in either broadcast or game/round timer, timer s, where s = broadcast for broadcast, gametimer for game/round timer, cycle for cycle, both for both, none for no timer and nothing to show current status"); + Console()->Register("r", "", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConRescue, this, "Teleport yourself out of freeze if auto rescue mode is enabled, otherwise it will set position for rescuing if grounded and teleport you out of freeze if not (use sv_rescue 1 to enable this feature)"); + Console()->Register("rescue", "", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConRescue, this, "Teleport yourself out of freeze if auto rescue mode is enabled, otherwise it will set position for rescuing if grounded and teleport you out of freeze if not (use sv_rescue 1 to enable this feature)"); + Console()->Register("rescuemode", "?r['auto'|'manual']", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConRescueMode, this, "Sets one of the two rescue modes (auto or manual). Prints current mode if no arguments provided"); + Console()->Register("tp", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleTo, this, "Depending on the number of supplied arguments, teleport yourself to; (0.) where you are spectating or aiming; (1.) the specified player name"); + Console()->Register("teleport", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleTo, this, "Depending on the number of supplied arguments, teleport yourself to; (0.) where you are spectating or aiming; (1.) the specified player name"); + Console()->Register("tpxy", "s[x] s[y]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleXY, this, "Teleport yourself to the specified coordinates. A tilde (~) can be used to denote your current position, e.g. '/tpxy ~1 ~' to teleport one tile to the right"); + Console()->Register("lasttp", "", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConLastTele, this, "Teleport yourself to the last location you teleported to"); + Console()->Register("tc", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleCursor, this, "Teleport yourself to player or to where you are spectating/or looking if no player name is given"); + Console()->Register("telecursor", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER | CMDFLAG_PRACTICE, ConTeleCursor, this, "Teleport yourself to player or to where you are spectating/or looking if no player name is given"); + Console()->Register("unsolo", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnSolo, this, "Puts you out of solo part"); + Console()->Register("solo", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeSolo, this, "Puts you into solo part"); + Console()->Register("undeep", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnDeep, this, "Puts you out of deep freeze"); + Console()->Register("deep", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeDeep, this, "Puts you into deep freeze"); + Console()->Register("addweapon", "i[weapon-id]", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeAddWeapon, this, "Gives weapon with id i to you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)"); + Console()->Register("removeweapon", "i[weapon-id]", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeRemoveWeapon, this, "removes weapon with id i from you (all = -1, hammer = 0, gun = 1, shotgun = 2, grenade = 3, laser = 4, ninja = 5)"); + Console()->Register("shotgun", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeShotgun, this, "Gives a shotgun to you"); + Console()->Register("grenade", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeGrenade, this, "Gives a grenade launcher to you"); + Console()->Register("laser", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeLaser, this, "Gives a laser to you"); + Console()->Register("rifle", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeLaser, this, "Gives a laser to you"); + Console()->Register("jetpack", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeJetpack, this, "Gives jetpack to you"); + Console()->Register("setjumps", "i[jumps]", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeSetJumps, this, "Gives you as many jumps as you specify"); + Console()->Register("weapons", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeWeapons, this, "Gives all weapons to you"); + Console()->Register("unshotgun", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnShotgun, this, "Removes the shotgun from you"); + Console()->Register("ungrenade", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnGrenade, this, "Removes the grenade launcher from you"); + Console()->Register("unlaser", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnLaser, this, "Removes the laser from you"); + Console()->Register("unrifle", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnLaser, this, "Removes the laser from you"); + Console()->Register("unjetpack", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnJetpack, this, "Removes the jetpack from you"); + Console()->Register("unweapons", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnWeapons, this, "Removes all weapons from you"); + Console()->Register("ninja", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeNinja, this, "Makes you a ninja"); + Console()->Register("unninja", "", CFGFLAG_CHAT | CMDFLAG_PRACTICE, ConPracticeUnNinja, this, "Removes ninja from you"); + Console()->Register("kill", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConProtectedKill, this, "Kill yourself when kill-protected during a long game (use f1, kill for regular kill)"); } void CGameContext::OnInit(const void *pPersistentData) @@ -3643,23 +3935,7 @@ void CGameContext::OnInit(const void *pPersistentData) else m_pController = new CGameControllerDDRace(this); - const char *pCensorFilename = "censorlist.txt"; - IOHANDLE File = Storage()->OpenFile(pCensorFilename, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL); - if(!File) - { - dbg_msg("censorlist", "failed to open '%s'", pCensorFilename); - } - else - { - CLineReader LineReader; - LineReader.Init(File); - char *pLine; - while((pLine = LineReader.Get())) - { - m_vCensorlist.emplace_back(pLine); - } - io_close(File); - } + ReadCensorList(); m_TeeHistorianActive = g_Config.m_SvTeeHistorian; if(m_TeeHistorianActive) @@ -3734,6 +4010,8 @@ void CGameContext::OnInit(const void *pPersistentData) } } + Server()->DemoRecorder_HandleAutoStart(); + if(!m_pScore) { m_pScore = new CScore(this, ((CServer *)Server())->DbPool()); @@ -3742,9 +4020,6 @@ void CGameContext::OnInit(const void *pPersistentData) // create all entities from the game layer CreateAllEntities(true); - if(GIT_SHORTREV_HASH) - Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "git-revision", GIT_SHORTREV_HASH); - m_pAntibot->RoundStart(this); } @@ -3863,36 +4138,28 @@ void CGameContext::OnMapChange(char *pNewMapName, int MapNameSize) char aConfig[IO_MAX_PATH_LENGTH]; str_format(aConfig, sizeof(aConfig), "maps/%s.cfg", g_Config.m_SvMap); - IOHANDLE File = Storage()->OpenFile(aConfig, IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL); - if(!File) + CLineReader LineReader; + if(!LineReader.OpenFile(Storage()->OpenFile(aConfig, IOFLAG_READ, IStorage::TYPE_ALL))) { // No map-specific config, just return. return; } - CLineReader LineReader; - LineReader.Init(File); - std::vector vLines; - char *pLine; + std::vector vpLines; int TotalLength = 0; - while((pLine = LineReader.Get())) + while(const char *pLine = LineReader.Get()) { - int Length = str_length(pLine) + 1; - char *pCopy = (char *)malloc(Length); - mem_copy(pCopy, pLine, Length); - vLines.push_back(pCopy); - TotalLength += Length; + vpLines.push_back(pLine); + TotalLength += str_length(pLine) + 1; } - io_close(File); char *pSettings = (char *)malloc(maximum(1, TotalLength)); int Offset = 0; - for(auto &Line : vLines) + for(const char *pLine : vpLines) { - int Length = str_length(Line) + 1; - mem_copy(pSettings + Offset, Line, Length); + int Length = str_length(pLine) + 1; + mem_copy(pSettings + Offset, pLine, Length); Offset += Length; - free(Line); } CDataFileReader Reader; @@ -3904,12 +4171,12 @@ void CGameContext::OnMapChange(char *pNewMapName, int MapNameSize) bool FoundInfo = false; for(int i = 0; i < Reader.NumItems(); i++) { - int TypeID; - int ItemID; - void *pData = Reader.GetItem(i, &TypeID, &ItemID); + int TypeId; + int ItemId; + void *pData = Reader.GetItem(i, &TypeId, &ItemId); int Size = Reader.GetItemSize(i); CMapItemInfoSettings MapInfo; - if(TypeID == MAPITEMTYPE_INFO && ItemID == 0) + if(TypeId == MAPITEMTYPE_INFO && ItemId == 0) { FoundInfo = true; if(Size >= (int)sizeof(CMapItemInfoSettings)) @@ -3944,7 +4211,7 @@ void CGameContext::OnMapChange(char *pNewMapName, int MapNameSize) Size = sizeof(MapInfo); } } - Writer.AddItem(TypeID, ItemID, Size, pData); + Writer.AddItem(TypeId, ItemId, Size, pData); } if(!FoundInfo) @@ -4013,7 +4280,8 @@ void CGameContext::OnShutdown(void *pPersistentData) DeleteTempfile(); ConfigManager()->ResetGameSettings(); - Collision()->Dest(); + Collision()->Unload(); + Layers()->Unload(); delete m_pController; m_pController = 0; Clear(); @@ -4026,10 +4294,10 @@ void CGameContext::LoadMapSettings() pMap->GetType(MAPITEMTYPE_INFO, &Start, &Num); for(int i = Start; i < Start + Num; i++) { - int ItemID; - CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(i, nullptr, &ItemID); + int ItemId; + CMapItemInfoSettings *pItem = (CMapItemInfoSettings *)pMap->GetItem(i, nullptr, &ItemId); int ItemSize = pMap->GetItemSize(i); - if(!pItem || ItemID != 0) + if(!pItem || ItemId != 0) continue; if(ItemSize < (int)sizeof(CMapItemInfoSettings)) @@ -4055,36 +4323,37 @@ void CGameContext::LoadMapSettings() Console()->ExecuteFile(aBuf, IConsole::CLIENT_ID_NO_GAME); } -void CGameContext::OnSnap(int ClientID) +void CGameContext::OnSnap(int ClientId) { // add tuning to demo CTuningParams StandardTuning; - if(Server()->IsRecording(ClientID > -1 ? ClientID : MAX_CLIENTS) && mem_comp(&StandardTuning, &m_Tuning, sizeof(CTuningParams)) != 0) + if(Server()->IsRecording(ClientId > -1 ? ClientId : MAX_CLIENTS) && mem_comp(&StandardTuning, &m_Tuning, sizeof(CTuningParams)) != 0) { CMsgPacker Msg(NETMSGTYPE_SV_TUNEPARAMS); int *pParams = (int *)&m_Tuning; for(unsigned i = 0; i < sizeof(m_Tuning) / sizeof(int); i++) Msg.AddInt(pParams[i]); - Server()->SendMsg(&Msg, MSGFLAG_RECORD | MSGFLAG_NOSEND, ClientID); + Server()->SendMsg(&Msg, MSGFLAG_RECORD | MSGFLAG_NOSEND, ClientId); } - m_pController->Snap(ClientID); + m_pController->Snap(ClientId); for(auto &pPlayer : m_apPlayers) { if(pPlayer) - pPlayer->Snap(ClientID); + pPlayer->Snap(ClientId); } - if(ClientID > -1) - m_apPlayers[ClientID]->FakeSnap(); + if(ClientId > -1) + m_apPlayers[ClientId]->FakeSnap(); - m_World.Snap(ClientID); - m_Events.Snap(ClientID); + m_World.Snap(ClientId); + m_Events.Snap(ClientId); } void CGameContext::OnPreSnap() {} void CGameContext::OnPostSnap() { + m_World.PostSnap(); m_Events.Clear(); } @@ -4149,14 +4418,14 @@ void CGameContext::UpdatePlayerMaps() } } -bool CGameContext::IsClientReady(int ClientID) const +bool CGameContext::IsClientReady(int ClientId) const { - return m_apPlayers[ClientID] && m_apPlayers[ClientID]->m_IsReady; + return m_apPlayers[ClientId] && m_apPlayers[ClientId]->m_IsReady; } -bool CGameContext::IsClientPlayer(int ClientID) const +bool CGameContext::IsClientPlayer(int ClientId) const { - return m_apPlayers[ClientID] && m_apPlayers[ClientID]->GetTeam() != TEAM_SPECTATORS; + return m_apPlayers[ClientId] && m_apPlayers[ClientId]->GetTeam() != TEAM_SPECTATORS; } CUuid CGameContext::GameUuid() const { return m_GameUuid; } @@ -4166,13 +4435,13 @@ const char *CGameContext::NetVersion() const { return GAME_NETVERSION; } IGameServer *CreateGameServer() { return new CGameContext; } -void CGameContext::OnSetAuthed(int ClientID, int Level) +void CGameContext::OnSetAuthed(int ClientId, int Level) { - if(m_apPlayers[ClientID]) + if(m_apPlayers[ClientId]) { - char aBuf[512], aIP[NETADDR_MAXSTRSIZE]; - Server()->GetClientAddr(ClientID, aIP, sizeof(aIP)); - str_format(aBuf, sizeof(aBuf), "ban %s %d Banned by vote", aIP, g_Config.m_SvVoteKickBantime); + char aBuf[512], aIp[NETADDR_MAXSTRSIZE]; + Server()->GetClientAddr(ClientId, aIp, sizeof(aIp)); + str_format(aBuf, sizeof(aBuf), "ban %s %d Banned by vote", aIp, g_Config.m_SvVoteKickBantime); if(!str_comp_nocase(m_aVoteCommand, aBuf) && Level > Server()->GetAuthedState(m_VoteCreator)) { m_VoteEnforce = CGameContext::VOTE_ENFORCE_NO_ADMIN; @@ -4183,44 +4452,44 @@ void CGameContext::OnSetAuthed(int ClientID, int Level) { if(Level) { - m_TeeHistorian.RecordAuthLogin(ClientID, Level, Server()->GetAuthName(ClientID)); + m_TeeHistorian.RecordAuthLogin(ClientId, Level, Server()->GetAuthName(ClientId)); } else { - m_TeeHistorian.RecordAuthLogout(ClientID); + m_TeeHistorian.RecordAuthLogout(ClientId); } } } -void CGameContext::SendRecord(int ClientID) +void CGameContext::SendRecord(int ClientId) { CNetMsg_Sv_Record Msg; CNetMsg_Sv_RecordLegacy MsgLegacy; - MsgLegacy.m_PlayerTimeBest = Msg.m_PlayerTimeBest = Score()->PlayerData(ClientID)->m_BestTime * 100.0f; + MsgLegacy.m_PlayerTimeBest = Msg.m_PlayerTimeBest = Score()->PlayerData(ClientId)->m_BestTime * 100.0f; MsgLegacy.m_ServerTimeBest = Msg.m_ServerTimeBest = m_pController->m_CurrentRecord * 100.0f; //TODO: finish this - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); - if(!Server()->IsSixup(ClientID) && GetClientVersion(ClientID) < VERSION_DDNET_MSG_LEGACY) + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId); + if(!Server()->IsSixup(ClientId) && GetClientVersion(ClientId) < VERSION_DDNET_MSG_LEGACY) { - Server()->SendPackMsg(&MsgLegacy, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&MsgLegacy, MSGFLAG_VITAL, ClientId); } } -bool CGameContext::ProcessSpamProtection(int ClientID, bool RespectChatInitialDelay) +bool CGameContext::ProcessSpamProtection(int ClientId, bool RespectChatInitialDelay) { - if(!m_apPlayers[ClientID]) + if(!m_apPlayers[ClientId]) return false; - if(g_Config.m_SvSpamprotection && m_apPlayers[ClientID]->m_LastChat && m_apPlayers[ClientID]->m_LastChat + Server()->TickSpeed() * g_Config.m_SvChatDelay > Server()->Tick()) + if(g_Config.m_SvSpamprotection && m_apPlayers[ClientId]->m_LastChat && m_apPlayers[ClientId]->m_LastChat + Server()->TickSpeed() * g_Config.m_SvChatDelay > Server()->Tick()) return true; - else if(g_Config.m_SvDnsblChat && Server()->DnsblBlack(ClientID)) + else if(g_Config.m_SvDnsblChat && Server()->DnsblBlack(ClientId)) { - SendChatTarget(ClientID, "Players are not allowed to chat from VPNs at this time"); + SendChatTarget(ClientId, "Players are not allowed to chat from VPNs at this time"); return true; } else - m_apPlayers[ClientID]->m_LastChat = Server()->Tick(); + m_apPlayers[ClientId]->m_LastChat = Server()->Tick(); NETADDR Addr; - Server()->GetClientAddr(ClientID, &Addr); + Server()->GetClientAddr(ClientId, &Addr); CMute Muted; int Expires = 0; @@ -4243,23 +4512,23 @@ bool CGameContext::ProcessSpamProtection(int ClientID, bool RespectChatInitialDe str_format(aBuf, sizeof(aBuf), "This server has an initial chat delay, you will be able to talk in %d seconds.", Expires); else str_format(aBuf, sizeof(aBuf), "You are not permitted to talk for the next %d seconds.", Expires); - SendChatTarget(ClientID, aBuf); + SendChatTarget(ClientId, aBuf); return true; } - if(g_Config.m_SvSpamMuteDuration && (m_apPlayers[ClientID]->m_ChatScore += g_Config.m_SvChatPenalty) > g_Config.m_SvChatThreshold) + if(g_Config.m_SvSpamMuteDuration && (m_apPlayers[ClientId]->m_ChatScore += g_Config.m_SvChatPenalty) > g_Config.m_SvChatThreshold) { - Mute(&Addr, g_Config.m_SvSpamMuteDuration, Server()->ClientName(ClientID)); - m_apPlayers[ClientID]->m_ChatScore = 0; + Mute(&Addr, g_Config.m_SvSpamMuteDuration, Server()->ClientName(ClientId)); + m_apPlayers[ClientId]->m_ChatScore = 0; return true; } return false; } -int CGameContext::GetDDRaceTeam(int ClientID) const +int CGameContext::GetDDRaceTeam(int ClientId) const { - return m_pController->Teams().m_Core.Team(ClientID); + return m_pController->Teams().m_Core.Team(ClientId); } void CGameContext::ResetTuning() @@ -4274,14 +4543,14 @@ void CGameContext::ResetTuning() SendTuningParams(-1); } -bool CheckClientID2(int ClientID) +bool CheckClientId2(int ClientId) { - return ClientID >= 0 && ClientID < MAX_CLIENTS; + return ClientId >= 0 && ClientId < MAX_CLIENTS; } -void CGameContext::Whisper(int ClientID, char *pStr) +void CGameContext::Whisper(int ClientId, char *pStr) { - if(ProcessSpamProtection(ClientID)) + if(ProcessSpamProtection(ClientId)) return; pStr = str_skip_whitespaces(pStr); @@ -4369,118 +4638,124 @@ void CGameContext::Whisper(int ClientID, char *pStr) if(Error) { - SendChatTarget(ClientID, "Invalid whisper"); + SendChatTarget(ClientId, "Invalid whisper"); return; } - if(Victim >= MAX_CLIENTS || !CheckClientID2(Victim)) + if(Victim >= MAX_CLIENTS || !CheckClientId2(Victim)) { char aBuf[256]; str_format(aBuf, sizeof(aBuf), "No player with name \"%s\" found", pName); - SendChatTarget(ClientID, aBuf); + SendChatTarget(ClientId, aBuf); return; } - WhisperID(ClientID, Victim, pStr); + WhisperId(ClientId, Victim, pStr); } -void CGameContext::WhisperID(int ClientID, int VictimID, const char *pMessage) +void CGameContext::WhisperId(int ClientId, int VictimId, const char *pMessage) { - if(!CheckClientID2(ClientID)) + if(!CheckClientId2(ClientId)) return; - if(!CheckClientID2(VictimID)) + if(!CheckClientId2(VictimId)) return; - if(m_apPlayers[ClientID]) - m_apPlayers[ClientID]->m_LastWhisperTo = VictimID; + if(m_apPlayers[ClientId]) + m_apPlayers[ClientId]->m_LastWhisperTo = VictimId; char aCensoredMessage[256]; CensorMessage(aCensoredMessage, pMessage, sizeof(aCensoredMessage)); char aBuf[256]; - if(Server()->IsSixup(ClientID)) + if(Server()->IsSixup(ClientId)) { protocol7::CNetMsg_Sv_Chat Msg; - Msg.m_ClientID = ClientID; + Msg.m_ClientId = ClientId; Msg.m_Mode = protocol7::CHAT_WHISPER; Msg.m_pMessage = aCensoredMessage; - Msg.m_TargetID = VictimID; + Msg.m_TargetId = VictimId; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); } - else if(GetClientVersion(ClientID) >= VERSION_DDNET_WHISPER) + else if(GetClientVersion(ClientId) >= VERSION_DDNET_WHISPER) { CNetMsg_Sv_Chat Msg; - Msg.m_Team = CHAT_WHISPER_SEND; - Msg.m_ClientID = VictimID; + Msg.m_Team = TEAM_WHISPER_SEND; + Msg.m_ClientId = VictimId; Msg.m_pMessage = aCensoredMessage; if(g_Config.m_SvDemoChat) - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId); else - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); } else { - str_format(aBuf, sizeof(aBuf), "[→ %s] %s", Server()->ClientName(VictimID), aCensoredMessage); - SendChatTarget(ClientID, aBuf); + str_format(aBuf, sizeof(aBuf), "[→ %s] %s", Server()->ClientName(VictimId), aCensoredMessage); + SendChatTarget(ClientId, aBuf); + } + + if(!m_apPlayers[VictimId]->m_Whispers) + { + SendChatTarget(ClientId, "This person has disabled receiving whispers"); + return; } - if(Server()->IsSixup(VictimID)) + if(Server()->IsSixup(VictimId)) { protocol7::CNetMsg_Sv_Chat Msg; - Msg.m_ClientID = ClientID; + Msg.m_ClientId = ClientId; Msg.m_Mode = protocol7::CHAT_WHISPER; Msg.m_pMessage = aCensoredMessage; - Msg.m_TargetID = VictimID; + Msg.m_TargetId = VictimId; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, VictimID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, VictimId); } - else if(GetClientVersion(VictimID) >= VERSION_DDNET_WHISPER) + else if(GetClientVersion(VictimId) >= VERSION_DDNET_WHISPER) { CNetMsg_Sv_Chat Msg2; - Msg2.m_Team = CHAT_WHISPER_RECV; - Msg2.m_ClientID = ClientID; + Msg2.m_Team = TEAM_WHISPER_RECV; + Msg2.m_ClientId = ClientId; Msg2.m_pMessage = aCensoredMessage; if(g_Config.m_SvDemoChat) - Server()->SendPackMsg(&Msg2, MSGFLAG_VITAL, VictimID); + Server()->SendPackMsg(&Msg2, MSGFLAG_VITAL, VictimId); else - Server()->SendPackMsg(&Msg2, MSGFLAG_VITAL | MSGFLAG_NORECORD, VictimID); + Server()->SendPackMsg(&Msg2, MSGFLAG_VITAL | MSGFLAG_NORECORD, VictimId); } else { - str_format(aBuf, sizeof(aBuf), "[← %s] %s", Server()->ClientName(ClientID), aCensoredMessage); - SendChatTarget(VictimID, aBuf); + str_format(aBuf, sizeof(aBuf), "[← %s] %s", Server()->ClientName(ClientId), aCensoredMessage); + SendChatTarget(VictimId, aBuf); } } -void CGameContext::Converse(int ClientID, char *pStr) +void CGameContext::Converse(int ClientId, char *pStr) { - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; if(!pPlayer) return; - if(ProcessSpamProtection(ClientID)) + if(ProcessSpamProtection(ClientId)) return; if(pPlayer->m_LastWhisperTo < 0) - SendChatTarget(ClientID, "You do not have an ongoing conversation. Whisper to someone to start one"); + SendChatTarget(ClientId, "You do not have an ongoing conversation. Whisper to someone to start one"); else { - WhisperID(ClientID, pPlayer->m_LastWhisperTo, pStr); + WhisperId(ClientId, pPlayer->m_LastWhisperTo, pStr); } } bool CGameContext::IsVersionBanned(int Version) { char aVersion[16]; - str_from_int(Version, aVersion); + str_format(aVersion, sizeof(aVersion), "%d", Version); return str_in_list(g_Config.m_SvBannedVersions, ",", aVersion); } -void CGameContext::List(int ClientID, const char *pFilter) +void CGameContext::List(int ClientId, const char *pFilter) { int Total = 0; char aBuf[256]; @@ -4489,7 +4764,7 @@ void CGameContext::List(int ClientID, const char *pFilter) str_format(aBuf, sizeof(aBuf), "Listing players with \"%s\" in name:", pFilter); else str_copy(aBuf, "Listing all players:"); - SendChatTarget(ClientID, aBuf); + SendChatTarget(ClientId, aBuf); for(int i = 0; i < MAX_CLIENTS; i++) { if(m_apPlayers[i]) @@ -4500,7 +4775,7 @@ void CGameContext::List(int ClientID, const char *pFilter) continue; if(Bufcnt + str_length(pName) + 4 > 256) { - SendChatTarget(ClientID, aBuf); + SendChatTarget(ClientId, aBuf); Bufcnt = 0; } if(Bufcnt != 0) @@ -4516,14 +4791,14 @@ void CGameContext::List(int ClientID, const char *pFilter) } } if(Bufcnt != 0) - SendChatTarget(ClientID, aBuf); + SendChatTarget(ClientId, aBuf); str_format(aBuf, sizeof(aBuf), "%d players online", Total); - SendChatTarget(ClientID, aBuf); + SendChatTarget(ClientId, aBuf); } -int CGameContext::GetClientVersion(int ClientID) const +int CGameContext::GetClientVersion(int ClientId) const { - return Server()->GetClientVersion(ClientID); + return Server()->GetClientVersion(ClientId); } CClientMask CGameContext::ClientsMaskExcludeClientVersionAndHigher(int Version) const @@ -4543,14 +4818,13 @@ bool CGameContext::PlayerModerating() const return std::any_of(std::begin(m_apPlayers), std::end(m_apPlayers), [](const CPlayer *pPlayer) { return pPlayer && pPlayer->m_Moderating; }); } -void CGameContext::ForceVote(int EnforcerID, bool Success) +void CGameContext::ForceVote(int EnforcerId, bool Success) { // check if there is a vote running if(!m_VoteCloseTime) return; m_VoteEnforce = Success ? CGameContext::VOTE_ENFORCE_YES_ADMIN : CGameContext::VOTE_ENFORCE_NO_ADMIN; - m_VoteEnforcer = EnforcerID; char aBuf[256]; const char *pOption = Success ? "yes" : "no"; @@ -4560,28 +4834,28 @@ void CGameContext::ForceVote(int EnforcerID, bool Success) Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", aBuf); } -bool CGameContext::RateLimitPlayerVote(int ClientID) +bool CGameContext::RateLimitPlayerVote(int ClientId) { int64_t Now = Server()->Tick(); int64_t TickSpeed = Server()->TickSpeed(); - CPlayer *pPlayer = m_apPlayers[ClientID]; + CPlayer *pPlayer = m_apPlayers[ClientId]; - if(g_Config.m_SvRconVote && !Server()->GetAuthedState(ClientID)) + if(g_Config.m_SvRconVote && !Server()->GetAuthedState(ClientId)) { - SendChatTarget(ClientID, "You can only vote after logging in."); + SendChatTarget(ClientId, "You can only vote after logging in."); return true; } if(g_Config.m_SvDnsblVote && Server()->DistinctClientCount() > 1) { - if(m_pServer->DnsblPending(ClientID)) + if(m_pServer->DnsblPending(ClientId)) { - SendChatTarget(ClientID, "You are not allowed to vote because we're currently checking for VPNs. Try again in ~30 seconds."); + SendChatTarget(ClientId, "You are not allowed to vote because we're currently checking for VPNs. Try again in ~30 seconds."); return true; } - else if(m_pServer->DnsblBlack(ClientID)) + else if(m_pServer->DnsblBlack(ClientId)) { - SendChatTarget(ClientID, "You are not allowed to vote because you appear to be using a VPN. Try connecting without a VPN or contacting an admin if you think this is a mistake."); + SendChatTarget(ClientId, "You are not allowed to vote because you appear to be using a VPN. Try connecting without a VPN or contacting an admin if you think this is a mistake."); return true; } } @@ -4592,7 +4866,7 @@ bool CGameContext::RateLimitPlayerVote(int ClientID) pPlayer->m_LastVoteTry = Now; if(m_VoteCloseTime) { - SendChatTarget(ClientID, "Wait for current vote to end before calling a new one."); + SendChatTarget(ClientId, "Wait for current vote to end before calling a new one."); return true; } @@ -4600,7 +4874,7 @@ bool CGameContext::RateLimitPlayerVote(int ClientID) { char aBuf[64]; str_format(aBuf, sizeof(aBuf), "You must wait %d seconds before making your first vote.", (int)((pPlayer->m_FirstVoteTick - Now) / TickSpeed) + 1); - SendChatTarget(ClientID, aBuf); + SendChatTarget(ClientId, aBuf); return true; } @@ -4609,12 +4883,12 @@ bool CGameContext::RateLimitPlayerVote(int ClientID) { char aChatmsg[64]; str_format(aChatmsg, sizeof(aChatmsg), "You must wait %d seconds before making another vote.", (int)(TimeLeft / TickSpeed) + 1); - SendChatTarget(ClientID, aChatmsg); + SendChatTarget(ClientId, aChatmsg); return true; } NETADDR Addr; - Server()->GetClientAddr(ClientID, &Addr); + Server()->GetClientAddr(ClientId, &Addr); int VoteMuted = 0; for(int i = 0; i < m_NumVoteMutes && !VoteMuted; i++) if(!net_addr_comp_noport(&Addr, &m_aVoteMutes[i].m_Addr)) @@ -4628,96 +4902,98 @@ bool CGameContext::RateLimitPlayerVote(int ClientID) { char aChatmsg[64]; str_format(aChatmsg, sizeof(aChatmsg), "You are not permitted to vote for the next %d seconds.", VoteMuted); - SendChatTarget(ClientID, aChatmsg); + SendChatTarget(ClientId, aChatmsg); return true; } return false; } -bool CGameContext::RateLimitPlayerMapVote(int ClientID) const +bool CGameContext::RateLimitPlayerMapVote(int ClientId) const { - if(!Server()->GetAuthedState(ClientID) && time_get() < m_LastMapVote + (time_freq() * g_Config.m_SvVoteMapTimeDelay)) + if(!Server()->GetAuthedState(ClientId) && time_get() < m_LastMapVote + (time_freq() * g_Config.m_SvVoteMapTimeDelay)) { char aChatmsg[512] = {0}; str_format(aChatmsg, sizeof(aChatmsg), "There's a %d second delay between map-votes, please wait %d seconds.", g_Config.m_SvVoteMapTimeDelay, (int)((m_LastMapVote + g_Config.m_SvVoteMapTimeDelay * time_freq() - time_get()) / time_freq())); - SendChatTarget(ClientID, aChatmsg); + SendChatTarget(ClientId, aChatmsg); return true; } return false; } -void CGameContext::OnUpdatePlayerServerInfo(char *aBuf, int BufSize, int ID) +void CGameContext::OnUpdatePlayerServerInfo(CJsonStringWriter *pJSonWriter, int Id) { - if(BufSize <= 0) + if(!m_apPlayers[Id]) return; - aBuf[0] = '\0'; + CTeeInfo &TeeInfo = m_apPlayers[Id]->m_TeeInfos; - if(!m_apPlayers[ID]) - return; - - char aCSkinName[64]; - - CTeeInfo &TeeInfo = m_apPlayers[ID]->m_TeeInfos; + pJSonWriter->WriteAttribute("skin"); + pJSonWriter->BeginObject(); - char aJsonSkin[400]; - aJsonSkin[0] = '\0'; - - if(!Server()->IsSixup(ID)) + // 0.6 + if(!Server()->IsSixup(Id)) { - // 0.6 + pJSonWriter->WriteAttribute("name"); + pJSonWriter->WriteStrValue(TeeInfo.m_aSkinName); + if(TeeInfo.m_UseCustomColor) { - str_format(aJsonSkin, sizeof(aJsonSkin), - "\"name\":\"%s\"," - "\"color_body\":%d," - "\"color_feet\":%d", - EscapeJson(aCSkinName, sizeof(aCSkinName), TeeInfo.m_aSkinName), - TeeInfo.m_ColorBody, - TeeInfo.m_ColorFeet); - } - else - { - str_format(aJsonSkin, sizeof(aJsonSkin), - "\"name\":\"%s\"", - EscapeJson(aCSkinName, sizeof(aCSkinName), TeeInfo.m_aSkinName)); + pJSonWriter->WriteAttribute("color_body"); + pJSonWriter->WriteIntValue(TeeInfo.m_ColorBody); + + pJSonWriter->WriteAttribute("color_feet"); + pJSonWriter->WriteIntValue(TeeInfo.m_ColorFeet); } } + // 0.7 else { const char *apPartNames[protocol7::NUM_SKINPARTS] = {"body", "marking", "decoration", "hands", "feet", "eyes"}; - char aPartBuf[64]; for(int i = 0; i < protocol7::NUM_SKINPARTS; ++i) { - str_format(aPartBuf, sizeof(aPartBuf), - "%s\"%s\":{" - "\"name\":\"%s\"", - i == 0 ? "" : ",", - apPartNames[i], - EscapeJson(aCSkinName, sizeof(aCSkinName), TeeInfo.m_apSkinPartNames[i])); + pJSonWriter->WriteAttribute(apPartNames[i]); + pJSonWriter->BeginObject(); - str_append(aJsonSkin, aPartBuf); + pJSonWriter->WriteAttribute("name"); + pJSonWriter->WriteStrValue(TeeInfo.m_apSkinPartNames[i]); if(TeeInfo.m_aUseCustomColors[i]) { - str_format(aPartBuf, sizeof(aPartBuf), - ",\"color\":%d", - TeeInfo.m_aSkinPartColors[i]); - str_append(aJsonSkin, aPartBuf); + pJSonWriter->WriteAttribute("color"); + pJSonWriter->WriteIntValue(TeeInfo.m_aSkinPartColors[i]); } - str_append(aJsonSkin, "}"); + + pJSonWriter->EndObject(); } } - str_format(aBuf, BufSize, - ",\"skin\":{" - "%s" - "}," - "\"afk\":%s," - "\"team\":%d", - aJsonSkin, - JsonBool(m_apPlayers[ID]->IsAfk()), - m_apPlayers[ID]->GetTeam()); + pJSonWriter->EndObject(); + + pJSonWriter->WriteAttribute("afk"); + pJSonWriter->WriteBoolValue(m_apPlayers[Id]->IsAfk()); + + const int Team = m_pController->IsTeamPlay() ? m_apPlayers[Id]->GetTeam() : m_apPlayers[Id]->GetTeam() == TEAM_SPECTATORS ? -1 : GetDDRaceTeam(Id); + + pJSonWriter->WriteAttribute("team"); + pJSonWriter->WriteIntValue(Team); +} + +void CGameContext::ReadCensorList() +{ + const char *pCensorFilename = "censorlist.txt"; + CLineReader LineReader; + m_vCensorlist.clear(); + if(LineReader.OpenFile(Storage()->OpenFile(pCensorFilename, IOFLAG_READ, IStorage::TYPE_ALL))) + { + while(const char *pLine = LineReader.Get()) + { + m_vCensorlist.emplace_back(pLine); + } + } + else + { + dbg_msg("censorlist", "failed to open '%s'", pCensorFilename); + } } diff --git a/src/game/server/gamecontext.h b/src/game/server/gamecontext.h index 67a2a37597..d25758c538 100644 --- a/src/game/server/gamecontext.h +++ b/src/game/server/gamecontext.h @@ -101,7 +101,7 @@ class CGameContext : public IGameServer bool m_Resetting; - static void CommandCallback(int ClientID, int FlagMask, const char *pCmd, IConsole::IResult *pResult, void *pUser); + static void CommandCallback(int ClientId, int FlagMask, const char *pCmd, IConsole::IResult *pResult, void *pUser); static void TeeHistorianWrite(const void *pData, int DataSize, void *pUser); static void ConTuneParam(IConsole::IResult *pResult, void *pUserData); @@ -124,6 +124,7 @@ class CGameContext : public IGameServer static void ConSay(IConsole::IResult *pResult, void *pUserData); static void ConSetTeam(IConsole::IResult *pResult, void *pUserData); static void ConSetTeamAll(IConsole::IResult *pResult, void *pUserData); + static void ConHotReload(IConsole::IResult *pResult, void *pUserData); static void ConAddVote(IConsole::IResult *pResult, void *pUserData); static void ConRemoveVote(IConsole::IResult *pResult, void *pUserData); static void ConForceVote(IConsole::IResult *pResult, void *pUserData); @@ -180,25 +181,29 @@ class CGameContext : public IGameServer // keep last input to always apply when none is sent CNetObj_PlayerInput m_aLastPlayerInput[MAX_CLIENTS]; bool m_aPlayerHasInput[MAX_CLIENTS]; + CSaveTeam *m_apSavedTeams[MAX_CLIENTS]; + CSaveTee *m_apSavedTees[MAX_CLIENTS]; + CSaveTee *m_apSavedTeleTees[MAX_CLIENTS]; + int m_aTeamMapping[MAX_CLIENTS]; // returns last input if available otherwise nulled PlayerInput object - // ClientID has to be valid - CNetObj_PlayerInput GetLastPlayerInput(int ClientID) const; + // ClientId has to be valid + CNetObj_PlayerInput GetLastPlayerInput(int ClientId) const; IGameController *m_pController; CGameWorld m_World; // helper functions - class CCharacter *GetPlayerChar(int ClientID); + class CCharacter *GetPlayerChar(int ClientId); bool EmulateBug(int Bug); std::vector &Switchers() { return m_World.m_Core.m_vSwitchers; } // voting void StartVote(const char *pDesc, const char *pCommand, const char *pReason, const char *pSixupDesc); void EndVote(); - void SendVoteSet(int ClientID); - void SendVoteStatus(int ClientID, int Total, int Yes, int No); - void AbortVoteKickOnDisconnect(int ClientID); + void SendVoteSet(int ClientId); + void SendVoteStatus(int ClientId, int Total, int Yes, int No); + void AbortVoteKickOnDisconnect(int ClientId); int m_VoteCreator; int m_VoteType; @@ -224,7 +229,10 @@ class CGameContext : public IGameServer VOTE_ENFORCE_UNKNOWN = 0, VOTE_ENFORCE_NO, VOTE_ENFORCE_YES, + VOTE_ENFORCE_NO_ADMIN, + VOTE_ENFORCE_YES_ADMIN, VOTE_ENFORCE_ABORT, + VOTE_ENFORCE_CANCEL, }; CHeap *m_pVoteOptionHeap; CVoteOptionServer *m_pVoteOptionFirst; @@ -235,47 +243,42 @@ class CGameContext : public IGameServer void CreateExplosion(vec2 Pos, int Owner, int Weapon, bool NoDamage, int ActivatedTeam, CClientMask Mask = CClientMask().set()); void CreateHammerHit(vec2 Pos, CClientMask Mask = CClientMask().set()); void CreatePlayerSpawn(vec2 Pos, CClientMask Mask = CClientMask().set()); - void CreateDeath(vec2 Pos, int ClientID, CClientMask Mask = CClientMask().set()); + void CreateDeath(vec2 Pos, int ClientId, CClientMask Mask = CClientMask().set()); + void CreateBirthdayEffect(vec2 Pos, CClientMask Mask = CClientMask().set()); + void CreateFinishEffect(vec2 Pos, CClientMask Mask = CClientMask().set()); void CreateSound(vec2 Pos, int Sound, CClientMask Mask = CClientMask().set()); void CreateSoundGlobal(int Sound, int Target = -1) const; void SnapSwitchers(int SnappingClient); - bool SnapLaserObject(const CSnapContext &Context, int SnapID, const vec2 &To, const vec2 &From, int StartTick, int Owner = -1, int LaserType = -1, int Subtype = -1, int SwitchNumber = -1) const; - bool SnapPickup(const CSnapContext &Context, int SnapID, const vec2 &Pos, int Type, int SubType, int SwitchNumber) const; + bool SnapLaserObject(const CSnapContext &Context, int SnapId, const vec2 &To, const vec2 &From, int StartTick, int Owner = -1, int LaserType = -1, int Subtype = -1, int SwitchNumber = -1) const; + bool SnapPickup(const CSnapContext &Context, int SnapId, const vec2 &Pos, int Type, int SubType, int SwitchNumber) const; enum { - CHAT_ALL = -2, - CHAT_SPEC = -1, - CHAT_RED = 0, - CHAT_BLUE = 1, - CHAT_WHISPER_SEND = 2, - CHAT_WHISPER_RECV = 3, - - CHAT_SIX = 1 << 0, - CHAT_SIXUP = 1 << 1, + FLAG_SIX = 1 << 0, + FLAG_SIXUP = 1 << 1, }; // network - void CallVote(int ClientID, const char *pDesc, const char *pCmd, const char *pReason, const char *pChatmsg, const char *pSixupDesc = 0); - void SendChatTarget(int To, const char *pText, int Flags = CHAT_SIX | CHAT_SIXUP) const; + void CallVote(int ClientId, const char *pDesc, const char *pCmd, const char *pReason, const char *pChatmsg, const char *pSixupDesc = 0); + void SendChatTarget(int To, const char *pText, int VersionFlags = FLAG_SIX | FLAG_SIXUP) const; void SendChatTeam(int Team, const char *pText) const; - void SendChat(int ClientID, int Team, const char *pText, int SpamProtectionClientID = -1, int Flags = CHAT_SIX | CHAT_SIXUP); - void SendStartWarning(int ClientID, const char *pMessage); - void SendEmoticon(int ClientID, int Emoticon, int TargetClientID) const; - void SendWeaponPickup(int ClientID, int Weapon) const; - void SendMotd(int ClientID) const; - void SendSettings(int ClientID) const; - void SendBroadcast(const char *pText, int ClientID, bool IsImportant = true); + void SendChat(int ClientId, int Team, const char *pText, int SpamProtectionClientId = -1, int VersionFlags = FLAG_SIX | FLAG_SIXUP); + void SendStartWarning(int ClientId, const char *pMessage); + void SendEmoticon(int ClientId, int Emoticon, int TargetClientId) const; + void SendWeaponPickup(int ClientId, int Weapon) const; + void SendMotd(int ClientId) const; + void SendSettings(int ClientId) const; + void SendBroadcast(const char *pText, int ClientId, bool IsImportant = true); - void List(int ClientID, const char *pFilter); + void List(int ClientId, const char *pFilter); // void CheckPureTuning(); - void SendTuningParams(int ClientID, int Zone = 0); + void SendTuningParams(int ClientId, int Zone = 0); const CVoteOptionServer *GetVoteOption(int Index) const; - void ProgressVoteOptions(int ClientID); + void ProgressVoteOptions(int ClientId); // void LoadMapSettings(); @@ -283,49 +286,54 @@ class CGameContext : public IGameServer // engine events void OnInit(const void *pPersistentData) override; void OnConsoleInit() override; + void RegisterDDRaceCommands(); + void RegisterChatCommands(); void OnMapChange(char *pNewMapName, int MapNameSize) override; void OnShutdown(void *pPersistentData) override; void OnTick() override; void OnPreSnap() override; - void OnSnap(int ClientID) override; + void OnSnap(int ClientId) override; void OnPostSnap() override; void UpdatePlayerMaps(); - void *PreProcessMsg(int *pMsgID, CUnpacker *pUnpacker, int ClientID); + void *PreProcessMsg(int *pMsgId, CUnpacker *pUnpacker, int ClientId); void CensorMessage(char *pCensoredMessage, const char *pMessage, int Size); - void OnMessage(int MsgID, CUnpacker *pUnpacker, int ClientID) override; - void OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientID, const CUnpacker *pUnpacker); - void OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int ClientID); - void OnVoteNetMessage(const CNetMsg_Cl_Vote *pMsg, int ClientID); - void OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int ClientID); - void OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMsg, int ClientID, CUnpacker *pUnpacker); - void OnShowOthersLegacyNetMessage(const CNetMsg_Cl_ShowOthersLegacy *pMsg, int ClientID); - void OnShowOthersNetMessage(const CNetMsg_Cl_ShowOthers *pMsg, int ClientID); - void OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, int ClientID); - void OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientID); - void OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int ClientID); - void OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int ClientID); - void OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientID); - void OnStartInfoNetMessage(const CNetMsg_Cl_StartInfo *pMsg, int ClientID); - - bool OnClientDataPersist(int ClientID, void *pData) override; - void OnClientConnected(int ClientID, void *pData) override; - void OnClientEnter(int ClientID) override; - void OnClientDrop(int ClientID, const char *pReason) override; - void OnClientPrepareInput(int ClientID, void *pInput) override; - void OnClientDirectInput(int ClientID, void *pInput) override; - void OnClientPredictedInput(int ClientID, void *pInput) override; - void OnClientPredictedEarlyInput(int ClientID, void *pInput) override; + void OnMessage(int MsgId, CUnpacker *pUnpacker, int ClientId) override; + void OnSayNetMessage(const CNetMsg_Cl_Say *pMsg, int ClientId, const CUnpacker *pUnpacker); + void OnCallVoteNetMessage(const CNetMsg_Cl_CallVote *pMsg, int ClientId); + void OnVoteNetMessage(const CNetMsg_Cl_Vote *pMsg, int ClientId); + void OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int ClientId); + void OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMsg, int ClientId, CUnpacker *pUnpacker); + void OnShowOthersLegacyNetMessage(const CNetMsg_Cl_ShowOthersLegacy *pMsg, int ClientId); + void OnShowOthersNetMessage(const CNetMsg_Cl_ShowOthers *pMsg, int ClientId); + void OnShowDistanceNetMessage(const CNetMsg_Cl_ShowDistance *pMsg, int ClientId); + void OnSetSpectatorModeNetMessage(const CNetMsg_Cl_SetSpectatorMode *pMsg, int ClientId); + void OnChangeInfoNetMessage(const CNetMsg_Cl_ChangeInfo *pMsg, int ClientId); + void OnEmoticonNetMessage(const CNetMsg_Cl_Emoticon *pMsg, int ClientId); + void OnKillNetMessage(const CNetMsg_Cl_Kill *pMsg, int ClientId); + void OnStartInfoNetMessage(const CNetMsg_Cl_StartInfo *pMsg, int ClientId); + + bool OnClientDataPersist(int ClientId, void *pData) override; + void OnClientConnected(int ClientId, void *pData) override; + void OnClientEnter(int ClientId) override; + void OnClientDrop(int ClientId, const char *pReason) override; + void OnClientPrepareInput(int ClientId, void *pInput) override; + void OnClientDirectInput(int ClientId, void *pInput) override; + void OnClientPredictedInput(int ClientId, void *pInput) override; + void OnClientPredictedEarlyInput(int ClientId, void *pInput) override; void TeehistorianRecordAntibot(const void *pData, int DataSize) override; - void TeehistorianRecordPlayerJoin(int ClientID, bool Sixup) override; - void TeehistorianRecordPlayerDrop(int ClientID, const char *pReason) override; - void TeehistorianRecordPlayerRejoin(int ClientID) override; - - bool IsClientReady(int ClientID) const override; - bool IsClientPlayer(int ClientID) const override; + void TeehistorianRecordPlayerJoin(int ClientId, bool Sixup) override; + void TeehistorianRecordPlayerDrop(int ClientId, const char *pReason) override; + void TeehistorianRecordPlayerRejoin(int ClientId) override; + void TeehistorianRecordPlayerName(int ClientId, const char *pName) override; + void TeehistorianRecordPlayerFinish(int ClientId, int TimeTicks) override; + void TeehistorianRecordTeamFinish(int TeamId, int TimeTicks) override; + + bool IsClientReady(int ClientId) const override; + bool IsClientPlayer(int ClientId) const override; int PersistentDataSize() const override { return sizeof(CPersistentData); } int PersistentClientDataSize() const override { return sizeof(CPersistentClientData); } @@ -336,31 +344,32 @@ class CGameContext : public IGameServer // DDRace void OnPreTickTeehistorian() override; - bool OnClientDDNetVersionKnown(int ClientID); + bool OnClientDDNetVersionKnown(int ClientId); void FillAntibot(CAntibotRoundData *pData) override; - bool ProcessSpamProtection(int ClientID, bool RespectChatInitialDelay = true); - int GetDDRaceTeam(int ClientID) const; + bool ProcessSpamProtection(int ClientId, bool RespectChatInitialDelay = true); + int GetDDRaceTeam(int ClientId) const; // Describes the time when the first player joined the server. int64_t m_NonEmptySince; int64_t m_LastMapVote; - int GetClientVersion(int ClientID) const; + int GetClientVersion(int ClientId) const; CClientMask ClientsMaskExcludeClientVersionAndHigher(int Version) const; - bool PlayerExists(int ClientID) const override { return m_apPlayers[ClientID]; } + bool PlayerExists(int ClientId) const override { return m_apPlayers[ClientId]; } // Returns true if someone is actively moderating. bool PlayerModerating() const; - void ForceVote(int EnforcerID, bool Success); + void ForceVote(int EnforcerId, bool Success); // Checks if player can vote and notify them about the reason - bool RateLimitPlayerVote(int ClientID); - bool RateLimitPlayerMapVote(int ClientID) const; + bool RateLimitPlayerVote(int ClientId); + bool RateLimitPlayerMapVote(int ClientId) const; - void OnUpdatePlayerServerInfo(char *aBuf, int BufSize, int ID) override; + void OnUpdatePlayerServerInfo(CJsonStringWriter *pJSonWriter, int Id) override; + void ReadCensorList(); std::shared_ptr m_SqlRandomMapResult; private: // starting 1 to make 0 the special value "no client id" - uint32_t NextUniqueClientID = 1; + uint32_t NextUniqueClientId = 1; bool m_VoteWillPass; CScore *m_pScore; @@ -386,6 +395,7 @@ class CGameContext : public IGameServer static void ConGrenade(IConsole::IResult *pResult, void *pUserData); static void ConLaser(IConsole::IResult *pResult, void *pUserData); static void ConJetpack(IConsole::IResult *pResult, void *pUserData); + static void ConSetJumps(IConsole::IResult *pResult, void *pUserData); static void ConWeapons(IConsole::IResult *pResult, void *pUserData); static void ConUnShotgun(IConsole::IResult *pResult, void *pUserData); static void ConUnGrenade(IConsole::IResult *pResult, void *pUserData); @@ -394,9 +404,8 @@ class CGameContext : public IGameServer static void ConUnWeapons(IConsole::IResult *pResult, void *pUserData); static void ConAddWeapon(IConsole::IResult *pResult, void *pUserData); static void ConRemoveWeapon(IConsole::IResult *pResult, void *pUserData); - void ModifyWeapons(IConsole::IResult *pResult, void *pUserData, int Weapon, bool Remove); - void MoveCharacter(int ClientID, int X, int Y, bool Raw = false); + void MoveCharacter(int ClientId, int X, int Y, bool Raw = false); static void ConGoLeft(IConsole::IResult *pResult, void *pUserData); static void ConGoRight(IConsole::IResult *pResult, void *pUserData); static void ConGoUp(IConsole::IResult *pResult, void *pUserData); @@ -428,9 +437,11 @@ class CGameContext : public IGameServer static void ConTimeCP(IConsole::IResult *pResult, void *pUserData); static void ConDND(IConsole::IResult *pResult, void *pUserData); + static void ConWhispers(IConsole::IResult *pResult, void *pUserData); static void ConMapInfo(IConsole::IResult *pResult, void *pUserData); static void ConTimeout(IConsole::IResult *pResult, void *pUserData); static void ConPractice(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeCmdList(IConsole::IResult *pResult, void *pUserData); static void ConSwap(IConsole::IResult *pResult, void *pUserData); static void ConSave(IConsole::IResult *pResult, void *pUserData); static void ConLoad(IConsole::IResult *pResult, void *pUserData); @@ -442,7 +453,7 @@ class CGameContext : public IGameServer static void ConUnlock(IConsole::IResult *pResult, void *pUserData); static void ConInvite(IConsole::IResult *pResult, void *pUserData); static void ConJoin(IConsole::IResult *pResult, void *pUserData); - static void ConAccept(IConsole::IResult *pResult, void *pUserData); + static void ConTeam0Mode(IConsole::IResult *pResult, void *pUserData); static void ConMe(IConsole::IResult *pResult, void *pUserData); static void ConWhisper(IConsole::IResult *pResult, void *pUserData); static void ConConverse(IConsole::IResult *pResult, void *pUserData); @@ -457,21 +468,44 @@ class CGameContext : public IGameServer static void ConTime(IConsole::IResult *pResult, void *pUserData); static void ConSetTimerType(IConsole::IResult *pResult, void *pUserData); static void ConRescue(IConsole::IResult *pResult, void *pUserData); - static void ConTele(IConsole::IResult *pResult, void *pUserData); + static void ConRescueMode(IConsole::IResult *pResult, void *pUserData); + static void ConTeleTo(IConsole::IResult *pResult, void *pUserData); + static void ConTeleXY(IConsole::IResult *pResult, void *pUserData); static void ConTeleCursor(IConsole::IResult *pResult, void *pUserData); static void ConLastTele(IConsole::IResult *pResult, void *pUserData); + + // Chat commands for practice mode static void ConPracticeUnSolo(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeSolo(IConsole::IResult *pResult, void *pUserData); static void ConPracticeUnDeep(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeDeep(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeShotgun(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeGrenade(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeLaser(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeJetpack(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeSetJumps(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeWeapons(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeUnShotgun(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeUnGrenade(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeUnLaser(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeUnJetpack(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeUnWeapons(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeNinja(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeUnNinja(IConsole::IResult *pResult, void *pUserData); + + static void ConPracticeAddWeapon(IConsole::IResult *pResult, void *pUserData); + static void ConPracticeRemoveWeapon(IConsole::IResult *pResult, void *pUserData); + static void ConProtectedKill(IConsole::IResult *pResult, void *pUserData); static void ConVoteMute(IConsole::IResult *pResult, void *pUserData); static void ConVoteUnmute(IConsole::IResult *pResult, void *pUserData); static void ConVoteMutes(IConsole::IResult *pResult, void *pUserData); static void ConMute(IConsole::IResult *pResult, void *pUserData); - static void ConMuteID(IConsole::IResult *pResult, void *pUserData); - static void ConMuteIP(IConsole::IResult *pResult, void *pUserData); + static void ConMuteId(IConsole::IResult *pResult, void *pUserData); + static void ConMuteIp(IConsole::IResult *pResult, void *pUserData); static void ConUnmute(IConsole::IResult *pResult, void *pUserData); - static void ConUnmuteID(IConsole::IResult *pResult, void *pUserData); + static void ConUnmuteId(IConsole::IResult *pResult, void *pUserData); static void ConMutes(IConsole::IResult *pResult, void *pUserData); static void ConModerate(IConsole::IResult *pResult, void *pUserData); @@ -481,6 +515,11 @@ class CGameContext : public IGameServer static void ConFreezeHammer(IConsole::IResult *pResult, void *pUserData); static void ConUnFreezeHammer(IConsole::IResult *pResult, void *pUserData); + static void ConReloadCensorlist(IConsole::IResult *pResult, void *pUserData); + static void ConReloadAnnouncement(IConsole::IResult *pResult, void *pUserData); + + CCharacter *GetPracticeCharacter(IConsole::IResult *pResult); + enum { MAX_MUTES = 128, @@ -501,14 +540,14 @@ class CGameContext : public IGameServer bool TryMute(const NETADDR *pAddr, int Secs, const char *pReason, bool InitialChatDelay); void Mute(const NETADDR *pAddr, int Secs, const char *pDisplayName, const char *pReason = "", bool InitialChatDelay = false); bool TryVoteMute(const NETADDR *pAddr, int Secs, const char *pReason); - void VoteMute(const NETADDR *pAddr, int Secs, const char *pReason, const char *pDisplayName, int AuthedID); - bool VoteUnmute(const NETADDR *pAddr, const char *pDisplayName, int AuthedID); - void Whisper(int ClientID, char *pStr); - void WhisperID(int ClientID, int VictimID, const char *pMessage); - void Converse(int ClientID, char *pStr); + void VoteMute(const NETADDR *pAddr, int Secs, const char *pReason, const char *pDisplayName, int AuthedId); + bool VoteUnmute(const NETADDR *pAddr, const char *pDisplayName, int AuthedId); + void Whisper(int ClientId, char *pStr); + void WhisperId(int ClientId, int VictimId, const char *pMessage); + void Converse(int ClientId, char *pStr); bool IsVersionBanned(int Version); - void UnlockTeam(int ClientID, int Team) const; - void AttemptJoinTeam(int ClientID, int Team); + void UnlockTeam(int ClientId, int Team) const; + void AttemptJoinTeam(int ClientId, int Team); enum { @@ -527,7 +566,7 @@ class CGameContext : public IGameServer CLog m_aLogs[MAX_LOGS]; int m_LatestLog; - void LogEvent(const char *Description, int ClientID); + void LogEvent(const char *Description, int ClientId); public: CLayers *Layers() { return &m_Layers; } @@ -535,23 +574,19 @@ class CGameContext : public IGameServer enum { - VOTE_ENFORCE_NO_ADMIN = VOTE_ENFORCE_YES + 1, - VOTE_ENFORCE_YES_ADMIN, - VOTE_TYPE_UNKNOWN = 0, VOTE_TYPE_OPTION, VOTE_TYPE_KICK, VOTE_TYPE_SPECTATE, }; int m_VoteVictim; - int m_VoteEnforcer; inline bool IsOptionVote() const { return m_VoteType == VOTE_TYPE_OPTION; } inline bool IsKickVote() const { return m_VoteType == VOTE_TYPE_KICK; } inline bool IsSpecVote() const { return m_VoteType == VOTE_TYPE_SPECTATE; } - void SendRecord(int ClientID); - void OnSetAuthed(int ClientID, int Level) override; + void SendRecord(int ClientId); + void OnSetAuthed(int ClientId, int Level) override; void ResetTuning(); }; diff --git a/src/game/server/gamecontroller.cpp b/src/game/server/gamecontroller.cpp index 89f4f59122..ab5be9dd59 100644 --- a/src/game/server/gamecontroller.cpp +++ b/src/game/server/gamecontroller.cpp @@ -2,6 +2,7 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include +#include #include #include #include @@ -40,8 +41,6 @@ IGameController::IGameController(class CGameContext *pGameServer) : m_ForceBalanced = false; m_CurrentRecord = 0; - - InitTeleporter(); } IGameController::~IGameController() = default; @@ -96,7 +95,7 @@ float IGameController::EvaluateSpawnPos(CSpawnEval *pEval, vec2 Pos, int DDTeam) for(; pC; pC = (CCharacter *)pC->TypeNext()) { // ignore players in other teams - if(GameServer()->GetDDRaceTeam(pC->GetPlayer()->GetCID()) != DDTeam) + if(GameServer()->GetDDRaceTeam(pC->GetPlayer()->GetCid()) != DDTeam) continue; float d = distance(Pos, pC->m_Pos); @@ -390,31 +389,51 @@ bool IGameController::OnEntity(int Index, int x, int y, int Layer, int Flags, bo void IGameController::OnPlayerConnect(CPlayer *pPlayer) { - int ClientID = pPlayer->GetCID(); + int ClientId = pPlayer->GetCid(); pPlayer->Respawn(); - if(!Server()->ClientPrevIngame(ClientID)) + if(!Server()->ClientPrevIngame(ClientId)) { char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "team_join player='%d:%s' team=%d", ClientID, Server()->ClientName(ClientID), pPlayer->GetTeam()); + str_format(aBuf, sizeof(aBuf), "team_join player='%d:%s' team=%d", ClientId, Server()->ClientName(ClientId), pPlayer->GetTeam()); GameServer()->Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "game", aBuf); } + + if(Server()->IsSixup(ClientId)) + { + { + protocol7::CNetMsg_Sv_GameInfo Msg; + Msg.m_GameFlags = m_GameFlags; + Msg.m_MatchCurrent = 1; + Msg.m_MatchNum = 0; + Msg.m_ScoreLimit = 0; + Msg.m_TimeLimit = 0; + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); + } + + // /team is essential + { + protocol7::CNetMsg_Sv_CommandInfoRemove Msg; + Msg.m_pName = "team"; + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientId); + } + } } void IGameController::OnPlayerDisconnect(class CPlayer *pPlayer, const char *pReason) { pPlayer->OnDisconnect(); - int ClientID = pPlayer->GetCID(); - if(Server()->ClientIngame(ClientID)) + int ClientId = pPlayer->GetCid(); + if(Server()->ClientIngame(ClientId)) { char aBuf[512]; if(pReason && *pReason) - str_format(aBuf, sizeof(aBuf), "'%s' has left the game (%s)", Server()->ClientName(ClientID), pReason); + str_format(aBuf, sizeof(aBuf), "'%s' has left the game (%s)", Server()->ClientName(ClientId), pReason); else - str_format(aBuf, sizeof(aBuf), "'%s' has left the game", Server()->ClientName(ClientID)); - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf, -1, CGameContext::CHAT_SIX); + str_format(aBuf, sizeof(aBuf), "'%s' has left the game", Server()->ClientName(ClientId)); + GameServer()->SendChat(-1, TEAM_ALL, aBuf, -1, CGameContext::FLAG_SIX); - str_format(aBuf, sizeof(aBuf), "leave player='%d:%s'", ClientID, Server()->ClientName(ClientID)); + str_format(aBuf, sizeof(aBuf), "leave player='%d:%s'", ClientId, Server()->ClientName(ClientId)); GameServer()->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "game", aBuf); } } @@ -476,7 +495,7 @@ int IGameController::OnCharacterDeath(class CCharacter *pVictim, class CPlayer * void IGameController::OnCharacterSpawn(class CCharacter *pChr) { pChr->SetTeams(&Teams()); - Teams().OnCharacterSpawn(pChr->GetPlayer()->GetCID()); + Teams().OnCharacterSpawn(pChr->GetPlayer()->GetCid()); // default health pChr->IncreaseHealth(10); @@ -484,8 +503,6 @@ void IGameController::OnCharacterSpawn(class CCharacter *pChr) // give default weapons pChr->GiveWeapon(WEAPON_HAMMER); pChr->GiveWeapon(WEAPON_GUN); - - pChr->SetTeleports(&m_TeleOuts, &m_TeleCheckOuts); } void IGameController::HandleCharacterTiles(CCharacter *pChr, int MapIndex) @@ -506,7 +523,7 @@ bool IGameController::IsForceBalanced() return false; } -bool IGameController::CanBeMovedOnBalance(int ClientID) +bool IGameController::CanBeMovedOnBalance(int ClientId) { return true; } @@ -557,7 +574,7 @@ void IGameController::Snap(int SnappingClient) if(!pGameInfoObj) return; - pGameInfoObj->m_GameFlags = m_GameFlags; + pGameInfoObj->m_GameFlags = GameFlags_ClampToSix(m_GameFlags); pGameInfoObj->m_GameStateFlags = 0; if(m_GameOverTick != -1) pGameInfoObj->m_GameStateFlags |= GAMESTATEFLAG_GAMEOVER; @@ -577,7 +594,7 @@ void IGameController::Snap(int SnappingClient) if(pPlayer && (pPlayer->m_TimerType == CPlayer::TIMERTYPE_GAMETIMER || pPlayer->m_TimerType == CPlayer::TIMERTYPE_GAMETIMER_AND_BROADCAST) && pPlayer->GetClientVersion() >= VERSION_DDNET_GAMETICK) { - if((pPlayer->GetTeam() == TEAM_SPECTATORS || pPlayer->IsPaused()) && pPlayer->m_SpectatorID != SPEC_FREEVIEW && (pPlayer2 = GameServer()->m_apPlayers[pPlayer->m_SpectatorID])) + if((pPlayer->GetTeam() == TEAM_SPECTATORS || pPlayer->IsPaused()) && pPlayer->m_SpectatorId != SPEC_FREEVIEW && (pPlayer2 = GameServer()->m_apPlayers[pPlayer->m_SpectatorId])) { if((pChr = pPlayer2->GetCharacter()) && pChr->m_DDRaceState == DDRACE_STARTED) { @@ -642,18 +659,18 @@ void IGameController::Snap(int SnappingClient) pRaceData->m_BestTime = round_to_int(m_CurrentRecord * 1000); pRaceData->m_Precision = 2; - pRaceData->m_RaceFlags = protocol7::RACEFLAG_HIDE_KILLMSG | protocol7::RACEFLAG_KEEP_WANTED_WEAPON; + pRaceData->m_RaceFlags = protocol7::RACEFLAG_KEEP_WANTED_WEAPON; } GameServer()->SnapSwitchers(SnappingClient); } -int IGameController::GetAutoTeam(int NotThisID) +int IGameController::GetAutoTeam(int NotThisId) { int aNumplayers[2] = {0, 0}; for(int i = 0; i < MAX_CLIENTS; i++) { - if(GameServer()->m_apPlayers[i] && i != NotThisID) + if(GameServer()->m_apPlayers[i] && i != NotThisId) { if(GameServer()->m_apPlayers[i]->GetTeam() >= TEAM_RED && GameServer()->m_apPlayers[i]->GetTeam() <= TEAM_BLUE) aNumplayers[GameServer()->m_apPlayers[i]->GetTeam()]++; @@ -662,14 +679,14 @@ int IGameController::GetAutoTeam(int NotThisID) int Team = 0; - if(CanJoinTeam(Team, NotThisID, nullptr, 0)) + if(CanJoinTeam(Team, NotThisId, nullptr, 0)) return Team; return -1; } -bool IGameController::CanJoinTeam(int Team, int NotThisID, char *pErrorReason, int ErrorReasonSize) +bool IGameController::CanJoinTeam(int Team, int NotThisId, char *pErrorReason, int ErrorReasonSize) { - const CPlayer *pPlayer = GameServer()->m_apPlayers[NotThisID]; + const CPlayer *pPlayer = GameServer()->m_apPlayers[NotThisId]; if(pPlayer && pPlayer->IsPaused()) { if(pErrorReason) @@ -682,7 +699,7 @@ bool IGameController::CanJoinTeam(int Team, int NotThisID, char *pErrorReason, i int aNumplayers[2] = {0, 0}; for(int i = 0; i < MAX_CLIENTS; i++) { - if(GameServer()->m_apPlayers[i] && i != NotThisID) + if(GameServer()->m_apPlayers[i] && i != NotThisId) { if(GameServer()->m_apPlayers[i]->GetTeam() >= TEAM_RED && GameServer()->m_apPlayers[i]->GetTeam() <= TEAM_BLUE) aNumplayers[GameServer()->m_apPlayers[i]->GetTeam()]++; @@ -704,37 +721,12 @@ int IGameController::ClampTeam(int Team) return 0; } -CClientMask IGameController::GetMaskForPlayerWorldEvent(int Asker, int ExceptID) +CClientMask IGameController::GetMaskForPlayerWorldEvent(int Asker, int ExceptId) { if(Asker == -1) - return CClientMask().set().reset(ExceptID); - - return Teams().TeamMask(GameServer()->GetDDRaceTeam(Asker), ExceptID, Asker); -} + return CClientMask().set().reset(ExceptId); -void IGameController::InitTeleporter() -{ - if(!GameServer()->Collision()->Layers()->TeleLayer()) - return; - int Width = GameServer()->Collision()->Layers()->TeleLayer()->m_Width; - int Height = GameServer()->Collision()->Layers()->TeleLayer()->m_Height; - - for(int i = 0; i < Width * Height; i++) - { - int Number = GameServer()->Collision()->TeleLayer()[i].m_Number; - int Type = GameServer()->Collision()->TeleLayer()[i].m_Type; - if(Number > 0) - { - if(Type == TILE_TELEOUT) - { - m_TeleOuts[Number - 1].emplace_back(i % Width * 32.0f + 16.0f, i / Width * 32.0f + 16.0f); - } - else if(Type == TILE_TELECHECKOUT) - { - m_TeleCheckOuts[Number - 1].emplace_back(i % Width * 32.0f + 16.0f, i / Width * 32.0f + 16.0f); - } - } - } + return Teams().TeamMask(GameServer()->GetDDRaceTeam(Asker), ExceptId, Asker); } void IGameController::DoTeamChange(CPlayer *pPlayer, int Team, bool DoChatMsg) @@ -744,17 +736,17 @@ void IGameController::DoTeamChange(CPlayer *pPlayer, int Team, bool DoChatMsg) return; pPlayer->SetTeam(Team); - int ClientID = pPlayer->GetCID(); + int ClientId = pPlayer->GetCid(); char aBuf[128]; DoChatMsg = false; if(DoChatMsg) { - str_format(aBuf, sizeof(aBuf), "'%s' joined the %s", Server()->ClientName(ClientID), GameServer()->m_pController->GetTeamName(Team)); - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf); + str_format(aBuf, sizeof(aBuf), "'%s' joined the %s", Server()->ClientName(ClientId), GameServer()->m_pController->GetTeamName(Team)); + GameServer()->SendChat(-1, TEAM_ALL, aBuf); } - str_format(aBuf, sizeof(aBuf), "team_join player='%d:%s' m_Team=%d", ClientID, Server()->ClientName(ClientID), Team); + str_format(aBuf, sizeof(aBuf), "team_join player='%d:%s' m_Team=%d", ClientId, Server()->ClientName(ClientId), Team); GameServer()->Console()->Print(IConsole::OUTPUT_LEVEL_DEBUG, "game", aBuf); // OnPlayerInfoChange(pPlayer); diff --git a/src/game/server/gamecontroller.h b/src/game/server/gamecontroller.h index a3044d151a..a732123b4e 100644 --- a/src/game/server/gamecontroller.h +++ b/src/game/server/gamecontroller.h @@ -8,9 +8,6 @@ #include #include -#include -#include - struct CScoreLoadBestTimeResult; /* @@ -98,6 +95,7 @@ class IGameController virtual void OnCharacterSpawn(class CCharacter *pChr); virtual void HandleCharacterTiles(class CCharacter *pChr, int MapIndex); + virtual void SetArmorProgress(CCharacter *pCharacer, int Progress){}; /* Function: OnEntity @@ -130,7 +128,7 @@ class IGameController /* */ - virtual bool CanBeMovedOnBalance(int ClientID); + virtual bool CanBeMovedOnBalance(int ClientId); virtual void Tick(); @@ -144,18 +142,16 @@ class IGameController */ virtual const char *GetTeamName(int Team); - virtual int GetAutoTeam(int NotThisID); - virtual bool CanJoinTeam(int Team, int NotThisID, char *pErrorReason, int ErrorReasonSize); + virtual int GetAutoTeam(int NotThisId); + virtual bool CanJoinTeam(int Team, int NotThisId, char *pErrorReason, int ErrorReasonSize); int ClampTeam(int Team); CClientMask GetMaskForPlayerWorldEvent(int Asker, int ExceptID = -1); - virtual void InitTeleporter(); + bool IsTeamPlay() { return m_GameFlags & GAMEFLAG_TEAMS; } // DDRace float m_CurrentRecord; - std::map> m_TeleOuts; - std::map> m_TeleCheckOuts; CGameTeams &Teams() { return m_Teams; } std::shared_ptr m_pLoadBestTimeResult; }; diff --git a/src/game/server/gamemodes/DDRace.cpp b/src/game/server/gamemodes/DDRace.cpp index 48a6d71bd8..a2959a885d 100644 --- a/src/game/server/gamemodes/DDRace.cpp +++ b/src/game/server/gamemodes/DDRace.cpp @@ -18,6 +18,7 @@ CGameControllerDDRace::CGameControllerDDRace(class CGameContext *pGameServer) : IGameController(pGameServer) { m_pGameType = g_Config.m_SvTestingCommands ? TEST_TYPE_NAME : GAME_TYPE_NAME; + m_GameFlags = protocol7::GAMEFLAG_RACE; } CGameControllerDDRace::~CGameControllerDDRace() = default; @@ -30,10 +31,10 @@ CScore *CGameControllerDDRace::Score() void CGameControllerDDRace::HandleCharacterTiles(CCharacter *pChr, int MapIndex) { CPlayer *pPlayer = pChr->GetPlayer(); - const int ClientID = pPlayer->GetCID(); + const int ClientId = pPlayer->GetCid(); - int m_TileIndex = GameServer()->Collision()->GetTileIndex(MapIndex); - int m_TileFIndex = GameServer()->Collision()->GetFTileIndex(MapIndex); + int TileIndex = GameServer()->Collision()->GetTileIndex(MapIndex); + int TileFIndex = GameServer()->Collision()->GetFTileIndex(MapIndex); //Sensitivity int S1 = GameServer()->Collision()->GetPureMapIndex(vec2(pChr->GetPos().x + pChr->GetProximityRadius() / 3.f, pChr->GetPos().y - pChr->GetProximityRadius() / 3.f)); @@ -50,35 +51,35 @@ void CGameControllerDDRace::HandleCharacterTiles(CCharacter *pChr, int MapIndex) int FTile4 = GameServer()->Collision()->GetFTileIndex(S4); const int PlayerDDRaceState = pChr->m_DDRaceState; - bool IsOnStartTile = (m_TileIndex == TILE_START) || (m_TileFIndex == TILE_START) || FTile1 == TILE_START || FTile2 == TILE_START || FTile3 == TILE_START || FTile4 == TILE_START || Tile1 == TILE_START || Tile2 == TILE_START || Tile3 == TILE_START || Tile4 == TILE_START; + bool IsOnStartTile = (TileIndex == TILE_START) || (TileFIndex == TILE_START) || FTile1 == TILE_START || FTile2 == TILE_START || FTile3 == TILE_START || FTile4 == TILE_START || Tile1 == TILE_START || Tile2 == TILE_START || Tile3 == TILE_START || Tile4 == TILE_START; // start if(IsOnStartTile && PlayerDDRaceState != DDRACE_CHEAT) { - const int Team = GameServer()->GetDDRaceTeam(ClientID); + const int Team = GameServer()->GetDDRaceTeam(ClientId); if(Teams().GetSaving(Team)) { - GameServer()->SendStartWarning(ClientID, "You can't start while loading/saving of team is in progress"); - pChr->Die(ClientID, WEAPON_WORLD); + GameServer()->SendStartWarning(ClientId, "You can't start while loading/saving of team is in progress"); + pChr->Die(ClientId, WEAPON_WORLD); return; } if(g_Config.m_SvTeam == SV_TEAM_MANDATORY && (Team == TEAM_FLOCK || Teams().Count(Team) <= 1)) { - GameServer()->SendStartWarning(ClientID, "You have to be in a team with other tees to start"); - pChr->Die(ClientID, WEAPON_WORLD); + GameServer()->SendStartWarning(ClientId, "You have to be in a team with other tees to start"); + pChr->Die(ClientId, WEAPON_WORLD); return; } - if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && Team > TEAM_FLOCK && Team < TEAM_SUPER && Teams().Count(Team) < g_Config.m_SvMinTeamSize) + if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && Team > TEAM_FLOCK && Team < TEAM_SUPER && Teams().Count(Team) < g_Config.m_SvMinTeamSize && !Teams().TeamFlock(Team)) { char aBuf[128]; str_format(aBuf, sizeof(aBuf), "Your team has fewer than %d players, so your team rank won't count", g_Config.m_SvMinTeamSize); - GameServer()->SendStartWarning(ClientID, aBuf); + GameServer()->SendStartWarning(ClientId, aBuf); } if(g_Config.m_SvResetPickups) { pChr->ResetPickups(); } - Teams().OnCharacterStart(ClientID); + Teams().OnCharacterStart(ClientId); pChr->m_LastTimeCp = -1; pChr->m_LastTimeCpBroadcasted = -1; for(float &CurrentTimeCp : pChr->m_aCurrentTimeCp) @@ -88,64 +89,73 @@ void CGameControllerDDRace::HandleCharacterTiles(CCharacter *pChr, int MapIndex) } // finish - if(((m_TileIndex == TILE_FINISH) || (m_TileFIndex == TILE_FINISH) || FTile1 == TILE_FINISH || FTile2 == TILE_FINISH || FTile3 == TILE_FINISH || FTile4 == TILE_FINISH || Tile1 == TILE_FINISH || Tile2 == TILE_FINISH || Tile3 == TILE_FINISH || Tile4 == TILE_FINISH) && PlayerDDRaceState == DDRACE_STARTED) - Teams().OnCharacterFinish(ClientID); + if(((TileIndex == TILE_FINISH) || (TileFIndex == TILE_FINISH) || FTile1 == TILE_FINISH || FTile2 == TILE_FINISH || FTile3 == TILE_FINISH || FTile4 == TILE_FINISH || Tile1 == TILE_FINISH || Tile2 == TILE_FINISH || Tile3 == TILE_FINISH || Tile4 == TILE_FINISH) && PlayerDDRaceState == DDRACE_STARTED) + Teams().OnCharacterFinish(ClientId); // unlock team - else if(((m_TileIndex == TILE_UNLOCK_TEAM) || (m_TileFIndex == TILE_UNLOCK_TEAM)) && Teams().TeamLocked(GameServer()->GetDDRaceTeam(ClientID))) + else if(((TileIndex == TILE_UNLOCK_TEAM) || (TileFIndex == TILE_UNLOCK_TEAM)) && Teams().TeamLocked(GameServer()->GetDDRaceTeam(ClientId))) { - Teams().SetTeamLock(GameServer()->GetDDRaceTeam(ClientID), false); - GameServer()->SendChatTeam(GameServer()->GetDDRaceTeam(ClientID), "Your team was unlocked by an unlock team tile"); + Teams().SetTeamLock(GameServer()->GetDDRaceTeam(ClientId), false); + GameServer()->SendChatTeam(GameServer()->GetDDRaceTeam(ClientId), "Your team was unlocked by an unlock team tile"); } // solo part - if(((m_TileIndex == TILE_SOLO_ENABLE) || (m_TileFIndex == TILE_SOLO_ENABLE)) && !Teams().m_Core.GetSolo(ClientID)) + if(((TileIndex == TILE_SOLO_ENABLE) || (TileFIndex == TILE_SOLO_ENABLE)) && !Teams().m_Core.GetSolo(ClientId)) { - GameServer()->SendChatTarget(ClientID, "You are now in a solo part"); + GameServer()->SendChatTarget(ClientId, "You are now in a solo part"); pChr->SetSolo(true); } - else if(((m_TileIndex == TILE_SOLO_DISABLE) || (m_TileFIndex == TILE_SOLO_DISABLE)) && Teams().m_Core.GetSolo(ClientID)) + else if(((TileIndex == TILE_SOLO_DISABLE) || (TileFIndex == TILE_SOLO_DISABLE)) && Teams().m_Core.GetSolo(ClientId)) { - GameServer()->SendChatTarget(ClientID, "You are now out of the solo part"); + GameServer()->SendChatTarget(ClientId, "You are now out of the solo part"); pChr->SetSolo(false); } } +void CGameControllerDDRace::SetArmorProgress(CCharacter *pCharacer, int Progress) +{ + pCharacer->SetArmor(clamp(10 - (Progress / 15), 0, 10)); +} + void CGameControllerDDRace::OnPlayerConnect(CPlayer *pPlayer) { IGameController::OnPlayerConnect(pPlayer); - int ClientID = pPlayer->GetCID(); + int ClientId = pPlayer->GetCid(); // init the player - Score()->PlayerData(ClientID)->Reset(); + Score()->PlayerData(ClientId)->Reset(); // Can't set score here as LoadScore() is threaded, run it in // LoadScoreThreaded() instead - Score()->LoadPlayerData(ClientID); + Score()->LoadPlayerData(ClientId); - if(!Server()->ClientPrevIngame(ClientID)) + if(!Server()->ClientPrevIngame(ClientId)) { char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "'%s' entered and joined the %s", Server()->ClientName(ClientID), GetTeamName(pPlayer->GetTeam())); - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf, -1, CGameContext::CHAT_SIX); + str_format(aBuf, sizeof(aBuf), "'%s' entered and joined the %s", Server()->ClientName(ClientId), GetTeamName(pPlayer->GetTeam())); + GameServer()->SendChat(-1, TEAM_ALL, aBuf, -1, CGameContext::FLAG_SIX); - GameServer()->SendChatTarget(ClientID, "DDraceNetwork Mod. Version: " GAME_VERSION); - GameServer()->SendChatTarget(ClientID, "please visit DDNet.org or say /info and make sure to read our /rules"); + GameServer()->SendChatTarget(ClientId, "DDraceNetwork Mod. Version: " GAME_VERSION); + GameServer()->SendChatTarget(ClientId, "please visit DDNet.org or say /info and make sure to read our /rules"); } } void CGameControllerDDRace::OnPlayerDisconnect(CPlayer *pPlayer, const char *pReason) { - int ClientID = pPlayer->GetCID(); - bool WasModerator = pPlayer->m_Moderating && Server()->ClientIngame(ClientID); + int ClientId = pPlayer->GetCid(); + bool WasModerator = pPlayer->m_Moderating && Server()->ClientIngame(ClientId); IGameController::OnPlayerDisconnect(pPlayer, pReason); if(!GameServer()->PlayerModerating() && WasModerator) - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, "Server kick/spec votes are no longer actively moderated."); + GameServer()->SendChat(-1, TEAM_ALL, "Server kick/spec votes are no longer actively moderated."); if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO) - Teams().SetForceCharacterTeam(ClientID, TEAM_FLOCK); + Teams().SetForceCharacterTeam(ClientId, TEAM_FLOCK); + + for(int Team = TEAM_FLOCK + 1; Team < TEAM_SUPER; Team++) + if(Teams().IsInvited(Team, ClientId)) + Teams().SetClientInvited(Team, ClientId, false); } void CGameControllerDDRace::OnReset() @@ -176,7 +186,7 @@ void CGameControllerDDRace::DoTeamChange(class CPlayer *pPlayer, int Team, bool // Joining spectators should not kill a locked team, but should still // check if the team finished by you leaving it. int DDRTeam = pCharacter->Team(); - Teams().SetForceCharacterTeam(pPlayer->GetCID(), TEAM_FLOCK); + Teams().SetForceCharacterTeam(pPlayer->GetCid(), TEAM_FLOCK); Teams().CheckTeamFinished(DDRTeam); } } diff --git a/src/game/server/gamemodes/DDRace.h b/src/game/server/gamemodes/DDRace.h index 17626f26a3..bc307e0538 100644 --- a/src/game/server/gamemodes/DDRace.h +++ b/src/game/server/gamemodes/DDRace.h @@ -13,6 +13,7 @@ class CGameControllerDDRace : public IGameController CScore *Score(); void HandleCharacterTiles(class CCharacter *pChr, int MapIndex) override; + void SetArmorProgress(CCharacter *pCharacer, int Progress) override; void OnPlayerConnect(class CPlayer *pPlayer) override; void OnPlayerDisconnect(class CPlayer *pPlayer, const char *pReason) override; diff --git a/src/game/server/gameworld.cpp b/src/game/server/gameworld.cpp index 9247cca883..5f42290810 100644 --- a/src/game/server/gameworld.cpp +++ b/src/game/server/gameworld.cpp @@ -129,6 +129,19 @@ void CGameWorld::Snap(int SnappingClient) } } +void CGameWorld::PostSnap() +{ + for(auto *pEnt : m_apFirstEntityTypes) + { + for(; pEnt;) + { + m_pNextTraverseEntity = pEnt->m_pNextTypeEntity; + pEnt->PostSnap(); + pEnt = m_pNextTraverseEntity; + } + } +} + void CGameWorld::Reset() { // reset all entities @@ -163,7 +176,7 @@ void CGameWorld::RemoveEntitiesFromPlayers(int PlayerIds[], int NumPlayers) m_pNextTraverseEntity = pEnt->m_pNextTypeEntity; for(int i = 0; i < NumPlayers; i++) { - if(pEnt->GetOwnerID() == PlayerIds[i]) + if(pEnt->GetOwnerId() == PlayerIds[i]) { RemoveEntity(pEnt); pEnt->Destroy(); @@ -199,7 +212,7 @@ void CGameWorld::Tick() if(!m_Paused) { if(GameServer()->m_pController->IsForceBalanced()) - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, "Teams have been balanced"); + GameServer()->SendChat(-1, TEAM_ALL, "Teams have been balanced"); // update all objects for(int i = 0; i < NUM_ENTTYPES; i++) @@ -249,14 +262,29 @@ void CGameWorld::Tick() RemoveEntities(); // find the characters' strong/weak id - int StrongWeakID = 0; + int StrongWeakId = 0; for(CCharacter *pChar = (CCharacter *)FindFirst(ENTTYPE_CHARACTER); pChar; pChar = (CCharacter *)pChar->TypeNext()) { - pChar->m_StrongWeakID = StrongWeakID; - StrongWeakID++; + pChar->m_StrongWeakId = StrongWeakId; + StrongWeakId++; } } +ESaveResult CGameWorld::BlocksSave(int ClientId) +{ + // check all objects + for(auto *pEnt : m_apFirstEntityTypes) + for(; pEnt;) + { + m_pNextTraverseEntity = pEnt->m_pNextTypeEntity; + ESaveResult Result = pEnt->BlocksSave(ClientId); + if(Result != ESaveResult::SUCCESS) + return Result; + pEnt = m_pNextTraverseEntity; + } + return ESaveResult::SUCCESS; +} + void CGameWorld::SwapClients(int Client1, int Client2) { // update all objects @@ -356,17 +384,14 @@ std::vector CGameWorld::IntersectedCharacters(vec2 Pos0, vec2 Pos1 return vpCharacters; } -void CGameWorld::ReleaseHooked(int ClientID) +void CGameWorld::ReleaseHooked(int ClientId) { CCharacter *pChr = (CCharacter *)CGameWorld::FindFirst(CGameWorld::ENTTYPE_CHARACTER); for(; pChr; pChr = (CCharacter *)pChr->TypeNext()) { - CCharacterCore *pCore = pChr->Core(); - if(pCore->HookedPlayer() == ClientID && !pChr->IsSuper()) + if(pChr->Core()->HookedPlayer() == ClientId && !pChr->IsSuper()) { - pCore->SetHookedPlayer(-1); - pCore->m_TriggeredEvents |= COREEVENT_HOOK_RETRACT; - pCore->m_HookState = HOOK_RETRACTED; + pChr->ReleaseHook(); } } } diff --git a/src/game/server/gameworld.h b/src/game/server/gameworld.h index c86af59fd1..758f23203a 100644 --- a/src/game/server/gameworld.h +++ b/src/game/server/gameworld.h @@ -5,6 +5,8 @@ #include +#include "save.h" + #include class CEntity; @@ -133,6 +135,12 @@ class CGameWorld */ void Snap(int SnappingClient); + /* + Function: PostSnap + Called after all clients received their snapshot. + */ + void PostSnap(); + /* Function: Tick Calls Tick on all the entities in the world to progress @@ -147,8 +155,14 @@ class CGameWorld */ void SwapClients(int Client1, int Client2); + /* + Function: BlocksSave + Checks if any entity would block /save + */ + ESaveResult BlocksSave(int ClientId); + // DDRace - void ReleaseHooked(int ClientID); + void ReleaseHooked(int ClientId); /* Function: IntersectedCharacters diff --git a/src/game/server/player.cpp b/src/game/server/player.cpp index 77630471f5..f4c5404492 100644 --- a/src/game/server/player.cpp +++ b/src/game/server/player.cpp @@ -19,20 +19,20 @@ MACRO_ALLOC_POOL_ID_IMPL(CPlayer, MAX_CLIENTS) IServer *CPlayer::Server() const { return m_pGameServer->Server(); } -CPlayer::CPlayer(CGameContext *pGameServer, uint32_t UniqueClientID, int ClientID, int Team) : - m_UniqueClientID(UniqueClientID) +CPlayer::CPlayer(CGameContext *pGameServer, uint32_t UniqueClientId, int ClientId, int Team) : + m_UniqueClientId(UniqueClientId) { m_pGameServer = pGameServer; - m_ClientID = ClientID; + m_ClientId = ClientId; m_Team = GameServer()->m_pController->ClampTeam(Team); m_NumInputs = 0; Reset(); - GameServer()->Antibot()->OnPlayerInit(m_ClientID); + GameServer()->Antibot()->OnPlayerInit(m_ClientId); } CPlayer::~CPlayer() { - GameServer()->Antibot()->OnPlayerDestroy(m_ClientID); + GameServer()->Antibot()->OnPlayerDestroy(m_ClientId); delete m_pLastTarget; delete m_pCharacter; m_pCharacter = 0; @@ -45,18 +45,19 @@ void CPlayer::Reset() m_JoinTick = Server()->Tick(); delete m_pCharacter; m_pCharacter = 0; - m_SpectatorID = SPEC_FREEVIEW; + m_SpectatorId = SPEC_FREEVIEW; m_LastActionTick = Server()->Tick(); m_TeamChangeTick = Server()->Tick(); + m_LastSetTeam = 0; m_LastInvited = 0; m_WeakHookSpawn = false; - int *pIdMap = Server()->GetIdMap(m_ClientID); + int *pIdMap = Server()->GetIdMap(m_ClientId); for(int i = 1; i < VANILLA_MAX_CLIENTS; i++) { pIdMap[i] = -1; } - pIdMap[0] = m_ClientID; + pIdMap[0] = m_ClientId; // DDRace @@ -65,7 +66,7 @@ void CPlayer::Reset() m_ChatScore = 0; m_Moderating = false; m_EyeEmoteEnabled = true; - if(Server()->IsSixup(m_ClientID)) + if(Server()->IsSixup(m_ClientId)) m_TimerType = TIMERTYPE_SIXUP; else m_TimerType = (g_Config.m_SvDefaultTimerType == TIMERTYPE_GAMETIMER || g_Config.m_SvDefaultTimerType == TIMERTYPE_GAMETIMER_AND_BROADCAST) ? TIMERTYPE_BROADCAST : g_Config.m_SvDefaultTimerType; @@ -104,7 +105,7 @@ void CPlayer::Reset() } m_OverrideEmoteReset = -1; - GameServer()->Score()->PlayerData(m_ClientID)->Reset(); + GameServer()->Score()->PlayerData(m_ClientId)->Reset(); m_Last_KickVote = 0; m_Last_Team = 0; @@ -116,13 +117,14 @@ void CPlayer::Reset() m_Paused = PAUSE_NONE; m_DND = false; + m_Whispers = true; m_LastPause = 0; m_Score.reset(); // Variable initialized: m_Last_Team = 0; - m_LastSQLQuery = 0; + m_LastSqlQuery = 0; m_ScoreQueryResult = nullptr; m_ScoreFinishResult = nullptr; @@ -141,8 +143,9 @@ void CPlayer::Reset() m_NotEligibleForFinish = false; m_EligibleForFinishCheck = 0; m_VotedForPractice = false; - m_SwapTargetsClientID = -1; + m_SwapTargetsClientId = -1; m_BirthdayAnnounced = false; + m_RescueMode = RESCUEMODE_AUTO; } static int PlayerFlags_SixToSeven(int Flags) @@ -158,7 +161,7 @@ static int PlayerFlags_SixToSeven(int Flags) void CPlayer::Tick() { - if(m_ScoreQueryResult != nullptr && m_ScoreQueryResult->m_Completed) + if(m_ScoreQueryResult != nullptr && m_ScoreQueryResult->m_Completed && m_SentSnaps >= 3) { ProcessScoreResult(*m_ScoreQueryResult); m_ScoreQueryResult = nullptr; @@ -169,27 +172,27 @@ void CPlayer::Tick() m_ScoreFinishResult = nullptr; } - if(!Server()->ClientIngame(m_ClientID)) + if(!Server()->ClientIngame(m_ClientId)) return; if(m_ChatScore > 0) m_ChatScore--; - Server()->SetClientScore(m_ClientID, m_Score); + Server()->SetClientScore(m_ClientId, m_Score); if(m_Moderating && m_Afk) { m_Moderating = false; - GameServer()->SendChatTarget(m_ClientID, "Active moderator mode disabled because you are afk."); + GameServer()->SendChatTarget(m_ClientId, "Active moderator mode disabled because you are afk."); if(!GameServer()->PlayerModerating()) - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, "Server kick/spec votes are no longer actively moderated."); + GameServer()->SendChat(-1, TEAM_ALL, "Server kick/spec votes are no longer actively moderated."); } // do latency stuff { IServer::CClientInfo Info; - if(Server()->GetClientInfo(m_ClientID, &Info)) + if(Server()->GetClientInfo(m_ClientId, &Info)) { m_Latency.m_Accum += Info.m_Latency; m_Latency.m_AccumMax = maximum(m_Latency.m_AccumMax, Info.m_Latency); @@ -207,14 +210,14 @@ void CPlayer::Tick() } } - if(Server()->GetNetErrorString(m_ClientID)[0]) + if(Server()->GetNetErrorString(m_ClientId)[0]) { - SetAfk(true); + SetInitialAfk(true); char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "'%s' would have timed out, but can use timeout protection now", Server()->ClientName(m_ClientID)); - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf); - Server()->ResetNetErrorString(m_ClientID); + str_format(aBuf, sizeof(aBuf), "'%s' would have timed out, but can use timeout protection now", Server()->ClientName(m_ClientId)); + GameServer()->SendChat(-1, TEAM_ALL, aBuf); + Server()->ResetNetErrorString(m_ClientId); } if(!GameServer()->m_World.m_Paused) @@ -256,7 +259,7 @@ void CPlayer::Tick() if(m_TuneZone != m_TuneZoneOld) // don't send tunings all the time { - GameServer()->SendTuningParams(m_ClientID, m_TuneZone); + GameServer()->SendTuningParams(m_ClientId, m_TuneZone); } if(m_OverrideEmoteReset >= 0 && m_OverrideEmoteReset <= Server()->Tick()) @@ -268,7 +271,7 @@ void CPlayer::Tick() { if(1200 - ((Server()->Tick() - m_pCharacter->GetLastAction()) % (1200)) < 5) { - GameServer()->SendEmoticon(GetCID(), EMOTICON_GHOST, -1); + GameServer()->SendEmoticon(GetCid(), EMOTICON_GHOST, -1); } } } @@ -277,7 +280,7 @@ void CPlayer::PostTick() { // update latency value if(m_PlayerFlags & PLAYERFLAG_IN_MENU) - m_aCurLatency[m_ClientID] = GameServer()->m_apPlayers[m_ClientID]->m_Latency.m_Min; + m_aCurLatency[m_ClientId] = GameServer()->m_apPlayers[m_ClientId]->m_Latency.m_Min; if(m_PlayerFlags & PLAYERFLAG_SCOREBOARD) { @@ -289,13 +292,13 @@ void CPlayer::PostTick() } // update view pos for spectators - if((m_Team == TEAM_SPECTATORS || m_Paused) && m_SpectatorID != SPEC_FREEVIEW && GameServer()->m_apPlayers[m_SpectatorID] && GameServer()->m_apPlayers[m_SpectatorID]->GetCharacter()) - m_ViewPos = GameServer()->m_apPlayers[m_SpectatorID]->GetCharacter()->m_Pos; + if((m_Team == TEAM_SPECTATORS || m_Paused) && m_SpectatorId != SPEC_FREEVIEW && GameServer()->m_apPlayers[m_SpectatorId] && GameServer()->m_apPlayers[m_SpectatorId]->GetCharacter()) + m_ViewPos = GameServer()->m_apPlayers[m_SpectatorId]->GetCharacter()->m_Pos; } void CPlayer::PostPostTick() { - if(!Server()->ClientIngame(m_ClientID)) + if(!Server()->ClientIngame(m_ClientId)) return; if(!GameServer()->m_World.m_Paused && !m_pCharacter && m_Spawning && m_WeakHookSpawn) @@ -304,10 +307,10 @@ void CPlayer::PostPostTick() void CPlayer::Snap(int SnappingClient) { - if(!Server()->ClientIngame(m_ClientID)) + if(!Server()->ClientIngame(m_ClientId)) return; - int id = m_ClientID; + int id = m_ClientId; if(!Server()->Translate(id, SnappingClient)) return; @@ -315,16 +318,16 @@ void CPlayer::Snap(int SnappingClient) if(!pClientInfo) return; - StrToInts(&pClientInfo->m_Name0, 4, Server()->ClientName(m_ClientID)); - StrToInts(&pClientInfo->m_Clan0, 3, Server()->ClientClan(m_ClientID)); - pClientInfo->m_Country = Server()->ClientCountry(m_ClientID); + StrToInts(&pClientInfo->m_Name0, 4, Server()->ClientName(m_ClientId)); + StrToInts(&pClientInfo->m_Clan0, 3, Server()->ClientClan(m_ClientId)); + pClientInfo->m_Country = Server()->ClientCountry(m_ClientId); StrToInts(&pClientInfo->m_Skin0, 6, m_TeeInfos.m_aSkinName); pClientInfo->m_UseCustomColor = m_TeeInfos.m_UseCustomColor; pClientInfo->m_ColorBody = m_TeeInfos.m_ColorBody; pClientInfo->m_ColorFeet = m_TeeInfos.m_ColorFeet; int SnappingClientVersion = GameServer()->GetClientVersion(SnappingClient); - int Latency = SnappingClient == SERVER_DEMO_CLIENT ? m_Latency.m_Min : GameServer()->m_apPlayers[SnappingClient]->m_aCurLatency[m_ClientID]; + int Latency = SnappingClient == SERVER_DEMO_CLIENT ? m_Latency.m_Min : GameServer()->m_apPlayers[SnappingClient]->m_aCurLatency[m_ClientId]; int Score; // This is the time sent to the player while ingame (do not confuse to the one reported to the master server). @@ -346,7 +349,7 @@ void CPlayer::Snap(int SnappingClient) } // send 0 if times of others are not shown - if(SnappingClient != m_ClientID && g_Config.m_SvHideScore) + if(SnappingClient != m_ClientId && g_Config.m_SvHideScore) Score = -9999; if(!Server()->IsSixup(SnappingClient)) @@ -357,13 +360,13 @@ void CPlayer::Snap(int SnappingClient) pPlayerInfo->m_Latency = Latency; pPlayerInfo->m_Score = Score; - pPlayerInfo->m_Local = (int)(m_ClientID == SnappingClient && (m_Paused != PAUSE_PAUSED || SnappingClientVersion >= VERSION_DDNET_OLD)); - pPlayerInfo->m_ClientID = id; + pPlayerInfo->m_Local = (int)(m_ClientId == SnappingClient && (m_Paused != PAUSE_PAUSED || SnappingClientVersion >= VERSION_DDNET_OLD)); + pPlayerInfo->m_ClientId = id; pPlayerInfo->m_Team = m_Team; if(SnappingClientVersion < VERSION_DDNET_INDEPENDENT_SPECTATORS_TEAM) { // In older versions the SPECTATORS TEAM was also used if the own player is in PAUSE_PAUSED or if any player is in PAUSE_SPEC. - pPlayerInfo->m_Team = (m_Paused != PAUSE_PAUSED || m_ClientID != SnappingClient) && m_Paused < PAUSE_SPEC ? m_Team : TEAM_SPECTATORS; + pPlayerInfo->m_Team = (m_Paused != PAUSE_PAUSED || m_ClientId != SnappingClient) && m_Paused < PAUSE_SPEC ? m_Team : TEAM_SPECTATORS; } } else @@ -375,34 +378,34 @@ void CPlayer::Snap(int SnappingClient) pPlayerInfo->m_PlayerFlags = PlayerFlags_SixToSeven(m_PlayerFlags); if(SnappingClientVersion >= VERSION_DDRACE && (m_PlayerFlags & PLAYERFLAG_AIM)) pPlayerInfo->m_PlayerFlags |= protocol7::PLAYERFLAG_AIM; - if(Server()->ClientAuthed(m_ClientID)) + if(Server()->ClientAuthed(m_ClientId)) pPlayerInfo->m_PlayerFlags |= protocol7::PLAYERFLAG_ADMIN; // Times are in milliseconds for 0.7 - pPlayerInfo->m_Score = m_Score.has_value() ? GameServer()->Score()->PlayerData(m_ClientID)->m_BestTime * 1000 : -1; + pPlayerInfo->m_Score = m_Score.has_value() ? GameServer()->Score()->PlayerData(m_ClientId)->m_BestTime * 1000 : -1; pPlayerInfo->m_Latency = Latency; } - if(m_ClientID == SnappingClient && (m_Team == TEAM_SPECTATORS || m_Paused)) + if(m_ClientId == SnappingClient && (m_Team == TEAM_SPECTATORS || m_Paused)) { if(!Server()->IsSixup(SnappingClient)) { - CNetObj_SpectatorInfo *pSpectatorInfo = Server()->SnapNewItem(m_ClientID); + CNetObj_SpectatorInfo *pSpectatorInfo = Server()->SnapNewItem(m_ClientId); if(!pSpectatorInfo) return; - pSpectatorInfo->m_SpectatorID = m_SpectatorID; + pSpectatorInfo->m_SpectatorId = m_SpectatorId; pSpectatorInfo->m_X = m_ViewPos.x; pSpectatorInfo->m_Y = m_ViewPos.y; } else { - protocol7::CNetObj_SpectatorInfo *pSpectatorInfo = Server()->SnapNewItem(m_ClientID); + protocol7::CNetObj_SpectatorInfo *pSpectatorInfo = Server()->SnapNewItem(m_ClientId); if(!pSpectatorInfo) return; - pSpectatorInfo->m_SpecMode = m_SpectatorID == SPEC_FREEVIEW ? protocol7::SPEC_FREEVIEW : protocol7::SPEC_PLAYER; - pSpectatorInfo->m_SpectatorID = m_SpectatorID; + pSpectatorInfo->m_SpecMode = m_SpectatorId == SPEC_FREEVIEW ? protocol7::SPEC_FREEVIEW : protocol7::SPEC_PLAYER; + pSpectatorInfo->m_SpectatorId = m_SpectatorId; pSpectatorInfo->m_X = m_ViewPos.x; pSpectatorInfo->m_Y = m_ViewPos.y; } @@ -412,7 +415,7 @@ void CPlayer::Snap(int SnappingClient) if(!pDDNetPlayer) return; - pDDNetPlayer->m_AuthLevel = Server()->GetAuthedState(m_ClientID); + pDDNetPlayer->m_AuthLevel = Server()->GetAuthedState(m_ClientId); pDDNetPlayer->m_Flags = 0; if(m_Afk) pDDNetPlayer->m_Flags |= EXPLAYERFLAG_AFK; @@ -435,7 +438,7 @@ void CPlayer::Snap(int SnappingClient) if(SnappingClient != SERVER_DEMO_CLIENT) { CPlayer *pSnapPlayer = GameServer()->m_apPlayers[SnappingClient]; - ShowSpec = ShowSpec && (GameServer()->GetDDRaceTeam(m_ClientID) == GameServer()->GetDDRaceTeam(SnappingClient) || pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ON || (pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused())); + ShowSpec = ShowSpec && (GameServer()->GetDDRaceTeam(m_ClientId) == GameServer()->GetDDRaceTeam(SnappingClient) || pSnapPlayer->m_ShowOthers == SHOW_OTHERS_ON || (pSnapPlayer->GetTeam() == TEAM_SPECTATORS || pSnapPlayer->IsPaused())); } if(ShowSpec) @@ -451,15 +454,16 @@ void CPlayer::Snap(int SnappingClient) void CPlayer::FakeSnap() { + m_SentSnaps++; if(GetClientVersion() >= VERSION_DDNET_OLD) return; - if(Server()->IsSixup(m_ClientID)) + if(Server()->IsSixup(m_ClientId)) return; - int FakeID = VANILLA_MAX_CLIENTS - 1; + int FakeId = VANILLA_MAX_CLIENTS - 1; - CNetObj_ClientInfo *pClientInfo = Server()->SnapNewItem(FakeID); + CNetObj_ClientInfo *pClientInfo = Server()->SnapNewItem(FakeId); if(!pClientInfo) return; @@ -471,21 +475,21 @@ void CPlayer::FakeSnap() if(m_Paused != PAUSE_PAUSED) return; - CNetObj_PlayerInfo *pPlayerInfo = Server()->SnapNewItem(FakeID); + CNetObj_PlayerInfo *pPlayerInfo = Server()->SnapNewItem(FakeId); if(!pPlayerInfo) return; pPlayerInfo->m_Latency = m_Latency.m_Min; pPlayerInfo->m_Local = 1; - pPlayerInfo->m_ClientID = FakeID; + pPlayerInfo->m_ClientId = FakeId; pPlayerInfo->m_Score = -9999; pPlayerInfo->m_Team = TEAM_SPECTATORS; - CNetObj_SpectatorInfo *pSpectatorInfo = Server()->SnapNewItem(FakeID); + CNetObj_SpectatorInfo *pSpectatorInfo = Server()->SnapNewItem(FakeId); if(!pSpectatorInfo) return; - pSpectatorInfo->m_SpectatorID = m_SpectatorID; + pSpectatorInfo->m_SpectatorId = m_SpectatorId; pSpectatorInfo->m_X = m_ViewPos.x; pSpectatorInfo->m_Y = m_ViewPos.y; } @@ -512,18 +516,18 @@ void CPlayer::OnPredictedInput(CNetObj_PlayerInput *pNewInput) // Magic number when we can hope that client has successfully identified itself if(m_NumInputs == 20 && g_Config.m_SvClientSuggestion[0] != '\0' && GetClientVersion() <= VERSION_DDNET_OLD) - GameServer()->SendBroadcast(g_Config.m_SvClientSuggestion, m_ClientID); - else if(m_NumInputs == 200 && Server()->IsSixup(m_ClientID)) - GameServer()->SendBroadcast("This server uses an experimental translation from Teeworlds 0.7 to 0.6. Please report bugs on ddnet.org/discord", m_ClientID); + GameServer()->SendBroadcast(g_Config.m_SvClientSuggestion, m_ClientId); + else if(m_NumInputs == 200 && Server()->IsSixup(m_ClientId)) + GameServer()->SendBroadcast("This server uses an experimental translation from Teeworlds 0.7 to 0.6. Please report bugs on ddnet.org/discord", m_ClientId); } void CPlayer::OnDirectInput(CNetObj_PlayerInput *pNewInput) { - Server()->SetClientFlags(m_ClientID, pNewInput->m_PlayerFlags); + Server()->SetClientFlags(m_ClientId, pNewInput->m_PlayerFlags); AfkTimer(); - if(((!m_pCharacter && m_Team == TEAM_SPECTATORS) || m_Paused) && m_SpectatorID == SPEC_FREEVIEW) + if(((!m_pCharacter && m_Team == TEAM_SPECTATORS) || m_Paused) && m_SpectatorId == SPEC_FREEVIEW) m_ViewPos = vec2(pNewInput->m_TargetX, pNewInput->m_TargetY); // check for activity @@ -555,7 +559,7 @@ void CPlayer::OnPredictedEarlyInput(CNetObj_PlayerInput *pNewInput) int CPlayer::GetClientVersion() const { - return m_pGameServer->GetClientVersion(m_ClientID); + return m_pGameServer->GetClientVersion(m_ClientId); } CCharacter *CPlayer::GetCharacter() @@ -576,7 +580,7 @@ void CPlayer::KillCharacter(int Weapon, bool SendKillMsg) { if(m_pCharacter) { - m_pCharacter->Die(m_ClientID, Weapon, SendKillMsg); + m_pCharacter->Die(m_ClientId, Weapon, SendKillMsg); delete m_pCharacter; m_pCharacter = 0; @@ -595,7 +599,7 @@ void CPlayer::Respawn(bool WeakHook) CCharacter *CPlayer::ForceSpawn(vec2 Pos) { m_Spawning = false; - m_pCharacter = new(m_ClientID) CCharacter(&GameServer()->m_World, GameServer()->GetLastPlayerInput(m_ClientID)); + m_pCharacter = new(m_ClientId) CCharacter(&GameServer()->m_World, GameServer()->GetLastPlayerInput(m_ClientId)); m_pCharacter->Spawn(this, Pos); m_Team = 0; return m_pCharacter; @@ -608,10 +612,10 @@ void CPlayer::SetTeam(int Team, bool DoChatMsg) m_Team = Team; m_LastSetTeam = Server()->Tick(); m_LastActionTick = Server()->Tick(); - m_SpectatorID = SPEC_FREEVIEW; + m_SpectatorId = SPEC_FREEVIEW; protocol7::CNetMsg_Sv_Team Msg; - Msg.m_ClientID = m_ClientID; + Msg.m_ClientId = m_ClientId; Msg.m_Team = m_Team; Msg.m_Silent = !DoChatMsg; Msg.m_CooldownTick = m_LastSetTeam + Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay; @@ -622,17 +626,19 @@ void CPlayer::SetTeam(int Team, bool DoChatMsg) // update spectator modes for(auto &pPlayer : GameServer()->m_apPlayers) { - if(pPlayer && pPlayer->m_SpectatorID == m_ClientID) - pPlayer->m_SpectatorID = SPEC_FREEVIEW; + if(pPlayer && pPlayer->m_SpectatorId == m_ClientId) + pPlayer->m_SpectatorId = SPEC_FREEVIEW; } } + + Server()->ExpireServerInfo(); } bool CPlayer::SetTimerType(int TimerType) { if(TimerType == TIMERTYPE_DEFAULT) { - if(Server()->IsSixup(m_ClientID)) + if(Server()->IsSixup(m_ClientId)) m_TimerType = TIMERTYPE_SIXUP; else SetTimerType(g_Config.m_SvDefaultTimerType); @@ -640,7 +646,7 @@ bool CPlayer::SetTimerType(int TimerType) return true; } - if(Server()->IsSixup(m_ClientID)) + if(Server()->IsSixup(m_ClientId)) { if(TimerType == TIMERTYPE_SIXUP || TimerType == TIMERTYPE_NONE) { @@ -678,15 +684,15 @@ void CPlayer::TryRespawn() { vec2 SpawnPos; - if(!GameServer()->m_pController->CanSpawn(m_Team, &SpawnPos, GameServer()->GetDDRaceTeam(m_ClientID))) + if(!GameServer()->m_pController->CanSpawn(m_Team, &SpawnPos, GameServer()->GetDDRaceTeam(m_ClientId))) return; m_WeakHookSpawn = false; m_Spawning = false; - m_pCharacter = new(m_ClientID) CCharacter(&GameServer()->m_World, GameServer()->GetLastPlayerInput(m_ClientID)); + m_pCharacter = new(m_ClientId) CCharacter(&GameServer()->m_World, GameServer()->GetLastPlayerInput(m_ClientId)); m_ViewPos = SpawnPos; m_pCharacter->Spawn(this, SpawnPos); - GameServer()->CreatePlayerSpawn(SpawnPos, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientID)); + GameServer()->CreatePlayerSpawn(SpawnPos, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientId)); if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) m_pCharacter->SetSolo(true); @@ -748,6 +754,11 @@ bool CPlayer::CanOverrideDefaultEmote() const return m_LastEyeEmote == 0 || m_LastEyeEmote + (int64_t)g_Config.m_SvEyeEmoteChangeDelay * Server()->TickSpeed() < Server()->Tick(); } +bool CPlayer::CanSpec() const +{ + return m_pCharacter->IsGrounded() && m_pCharacter->m_Pos == m_pCharacter->m_PrevPos; +} + void CPlayer::ProcessPause() { if(m_ForcePauseTime && m_ForcePauseTime < Server()->Tick()) @@ -756,11 +767,11 @@ void CPlayer::ProcessPause() Pause(PAUSE_NONE, true); } - if(m_Paused == PAUSE_SPEC && !m_pCharacter->IsPaused() && m_pCharacter->IsGrounded() && m_pCharacter->m_Pos == m_pCharacter->m_PrevPos) + if(m_Paused == PAUSE_SPEC && !m_pCharacter->IsPaused() && CanSpec()) { m_pCharacter->Pause(true); - GameServer()->CreateDeath(m_pCharacter->m_Pos, m_ClientID, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientID)); - GameServer()->CreateSound(m_pCharacter->m_Pos, SOUND_PLAYER_DIE, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientID)); + GameServer()->CreateDeath(m_pCharacter->m_Pos, m_ClientId, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientId)); + GameServer()->CreateSound(m_pCharacter->m_Pos, SOUND_PLAYER_DIE, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientId)); } } @@ -784,19 +795,19 @@ int CPlayer::Pause(int State, bool Force) { if(!Force && m_LastPause && m_LastPause + (int64_t)g_Config.m_SvSpecFrequency * Server()->TickSpeed() > Server()->Tick()) { - GameServer()->SendChatTarget(m_ClientID, "Can't /spec that quickly."); + GameServer()->SendChatTarget(m_ClientId, "Can't /spec that quickly."); return m_Paused; // Do not update state. Do not collect $200 } m_pCharacter->Pause(false); m_ViewPos = m_pCharacter->m_Pos; - GameServer()->CreatePlayerSpawn(m_pCharacter->m_Pos, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientID)); + GameServer()->CreatePlayerSpawn(m_pCharacter->m_Pos, GameServer()->m_pController->GetMaskForPlayerWorldEvent(m_ClientId)); } [[fallthrough]]; case PAUSE_SPEC: if(g_Config.m_SvPauseMessages) { - str_format(aBuf, sizeof(aBuf), (State > PAUSE_NONE) ? "'%s' speced" : "'%s' resumed", Server()->ClientName(m_ClientID)); - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf); + str_format(aBuf, sizeof(aBuf), (State > PAUSE_NONE) ? "'%s' speced" : "'%s' resumed", Server()->ClientName(m_ClientId)); + GameServer()->SendChat(-1, TEAM_ALL, aBuf); } break; } @@ -807,12 +818,12 @@ int CPlayer::Pause(int State, bool Force) // Sixup needs a teamchange protocol7::CNetMsg_Sv_Team Msg; - Msg.m_ClientID = m_ClientID; + Msg.m_ClientId = m_ClientId; Msg.m_CooldownTick = Server()->Tick(); Msg.m_Silent = true; Msg.m_Team = m_Paused ? protocol7::TEAM_SPECTATORS : m_Team; - GameServer()->Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, m_ClientID); + GameServer()->Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, m_ClientId); } return m_Paused; @@ -825,8 +836,8 @@ int CPlayer::ForcePause(int Time) if(g_Config.m_SvPauseMessages) { char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "'%s' was force-paused for %ds", Server()->ClientName(m_ClientID), Time); - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf); + str_format(aBuf, sizeof(aBuf), "'%s' was force-paused for %ds", Server()->ClientName(m_ClientId), Time); + GameServer()->SendChat(-1, TEAM_ALL, aBuf); } return Pause(PAUSE_SPEC, true); @@ -849,9 +860,9 @@ void CPlayer::SpectatePlayerName(const char *pName) for(int i = 0; i < MAX_CLIENTS; ++i) { - if(i != m_ClientID && Server()->ClientIngame(i) && !str_comp(pName, Server()->ClientName(i))) + if(i != m_ClientId && Server()->ClientIngame(i) && !str_comp(pName, Server()->ClientName(i))) { - m_SpectatorID = i; + m_SpectatorId = i; return; } } @@ -868,7 +879,7 @@ void CPlayer::ProcessScoreResult(CScorePlayerResult &Result) { if(aMessage[0] == 0) break; - GameServer()->SendChatTarget(m_ClientID, aMessage); + GameServer()->SendChatTarget(m_ClientId, aMessage); } break; case CScorePlayerResult::ALL: @@ -879,10 +890,10 @@ void CPlayer::ProcessScoreResult(CScorePlayerResult &Result) if(aMessage[0] == 0) break; - if(GameServer()->ProcessSpamProtection(m_ClientID) && PrimaryMessage) + if(GameServer()->ProcessSpamProtection(m_ClientId) && PrimaryMessage) break; - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aMessage, -1); + GameServer()->SendChat(-1, TEAM_ALL, aMessage, -1); PrimaryMessage = false; } break; @@ -902,32 +913,43 @@ void CPlayer::ProcessScoreResult(CScorePlayerResult &Result) char aChatmsg[512]; str_format(aChatmsg, sizeof(aChatmsg), "'%s' called vote to change server option '%s' (%s)", - Server()->ClientName(m_ClientID), Result.m_Data.m_MapVote.m_aMap, "/map"); + Server()->ClientName(m_ClientId), Result.m_Data.m_MapVote.m_aMap, "/map"); - GameServer()->CallVote(m_ClientID, Result.m_Data.m_MapVote.m_aMap, aCmd, "/map", aChatmsg); + GameServer()->CallVote(m_ClientId, Result.m_Data.m_MapVote.m_aMap, aCmd, "/map", aChatmsg); break; case CScorePlayerResult::PLAYER_INFO: + { if(Result.m_Data.m_Info.m_Time.has_value()) { - GameServer()->Score()->PlayerData(m_ClientID)->Set(Result.m_Data.m_Info.m_Time.value(), Result.m_Data.m_Info.m_aTimeCp); + GameServer()->Score()->PlayerData(m_ClientId)->Set(Result.m_Data.m_Info.m_Time.value(), Result.m_Data.m_Info.m_aTimeCp); m_Score = Result.m_Data.m_Info.m_Time; } Server()->ExpireServerInfo(); int Birthday = Result.m_Data.m_Info.m_Birthday; - if(Birthday != 0 && !m_BirthdayAnnounced) + if(Birthday != 0 && !m_BirthdayAnnounced && GetCharacter()) { char aBuf[512]; str_format(aBuf, sizeof(aBuf), "Happy DDNet birthday to %s for finishing their first map %d year%s ago!", - Server()->ClientName(m_ClientID), Birthday, Birthday > 1 ? "s" : ""); - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf, m_ClientID); + Server()->ClientName(m_ClientId), Birthday, Birthday > 1 ? "s" : ""); + GameServer()->SendChat(-1, TEAM_ALL, aBuf, m_ClientId); str_format(aBuf, sizeof(aBuf), "Happy DDNet birthday, %s!\nYou have finished your first map exactly %d year%s ago!", - Server()->ClientName(m_ClientID), Birthday, Birthday > 1 ? "s" : ""); - GameServer()->SendBroadcast(aBuf, m_ClientID); + Server()->ClientName(m_ClientId), Birthday, Birthday > 1 ? "s" : ""); + GameServer()->SendBroadcast(aBuf, m_ClientId); m_BirthdayAnnounced = true; + + GameServer()->CreateBirthdayEffect(GetCharacter()->m_Pos, GetCharacter()->TeamMask()); } - GameServer()->SendRecord(m_ClientID); + GameServer()->SendRecord(m_ClientId); + break; + } + case CScorePlayerResult::PLAYER_TIMECP: + GameServer()->Score()->PlayerData(m_ClientId)->SetBestTimeCp(Result.m_Data.m_Info.m_aTimeCp); + char aBuf[128], aTime[32]; + str_time_float(Result.m_Data.m_Info.m_Time.value(), TIME_HOURS_CENTISECS, aTime, sizeof(aTime)); + str_format(aBuf, sizeof(aBuf), "Showing the checkpoint times for '%s' with a race time of %s", Result.m_Data.m_Info.m_aRequestedPlayer, aTime); + GameServer()->SendChatTarget(m_ClientId, aBuf); break; } } diff --git a/src/game/server/player.h b/src/game/server/player.h index 8afed78165..c281ac5b14 100644 --- a/src/game/server/player.h +++ b/src/game/server/player.h @@ -34,7 +34,7 @@ class CPlayer MACRO_ALLOC_POOL_ID() public: - CPlayer(CGameContext *pGameServer, uint32_t UniqueClientID, int ClientID, int Team); + CPlayer(CGameContext *pGameServer, uint32_t UniqueClientId, int ClientId, int Team); ~CPlayer(); void Reset(); @@ -44,8 +44,8 @@ class CPlayer CCharacter *ForceSpawn(vec2 Pos); // required for loading savegames void SetTeam(int Team, bool DoChatMsg = true); int GetTeam() const { return m_Team; } - int GetCID() const { return m_ClientID; } - uint32_t GetUniqueCID() const { return m_UniqueClientID; } + int GetCid() const { return m_ClientId; } + uint32_t GetUniqueCid() const { return m_UniqueClientId; } int GetClientVersion() const; bool SetTimerType(int TimerType); @@ -80,8 +80,10 @@ class CPlayer // used for snapping to just update latency if the scoreboard is active int m_aCurLatency[MAX_CLIENTS]; + int m_SentSnaps = 0; + // used for spectator mode - int m_SpectatorID; + int m_SpectatorId; bool m_IsReady; @@ -127,7 +129,7 @@ class CPlayer } m_Latency; private: - const uint32_t m_UniqueClientID; + const uint32_t m_UniqueClientId; CCharacter *m_pCharacter; int m_NumInputs; CGameContext *m_pGameServer; @@ -138,7 +140,7 @@ class CPlayer // bool m_Spawning; bool m_WeakHookSpawn; - int m_ClientID; + int m_ClientId; int m_Team; int m_Paused; @@ -170,6 +172,7 @@ class CPlayer }; bool m_DND; + bool m_Whispers; int64_t m_FirstVoteTick; char m_aTimeoutCode[64]; @@ -177,6 +180,7 @@ class CPlayer int Pause(int State, bool Force); int ForcePause(int Time); int IsPaused() const; + bool CanSpec() const; bool IsPlaying() const; int64_t m_Last_KickVote; @@ -213,16 +217,18 @@ class CPlayer bool CanOverrideDefaultEmote() const; bool m_FirstPacket; - int64_t m_LastSQLQuery; + int64_t m_LastSqlQuery; void ProcessScoreResult(CScorePlayerResult &Result); std::shared_ptr m_ScoreQueryResult; std::shared_ptr m_ScoreFinishResult; bool m_NotEligibleForFinish; int64_t m_EligibleForFinishCheck; bool m_VotedForPractice; - int m_SwapTargetsClientID; //Client ID of the swap target for the given player + int m_SwapTargetsClientId; //Client ID of the swap target for the given player bool m_BirthdayAnnounced; + int m_RescueMode; + CSaveTee m_LastTeleTee; }; diff --git a/src/game/server/save.cpp b/src/game/server/save.cpp index c5b1841d4d..b519335a7a 100644 --- a/src/game/server/save.cpp +++ b/src/game/server/save.cpp @@ -11,17 +11,24 @@ CSaveTee::CSaveTee() = default; -void CSaveTee::Save(CCharacter *pChr) +void CSaveTee::Save(CCharacter *pChr, bool AddPenalty) { - m_ClientID = pChr->m_pPlayer->GetCID(); - str_copy(m_aName, pChr->Server()->ClientName(m_ClientID), sizeof(m_aName)); + m_ClientId = pChr->m_pPlayer->GetCid(); + str_copy(m_aName, pChr->Server()->ClientName(m_ClientId), sizeof(m_aName)); m_Alive = pChr->m_Alive; + + // This is extremely suspect code, probably interacts badly with force pause m_Paused = absolute(pChr->m_pPlayer->IsPaused()); + if(m_Paused == CPlayer::PAUSE_SPEC && !pChr->m_Paused) + { + m_Paused = CPlayer::PAUSE_NONE; + } + m_NeededFaketuning = pChr->m_NeededFaketuning; - m_TeeStarted = pChr->Teams()->TeeStarted(m_ClientID); - m_TeeFinished = pChr->Teams()->TeeFinished(m_ClientID); + m_TeeStarted = pChr->Teams()->TeeStarted(m_ClientId); + m_TeeFinished = pChr->Teams()->TeeFinished(m_ClientId); m_IsSolo = pChr->m_Core.m_Solo; for(int i = 0; i < NUM_WEAPONS; i++) @@ -68,10 +75,13 @@ void CSaveTee::Save(CCharacter *pChr) m_TuneZoneOld = pChr->m_TuneZoneOld; if(pChr->m_StartTime) - m_Time = pChr->Server()->Tick() - pChr->m_StartTime + g_Config.m_SvSaveSwapGamesPenalty * pChr->Server()->TickSpeed(); + m_Time = pChr->Server()->Tick() - pChr->m_StartTime; else m_Time = 0; + if(AddPenalty && pChr->m_StartTime) + m_Time += g_Config.m_SvSaveSwapGamesPenalty * pChr->Server()->TickSpeed(); + m_Pos = pChr->m_Pos; m_PrevPos = pChr->m_PrevPos; m_TeleCheckpoint = pChr->m_TeleCheckpoint; @@ -121,8 +131,10 @@ void CSaveTee::Save(CCharacter *pChr) FormatUuid(pChr->GameServer()->GameUuid(), m_aGameUuid, sizeof(m_aGameUuid)); } -void CSaveTee::Load(CCharacter *pChr, int Team, bool IsSwap) +bool CSaveTee::Load(CCharacter *pChr, int Team, bool IsSwap) { + bool Valid = true; + pChr->m_pPlayer->Pause(m_Paused, true); pChr->m_Alive = m_Alive; @@ -130,9 +142,9 @@ void CSaveTee::Load(CCharacter *pChr, int Team, bool IsSwap) if(!IsSwap) { - pChr->Teams()->SetForceCharacterTeam(pChr->m_pPlayer->GetCID(), Team); - pChr->Teams()->SetStarted(pChr->m_pPlayer->GetCID(), m_TeeStarted); - pChr->Teams()->SetFinished(pChr->m_pPlayer->GetCID(), m_TeeFinished); + pChr->Teams()->SetForceCharacterTeam(pChr->m_pPlayer->GetCid(), Team); + pChr->Teams()->SetStarted(pChr->m_pPlayer->GetCid(), m_TeeStarted); + pChr->Teams()->SetFinished(pChr->m_pPlayer->GetCid(), m_TeeFinished); } for(int i = 0; i < NUM_WEAPONS; i++) @@ -236,8 +248,16 @@ void CSaveTee::Load(CCharacter *pChr, int Team, bool IsSwap) { // Always create a rescue tee at the exact location we loaded from so that // the old one gets overwritten. - pChr->SetRescue(); + pChr->ForceSetRescue(RESCUEMODE_AUTO); + pChr->ForceSetRescue(RESCUEMODE_MANUAL); } + + if(pChr->m_pPlayer->IsPaused() == -1 * CPlayer::PAUSE_SPEC && !pChr->m_pPlayer->CanSpec()) + { + Valid = false; + } + + return Valid; } char *CSaveTee::GetString(const CSaveTeam *pTeam) @@ -247,7 +267,7 @@ char *CSaveTee::GetString(const CSaveTeam *pTeam) { for(int n = 0; n < pTeam->GetMembersCount(); n++) { - if(m_HookedPlayer == pTeam->m_pSavedTees[n].GetClientID()) + if(m_HookedPlayer == pTeam->m_pSavedTees[n].GetClientId()) { HookedPlayer = n; break; @@ -454,7 +474,7 @@ void CSaveTee::LoadHookedPlayer(const CSaveTeam *pTeam) { if(m_HookedPlayer == -1) return; - m_HookedPlayer = pTeam->m_pSavedTees[m_HookedPlayer].GetClientID(); + m_HookedPlayer = pTeam->m_pSavedTees[m_HookedPlayer].GetClientId(); } bool CSaveTee::IsHooking() const @@ -473,25 +493,30 @@ CSaveTeam::~CSaveTeam() delete[] m_pSavedTees; } -int CSaveTeam::Save(CGameContext *pGameServer, int Team, bool Dry) +ESaveResult CSaveTeam::Save(CGameContext *pGameServer, int Team, bool Dry, bool Force) { - if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && (Team <= 0 || MAX_CLIENTS <= Team)) - return 1; + if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && (Team <= 0 || MAX_CLIENTS <= Team) && !Force) + return ESaveResult::TEAM_FLOCK; IGameController *pController = pGameServer->m_pController; CGameTeams *pTeams = &pController->Teams(); + if(pTeams->TeamFlock(Team) && !Force) + { + return ESaveResult::TEAM_0_MODE; + } + m_MembersCount = pTeams->Count(Team); - if(m_MembersCount <= 0) + if(m_MembersCount <= 0 && !Force) { - return 2; + return ESaveResult::TEAM_NOT_FOUND; } m_TeamState = pTeams->GetTeamState(Team); - if(m_TeamState != CGameTeams::TEAMSTATE_STARTED) + if(m_TeamState != CGameTeams::TEAMSTATE_STARTED && !Force) { - return 4; + return ESaveResult::NOT_STARTED; } m_HighestSwitchNumber = pGameServer->Collision()->m_HighestSwitchNumber; @@ -499,21 +524,24 @@ int CSaveTeam::Save(CGameContext *pGameServer, int Team, bool Dry) m_Practice = pTeams->IsPractice(Team); m_pSavedTees = new CSaveTee[m_MembersCount]; - int aPlayerCIDs[MAX_CLIENTS]; + int aPlayerCids[MAX_CLIENTS]; int j = 0; CCharacter *p = (CCharacter *)pGameServer->m_World.FindFirst(CGameWorld::ENTTYPE_CHARACTER); for(; p; p = (CCharacter *)p->TypeNext()) { - if(pTeams->m_Core.Team(p->GetPlayer()->GetCID()) != Team) + if(pTeams->m_Core.Team(p->GetPlayer()->GetCid()) != Team && !Force) continue; - if(m_MembersCount == j) - return 3; + if(m_MembersCount == j && !Force) + return ESaveResult::CHAR_NOT_FOUND; + ESaveResult Result = pGameServer->m_World.BlocksSave(p->GetPlayer()->GetCid()); + if(Result != ESaveResult::SUCCESS && !Force) + return Result; m_pSavedTees[j].Save(p); - aPlayerCIDs[j] = p->GetPlayer()->GetCID(); + aPlayerCids[j] = p->GetPlayer()->GetCid(); j++; } - if(m_MembersCount != j) - return 3; + if(m_MembersCount != j && !Force) + return ESaveResult::CHAR_NOT_FOUND; if(pGameServer->Collision()->m_HighestSwitchNumber) { @@ -531,37 +559,40 @@ int CSaveTeam::Save(CGameContext *pGameServer, int Team, bool Dry) } if(!Dry) { - pGameServer->m_World.RemoveEntitiesFromPlayers(aPlayerCIDs, m_MembersCount); + pGameServer->m_World.RemoveEntitiesFromPlayers(aPlayerCids, m_MembersCount); } - return 0; + return ESaveResult::SUCCESS; } -bool CSaveTeam::HandleSaveError(int Result, int ClientID, CGameContext *pGameContext) +bool CSaveTeam::HandleSaveError(ESaveResult Result, int ClientId, CGameContext *pGameContext) { switch(Result) { - case 0: + case ESaveResult::SUCCESS: return false; - case 1: - pGameContext->SendChatTarget(ClientID, "You have to be in a team (from 1-63)"); + case ESaveResult::TEAM_FLOCK: + pGameContext->SendChatTarget(ClientId, "You have to be in a team (from 1-63)"); break; - case 2: - pGameContext->SendChatTarget(ClientID, "Could not find your Team"); + case ESaveResult::TEAM_NOT_FOUND: + pGameContext->SendChatTarget(ClientId, "Could not find your Team"); break; - case 3: - pGameContext->SendChatTarget(ClientID, "To save all players in your team have to be alive and not in '/spec'"); + case ESaveResult::CHAR_NOT_FOUND: + pGameContext->SendChatTarget(ClientId, "To save all players in your team have to be alive and not in '/spec'"); break; - case 4: - pGameContext->SendChatTarget(ClientID, "Your team has not started yet"); + case ESaveResult::NOT_STARTED: + pGameContext->SendChatTarget(ClientId, "Your team has not started yet"); break; - default: // this state should never be reached - pGameContext->SendChatTarget(ClientID, "Unknown error while saving"); + case ESaveResult::TEAM_0_MODE: + pGameContext->SendChatTarget(ClientId, "Team can't be saved while in team 0 mode"); + break; + case ESaveResult::DRAGGER_ACTIVE: + pGameContext->SendChatTarget(ClientId, "Team can't be saved while a dragger is active"); break; } return true; } -void CSaveTeam::Load(CGameContext *pGameServer, int Team, bool KeepCurrentWeakStrong) +bool CSaveTeam::Load(CGameContext *pGameServer, int Team, bool KeepCurrentWeakStrong, bool IgnorePlayers) { IGameController *pController = pGameServer->m_pController; CGameTeams *pTeams = &pController->Teams(); @@ -570,15 +601,20 @@ void CSaveTeam::Load(CGameContext *pGameServer, int Team, bool KeepCurrentWeakSt pTeams->SetTeamLock(Team, m_TeamLocked); pTeams->SetPractice(Team, m_Practice); - int aPlayerCIDs[MAX_CLIENTS]; - for(int i = m_MembersCount; i-- > 0;) + bool ContainsInvalidPlayer = false; + int aPlayerCids[MAX_CLIENTS]; + + if(!IgnorePlayers) { - int ClientID = m_pSavedTees[i].GetClientID(); - aPlayerCIDs[i] = ClientID; - if(pGameServer->m_apPlayers[ClientID] && pTeams->m_Core.Team(ClientID) == Team) + for(int i = m_MembersCount; i-- > 0;) { - CCharacter *pChr = MatchCharacter(pGameServer, m_pSavedTees[i].GetClientID(), i, KeepCurrentWeakStrong); - m_pSavedTees[i].Load(pChr, Team); + int ClientId = m_pSavedTees[i].GetClientId(); + aPlayerCids[i] = ClientId; + if(pGameServer->m_apPlayers[ClientId] && pTeams->m_Core.Team(ClientId) == Team) + { + CCharacter *pChr = MatchCharacter(pGameServer, m_pSavedTees[i].GetClientId(), i, KeepCurrentWeakStrong); + ContainsInvalidPlayer |= !m_pSavedTees[i].Load(pChr, Team); + } } } @@ -593,18 +629,21 @@ void CSaveTeam::Load(CGameContext *pGameServer, int Team, bool KeepCurrentWeakSt } } // remove projectiles and laser - pGameServer->m_World.RemoveEntitiesFromPlayers(aPlayerCIDs, m_MembersCount); + if(!IgnorePlayers) + pGameServer->m_World.RemoveEntitiesFromPlayers(aPlayerCids, m_MembersCount); + + return !ContainsInvalidPlayer; } -CCharacter *CSaveTeam::MatchCharacter(CGameContext *pGameServer, int ClientID, int SaveID, bool KeepCurrentCharacter) const +CCharacter *CSaveTeam::MatchCharacter(CGameContext *pGameServer, int ClientId, int SaveId, bool KeepCurrentCharacter) const { - if(KeepCurrentCharacter && pGameServer->m_apPlayers[ClientID]->GetCharacter()) + if(KeepCurrentCharacter && pGameServer->m_apPlayers[ClientId]->GetCharacter()) { // keep old character to retain current weak/strong order - return pGameServer->m_apPlayers[ClientID]->GetCharacter(); + return pGameServer->m_apPlayers[ClientId]->GetCharacter(); } - pGameServer->m_apPlayers[ClientID]->KillCharacter(WEAPON_GAME); - return pGameServer->m_apPlayers[ClientID]->ForceSpawn(m_pSavedTees[SaveID].GetPos()); + pGameServer->m_apPlayers[ClientId]->KillCharacter(WEAPON_GAME); + return pGameServer->m_apPlayers[ClientId]->ForceSpawn(m_pSavedTees[SaveId].GetPos()); } char *CSaveTeam::GetString() @@ -785,7 +824,7 @@ int CSaveTeam::FromString(const char *pString) return 0; } -bool CSaveTeam::MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int *pClientID, int NumPlayer, char *pMessage, int MessageLen) const +bool CSaveTeam::MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int *pClientId, int NumPlayer, char *pMessage, int MessageLen) const { if(NumPlayer > m_MembersCount) { @@ -817,7 +856,7 @@ bool CSaveTeam::MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int * { if(str_comp(m_pSavedTees[i].GetName(), paNames[j]) == 0) { - m_pSavedTees[i].SetClientID(pClientID[j]); + m_pSavedTees[i].SetClientId(pClientId[j]); Found = true; break; } @@ -828,7 +867,7 @@ bool CSaveTeam::MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int * return false; } } - // match hook to correct ClientID + // match hook to correct ClientId for(int n = 0; n < m_MembersCount; n++) m_pSavedTees[n].LoadHookedPlayer(this); return true; diff --git a/src/game/server/save.h b/src/game/server/save.h index 14abd7f373..cf35e807e1 100644 --- a/src/game/server/save.h +++ b/src/game/server/save.h @@ -12,21 +12,39 @@ class CGameWorld; class CCharacter; class CSaveTeam; +enum +{ + RESCUEMODE_AUTO = 0, + RESCUEMODE_MANUAL, + NUM_RESCUEMODES +}; + +enum class ESaveResult +{ + SUCCESS, + TEAM_FLOCK, + TEAM_NOT_FOUND, + CHAR_NOT_FOUND, + NOT_STARTED, + TEAM_0_MODE, + DRAGGER_ACTIVE +}; + class CSaveTee { public: CSaveTee(); ~CSaveTee() = default; - void Save(CCharacter *pchr); - void Load(CCharacter *pchr, int Team, bool IsSwap = false); + void Save(CCharacter *pchr, bool AddPenalty = true); + bool Load(CCharacter *pchr, int Team, bool IsSwap = false); char *GetString(const CSaveTeam *pTeam); int FromString(const char *pString); void LoadHookedPlayer(const CSaveTeam *pTeam); bool IsHooking() const; vec2 GetPos() const { return m_Pos; } const char *GetName() const { return m_aName; } - int GetClientID() const { return m_ClientID; } - void SetClientID(int ClientID) { m_ClientID = ClientID; } + int GetClientId() const { return m_ClientId; } + void SetClientId(int ClientId) { m_ClientId = ClientId; } enum { @@ -38,7 +56,7 @@ class CSaveTee }; private: - int m_ClientID; + int m_ClientId; char m_aString[2048]; char m_aName[16]; @@ -140,17 +158,17 @@ class CSaveTeam // MatchPlayers has to be called afterwards int FromString(const char *pString); // returns true if a team can load, otherwise writes a nice error Message in pMessage - bool MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int *pClientID, int NumPlayer, char *pMessage, int MessageLen) const; - int Save(CGameContext *pGameServer, int Team, bool Dry = false); - void Load(CGameContext *pGameServer, int Team, bool KeepCurrentWeakStrong); + bool MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int *pClientId, int NumPlayer, char *pMessage, int MessageLen) const; + ESaveResult Save(CGameContext *pGameServer, int Team, bool Dry = false, bool Force = false); + bool Load(CGameContext *pGameServer, int Team, bool KeepCurrentWeakStrong, bool IgnorePlayers = false); CSaveTee *m_pSavedTees = nullptr; // returns true if an error occurred - static bool HandleSaveError(int Result, int ClientID, CGameContext *pGameContext); + static bool HandleSaveError(ESaveResult Result, int ClientId, CGameContext *pGameContext); private: - CCharacter *MatchCharacter(CGameContext *pGameServer, int ClientID, int SaveID, bool KeepCurrentCharacter) const; + CCharacter *MatchCharacter(CGameContext *pGameServer, int ClientId, int SaveId, bool KeepCurrentCharacter) const; char m_aString[65536]; diff --git a/src/game/server/score.cpp b/src/game/server/score.cpp index 610ebd1531..408ab62638 100644 --- a/src/game/server/score.cpp +++ b/src/game/server/score.cpp @@ -16,9 +16,9 @@ class IDbConnection; -std::shared_ptr CScore::NewSqlPlayerResult(int ClientID) +std::shared_ptr CScore::NewSqlPlayerResult(int ClientId) { - CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientID]; + CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientId]; if(pCurPlayer->m_ScoreQueryResult != nullptr) // TODO: send player a message: "too many requests" return nullptr; pCurPlayer->m_ScoreQueryResult = std::make_shared(); @@ -28,31 +28,31 @@ std::shared_ptr CScore::NewSqlPlayerResult(int ClientID) void CScore::ExecPlayerThread( bool (*pFuncPtr)(IDbConnection *, const ISqlData *, char *pError, int ErrorSize), const char *pThreadName, - int ClientID, + int ClientId, const char *pName, int Offset) { - auto pResult = NewSqlPlayerResult(ClientID); + auto pResult = NewSqlPlayerResult(ClientId); if(pResult == nullptr) return; auto Tmp = std::make_unique(pResult); str_copy(Tmp->m_aName, pName, sizeof(Tmp->m_aName)); - str_copy(Tmp->m_aMap, g_Config.m_SvMap, sizeof(Tmp->m_aMap)); + str_copy(Tmp->m_aMap, Server()->GetMapName(), sizeof(Tmp->m_aMap)); str_copy(Tmp->m_aServer, g_Config.m_SvSqlServerName, sizeof(Tmp->m_aServer)); - str_copy(Tmp->m_aRequestingPlayer, Server()->ClientName(ClientID), sizeof(Tmp->m_aRequestingPlayer)); + str_copy(Tmp->m_aRequestingPlayer, Server()->ClientName(ClientId), sizeof(Tmp->m_aRequestingPlayer)); Tmp->m_Offset = Offset; m_pPool->Execute(pFuncPtr, std::move(Tmp), pThreadName); } -bool CScore::RateLimitPlayer(int ClientID) +bool CScore::RateLimitPlayer(int ClientId) { - CPlayer *pPlayer = GameServer()->m_apPlayers[ClientID]; + CPlayer *pPlayer = GameServer()->m_apPlayers[ClientId]; if(pPlayer == 0) return true; - if(pPlayer->m_LastSQLQuery + (int64_t)g_Config.m_SvSqlQueriesDelay * Server()->TickSpeed() >= Server()->Tick()) + if(pPlayer->m_LastSqlQuery + (int64_t)g_Config.m_SvSqlQueriesDelay * Server()->TickSpeed() >= Server()->Tick()) return true; - pPlayer->m_LastSQLQuery = Server()->Tick(); + pPlayer->m_LastSqlQuery = Server()->Tick(); return false; } @@ -79,20 +79,16 @@ CScore::CScore(CGameContext *pGameServer, CDbConnectionPool *pPool) : secure_random_fill(aSeed, sizeof(aSeed)); m_Prng.Seed(aSeed); - IOHANDLE File = GameServer()->Storage()->OpenFile("wordlist.txt", IOFLAG_READ | IOFLAG_SKIP_BOM, IStorage::TYPE_ALL); - if(File) + CLineReader LineReader; + if(LineReader.OpenFile(GameServer()->Storage()->OpenFile("wordlist.txt", IOFLAG_READ, IStorage::TYPE_ALL))) { - CLineReader LineReader; - LineReader.Init(File); - char *pLine; - while((pLine = LineReader.Get())) + while(const char *pLine = LineReader.Get()) { char aWord[32] = {0}; sscanf(pLine, "%*s %31s", aWord); aWord[31] = 0; m_vWordlist.emplace_back(aWord); } - io_close(File); } else { @@ -116,46 +112,53 @@ void CScore::LoadBestTime() auto LoadBestTimeResult = std::make_shared(); m_pGameServer->m_pController->m_pLoadBestTimeResult = LoadBestTimeResult; - auto Tmp = std::make_unique(LoadBestTimeResult); - str_copy(Tmp->m_aMap, g_Config.m_SvMap, sizeof(Tmp->m_aMap)); + auto Tmp = std::make_unique(LoadBestTimeResult); + str_copy(Tmp->m_aMap, Server()->GetMapName(), sizeof(Tmp->m_aMap)); m_pPool->Execute(CScoreWorker::LoadBestTime, std::move(Tmp), "load best time"); } -void CScore::LoadPlayerData(int ClientID, const char *pName) +void CScore::LoadPlayerData(int ClientId, const char *pName) { - ExecPlayerThread(CScoreWorker::LoadPlayerData, "load player data", ClientID, pName, 0); + ExecPlayerThread(CScoreWorker::LoadPlayerData, "load player data", ClientId, pName, 0); } -void CScore::MapVote(int ClientID, const char *pMapName) +void CScore::LoadPlayerTimeCp(int ClientId, const char *pName) { - if(RateLimitPlayer(ClientID)) + ExecPlayerThread(CScoreWorker::LoadPlayerTimeCp, "load player timecp", ClientId, pName, 0); +} + +void CScore::MapVote(int ClientId, const char *pMapName) +{ + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::MapVote, "map vote", ClientID, pMapName, 0); + ExecPlayerThread(CScoreWorker::MapVote, "map vote", ClientId, pMapName, 0); } -void CScore::MapInfo(int ClientID, const char *pMapName) +void CScore::MapInfo(int ClientId, const char *pMapName) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::MapInfo, "map info", ClientID, pMapName, 0); + ExecPlayerThread(CScoreWorker::MapInfo, "map info", ClientId, pMapName, 0); } -void CScore::SaveScore(int ClientID, float Time, const char *pTimestamp, const float aTimeCp[NUM_CHECKPOINTS], bool NotEligible) +void CScore::SaveScore(int ClientId, int TimeTicks, const char *pTimestamp, const float aTimeCp[NUM_CHECKPOINTS], bool NotEligible) { CConsole *pCon = (CConsole *)GameServer()->Console(); if(pCon->Cheated() || NotEligible) return; - CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientID]; + GameServer()->TeehistorianRecordPlayerFinish(ClientId, TimeTicks); + + CPlayer *pCurPlayer = GameServer()->m_apPlayers[ClientId]; if(pCurPlayer->m_ScoreFinishResult != nullptr) dbg_msg("sql", "WARNING: previous save score result didn't complete, overwriting it now"); pCurPlayer->m_ScoreFinishResult = std::make_shared(); auto Tmp = std::make_unique(pCurPlayer->m_ScoreFinishResult); - str_copy(Tmp->m_aMap, g_Config.m_SvMap, sizeof(Tmp->m_aMap)); + str_copy(Tmp->m_aMap, Server()->GetMapName(), sizeof(Tmp->m_aMap)); FormatUuid(GameServer()->GameUuid(), Tmp->m_aGameUuid, sizeof(Tmp->m_aGameUuid)); - Tmp->m_ClientID = ClientID; - str_copy(Tmp->m_aName, Server()->ClientName(ClientID), sizeof(Tmp->m_aName)); - Tmp->m_Time = Time; + Tmp->m_ClientId = ClientId; + str_copy(Tmp->m_aName, Server()->ClientName(ClientId), sizeof(Tmp->m_aName)); + Tmp->m_Time = (float)(TimeTicks) / (float)Server()->TickSpeed(); str_copy(Tmp->m_aTimestamp, pTimestamp, sizeof(Tmp->m_aTimestamp)); for(int i = 0; i < NUM_CHECKPOINTS; i++) Tmp->m_aCurrentTimeCp[i] = aTimeCp[i]; @@ -163,145 +166,156 @@ void CScore::SaveScore(int ClientID, float Time, const char *pTimestamp, const f m_pPool->ExecuteWrite(CScoreWorker::SaveScore, std::move(Tmp), "save score"); } -void CScore::SaveTeamScore(int *pClientIDs, unsigned int Size, float Time, const char *pTimestamp) +void CScore::SaveTeamScore(int Team, int *pClientIds, unsigned int Size, int TimeTicks, const char *pTimestamp) { CConsole *pCon = (CConsole *)GameServer()->Console(); if(pCon->Cheated()) return; for(unsigned int i = 0; i < Size; i++) { - if(GameServer()->m_apPlayers[pClientIDs[i]]->m_NotEligibleForFinish) + if(GameServer()->m_apPlayers[pClientIds[i]]->m_NotEligibleForFinish) return; } + + GameServer()->TeehistorianRecordTeamFinish(Team, TimeTicks); + auto Tmp = std::make_unique(); for(unsigned int i = 0; i < Size; i++) - str_copy(Tmp->m_aaNames[i], Server()->ClientName(pClientIDs[i]), sizeof(Tmp->m_aaNames[i])); + str_copy(Tmp->m_aaNames[i], Server()->ClientName(pClientIds[i]), sizeof(Tmp->m_aaNames[i])); Tmp->m_Size = Size; - Tmp->m_Time = Time; + Tmp->m_Time = (float)TimeTicks / (float)Server()->TickSpeed(); str_copy(Tmp->m_aTimestamp, pTimestamp, sizeof(Tmp->m_aTimestamp)); FormatUuid(GameServer()->GameUuid(), Tmp->m_aGameUuid, sizeof(Tmp->m_aGameUuid)); - str_copy(Tmp->m_aMap, g_Config.m_SvMap, sizeof(Tmp->m_aMap)); + str_copy(Tmp->m_aMap, Server()->GetMapName(), sizeof(Tmp->m_aMap)); Tmp->m_TeamrankUuid = RandomUuid(); m_pPool->ExecuteWrite(CScoreWorker::SaveTeamScore, std::move(Tmp), "save team score"); } -void CScore::ShowRank(int ClientID, const char *pName) +void CScore::ShowRank(int ClientId, const char *pName) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::ShowRank, "show rank", ClientID, pName, 0); + ExecPlayerThread(CScoreWorker::ShowRank, "show rank", ClientId, pName, 0); } -void CScore::ShowTeamRank(int ClientID, const char *pName) +void CScore::ShowTeamRank(int ClientId, const char *pName) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::ShowTeamRank, "show team rank", ClientID, pName, 0); + ExecPlayerThread(CScoreWorker::ShowTeamRank, "show team rank", ClientId, pName, 0); } -void CScore::ShowTop(int ClientID, int Offset) +void CScore::ShowTop(int ClientId, int Offset) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::ShowTop, "show top5", ClientID, "", Offset); + ExecPlayerThread(CScoreWorker::ShowTop, "show top5", ClientId, "", Offset); } -void CScore::ShowTeamTop5(int ClientID, int Offset) +void CScore::ShowTeamTop5(int ClientId, int Offset) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::ShowTeamTop5, "show team top5", ClientID, "", Offset); + ExecPlayerThread(CScoreWorker::ShowTeamTop5, "show team top5", ClientId, "", Offset); } -void CScore::ShowPlayerTeamTop5(int ClientID, const char *pName, int Offset) +void CScore::ShowPlayerTeamTop5(int ClientId, const char *pName, int Offset) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::ShowPlayerTeamTop5, "show team top5 player", ClientID, pName, Offset); + ExecPlayerThread(CScoreWorker::ShowPlayerTeamTop5, "show team top5 player", ClientId, pName, Offset); } -void CScore::ShowTimes(int ClientID, int Offset) +void CScore::ShowTimes(int ClientId, int Offset) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::ShowTimes, "show times", ClientID, "", Offset); + ExecPlayerThread(CScoreWorker::ShowTimes, "show times", ClientId, "", Offset); } -void CScore::ShowTimes(int ClientID, const char *pName, int Offset) +void CScore::ShowTimes(int ClientId, const char *pName, int Offset) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::ShowTimes, "show times", ClientID, pName, Offset); + ExecPlayerThread(CScoreWorker::ShowTimes, "show times", ClientId, pName, Offset); } -void CScore::ShowPoints(int ClientID, const char *pName) +void CScore::ShowPoints(int ClientId, const char *pName) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::ShowPoints, "show points", ClientID, pName, 0); + ExecPlayerThread(CScoreWorker::ShowPoints, "show points", ClientId, pName, 0); } -void CScore::ShowTopPoints(int ClientID, int Offset) +void CScore::ShowTopPoints(int ClientId, int Offset) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::ShowTopPoints, "show top points", ClientID, "", Offset); + ExecPlayerThread(CScoreWorker::ShowTopPoints, "show top points", ClientId, "", Offset); } -void CScore::RandomMap(int ClientID, int Stars) +void CScore::RandomMap(int ClientId, int Stars) { - auto pResult = std::make_shared(ClientID); + auto pResult = std::make_shared(ClientId); GameServer()->m_SqlRandomMapResult = pResult; auto Tmp = std::make_unique(pResult); Tmp->m_Stars = Stars; - str_copy(Tmp->m_aCurrentMap, g_Config.m_SvMap, sizeof(Tmp->m_aCurrentMap)); + str_copy(Tmp->m_aCurrentMap, Server()->GetMapName(), sizeof(Tmp->m_aCurrentMap)); str_copy(Tmp->m_aServerType, g_Config.m_SvServerType, sizeof(Tmp->m_aServerType)); - str_copy(Tmp->m_aRequestingPlayer, GameServer()->Server()->ClientName(ClientID), sizeof(Tmp->m_aRequestingPlayer)); + str_copy(Tmp->m_aRequestingPlayer, GameServer()->Server()->ClientName(ClientId), sizeof(Tmp->m_aRequestingPlayer)); m_pPool->Execute(CScoreWorker::RandomMap, std::move(Tmp), "random map"); } -void CScore::RandomUnfinishedMap(int ClientID, int Stars) +void CScore::RandomUnfinishedMap(int ClientId, int Stars) { - auto pResult = std::make_shared(ClientID); + auto pResult = std::make_shared(ClientId); GameServer()->m_SqlRandomMapResult = pResult; auto Tmp = std::make_unique(pResult); Tmp->m_Stars = Stars; - str_copy(Tmp->m_aCurrentMap, g_Config.m_SvMap, sizeof(Tmp->m_aCurrentMap)); + str_copy(Tmp->m_aCurrentMap, Server()->GetMapName(), sizeof(Tmp->m_aCurrentMap)); str_copy(Tmp->m_aServerType, g_Config.m_SvServerType, sizeof(Tmp->m_aServerType)); - str_copy(Tmp->m_aRequestingPlayer, GameServer()->Server()->ClientName(ClientID), sizeof(Tmp->m_aRequestingPlayer)); + str_copy(Tmp->m_aRequestingPlayer, GameServer()->Server()->ClientName(ClientId), sizeof(Tmp->m_aRequestingPlayer)); m_pPool->Execute(CScoreWorker::RandomUnfinishedMap, std::move(Tmp), "random unfinished map"); } -void CScore::SaveTeam(int ClientID, const char *pCode, const char *pServer) +void CScore::SaveTeam(int ClientId, const char *pCode, const char *pServer) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; auto *pController = GameServer()->m_pController; - int Team = pController->Teams().m_Core.Team(ClientID); + int Team = pController->Teams().m_Core.Team(ClientId); + char aBuf[512]; if(pController->Teams().GetSaving(Team)) + { + GameServer()->SendChatTarget(ClientId, "Team save already in progress"); + return; + } + if(pController->Teams().IsPractice(Team)) + { + GameServer()->SendChatTarget(ClientId, "Team save disabled for teams in practice mode"); return; + } - auto SaveResult = std::make_shared(ClientID); - SaveResult->m_SaveID = RandomUuid(); - int Result = SaveResult->m_SavedTeam.Save(GameServer(), Team); - if(CSaveTeam::HandleSaveError(Result, ClientID, GameServer())) + auto SaveResult = std::make_shared(ClientId); + SaveResult->m_SaveId = RandomUuid(); + ESaveResult Result = SaveResult->m_SavedTeam.Save(GameServer(), Team); + if(CSaveTeam::HandleSaveError(Result, ClientId, GameServer())) return; pController->Teams().SetSaving(Team, SaveResult); - auto Tmp = std::make_unique(SaveResult); + auto Tmp = std::make_unique(SaveResult); str_copy(Tmp->m_aCode, pCode, sizeof(Tmp->m_aCode)); - str_copy(Tmp->m_aMap, g_Config.m_SvMap, sizeof(Tmp->m_aMap)); + str_copy(Tmp->m_aMap, Server()->GetMapName(), sizeof(Tmp->m_aMap)); str_copy(Tmp->m_aServer, pServer, sizeof(Tmp->m_aServer)); - str_copy(Tmp->m_aClientName, this->Server()->ClientName(ClientID), sizeof(Tmp->m_aClientName)); + str_copy(Tmp->m_aClientName, this->Server()->ClientName(ClientId), sizeof(Tmp->m_aClientName)); Tmp->m_aGeneratedCode[0] = '\0'; GeneratePassphrase(Tmp->m_aGeneratedCode, sizeof(Tmp->m_aGeneratedCode)); - char aBuf[512]; if(Tmp->m_aCode[0] == '\0') { str_format(aBuf, @@ -317,37 +331,49 @@ void CScore::SaveTeam(int ClientID, const char *pCode, const char *pServer) Tmp->m_aCode, Tmp->m_aGeneratedCode); } - pController->Teams().KillSavedTeam(ClientID, Team); + pController->Teams().KillSavedTeam(ClientId, Team); GameServer()->SendChatTeam(Team, aBuf); m_pPool->ExecuteWrite(CScoreWorker::SaveTeam, std::move(Tmp), "save team"); } -void CScore::LoadTeam(const char *pCode, int ClientID) +void CScore::LoadTeam(const char *pCode, int ClientId) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; auto *pController = GameServer()->m_pController; - int Team = pController->Teams().m_Core.Team(ClientID); + int Team = pController->Teams().m_Core.Team(ClientId); if(pController->Teams().GetSaving(Team)) + { + GameServer()->SendChatTarget(ClientId, "Team load already in progress"); return; + } if(Team < TEAM_FLOCK || Team >= MAX_CLIENTS || (g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && Team == TEAM_FLOCK)) { - GameServer()->SendChatTarget(ClientID, "You have to be in a team (from 1-63)"); + GameServer()->SendChatTarget(ClientId, "You have to be in a team (from 1-63)"); return; } if(pController->Teams().GetTeamState(Team) != CGameTeams::TEAMSTATE_OPEN) { - GameServer()->SendChatTarget(ClientID, "Team can't be loaded while racing"); + GameServer()->SendChatTarget(ClientId, "Team can't be loaded while racing"); + return; + } + if(pController->Teams().TeamFlock(Team)) + { + GameServer()->SendChatTarget(ClientId, "Team can't be loaded while in team 0 mode"); + return; + } + if(pController->Teams().IsPractice(Team)) + { + GameServer()->SendChatTarget(ClientId, "Team can't be loaded while practice is enabled"); return; } - auto SaveResult = std::make_shared(ClientID); + auto SaveResult = std::make_shared(ClientId); SaveResult->m_Status = CScoreSaveResult::LOAD_FAILED; pController->Teams().SetSaving(Team, SaveResult); - auto Tmp = std::make_unique(SaveResult); + auto Tmp = std::make_unique(SaveResult); str_copy(Tmp->m_aCode, pCode, sizeof(Tmp->m_aCode)); - str_copy(Tmp->m_aMap, g_Config.m_SvMap, sizeof(Tmp->m_aMap)); - Tmp->m_ClientID = ClientID; - str_copy(Tmp->m_aRequestingPlayer, Server()->ClientName(ClientID), sizeof(Tmp->m_aRequestingPlayer)); + str_copy(Tmp->m_aMap, Server()->GetMapName(), sizeof(Tmp->m_aMap)); + str_copy(Tmp->m_aRequestingPlayer, Server()->ClientName(ClientId), sizeof(Tmp->m_aRequestingPlayer)); Tmp->m_NumPlayer = 0; for(int i = 0; i < MAX_CLIENTS; i++) { @@ -355,16 +381,16 @@ void CScore::LoadTeam(const char *pCode, int ClientID) { // put all names at the beginning of the array str_copy(Tmp->m_aClientNames[Tmp->m_NumPlayer], Server()->ClientName(i), sizeof(Tmp->m_aClientNames[Tmp->m_NumPlayer])); - Tmp->m_aClientID[Tmp->m_NumPlayer] = i; + Tmp->m_aClientId[Tmp->m_NumPlayer] = i; Tmp->m_NumPlayer++; } } m_pPool->ExecuteWrite(CScoreWorker::LoadTeam, std::move(Tmp), "load team"); } -void CScore::GetSaves(int ClientID) +void CScore::GetSaves(int ClientId) { - if(RateLimitPlayer(ClientID)) + if(RateLimitPlayer(ClientId)) return; - ExecPlayerThread(CScoreWorker::GetSaves, "get saves", ClientID, "", 0); + ExecPlayerThread(CScoreWorker::GetSaves, "get saves", ClientId, "", 0); } diff --git a/src/game/server/score.h b/src/game/server/score.h index 84380068bf..d3d4c7dc45 100644 --- a/src/game/server/score.h +++ b/src/game/server/score.h @@ -26,51 +26,52 @@ class CScore void GeneratePassphrase(char *pBuf, int BufSize); // returns new SqlResult bound to the player, if no current Thread is active for this player - std::shared_ptr NewSqlPlayerResult(int ClientID); + std::shared_ptr NewSqlPlayerResult(int ClientId); // Creates for player database requests void ExecPlayerThread( bool (*pFuncPtr)(IDbConnection *, const ISqlData *, char *pError, int ErrorSize), const char *pThreadName, - int ClientID, + int ClientId, const char *pName, int Offset); // returns true if the player should be rate limited - bool RateLimitPlayer(int ClientID); + bool RateLimitPlayer(int ClientId); public: CScore(CGameContext *pGameServer, CDbConnectionPool *pPool); ~CScore() {} - CPlayerData *PlayerData(int ID) { return &m_aPlayerData[ID]; } + CPlayerData *PlayerData(int Id) { return &m_aPlayerData[Id]; } void LoadBestTime(); - void MapInfo(int ClientID, const char *pMapName); - void MapVote(int ClientID, const char *pMapName); - void LoadPlayerData(int ClientID, const char *pName = ""); - void SaveScore(int ClientID, float Time, const char *pTimestamp, const float aTimeCp[NUM_CHECKPOINTS], bool NotEligible); + void MapInfo(int ClientId, const char *pMapName); + void MapVote(int ClientId, const char *pMapName); + void LoadPlayerData(int ClientId, const char *pName = ""); + void LoadPlayerTimeCp(int ClientId, const char *pName = ""); + void SaveScore(int ClientId, int TimeTicks, const char *pTimestamp, const float aTimeCp[NUM_CHECKPOINTS], bool NotEligible); - void SaveTeamScore(int *pClientIDs, unsigned int Size, float Time, const char *pTimestamp); + void SaveTeamScore(int Team, int *pClientIds, unsigned int Size, int TimeTicks, const char *pTimestamp); - void ShowTop(int ClientID, int Offset = 1); - void ShowRank(int ClientID, const char *pName); + void ShowTop(int ClientId, int Offset = 1); + void ShowRank(int ClientId, const char *pName); - void ShowTeamTop5(int ClientID, int Offset = 1); - void ShowPlayerTeamTop5(int ClientID, const char *pName, int Offset = 1); - void ShowTeamRank(int ClientID, const char *pName); + void ShowTeamTop5(int ClientId, int Offset = 1); + void ShowPlayerTeamTop5(int ClientId, const char *pName, int Offset = 1); + void ShowTeamRank(int ClientId, const char *pName); - void ShowTopPoints(int ClientID, int Offset = 1); - void ShowPoints(int ClientID, const char *pName); + void ShowTopPoints(int ClientId, int Offset = 1); + void ShowPoints(int ClientId, const char *pName); - void ShowTimes(int ClientID, const char *pName, int Offset = 1); - void ShowTimes(int ClientID, int Offset = 1); + void ShowTimes(int ClientId, const char *pName, int Offset = 1); + void ShowTimes(int ClientId, int Offset = 1); - void RandomMap(int ClientID, int Stars); - void RandomUnfinishedMap(int ClientID, int Stars); + void RandomMap(int ClientId, int Stars); + void RandomUnfinishedMap(int ClientId, int Stars); - void SaveTeam(int ClientID, const char *pCode, const char *pServer); - void LoadTeam(const char *pCode, int ClientID); - void GetSaves(int ClientID); + void SaveTeam(int ClientId, const char *pCode, const char *pServer); + void LoadTeam(const char *pCode, int ClientId); + void GetSaves(int ClientId); }; #endif // GAME_SERVER_SCORE_H diff --git a/src/game/server/scoreworker.cpp b/src/game/server/scoreworker.cpp index 393f1ca8e6..76d85f5a56 100644 --- a/src/game/server/scoreworker.cpp +++ b/src/game/server/scoreworker.cpp @@ -43,6 +43,13 @@ void CScorePlayerResult::SetVariant(Variant v) m_Data.m_Info.m_Time.reset(); for(float &TimeCp : m_Data.m_Info.m_aTimeCp) TimeCp = 0; + break; + case PLAYER_TIMECP: + m_Data.m_Info.m_aRequestedPlayer[0] = '\0'; + m_Data.m_Info.m_Time.reset(); + for(float &TimeCp : m_Data.m_Info.m_aTimeCp) + TimeCp = 0; + break; } } @@ -51,20 +58,20 @@ CTeamrank::CTeamrank() : { for(auto &aName : m_aaNames) aName[0] = '\0'; - mem_zero(&m_TeamID.m_aData, sizeof(m_TeamID)); + mem_zero(&m_TeamId.m_aData, sizeof(m_TeamId)); } bool CTeamrank::NextSqlResult(IDbConnection *pSqlServer, bool *pEnd, char *pError, int ErrorSize) { - pSqlServer->GetBlob(1, m_TeamID.m_aData, sizeof(m_TeamID.m_aData)); + pSqlServer->GetBlob(1, m_TeamId.m_aData, sizeof(m_TeamId.m_aData)); pSqlServer->GetString(2, m_aaNames[0], sizeof(m_aaNames[0])); m_NumNames = 1; bool End = false; while(!pSqlServer->Step(&End, pError, ErrorSize) && !End) { - CUuid TeamID; - pSqlServer->GetBlob(1, TeamID.m_aData, sizeof(TeamID.m_aData)); - if(m_TeamID != TeamID) + CUuid TeamId; + pSqlServer->GetBlob(1, TeamId.m_aData, sizeof(TeamId.m_aData)); + if(m_TeamId != TeamId) { *pEnd = false; return false; @@ -94,7 +101,7 @@ bool CTeamrank::SamePlayers(const std::vector *pvSortedNames) bool CScoreWorker::LoadBestTime(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize) { - const auto *pData = dynamic_cast(pGameData); + const auto *pData = dynamic_cast(pGameData); auto *pResult = dynamic_cast(pGameData->m_pResult.get()); char aBuf[512]; @@ -203,6 +210,56 @@ bool CScoreWorker::LoadPlayerData(IDbConnection *pSqlServer, const ISqlData *pGa return false; } +bool CScoreWorker::LoadPlayerTimeCp(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize) +{ + const auto *pData = dynamic_cast(pGameData); + auto *pResult = dynamic_cast(pGameData->m_pResult.get()); + auto *paMessages = pResult->m_Data.m_aaMessages; + + char aBuf[1024]; + str_format(aBuf, sizeof(aBuf), + "SELECT" + " Time, cp1, cp2, cp3, cp4, cp5, cp6, cp7, cp8, cp9, cp10, cp11, cp12, cp13, " + " cp14, cp15, cp16, cp17, cp18, cp19, cp20, cp21, cp22, cp23, cp24, cp25 " + "FROM %s_race " + "WHERE Map = ? AND Name = ? AND " + " (cp1 + cp2 + cp3 + cp4 + cp5 + cp6 + cp7 + cp8 + cp9 + cp10 + cp11 + cp12 + cp13 + cp14 + " + " cp15 + cp16 + cp17 + cp18 + cp19 + cp20 + cp21 + cp22 + cp23 + cp24 + cp25) > 0 " + "ORDER BY Time ASC " + "LIMIT 1", + pSqlServer->GetPrefix()); + if(pSqlServer->PrepareStatement(aBuf, pError, ErrorSize)) + { + return true; + } + + const char *pPlayer = pData->m_aName[0] != '\0' ? pData->m_aName : pData->m_aRequestingPlayer; + pSqlServer->BindString(1, pData->m_aMap); + pSqlServer->BindString(2, pPlayer); + + bool End; + if(pSqlServer->Step(&End, pError, ErrorSize)) + { + return true; + } + if(!End) + { + pResult->SetVariant(CScorePlayerResult::PLAYER_TIMECP); + pResult->m_Data.m_Info.m_Time = pSqlServer->GetFloat(1); + for(int i = 0; i < NUM_CHECKPOINTS; i++) + { + pResult->m_Data.m_Info.m_aTimeCp[i] = pSqlServer->GetFloat(i + 2); + } + str_copy(pResult->m_Data.m_Info.m_aRequestedPlayer, pPlayer, sizeof(pResult->m_Data.m_Info.m_aRequestedPlayer)); + } + else + { + pResult->SetVariant(CScorePlayerResult::DIRECT); + str_format(paMessages[0], sizeof(paMessages[0]), "'%s' has no checkpoint times available", pPlayer); + } + return false; +} + bool CScoreWorker::MapVote(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize) { const auto *pData = dynamic_cast(pGameData); @@ -517,7 +574,7 @@ bool CScoreWorker::SaveScore(IDbConnection *pSqlServer, const ISqlData *pGameDat " Map, Name, Timestamp, Time, Server, " " cp1, cp2, cp3, cp4, cp5, cp6, cp7, cp8, cp9, cp10, cp11, cp12, cp13, " " cp14, cp15, cp16, cp17, cp18, cp19, cp20, cp21, cp22, cp23, cp24, cp25, " - " GameID, DDNet7) " + " GameId, DDNet7) " "VALUES (?, ?, %s, %.2f, ?, " " %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, " " %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, %.2f, " @@ -558,7 +615,7 @@ bool CScoreWorker::SaveTeamScore(IDbConnection *pSqlServer, const ISqlData *pGam if(w == Write::NORMAL_SUCCEEDED) { str_format(aBuf, sizeof(aBuf), - "DELETE FROM %s_teamrace_backup WHERE ID=?", + "DELETE FROM %s_teamrace_backup WHERE Id=?", pSqlServer->GetPrefix()); if(pSqlServer->PrepareStatement(aBuf, pError, ErrorSize)) { @@ -586,7 +643,7 @@ bool CScoreWorker::SaveTeamScore(IDbConnection *pSqlServer, const ISqlData *pGam CUuid TeamrankId = pData->m_TeamrankUuid; str_format(aBuf, sizeof(aBuf), - "INSERT INTO %s_teamrace SELECT * FROM %s_teamrace_backup WHERE ID=?", + "INSERT INTO %s_teamrace SELECT * FROM %s_teamrace_backup WHERE Id=?", pSqlServer->GetPrefix(), pSqlServer->GetPrefix()); if(pSqlServer->PrepareStatement(aBuf, pError, ErrorSize)) { @@ -600,7 +657,7 @@ bool CScoreWorker::SaveTeamScore(IDbConnection *pSqlServer, const ISqlData *pGam } str_format(aBuf, sizeof(aBuf), - "DELETE FROM %s_teamrace_backup WHERE ID=?", + "DELETE FROM %s_teamrace_backup WHERE Id=?", pSqlServer->GetPrefix()); if(pSqlServer->PrepareStatement(aBuf, pError, ErrorSize)) { @@ -620,13 +677,13 @@ bool CScoreWorker::SaveTeamScore(IDbConnection *pSqlServer, const ISqlData *pGam std::sort(vNames.begin(), vNames.end()); str_format(aBuf, sizeof(aBuf), - "SELECT l.ID, Name, Time " + "SELECT l.Id, Name, Time " "FROM (" // preselect teams with first name in team " SELECT ID " " FROM %s_teamrace " " WHERE Map = ? AND Name = ? AND DDNet7 = %s" - ") as l INNER JOIN %s_teamrace AS r ON l.ID = r.ID " - "ORDER BY l.ID, Name COLLATE %s", + ") as l INNER JOIN %s_teamrace AS r ON l.Id = r.Id " + "ORDER BY l.Id, Name COLLATE %s", pSqlServer->GetPrefix(), pSqlServer->False(), pSqlServer->GetPrefix(), pSqlServer->BinaryCollate()); if(pSqlServer->PrepareStatement(aBuf, pError, ErrorSize)) { @@ -666,7 +723,7 @@ bool CScoreWorker::SaveTeamScore(IDbConnection *pSqlServer, const ISqlData *pGam if(pData->m_Time < Time) { str_format(aBuf, sizeof(aBuf), - "UPDATE %s_teamrace SET Time=%.2f, Timestamp=%s, DDNet7=%s, GameID=? WHERE ID = ?", + "UPDATE %s_teamrace SET Time=%.2f, Timestamp=%s, DDNet7=%s, GameId=? WHERE Id = ?", pSqlServer->GetPrefix(), pData->m_Time, pSqlServer->InsertTimestampAsUtc(), pSqlServer->False()); if(pSqlServer->PrepareStatement(aBuf, pError, ErrorSize)) { @@ -674,7 +731,7 @@ bool CScoreWorker::SaveTeamScore(IDbConnection *pSqlServer, const ISqlData *pGam } pSqlServer->BindString(1, pData->m_aTimestamp); pSqlServer->BindString(2, pData->m_aGameUuid); - pSqlServer->BindBlob(3, Teamrank.m_TeamID.m_aData, sizeof(Teamrank.m_TeamID.m_aData)); + pSqlServer->BindBlob(3, Teamrank.m_TeamId.m_aData, sizeof(Teamrank.m_TeamId.m_aData)); pSqlServer->Print(); int NumUpdated; if(pSqlServer->ExecuteUpdate(&NumUpdated, pError, ErrorSize)) @@ -692,7 +749,7 @@ bool CScoreWorker::SaveTeamScore(IDbConnection *pSqlServer, const ISqlData *pGam { // if no entry found... create a new one str_format(aBuf, sizeof(aBuf), - "%s INTO %s_teamrace%s(Map, Name, Timestamp, Time, ID, GameID, DDNet7) " + "%s INTO %s_teamrace%s(Map, Name, Timestamp, Time, Id, GameId, DDNet7) " "VALUES (?, ?, %s, %.2f, ?, ?, %s)", pSqlServer->InsertIgnore(), pSqlServer->GetPrefix(), w == Write::NORMAL ? "" : "_backup", @@ -839,9 +896,9 @@ bool CScoreWorker::ShowTeamRank(IDbConnection *pSqlServer, const ISqlData *pGame char aBuf[2400]; str_format(aBuf, sizeof(aBuf), - "SELECT l.ID, Name, Time, Ranking, PercentRank " + "SELECT l.Id, Name, Time, Ranking, PercentRank " "FROM (" // teamrank score board - " SELECT RANK() OVER w AS Ranking, PERCENT_RANK() OVER w AS PercentRank, ID " + " SELECT RANK() OVER w AS Ranking, PERCENT_RANK() OVER w AS PercentRank, Id " " FROM %s_teamrace " " WHERE Map = ? " " GROUP BY ID " @@ -852,8 +909,8 @@ bool CScoreWorker::ShowTeamRank(IDbConnection *pSqlServer, const ISqlData *pGame " WHERE Map = ? AND Name = ? " " ORDER BY Time " " LIMIT 1" - ") AS l ON TeamRank.ID = l.ID " - "INNER JOIN %s_teamrace AS r ON l.ID = r.ID ", + ") AS l ON TeamRank.Id = l.Id " + "INNER JOIN %s_teamrace AS r ON l.Id = r.Id ", pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pSqlServer->GetPrefix()); if(pSqlServer->PrepareStatement(aBuf, pError, ErrorSize)) { @@ -971,7 +1028,7 @@ bool CScoreWorker::ShowTop(IDbConnection *pSqlServer, const ISqlData *pGameData, if(!g_Config.m_SvRegionalRankings) { - str_copy(pResult->m_Data.m_aaMessages[Line], "----------------------------------------", sizeof(pResult->m_Data.m_aaMessages[Line])); + str_copy(pResult->m_Data.m_aaMessages[Line], "-----------------------------------------", sizeof(pResult->m_Data.m_aaMessages[Line])); return !End; } @@ -1021,9 +1078,9 @@ bool CScoreWorker::ShowTeamTop5(IDbConnection *pSqlServer, const ISqlData *pGame str_format(aBuf, sizeof(aBuf), "SELECT Name, Time, Ranking, TeamSize " "FROM (" // limit to 5 - " SELECT TeamSize, Ranking, ID " + " SELECT TeamSize, Ranking, Id " " FROM (" // teamrank score board - " SELECT RANK() OVER w AS Ranking, COUNT(*) AS Teamsize, ID " + " SELECT RANK() OVER w AS Ranking, COUNT(*) AS Teamsize, Id " " FROM %s_teamrace " " WHERE Map = ? " " GROUP BY ID " @@ -1032,8 +1089,8 @@ bool CScoreWorker::ShowTeamTop5(IDbConnection *pSqlServer, const ISqlData *pGame " ORDER BY Ranking %s " " LIMIT %d, 5" ") as l2 " - "INNER JOIN %s_teamrace as r ON l2.ID = r.ID " - "ORDER BY Ranking %s, r.ID, Name ASC", + "INNER JOIN %s_teamrace as r ON l2.Id = r.Id " + "ORDER BY Ranking %s, r.Id, Name ASC", pSqlServer->GetPrefix(), pOrder, LimitStart, pSqlServer->GetPrefix(), pOrder); if(pSqlServer->PrepareStatement(aBuf, pError, ErrorSize)) { @@ -1089,7 +1146,7 @@ bool CScoreWorker::ShowTeamTop5(IDbConnection *pSqlServer, const ISqlData *pGame } } - str_copy(paMessages[Line], "-------------------------------", sizeof(paMessages[Line])); + str_copy(paMessages[Line], "---------------------------------", sizeof(paMessages[Line])); return false; } @@ -1106,9 +1163,9 @@ bool CScoreWorker::ShowPlayerTeamTop5(IDbConnection *pSqlServer, const ISqlData char aBuf[2400]; str_format(aBuf, sizeof(aBuf), - "SELECT l.ID, Name, Time, Ranking " + "SELECT l.Id, Name, Time, Ranking " "FROM (" // teamrank score board - " SELECT RANK() OVER w AS Ranking, ID " + " SELECT RANK() OVER w AS Ranking, Id " " FROM %s_teamrace " " WHERE Map = ? " " GROUP BY ID " @@ -1119,9 +1176,9 @@ bool CScoreWorker::ShowPlayerTeamTop5(IDbConnection *pSqlServer, const ISqlData " WHERE Map = ? AND Name = ? " " ORDER BY Time %s " " LIMIT %d, 5 " - ") AS l ON TeamRank.ID = l.ID " - "INNER JOIN %s_teamrace AS r ON l.ID = r.ID " - "ORDER BY Time %s, l.ID, Name ASC", + ") AS l ON TeamRank.Id = l.Id " + "INNER JOIN %s_teamrace AS r ON l.Id = r.Id " + "ORDER BY Time %s, l.Id, Name ASC", pSqlServer->GetPrefix(), pSqlServer->GetPrefix(), pOrder, LimitStart, pSqlServer->GetPrefix(), pOrder); if(pSqlServer->PrepareStatement(aBuf, pError, ErrorSize)) { @@ -1173,7 +1230,7 @@ bool CScoreWorker::ShowPlayerTeamTop5(IDbConnection *pSqlServer, const ISqlData break; } } - str_copy(paMessages[Line], "-------------------------------", sizeof(paMessages[Line])); + str_copy(paMessages[Line], "---------------------------------", sizeof(paMessages[Line])); } else { @@ -1295,7 +1352,7 @@ bool CScoreWorker::ShowTimes(IDbConnection *pSqlServer, const ISqlData *pGameDat { return true; } - str_copy(paMessages[Line], "----------------------------------------------------", sizeof(paMessages[Line])); + str_copy(paMessages[Line], "-------------------------------------------", sizeof(paMessages[Line])); return false; } @@ -1503,14 +1560,15 @@ bool CScoreWorker::RandomUnfinishedMap(IDbConnection *pSqlServer, const ISqlData } else { - str_copy(pResult->m_aMessage, "You have no more unfinished maps on this server!", sizeof(pResult->m_aMessage)); + str_format(aBuf, sizeof(aBuf), "%s has no more unfinished maps on this server!", pData->m_aRequestingPlayer); + str_copy(pResult->m_aMessage, aBuf, sizeof(pResult->m_aMessage)); } return false; } bool CScoreWorker::SaveTeam(IDbConnection *pSqlServer, const ISqlData *pGameData, Write w, char *pError, int ErrorSize) { - const auto *pData = dynamic_cast(pGameData); + const auto *pData = dynamic_cast(pGameData); auto *pResult = dynamic_cast(pGameData->m_pResult.get()); if(w == Write::NORMAL_SUCCEEDED) @@ -1558,8 +1616,8 @@ bool CScoreWorker::SaveTeam(IDbConnection *pSqlServer, const ISqlData *pGameData return pSqlServer->Step(&End, pError, ErrorSize); } - char aSaveID[UUID_MAXSTRSIZE]; - FormatUuid(pResult->m_SaveID, aSaveID, UUID_MAXSTRSIZE); + char aSaveId[UUID_MAXSTRSIZE]; + FormatUuid(pResult->m_SaveId, aSaveId, UUID_MAXSTRSIZE); char *pSaveState = pResult->m_SavedTeam.GetString(); char aBuf[65536]; @@ -1579,7 +1637,7 @@ bool CScoreWorker::SaveTeam(IDbConnection *pSqlServer, const ISqlData *pGameData str_copy(aCode, pData->m_aCode, sizeof(aCode)); str_format(aBuf, sizeof(aBuf), - "%s INTO %s_saves%s(Savegame, Map, Code, Timestamp, Server, SaveID, DDNet7) " + "%s INTO %s_saves%s(Savegame, Map, Code, Timestamp, Server, SaveId, DDNet7) " "VALUES (?, ?, ?, CURRENT_TIMESTAMP, ?, ?, %s)", pSqlServer->InsertIgnore(), pSqlServer->GetPrefix(), w == Write::NORMAL ? "" : "_backup", pSqlServer->False()); @@ -1591,7 +1649,7 @@ bool CScoreWorker::SaveTeam(IDbConnection *pSqlServer, const ISqlData *pGameData pSqlServer->BindString(2, pData->m_aMap); pSqlServer->BindString(3, aCode); pSqlServer->BindString(4, pData->m_aServer); - pSqlServer->BindString(5, aSaveID); + pSqlServer->BindString(5, aSaveId); pSqlServer->Print(); int NumInserted; if(pSqlServer->ExecuteUpdate(&NumInserted, pError, ErrorSize)) @@ -1657,7 +1715,7 @@ bool CScoreWorker::LoadTeam(IDbConnection *pSqlServer, const ISqlData *pGameData { if(w == Write::NORMAL_SUCCEEDED || w == Write::BACKUP_FIRST) return false; - const auto *pData = dynamic_cast(pGameData); + const auto *pData = dynamic_cast(pGameData); auto *pResult = dynamic_cast(pGameData->m_pResult.get()); pResult->m_Status = CScoreSaveResult::LOAD_FAILED; @@ -1668,7 +1726,7 @@ bool CScoreWorker::LoadTeam(IDbConnection *pSqlServer, const ISqlData *pGameData char aBuf[512]; str_format(aBuf, sizeof(aBuf), - "SELECT Savegame, %s-%s AS Ago, SaveID " + "SELECT Savegame, %s-%s AS Ago, SaveId " "FROM %s_saves " "where Code = ? AND Map = ? AND DDNet7 = %s", aCurrentTimestamp, aTimestamp, @@ -1691,14 +1749,14 @@ bool CScoreWorker::LoadTeam(IDbConnection *pSqlServer, const ISqlData *pGameData return false; } - pResult->m_SaveID = UUID_NO_SAVE_ID; + pResult->m_SaveId = UUID_NO_SAVE_ID; if(!pSqlServer->IsNull(3)) { - char aSaveID[UUID_MAXSTRSIZE]; - pSqlServer->GetString(3, aSaveID, sizeof(aSaveID)); - if(ParseUuid(&pResult->m_SaveID, aSaveID) || pResult->m_SaveID == UUID_NO_SAVE_ID) + char aSaveId[UUID_MAXSTRSIZE]; + pSqlServer->GetString(3, aSaveId, sizeof(aSaveId)); + if(ParseUuid(&pResult->m_SaveId, aSaveId) || pResult->m_SaveId == UUID_NO_SAVE_ID) { - str_copy(pResult->m_aMessage, "Unable to load savegame: SaveID corrupted", sizeof(pResult->m_aMessage)); + str_copy(pResult->m_aMessage, "Unable to load savegame: SaveId corrupted", sizeof(pResult->m_aMessage)); return false; } } @@ -1742,7 +1800,7 @@ bool CScoreWorker::LoadTeam(IDbConnection *pSqlServer, const ISqlData *pGameData } bool CanLoad = pResult->m_SavedTeam.MatchPlayers( - pData->m_aClientNames, pData->m_aClientID, pData->m_NumPlayer, + pData->m_aClientNames, pData->m_aClientId, pData->m_NumPlayer, pResult->m_aMessage, sizeof(pResult->m_aMessage)); if(!CanLoad) @@ -1750,9 +1808,9 @@ bool CScoreWorker::LoadTeam(IDbConnection *pSqlServer, const ISqlData *pGameData str_format(aBuf, sizeof(aBuf), "DELETE FROM %s_saves " - "WHERE Code = ? AND Map = ? AND SaveID %s", + "WHERE Code = ? AND Map = ? AND SaveId %s", pSqlServer->GetPrefix(), - pResult->m_SaveID != UUID_NO_SAVE_ID ? "= ?" : "IS NULL"); + pResult->m_SaveId != UUID_NO_SAVE_ID ? "= ?" : "IS NULL"); if(pSqlServer->PrepareStatement(aBuf, pError, ErrorSize)) { return true; @@ -1760,9 +1818,9 @@ bool CScoreWorker::LoadTeam(IDbConnection *pSqlServer, const ISqlData *pGameData pSqlServer->BindString(1, pData->m_aCode); pSqlServer->BindString(2, pData->m_aMap); char aUuid[UUID_MAXSTRSIZE]; - if(pResult->m_SaveID != UUID_NO_SAVE_ID) + if(pResult->m_SaveId != UUID_NO_SAVE_ID) { - FormatUuid(pResult->m_SaveID, aUuid, sizeof(aUuid)); + FormatUuid(pResult->m_SaveId, aUuid, sizeof(aUuid)); pSqlServer->BindString(3, aUuid); } pSqlServer->Print(); diff --git a/src/game/server/scoreworker.h b/src/game/server/scoreworker.h index 411c25044c..3001bad236 100644 --- a/src/game/server/scoreworker.h +++ b/src/game/server/scoreworker.h @@ -39,6 +39,7 @@ struct CScorePlayerResult : ISqlResult BROADCAST, MAP_VOTE, PLAYER_INFO, + PLAYER_TIMECP, } m_MessageKind; union { @@ -49,6 +50,7 @@ struct CScorePlayerResult : ISqlResult std::optional m_Time; float m_aTimeCp[NUM_CHECKPOINTS]; int m_Birthday; // 0 indicates no birthday + char m_aRequestedPlayer[MAX_NAME_LENGTH]; } m_Info = {}; struct { @@ -70,9 +72,9 @@ struct CScoreLoadBestTimeResult : ISqlResult float m_CurrentRecord; }; -struct CSqlLoadBestTimeData : ISqlData +struct CSqlLoadBestTimeRequest : ISqlData { - CSqlLoadBestTimeData(std::shared_ptr pResult) : + CSqlLoadBestTimeRequest(std::shared_ptr pResult) : ISqlData(std::move(pResult)) { } @@ -100,13 +102,13 @@ struct CSqlPlayerRequest : ISqlData struct CScoreRandomMapResult : ISqlResult { - CScoreRandomMapResult(int ClientID) : - m_ClientID(ClientID) + CScoreRandomMapResult(int ClientId) : + m_ClientId(ClientId) { m_aMap[0] = '\0'; m_aMessage[0] = '\0'; } - int m_ClientID; + int m_ClientId; char m_aMap[MAX_MAP_LENGTH]; char m_aMessage[512]; }; @@ -137,7 +139,7 @@ struct CSqlScoreData : ISqlData char m_aGameUuid[UUID_MAXSTRSIZE]; char m_aName[MAX_MAP_LENGTH]; - int m_ClientID; + int m_ClientId; float m_Time; char m_aTimestamp[TIMESTAMP_STR_LENGTH]; float m_aCurrentTimeCp[NUM_CHECKPOINTS]; @@ -148,9 +150,9 @@ struct CSqlScoreData : ISqlData struct CScoreSaveResult : ISqlResult { - CScoreSaveResult(int PlayerID) : + CScoreSaveResult(int PlayerId) : m_Status(SAVE_FAILED), - m_RequestingPlayer(PlayerID) + m_RequestingPlayer(PlayerId) { m_aMessage[0] = '\0'; m_aBroadcast[0] = '\0'; @@ -167,7 +169,7 @@ struct CScoreSaveResult : ISqlResult char m_aBroadcast[512]; CSaveTeam m_SavedTeam; int m_RequestingPlayer; - CUuid m_SaveID; + CUuid m_SaveId; }; struct CSqlTeamScoreData : ISqlData @@ -186,13 +188,13 @@ struct CSqlTeamScoreData : ISqlData CUuid m_TeamrankUuid; }; -struct CSqlTeamSave : ISqlData +struct CSqlTeamSaveData : ISqlData { - CSqlTeamSave(std::shared_ptr pResult) : + CSqlTeamSaveData(std::shared_ptr pResult) : ISqlData(std::move(pResult)) { } - virtual ~CSqlTeamSave(){}; + virtual ~CSqlTeamSaveData(){}; char m_aClientName[MAX_NAME_LENGTH]; char m_aMap[MAX_MAP_LENGTH]; @@ -201,21 +203,20 @@ struct CSqlTeamSave : ISqlData char m_aServer[5]; }; -struct CSqlTeamLoad : ISqlData +struct CSqlTeamLoadRequest : ISqlData { - CSqlTeamLoad(std::shared_ptr pResult) : + CSqlTeamLoadRequest(std::shared_ptr pResult) : ISqlData(std::move(pResult)) { } - virtual ~CSqlTeamLoad(){}; + virtual ~CSqlTeamLoadRequest(){}; char m_aCode[128]; char m_aMap[MAX_MAP_LENGTH]; char m_aRequestingPlayer[MAX_NAME_LENGTH]; - int m_ClientID; // struct holding all player names in the team or an empty string char m_aClientNames[MAX_CLIENTS][MAX_NAME_LENGTH]; - int m_aClientID[MAX_CLIENTS]; + int m_aClientId[MAX_CLIENTS]; int m_NumPlayer; }; @@ -244,6 +245,12 @@ class CPlayerData m_aBestTimeCp[i] = aTimeCp[i]; } + void SetBestTimeCp(const float aTimeCp[NUM_CHECKPOINTS]) + { + for(int i = 0; i < NUM_CHECKPOINTS; i++) + m_aBestTimeCp[i] = aTimeCp[i]; + } + float m_BestTime; float m_aBestTimeCp[NUM_CHECKPOINTS]; @@ -253,16 +260,16 @@ class CPlayerData struct CTeamrank { - CUuid m_TeamID; + CUuid m_TeamId; char m_aaNames[MAX_CLIENTS][MAX_NAME_LENGTH]; unsigned int m_NumNames; CTeamrank(); // Assumes that a database query equivalent to // - // SELECT TeamID, Name [, ...] -- the order is important + // SELECT TeamId, Name [, ...] -- the order is important // FROM record_teamrace - // ORDER BY TeamID, Name + // ORDER BY TeamId, Name // // was executed and that the result line of the first team member is already selected. // Afterwards the team member of the next team is selected. @@ -284,6 +291,7 @@ struct CScoreWorker static bool MapVote(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize); static bool LoadPlayerData(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize); + static bool LoadPlayerTimeCp(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize); static bool MapInfo(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize); static bool ShowRank(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize); static bool ShowTeamRank(IDbConnection *pSqlServer, const ISqlData *pGameData, char *pError, int ErrorSize); diff --git a/src/game/server/teams.cpp b/src/game/server/teams.cpp index aa0cd418ac..2f4b58bdf8 100644 --- a/src/game/server/teams.cpp +++ b/src/game/server/teams.cpp @@ -28,10 +28,11 @@ void CGameTeams::Reset() SendTeamsState(i); } - for(int i = 0; i < NUM_TEAMS; ++i) + for(int i = 0; i < NUM_DDRACE_TEAMS; ++i) { m_aTeamState[i] = TEAMSTATE_EMPTY; m_aTeamLocked[i] = false; + m_aTeamFlock[i] = false; m_apSaveTeamResult[i] = nullptr; m_aTeamSentStartWarning[i] = false; ResetRoundState(i); @@ -51,7 +52,7 @@ void CGameTeams::ResetRoundState(int Team) if(m_Core.Team(i) == Team && GameServer()->m_apPlayers[i]) { GameServer()->m_apPlayers[i]->m_VotedForPractice = false; - GameServer()->m_apPlayers[i]->m_SwapTargetsClientID = -1; + GameServer()->m_apPlayers[i]->m_SwapTargetsClientId = -1; m_aLastSwap[i] = 0; } } @@ -67,20 +68,23 @@ void CGameTeams::ResetSwitchers(int Team) } } -void CGameTeams::OnCharacterStart(int ClientID) +void CGameTeams::OnCharacterStart(int ClientId) { int Tick = Server()->Tick(); - CCharacter *pStartingChar = Character(ClientID); + CCharacter *pStartingChar = Character(ClientId); if(!pStartingChar) return; if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO && pStartingChar->m_DDRaceState == DDRACE_STARTED) return; - if((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || m_Core.Team(ClientID) != TEAM_FLOCK) && pStartingChar->m_DDRaceState == DDRACE_FINISHED) + if((g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO || (m_Core.Team(ClientId) != TEAM_FLOCK && !m_aTeamFlock[m_Core.Team(ClientId)])) && pStartingChar->m_DDRaceState == DDRACE_FINISHED) return; if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && - (m_Core.Team(ClientID) == TEAM_FLOCK || m_Core.Team(ClientID) == TEAM_SUPER)) + (m_Core.Team(ClientId) == TEAM_FLOCK || TeamFlock(m_Core.Team(ClientId)) || m_Core.Team(ClientId) == TEAM_SUPER)) { - m_aTeeStarted[ClientID] = true; + if(TeamFlock(m_Core.Team(ClientId)) && (m_aTeamState[m_Core.Team(ClientId)] < TEAMSTATE_STARTED)) + ChangeTeamState(m_Core.Team(ClientId), TEAMSTATE_STARTED); + + m_aTeeStarted[ClientId] = true; pStartingChar->m_DDRaceState = DDRACE_STARTED; pStartingChar->m_StartTime = Tick; return; @@ -88,7 +92,7 @@ void CGameTeams::OnCharacterStart(int ClientID) bool Waiting = false; for(int i = 0; i < MAX_CLIENTS; ++i) { - if(m_Core.Team(ClientID) != m_Core.Team(i)) + if(m_Core.Team(ClientId) != m_Core.Team(i)) continue; CPlayer *pPlayer = GetPlayer(i); if(!pPlayer || !pPlayer->IsPlaying()) @@ -99,7 +103,7 @@ void CGameTeams::OnCharacterStart(int ClientID) Waiting = true; pStartingChar->m_DDRaceState = DDRACE_NONE; - if(m_aLastChat[ClientID] + Server()->TickSpeed() + g_Config.m_SvChatDelay < Tick) + if(m_aLastChat[ClientId] + Server()->TickSpeed() + g_Config.m_SvChatDelay < Tick) { char aBuf[128]; str_format( @@ -107,8 +111,8 @@ void CGameTeams::OnCharacterStart(int ClientID) sizeof(aBuf), "%s has finished and didn't go through start yet, wait for him or join another team.", Server()->ClientName(i)); - GameServer()->SendChatTarget(ClientID, aBuf); - m_aLastChat[ClientID] = Tick; + GameServer()->SendChatTarget(ClientId, aBuf); + m_aLastChat[ClientId] = Tick; } if(m_aLastChat[i] + Server()->TickSpeed() + g_Config.m_SvChatDelay < Tick) { @@ -117,7 +121,7 @@ void CGameTeams::OnCharacterStart(int ClientID) aBuf, sizeof(aBuf), "%s wants to start a new round, kill or walk to start.", - Server()->ClientName(ClientID)); + Server()->ClientName(ClientId)); GameServer()->SendChatTarget(i, aBuf); m_aLastChat[i] = Tick; } @@ -125,23 +129,23 @@ void CGameTeams::OnCharacterStart(int ClientID) if(!Waiting) { - m_aTeeStarted[ClientID] = true; + m_aTeeStarted[ClientId] = true; } - if(m_aTeamState[m_Core.Team(ClientID)] < TEAMSTATE_STARTED && !Waiting) + if(m_aTeamState[m_Core.Team(ClientId)] < TEAMSTATE_STARTED && !Waiting) { - ChangeTeamState(m_Core.Team(ClientID), TEAMSTATE_STARTED); - m_aTeamSentStartWarning[m_Core.Team(ClientID)] = false; - m_aTeamUnfinishableKillTick[m_Core.Team(ClientID)] = -1; + ChangeTeamState(m_Core.Team(ClientId), TEAMSTATE_STARTED); + m_aTeamSentStartWarning[m_Core.Team(ClientId)] = false; + m_aTeamUnfinishableKillTick[m_Core.Team(ClientId)] = -1; - int NumPlayers = Count(m_Core.Team(ClientID)); + int NumPlayers = Count(m_Core.Team(ClientId)); char aBuf[512]; str_format( aBuf, sizeof(aBuf), "Team %d started with %d player%s: ", - m_Core.Team(ClientID), + m_Core.Team(ClientId), NumPlayers, NumPlayers == 1 ? "" : "s"); @@ -149,11 +153,11 @@ void CGameTeams::OnCharacterStart(int ClientID) for(int i = 0; i < MAX_CLIENTS; ++i) { - if(m_Core.Team(ClientID) == m_Core.Team(i)) + if(m_Core.Team(ClientId) == m_Core.Team(i)) { CPlayer *pPlayer = GetPlayer(i); // TODO: THE PROBLEM IS THAT THERE IS NO CHARACTER SO START TIME CAN'T BE SET! - if(pPlayer && (pPlayer->IsPlaying() || TeamLocked(m_Core.Team(ClientID)))) + if(pPlayer && (pPlayer->IsPlaying() || TeamLocked(m_Core.Team(ClientId)))) { SetDDRaceState(pPlayer, DDRACE_STARTED); SetStartTime(pPlayer, Tick); @@ -168,12 +172,12 @@ void CGameTeams::OnCharacterStart(int ClientID) } } - if(g_Config.m_SvTeam < SV_TEAM_FORCED_SOLO && g_Config.m_SvMaxTeamSize != 2 && g_Config.m_SvPauseable) + if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && g_Config.m_SvMaxTeamSize != 2 && g_Config.m_SvPauseable) { for(int i = 0; i < MAX_CLIENTS; ++i) { CPlayer *pPlayer = GetPlayer(i); - if(m_Core.Team(ClientID) == m_Core.Team(i) && pPlayer && (pPlayer->IsPlaying() || TeamLocked(m_Core.Team(ClientID)))) + if(m_Core.Team(ClientId) == m_Core.Team(i) && pPlayer && (pPlayer->IsPlaying() || TeamLocked(m_Core.Team(ClientId)))) { GameServer()->SendChatTarget(i, aBuf); } @@ -182,29 +186,29 @@ void CGameTeams::OnCharacterStart(int ClientID) } } -void CGameTeams::OnCharacterFinish(int ClientID) +void CGameTeams::OnCharacterFinish(int ClientId) { - if((m_Core.Team(ClientID) == TEAM_FLOCK && g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO) || m_Core.Team(ClientID) == TEAM_SUPER) + if(((m_Core.Team(ClientId) == TEAM_FLOCK || m_aTeamFlock[m_Core.Team(ClientId)]) && g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO) || m_Core.Team(ClientId) == TEAM_SUPER) { - CPlayer *pPlayer = GetPlayer(ClientID); + CPlayer *pPlayer = GetPlayer(ClientId); if(pPlayer && pPlayer->IsPlaying()) { - float Time = (float)(Server()->Tick() - GetStartTime(pPlayer)) / ((float)Server()->TickSpeed()); - if(Time < 0.000001f) + int TimeTicks = Server()->Tick() - GetStartTime(pPlayer); + if(TimeTicks <= 0) return; char aTimestamp[TIMESTAMP_STR_LENGTH]; str_timestamp_format(aTimestamp, sizeof(aTimestamp), FORMAT_SPACE); // 2019-04-02 19:41:58 - OnFinish(pPlayer, Time, aTimestamp); + OnFinish(pPlayer, TimeTicks, aTimestamp); } } else { - if(m_aTeeStarted[ClientID]) + if(m_aTeeStarted[ClientId]) { - m_aTeeFinished[ClientID] = true; + m_aTeeFinished[ClientId] = true; } - CheckTeamFinished(m_Core.Team(ClientID)); + CheckTeamFinished(m_Core.Team(ClientId)); } } @@ -250,7 +254,7 @@ void CGameTeams::Tick() { CCharacter *pChar = GameServer()->m_apPlayers[i] ? GameServer()->m_apPlayers[i]->GetCharacter() : nullptr; int Team = m_Core.Team(i); - if(!pChar || m_aTeamState[Team] != TEAMSTATE_STARTED || m_aTeeStarted[i] || m_aPractice[m_Core.Team(i)]) + if(!pChar || m_aTeamState[Team] != TEAMSTATE_STARTED || m_aTeamFlock[Team] || m_aTeeStarted[i] || m_aPractice[m_Core.Team(i)]) { continue; } @@ -274,11 +278,14 @@ void CGameTeams::Tick() { continue; } + bool TeamHasCheatCharacter = false; int NumPlayersNotStarted = 0; char aPlayerNames[256]; aPlayerNames[0] = 0; for(int j = 0; j < MAX_CLIENTS; j++) { + if(Character(j) && Character(j)->m_DDRaceState == DDRACE_CHEAT) + TeamHasCheatCharacter = true; if(m_Core.Team(j) == i && !m_aTeeStarted[j]) { if(aPlayerNames[0]) @@ -289,7 +296,7 @@ void CGameTeams::Tick() NumPlayersNotStarted += 1; } } - if(!aPlayerNames[0]) + if(!aPlayerNames[0] || TeamHasCheatCharacter) { continue; } @@ -328,8 +335,9 @@ void CGameTeams::CheckTeamFinished(int Team) if(PlayersCount > 0) { - float Time = (float)(Server()->Tick() - GetStartTime(apTeamPlayers[0])) / ((float)Server()->TickSpeed()); - if(Time < 0.000001f) + int TimeTicks = Server()->Tick() - GetStartTime(apTeamPlayers[0]); + float Time = (float)TimeTicks / (float)Server()->TickSpeed(); + if(TimeTicks <= 0) { return; } @@ -338,10 +346,13 @@ void CGameTeams::CheckTeamFinished(int Team) { ChangeTeamState(Team, TEAMSTATE_FINISHED); + int min = (int)Time / 60; + float sec = Time - (min * 60.0f); + char aBuf[256]; str_format(aBuf, sizeof(aBuf), "Your team would've finished in: %d minute(s) %5.2f second(s). Since you had practice mode enabled your rank doesn't count.", - (int)Time / 60, Time - ((int)Time / 60 * 60)); + min, sec); GameServer()->SendChatTeam(Team, aBuf); for(unsigned int i = 0; i < PlayersCount; ++i) @@ -356,49 +367,49 @@ void CGameTeams::CheckTeamFinished(int Team) str_timestamp_format(aTimestamp, sizeof(aTimestamp), FORMAT_SPACE); // 2019-04-02 19:41:58 for(unsigned int i = 0; i < PlayersCount; ++i) - OnFinish(apTeamPlayers[i], Time, aTimestamp); + OnFinish(apTeamPlayers[i], TimeTicks, aTimestamp); ChangeTeamState(Team, TEAMSTATE_FINISHED); // TODO: Make it better - OnTeamFinish(apTeamPlayers, PlayersCount, Time, aTimestamp); + OnTeamFinish(Team, apTeamPlayers, PlayersCount, TimeTicks, aTimestamp); } } } -const char *CGameTeams::SetCharacterTeam(int ClientID, int Team) +const char *CGameTeams::SetCharacterTeam(int ClientId, int Team) { - if(ClientID < 0 || ClientID >= MAX_CLIENTS) + if(ClientId < 0 || ClientId >= MAX_CLIENTS) return "Invalid client ID"; if(Team < 0 || Team >= MAX_CLIENTS + 1) return "Invalid team number"; - if(Team != TEAM_SUPER && m_aTeamState[Team] > TEAMSTATE_OPEN) + if(Team != TEAM_SUPER && m_aTeamState[Team] > TEAMSTATE_OPEN && !m_aPractice[Team] && !m_aTeamFlock[Team]) return "This team started already"; - if(m_Core.Team(ClientID) == Team) + if(m_Core.Team(ClientId) == Team) return "You are in this team already"; - if(!Character(ClientID)) + if(!Character(ClientId)) return "Your character is not valid"; - if(Team == TEAM_SUPER && !Character(ClientID)->IsSuper()) + if(Team == TEAM_SUPER && !Character(ClientId)->IsSuper()) return "You can't join super team if you don't have super rights"; - if(Team != TEAM_SUPER && Character(ClientID)->m_DDRaceState != DDRACE_NONE) + if(Team != TEAM_SUPER && Character(ClientId)->m_DDRaceState != DDRACE_NONE) return "You have started racing already"; // No cheating through noob filter with practice and then leaving team - if(m_aPractice[m_Core.Team(ClientID)]) + if(m_aPractice[m_Core.Team(ClientId)]) return "You have used practice mode already"; // you can not join a team which is currently in the process of saving, // because the save-process can fail and then the team is reset into the game if(Team != TEAM_SUPER && GetSaving(Team)) return "Your team is currently saving"; - if(m_Core.Team(ClientID) != TEAM_SUPER && GetSaving(m_Core.Team(ClientID))) + if(m_Core.Team(ClientId) != TEAM_SUPER && GetSaving(m_Core.Team(ClientId))) return "This team is currently saving"; - SetForceCharacterTeam(ClientID, Team); + SetForceCharacterTeam(ClientId, Team); return nullptr; } -void CGameTeams::SetForceCharacterTeam(int ClientID, int Team) +void CGameTeams::SetForceCharacterTeam(int ClientId, int Team) { - m_aTeeStarted[ClientID] = false; - m_aTeeFinished[ClientID] = false; - int OldTeam = m_Core.Team(ClientID); + m_aTeeStarted[ClientId] = false; + m_aTeeFinished[ClientId] = false; + int OldTeam = m_Core.Team(ClientId); if(Team != OldTeam && (OldTeam != TEAM_FLOCK || g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) && OldTeam != TEAM_SUPER && m_aTeamState[OldTeam] != TEAMSTATE_EMPTY) { @@ -409,28 +420,29 @@ void CGameTeams::SetForceCharacterTeam(int ClientID, int Team) // unlock team when last player leaves SetTeamLock(OldTeam, false); + SetTeamFlock(OldTeam, false); ResetRoundState(OldTeam); // do not reset SaveTeamResult, because it should be logged into teehistorian even if the team leaves } } - m_Core.Team(ClientID, Team); + m_Core.Team(ClientId, Team); if(OldTeam != Team) { - for(int LoopClientID = 0; LoopClientID < MAX_CLIENTS; ++LoopClientID) - if(GetPlayer(LoopClientID)) - SendTeamsState(LoopClientID); + for(int LoopClientId = 0; LoopClientId < MAX_CLIENTS; ++LoopClientId) + if(GetPlayer(LoopClientId)) + SendTeamsState(LoopClientId); - if(GetPlayer(ClientID)) + if(GetPlayer(ClientId)) { - GetPlayer(ClientID)->m_VotedForPractice = false; - GetPlayer(ClientID)->m_SwapTargetsClientID = -1; + GetPlayer(ClientId)->m_VotedForPractice = false; + GetPlayer(ClientId)->m_SwapTargetsClientId = -1; } - m_pGameContext->m_World.RemoveEntitiesFromPlayer(ClientID); + m_pGameContext->m_World.RemoveEntitiesFromPlayer(ClientId); } - if(Team != TEAM_SUPER && (m_aTeamState[Team] == TEAMSTATE_EMPTY || m_aTeamLocked[Team])) + if(Team != TEAM_SUPER && (m_aTeamState[Team] == TEAMSTATE_EMPTY || (m_aTeamLocked[Team] && !m_aTeamFlock[Team]))) { if(!m_aTeamLocked[Team]) ChangeTeamState(Team, TEAMSTATE_OPEN); @@ -458,17 +470,17 @@ void CGameTeams::ChangeTeamState(int Team, int State) m_aTeamState[Team] = State; } -void CGameTeams::KillTeam(int Team, int NewStrongID, int ExceptID) +void CGameTeams::KillTeam(int Team, int NewStrongId, int ExceptId) { for(int i = 0; i < MAX_CLIENTS; i++) { if(m_Core.Team(i) == Team && GameServer()->m_apPlayers[i]) { GameServer()->m_apPlayers[i]->m_VotedForPractice = false; - if(i != ExceptID) + if(i != ExceptId) { GameServer()->m_apPlayers[i]->KillCharacter(WEAPON_SELF, false); - if(NewStrongID != -1 && i != NewStrongID) + if(NewStrongId != -1 && i != NewStrongId) { GameServer()->m_apPlayers[i]->Respawn(true); // spawn the rest of team with weak hook on the killer } @@ -479,7 +491,7 @@ void CGameTeams::KillTeam(int Team, int NewStrongID, int ExceptID) // send the team kill message CNetMsg_Sv_KillMsgTeam Msg; Msg.m_Team = Team; - Msg.m_First = NewStrongID; + Msg.m_First = NewStrongId; Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, -1); } @@ -495,22 +507,25 @@ bool CGameTeams::TeamFinished(int Team) return true; } -CClientMask CGameTeams::TeamMask(int Team, int ExceptID, int Asker) +CClientMask CGameTeams::TeamMask(int Team, int ExceptId, int Asker, int VersionFlags) { if(Team == TEAM_SUPER) { - if(ExceptID == -1) + if(ExceptId == -1) return CClientMask().set(); - return CClientMask().set().reset(ExceptID); + return CClientMask().set().reset(ExceptId); } CClientMask Mask; for(int i = 0; i < MAX_CLIENTS; ++i) { - if(i == ExceptID) + if(i == ExceptId) continue; // Explicitly excluded if(!GetPlayer(i)) continue; // Player doesn't exist + if(!((Server()->IsSixup(i) && (VersionFlags & CGameContext::FLAG_SIXUP)) || + (!Server()->IsSixup(i) && (VersionFlags & CGameContext::FLAG_SIX)))) + continue; if(!(GetPlayer(i)->GetTeam() == TEAM_SPECTATORS || GetPlayer(i)->IsPaused())) { // Not spectator @@ -534,24 +549,24 @@ CClientMask CGameTeams::TeamMask(int Team, int ExceptID, int Asker) } } // See everything of yourself } - else if(GetPlayer(i)->m_SpectatorID != SPEC_FREEVIEW) + else if(GetPlayer(i)->m_SpectatorId != SPEC_FREEVIEW) { // Spectating specific player - if(GetPlayer(i)->m_SpectatorID != Asker) + if(GetPlayer(i)->m_SpectatorId != Asker) { // Actions of other players - if(!Character(GetPlayer(i)->m_SpectatorID)) + if(!Character(GetPlayer(i)->m_SpectatorId)) continue; // Player is currently dead if(GetPlayer(i)->m_ShowOthers == SHOW_OTHERS_ONLY_TEAM) { - if(m_Core.Team(GetPlayer(i)->m_SpectatorID) != Team && m_Core.Team(GetPlayer(i)->m_SpectatorID) != TEAM_SUPER) + if(m_Core.Team(GetPlayer(i)->m_SpectatorId) != Team && m_Core.Team(GetPlayer(i)->m_SpectatorId) != TEAM_SUPER) continue; // In different teams } else if(GetPlayer(i)->m_ShowOthers == SHOW_OTHERS_OFF) { if(m_Core.GetSolo(Asker)) continue; // When in solo part don't show others - if(m_Core.GetSolo(GetPlayer(i)->m_SpectatorID)) + if(m_Core.GetSolo(GetPlayer(i)->m_SpectatorId)) continue; // When in solo part don't show others - if(m_Core.Team(GetPlayer(i)->m_SpectatorID) != Team && m_Core.Team(GetPlayer(i)->m_SpectatorID) != TEAM_SUPER) + if(m_Core.Team(GetPlayer(i)->m_SpectatorId) != Team && m_Core.Team(GetPlayer(i)->m_SpectatorId) != TEAM_SUPER) continue; // In different teams } } // See everything of player you're spectating @@ -570,12 +585,12 @@ CClientMask CGameTeams::TeamMask(int Team, int ExceptID, int Asker) return Mask; } -void CGameTeams::SendTeamsState(int ClientID) +void CGameTeams::SendTeamsState(int ClientId) { if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) return; - if(!m_pGameContext->m_apPlayers[ClientID]) + if(!m_pGameContext->m_apPlayers[ClientId]) return; CMsgPacker Msg(NETMSGTYPE_SV_TEAMSSTATE); @@ -587,11 +602,11 @@ void CGameTeams::SendTeamsState(int ClientID) MsgLegacy.AddInt(m_Core.Team(i)); } - Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientID); - int ClientVersion = m_pGameContext->m_apPlayers[ClientID]->GetClientVersion(); - if(!Server()->IsSixup(ClientID) && VERSION_DDRACE < ClientVersion && ClientVersion < VERSION_DDNET_MSG_LEGACY) + Server()->SendMsg(&Msg, MSGFLAG_VITAL, ClientId); + int ClientVersion = m_pGameContext->m_apPlayers[ClientId]->GetClientVersion(); + if(!Server()->IsSixup(ClientId) && VERSION_DDRACE < ClientVersion && ClientVersion < VERSION_DDNET_MSG_LEGACY) { - Server()->SendMsg(&MsgLegacy, MSGFLAG_VITAL, ClientID); + Server()->SendMsg(&MsgLegacy, MSGFLAG_VITAL, ClientId); } } @@ -658,47 +673,50 @@ float *CGameTeams::GetCurrentTimeCp(CPlayer *Player) return NULL; } -void CGameTeams::OnTeamFinish(CPlayer **Players, unsigned int Size, float Time, const char *pTimestamp) +void CGameTeams::OnTeamFinish(int Team, CPlayer **Players, unsigned int Size, int TimeTicks, const char *pTimestamp) { - int aPlayerCIDs[MAX_CLIENTS]; + int aPlayerCids[MAX_CLIENTS]; for(unsigned int i = 0; i < Size; i++) { - aPlayerCIDs[i] = Players[i]->GetCID(); + aPlayerCids[i] = Players[i]->GetCid(); - if(g_Config.m_SvRejoinTeam0 && g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && (m_Core.Team(Players[i]->GetCID()) >= TEAM_SUPER || !m_aTeamLocked[m_Core.Team(Players[i]->GetCID())])) + if(g_Config.m_SvRejoinTeam0 && g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && (m_Core.Team(Players[i]->GetCid()) >= TEAM_SUPER || !m_aTeamLocked[m_Core.Team(Players[i]->GetCid())])) { - SetForceCharacterTeam(Players[i]->GetCID(), TEAM_FLOCK); + SetForceCharacterTeam(Players[i]->GetCid(), TEAM_FLOCK); char aBuf[512]; str_format(aBuf, sizeof(aBuf), "'%s' joined team 0", - GameServer()->Server()->ClientName(Players[i]->GetCID())); - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf); + GameServer()->Server()->ClientName(Players[i]->GetCid())); + GameServer()->SendChat(-1, TEAM_ALL, aBuf); } } if(Size >= (unsigned int)g_Config.m_SvMinTeamSize) - GameServer()->Score()->SaveTeamScore(aPlayerCIDs, Size, Time, pTimestamp); + GameServer()->Score()->SaveTeamScore(Team, aPlayerCids, Size, TimeTicks, pTimestamp); } -void CGameTeams::OnFinish(CPlayer *Player, float Time, const char *pTimestamp) +void CGameTeams::OnFinish(CPlayer *Player, int TimeTicks, const char *pTimestamp) { if(!Player || !Player->IsPlaying()) return; + + float Time = TimeTicks / (float)Server()->TickSpeed(); + // TODO:DDRace:btd: this ugly - const int ClientID = Player->GetCID(); - CPlayerData *pData = GameServer()->Score()->PlayerData(ClientID); + const int ClientId = Player->GetCid(); + CPlayerData *pData = GameServer()->Score()->PlayerData(ClientId); char aBuf[128]; SetLastTimeCp(Player, -1); // Note that the "finished in" message is parsed by the client str_format(aBuf, sizeof(aBuf), "%s finished in: %d minute(s) %5.2f second(s)", - Server()->ClientName(ClientID), (int)Time / 60, + Server()->ClientName(ClientId), (int)Time / 60, Time - ((int)Time / 60 * 60)); if(g_Config.m_SvHideScore || !g_Config.m_SvSaveWorseScores) - GameServer()->SendChatTarget(ClientID, aBuf, CGameContext::CHAT_SIX); + GameServer()->SendChatTarget(ClientId, aBuf, CGameContext::FLAG_SIX); else - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf, -1., CGameContext::CHAT_SIX); + GameServer()->SendChat(-1, TEAM_ALL, aBuf, -1., CGameContext::FLAG_SIX); float Diff = absolute(Time - pData->m_BestTime); @@ -715,17 +733,17 @@ void CGameTeams::OnFinish(CPlayer *Player, float Time, const char *pTimestamp) str_format(aBuf, sizeof(aBuf), "New record: %5.2f second(s) better.", Diff); if(g_Config.m_SvHideScore || !g_Config.m_SvSaveWorseScores) - GameServer()->SendChatTarget(ClientID, aBuf, CGameContext::CHAT_SIX); + GameServer()->SendChatTarget(ClientId, aBuf, CGameContext::FLAG_SIX); else - GameServer()->SendChat(-1, CGameContext::CHAT_ALL, aBuf, -1, CGameContext::CHAT_SIX); + GameServer()->SendChat(-1, TEAM_ALL, aBuf, -1, CGameContext::FLAG_SIX); } else if(pData->m_BestTime != 0) // tee has already finished? { - Server()->StopRecord(ClientID); + Server()->StopRecord(ClientId); if(Diff <= 0.005f) { - GameServer()->SendChatTarget(ClientID, + GameServer()->SendChatTarget(ClientId, "You finished with your best time."); } else @@ -737,7 +755,7 @@ void CGameTeams::OnFinish(CPlayer *Player, float Time, const char *pTimestamp) str_format(aBuf, sizeof(aBuf), "%5.2f second(s) worse, better luck next time.", Diff); - GameServer()->SendChatTarget(ClientID, aBuf, CGameContext::CHAT_SIX); // this is private, sent only to the tee + GameServer()->SendChatTarget(ClientId, aBuf, CGameContext::FLAG_SIX); // this is private, sent only to the tee } } else @@ -746,7 +764,7 @@ void CGameTeams::OnFinish(CPlayer *Player, float Time, const char *pTimestamp) pData->m_RecordFinishTime = Time; } - if(!Server()->IsSixup(ClientID)) + if(!Server()->IsSixup(ClientId)) { CNetMsg_Sv_DDRaceTime Msg; CNetMsg_Sv_DDRaceTimeLegacy MsgLegacy; @@ -763,40 +781,26 @@ void CGameTeams::OnFinish(CPlayer *Player, float Time, const char *pTimestamp) { if(Player->GetClientVersion() < VERSION_DDNET_MSG_LEGACY) { - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientId); } else { - Server()->SendPackMsg(&MsgLegacy, MSGFLAG_VITAL, ClientID); + Server()->SendPackMsg(&MsgLegacy, MSGFLAG_VITAL, ClientId); } } - - CNetMsg_Sv_RaceFinish RaceFinishMsg; - RaceFinishMsg.m_ClientID = ClientID; - RaceFinishMsg.m_Time = Time * 1000; - RaceFinishMsg.m_Diff = 0; - if(pData->m_BestTime) - { - RaceFinishMsg.m_Diff = Diff * 1000 * (Time < pData->m_BestTime ? -1 : 1); - } - RaceFinishMsg.m_RecordPersonal = (Time < pData->m_BestTime || !pData->m_BestTime); - RaceFinishMsg.m_RecordServer = Time < GameServer()->m_pController->m_CurrentRecord; - Server()->SendPackMsg(&RaceFinishMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1); } - else + + CNetMsg_Sv_RaceFinish RaceFinishMsg; + RaceFinishMsg.m_ClientId = ClientId; + RaceFinishMsg.m_Time = Time * 1000; + RaceFinishMsg.m_Diff = 0; + if(pData->m_BestTime) { - protocol7::CNetMsg_Sv_RaceFinish Msg; - Msg.m_ClientID = ClientID; - Msg.m_Time = Time * 1000; - Msg.m_Diff = 0; - if(pData->m_BestTime) - { - Msg.m_Diff = Diff * 1000 * (Time < pData->m_BestTime ? -1 : 1); - } - Msg.m_RecordPersonal = (Time < pData->m_BestTime || !pData->m_BestTime); - Msg.m_RecordServer = Time < GameServer()->m_pController->m_CurrentRecord; - Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1); + RaceFinishMsg.m_Diff = Diff * 1000 * (Time < pData->m_BestTime ? -1 : 1); } + RaceFinishMsg.m_RecordPersonal = (Time < pData->m_BestTime || !pData->m_BestTime); + RaceFinishMsg.m_RecordServer = Time < GameServer()->m_pController->m_CurrentRecord; + Server()->SendPackMsg(&RaceFinishMsg, MSGFLAG_VITAL | MSGFLAG_NORECORD, -1); bool CallSaveScore = g_Config.m_SvSaveWorseScores; bool NeedToSendNewPersonalRecord = false; @@ -809,8 +813,8 @@ void CGameTeams::OnFinish(CPlayer *Player, float Time, const char *pTimestamp) } if(CallSaveScore) - if(g_Config.m_SvNamelessScore || !str_startswith(Server()->ClientName(ClientID), "nameless tee")) - GameServer()->Score()->SaveScore(ClientID, Time, pTimestamp, + if(g_Config.m_SvNamelessScore || !str_startswith(Server()->ClientName(ClientId), "nameless tee")) + GameServer()->Score()->SaveScore(ClientId, TimeTicks, pTimestamp, GetCurrentTimeCp(Player), Player->m_NotEligibleForFinish); bool NeedToSendNewServerRecord = false; @@ -822,7 +826,7 @@ void CGameTeams::OnFinish(CPlayer *Player, float Time, const char *pTimestamp) else if(Time < GameServer()->m_pController->m_CurrentRecord) { // check for nameless - if(g_Config.m_SvNamelessScore || !str_startswith(Server()->ClientName(ClientID), "nameless tee")) + if(g_Config.m_SvNamelessScore || !str_startswith(Server()->ClientName(ClientId), "nameless tee")) { GameServer()->m_pController->m_CurrentRecord = Time; NeedToSendNewServerRecord = true; @@ -842,7 +846,7 @@ void CGameTeams::OnFinish(CPlayer *Player, float Time, const char *pTimestamp) } if(!NeedToSendNewServerRecord && NeedToSendNewPersonalRecord && Player->GetClientVersion() >= VERSION_DDRACE) { - GameServer()->SendRecord(ClientID); + GameServer()->SendRecord(ClientId); } int TTime = (int)Time; @@ -850,6 +854,10 @@ void CGameTeams::OnFinish(CPlayer *Player, float Time, const char *pTimestamp) { Player->m_Score = TTime; } + + // Confetti + CCharacter *pChar = Player->GetCharacter(); + m_pGameContext->CreateFinishEffect(pChar->m_Pos, pChar->TeamMask()); } void CGameTeams::RequestTeamSwap(CPlayer *pPlayer, CPlayer *pTargetPlayer, int Team) @@ -858,45 +866,45 @@ void CGameTeams::RequestTeamSwap(CPlayer *pPlayer, CPlayer *pTargetPlayer, int T return; char aBuf[512]; - if(pPlayer->m_SwapTargetsClientID == pTargetPlayer->GetCID()) + if(pPlayer->m_SwapTargetsClientId == pTargetPlayer->GetCid()) { str_format(aBuf, sizeof(aBuf), - "You have already requested to swap with %s.", Server()->ClientName(pTargetPlayer->GetCID())); + "You have already requested to swap with %s.", Server()->ClientName(pTargetPlayer->GetCid())); - GameServer()->SendChatTarget(pPlayer->GetCID(), aBuf); + GameServer()->SendChatTarget(pPlayer->GetCid(), aBuf); return; } // Notification for the swap initiator str_format(aBuf, sizeof(aBuf), "You have requested to swap with %s.", - Server()->ClientName(pTargetPlayer->GetCID())); - GameServer()->SendChatTarget(pPlayer->GetCID(), aBuf); + Server()->ClientName(pTargetPlayer->GetCid())); + GameServer()->SendChatTarget(pPlayer->GetCid(), aBuf); // Notification to the target swap player str_format(aBuf, sizeof(aBuf), "%s has requested to swap with you. To complete the swap process please wait %d seconds and then type /swap %s.", - Server()->ClientName(pPlayer->GetCID()), g_Config.m_SvSaveSwapGamesDelay, Server()->ClientName(pPlayer->GetCID())); - GameServer()->SendChatTarget(pTargetPlayer->GetCID(), aBuf); + Server()->ClientName(pPlayer->GetCid()), g_Config.m_SvSaveSwapGamesDelay, Server()->ClientName(pPlayer->GetCid())); + GameServer()->SendChatTarget(pTargetPlayer->GetCid(), aBuf); // Notification for the remaining team str_format(aBuf, sizeof(aBuf), "%s has requested to swap with %s.", - Server()->ClientName(pPlayer->GetCID()), Server()->ClientName(pTargetPlayer->GetCID())); + Server()->ClientName(pPlayer->GetCid()), Server()->ClientName(pTargetPlayer->GetCid())); // Do not send the team notification for team 0 if(Team != 0) { for(int i = 0; i < MAX_CLIENTS; i++) { - if(m_Core.Team(i) == Team && i != pTargetPlayer->GetCID() && i != pPlayer->GetCID()) + if(m_Core.Team(i) == Team && i != pTargetPlayer->GetCid() && i != pPlayer->GetCid()) { GameServer()->SendChatTarget(i, aBuf); } } } - pPlayer->m_SwapTargetsClientID = pTargetPlayer->GetCID(); - m_aLastSwap[pPlayer->GetCID()] = Server()->Tick(); + pPlayer->m_SwapTargetsClientId = pTargetPlayer->GetCid(); + m_aLastSwap[pPlayer->GetCid()] = Server()->Tick(); } void CGameTeams::SwapTeamCharacters(CPlayer *pPrimaryPlayer, CPlayer *pTargetPlayer, int Team) @@ -906,20 +914,20 @@ void CGameTeams::SwapTeamCharacters(CPlayer *pPrimaryPlayer, CPlayer *pTargetPla char aBuf[128]; - int Since = (Server()->Tick() - m_aLastSwap[pTargetPlayer->GetCID()]) / Server()->TickSpeed(); + int Since = (Server()->Tick() - m_aLastSwap[pTargetPlayer->GetCid()]) / Server()->TickSpeed(); if(Since < g_Config.m_SvSaveSwapGamesDelay) { str_format(aBuf, sizeof(aBuf), "You have to wait %d seconds until you can swap.", g_Config.m_SvSaveSwapGamesDelay - Since); - GameServer()->SendChatTarget(pPrimaryPlayer->GetCID(), aBuf); + GameServer()->SendChatTarget(pPrimaryPlayer->GetCid(), aBuf); return; } - pPrimaryPlayer->m_SwapTargetsClientID = -1; - pTargetPlayer->m_SwapTargetsClientID = -1; + pPrimaryPlayer->m_SwapTargetsClientId = -1; + pTargetPlayer->m_SwapTargetsClientId = -1; int TimeoutAfterDelay = g_Config.m_SvSaveSwapGamesDelay + g_Config.m_SvSwapTimeout; if(Since >= TimeoutAfterDelay) @@ -928,7 +936,7 @@ void CGameTeams::SwapTeamCharacters(CPlayer *pPrimaryPlayer, CPlayer *pTargetPla "Your swap request timed out %d seconds ago. Use /swap again to re-initiate it.", Since - g_Config.m_SvSwapTimeout); - GameServer()->SendChatTarget(pPrimaryPlayer->GetCID(), aBuf); + GameServer()->SendChatTarget(pPrimaryPlayer->GetCid(), aBuf); return; } @@ -942,7 +950,7 @@ void CGameTeams::SwapTeamCharacters(CPlayer *pPrimaryPlayer, CPlayer *pTargetPla PrimarySavedTee.Load(pTargetPlayer->GetCharacter(), Team, true); SecondarySavedTee.Load(pPrimaryPlayer->GetCharacter(), Team, true); - if(Team >= 1) + if(Team >= 1 && !m_aTeamFlock[Team]) { for(const auto &pPlayer : GameServer()->m_apPlayers) { @@ -951,27 +959,28 @@ void CGameTeams::SwapTeamCharacters(CPlayer *pPrimaryPlayer, CPlayer *pTargetPla pChar->m_StartTime = pPrimaryPlayer->GetCharacter()->m_StartTime; } } - std::swap(m_aTeeStarted[pPrimaryPlayer->GetCID()], m_aTeeStarted[pTargetPlayer->GetCID()]); - std::swap(m_aTeeFinished[pPrimaryPlayer->GetCID()], m_aTeeFinished[pTargetPlayer->GetCID()]); - std::swap(pPrimaryPlayer->GetCharacter()->GetRescueTeeRef(), pTargetPlayer->GetCharacter()->GetRescueTeeRef()); + std::swap(m_aTeeStarted[pPrimaryPlayer->GetCid()], m_aTeeStarted[pTargetPlayer->GetCid()]); + std::swap(m_aTeeFinished[pPrimaryPlayer->GetCid()], m_aTeeFinished[pTargetPlayer->GetCid()]); + std::swap(pPrimaryPlayer->GetCharacter()->GetLastRescueTeeRef(RESCUEMODE_AUTO), pTargetPlayer->GetCharacter()->GetLastRescueTeeRef(RESCUEMODE_AUTO)); + std::swap(pPrimaryPlayer->GetCharacter()->GetLastRescueTeeRef(RESCUEMODE_MANUAL), pTargetPlayer->GetCharacter()->GetLastRescueTeeRef(RESCUEMODE_MANUAL)); - GameServer()->m_World.SwapClients(pPrimaryPlayer->GetCID(), pTargetPlayer->GetCID()); + GameServer()->m_World.SwapClients(pPrimaryPlayer->GetCid(), pTargetPlayer->GetCid()); if(GameServer()->TeeHistorianActive()) { - GameServer()->TeeHistorian()->RecordPlayerSwap(pPrimaryPlayer->GetCID(), pTargetPlayer->GetCID()); + GameServer()->TeeHistorian()->RecordPlayerSwap(pPrimaryPlayer->GetCid(), pTargetPlayer->GetCid()); } str_format(aBuf, sizeof(aBuf), "%s has swapped with %s.", - Server()->ClientName(pPrimaryPlayer->GetCID()), Server()->ClientName(pTargetPlayer->GetCID())); + Server()->ClientName(pPrimaryPlayer->GetCid()), Server()->ClientName(pTargetPlayer->GetCid())); GameServer()->SendChatTeam(Team, aBuf); } void CGameTeams::ProcessSaveTeam() { - for(int Team = 0; Team < NUM_TEAMS; Team++) + for(int Team = 0; Team < NUM_DDRACE_TEAMS; Team++) { if(m_apSaveTeamResult[Team] == nullptr || !m_apSaveTeamResult[Team]->m_Completed) continue; @@ -987,22 +996,22 @@ void CGameTeams::ProcessSaveTeam() { GameServer()->TeeHistorian()->RecordTeamSaveSuccess( Team, - m_apSaveTeamResult[Team]->m_SaveID, + m_apSaveTeamResult[Team]->m_SaveId, m_apSaveTeamResult[Team]->m_SavedTeam.GetString()); } for(int i = 0; i < m_apSaveTeamResult[Team]->m_SavedTeam.GetMembersCount(); i++) { if(m_apSaveTeamResult[Team]->m_SavedTeam.m_pSavedTees->IsHooking()) { - int ClientID = m_apSaveTeamResult[Team]->m_SavedTeam.m_pSavedTees->GetClientID(); - if(GameServer()->m_apPlayers[ClientID] != nullptr) - GameServer()->SendChatTarget(ClientID, "Start holding the hook before loading the savegame to keep the hook"); + int ClientId = m_apSaveTeamResult[Team]->m_SavedTeam.m_pSavedTees->GetClientId(); + if(GameServer()->m_apPlayers[ClientId] != nullptr) + GameServer()->SendChatTarget(ClientId, "Start holding the hook before loading the savegame to keep the hook"); } } ResetSavedTeam(m_apSaveTeamResult[Team]->m_RequestingPlayer, Team); - char aSaveID[UUID_MAXSTRSIZE]; - FormatUuid(m_apSaveTeamResult[Team]->m_SaveID, aSaveID, UUID_MAXSTRSIZE); - dbg_msg("save", "Save successful: %s", aSaveID); + char aSaveId[UUID_MAXSTRSIZE]; + FormatUuid(m_apSaveTeamResult[Team]->m_SaveId, aSaveId, UUID_MAXSTRSIZE); + dbg_msg("save", "Save successful: %s", aSaveId); break; } case CScoreSaveResult::SAVE_FAILED: @@ -1020,17 +1029,26 @@ void CGameTeams::ProcessSaveTeam() { GameServer()->TeeHistorian()->RecordTeamLoadSuccess( Team, - m_apSaveTeamResult[Team]->m_SaveID, + m_apSaveTeamResult[Team]->m_SaveId, m_apSaveTeamResult[Team]->m_SavedTeam.GetString()); } + + bool TeamValid = false; if(Count(Team) > 0) { // keep current weak/strong order as on some maps there is no other way of switching - m_apSaveTeamResult[Team]->m_SavedTeam.Load(GameServer(), Team, true); + TeamValid = m_apSaveTeamResult[Team]->m_SavedTeam.Load(GameServer(), Team, true); } - char aSaveID[UUID_MAXSTRSIZE]; - FormatUuid(m_apSaveTeamResult[Team]->m_SaveID, aSaveID, UUID_MAXSTRSIZE); - dbg_msg("save", "Load successful: %s", aSaveID); + + if(!TeamValid) + { + GameServer()->SendChatTeam(Team, "Your team has been killed because it contains an invalid tee state"); + KillTeam(Team, -1, -1); + } + + char aSaveId[UUID_MAXSTRSIZE]; + FormatUuid(m_apSaveTeamResult[Team]->m_SaveId, aSaveId, UUID_MAXSTRSIZE); + dbg_msg("save", "Load successful: %s", aSaveId); break; } case CScoreSaveResult::LOAD_FAILED: @@ -1044,29 +1062,30 @@ void CGameTeams::ProcessSaveTeam() } } -void CGameTeams::OnCharacterSpawn(int ClientID) +void CGameTeams::OnCharacterSpawn(int ClientId) { - m_Core.SetSolo(ClientID, false); - int Team = m_Core.Team(ClientID); + m_Core.SetSolo(ClientId, false); + int Team = m_Core.Team(ClientId); if(GetSaving(Team)) return; - if(m_Core.Team(ClientID) >= TEAM_SUPER || !m_aTeamLocked[Team]) + if(m_Core.Team(ClientId) >= TEAM_SUPER || !m_aTeamLocked[Team]) { if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO) - SetForceCharacterTeam(ClientID, TEAM_FLOCK); + SetForceCharacterTeam(ClientId, TEAM_FLOCK); else - SetForceCharacterTeam(ClientID, ClientID); // initialize team - CheckTeamFinished(Team); + SetForceCharacterTeam(ClientId, ClientId); // initialize team + if(!m_aTeamFlock[Team]) + CheckTeamFinished(Team); } } -void CGameTeams::OnCharacterDeath(int ClientID, int Weapon) +void CGameTeams::OnCharacterDeath(int ClientId, int Weapon) { - m_Core.SetSolo(ClientID, false); + m_Core.SetSolo(ClientId, false); - int Team = m_Core.Team(ClientID); + int Team = m_Core.Team(ClientId); if(GetSaving(Team)) return; bool Locked = TeamLocked(Team) && Weapon != WEAPON_GAME; @@ -1076,7 +1095,7 @@ void CGameTeams::OnCharacterDeath(int ClientID, int Weapon) ChangeTeamState(Team, CGameTeams::TEAMSTATE_OPEN); if(m_aPractice[Team]) { - if(Weapon == WEAPON_SELF) + if(Weapon != WEAPON_WORLD) { ResetRoundState(Team); } @@ -1092,9 +1111,9 @@ void CGameTeams::OnCharacterDeath(int ClientID, int Weapon) } else if(Locked) { - SetForceCharacterTeam(ClientID, Team); + SetForceCharacterTeam(ClientId, Team); - if(GetTeamState(Team) != TEAMSTATE_OPEN) + if(GetTeamState(Team) != TEAMSTATE_OPEN && !m_aTeamFlock[m_Core.Team(ClientId)]) { ChangeTeamState(Team, CGameTeams::TEAMSTATE_OPEN); @@ -1102,10 +1121,19 @@ void CGameTeams::OnCharacterDeath(int ClientID, int Weapon) if(Count(Team) > 1) { - KillTeam(Team, Weapon == WEAPON_SELF ? ClientID : -1, ClientID); + // Disband team if the team has more players than allowed. + if(Count(Team) > g_Config.m_SvMaxTeamSize) + { + GameServer()->SendChatTeam(Team, "This team was disbanded because there are more players than allowed in the team."); + SetTeamLock(Team, false); + KillTeam(Team, Weapon == WEAPON_SELF ? ClientId : -1, ClientId); + return; + } + + KillTeam(Team, Weapon == WEAPON_SELF ? ClientId : -1, ClientId); char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "Everyone in your locked team was killed because '%s' %s.", Server()->ClientName(ClientID), Weapon == WEAPON_SELF ? "killed" : "died"); + str_format(aBuf, sizeof(aBuf), "Everyone in your locked team was killed because '%s' %s.", Server()->ClientName(ClientId), Weapon == WEAPON_SELF ? "killed" : "died"); GameServer()->SendChatTeam(Team, aBuf); } @@ -1113,18 +1141,19 @@ void CGameTeams::OnCharacterDeath(int ClientID, int Weapon) } else { - if(m_aTeamState[m_Core.Team(ClientID)] == CGameTeams::TEAMSTATE_STARTED && !m_aTeeStarted[ClientID]) + if(m_aTeamState[m_Core.Team(ClientId)] == CGameTeams::TEAMSTATE_STARTED && !m_aTeeStarted[ClientId] && !m_aTeamFlock[m_Core.Team(ClientId)]) { char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "This team cannot finish anymore because '%s' left the team before hitting the start", Server()->ClientName(ClientID)); + str_format(aBuf, sizeof(aBuf), "This team cannot finish anymore because '%s' left the team before hitting the start", Server()->ClientName(ClientId)); GameServer()->SendChatTeam(Team, aBuf); GameServer()->SendChatTeam(Team, "Enter /practice mode or restart to avoid the entire team being killed in 60 seconds"); m_aTeamUnfinishableKillTick[Team] = Server()->Tick() + 60 * Server()->TickSpeed(); ChangeTeamState(Team, CGameTeams::TEAMSTATE_STARTED_UNFINISHABLE); } - SetForceCharacterTeam(ClientID, TEAM_FLOCK); - CheckTeamFinished(Team); + SetForceCharacterTeam(ClientId, TEAM_FLOCK); + if(!m_aTeamFlock[m_Core.Team(ClientId)]) + CheckTeamFinished(Team); } } @@ -1134,27 +1163,33 @@ void CGameTeams::SetTeamLock(int Team, bool Lock) m_aTeamLocked[Team] = Lock; } +void CGameTeams::SetTeamFlock(int Team, bool Mode) +{ + if(Team > TEAM_FLOCK && Team < TEAM_SUPER) + m_aTeamFlock[Team] = Mode; +} + void CGameTeams::ResetInvited(int Team) { m_aInvited[Team].reset(); } -void CGameTeams::SetClientInvited(int Team, int ClientID, bool Invited) +void CGameTeams::SetClientInvited(int Team, int ClientId, bool Invited) { if(Team > TEAM_FLOCK && Team < TEAM_SUPER) { if(Invited) - m_aInvited[Team].set(ClientID); + m_aInvited[Team].set(ClientId); else - m_aInvited[Team].reset(ClientID); + m_aInvited[Team].reset(ClientId); } } -void CGameTeams::KillSavedTeam(int ClientID, int Team) +void CGameTeams::KillSavedTeam(int ClientId, int Team) { if(g_Config.m_SvSoloServer || !g_Config.m_SvTeam) { - GameServer()->m_apPlayers[ClientID]->KillCharacter(WEAPON_SELF, true); + GameServer()->m_apPlayers[ClientId]->KillCharacter(WEAPON_SELF, true); } else { @@ -1162,7 +1197,7 @@ void CGameTeams::KillSavedTeam(int ClientID, int Team) } } -void CGameTeams::ResetSavedTeam(int ClientID, int Team) +void CGameTeams::ResetSavedTeam(int ClientId, int Team) { if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) { diff --git a/src/game/server/teams.h b/src/game/server/teams.h index 25a758fed2..a836dc71ef 100644 --- a/src/game/server/teams.h +++ b/src/game/server/teams.h @@ -23,31 +23,32 @@ class CGameTeams bool m_aTeeFinished[MAX_CLIENTS]; int m_aLastChat[MAX_CLIENTS]; - int m_aTeamState[NUM_TEAMS]; - bool m_aTeamLocked[NUM_TEAMS]; - CClientMask m_aInvited[NUM_TEAMS]; - bool m_aPractice[NUM_TEAMS]; - std::shared_ptr m_apSaveTeamResult[NUM_TEAMS]; + int m_aTeamState[NUM_DDRACE_TEAMS]; + bool m_aTeamLocked[NUM_DDRACE_TEAMS]; + bool m_aTeamFlock[NUM_DDRACE_TEAMS]; + CClientMask m_aInvited[NUM_DDRACE_TEAMS]; + bool m_aPractice[NUM_DDRACE_TEAMS]; + std::shared_ptr m_apSaveTeamResult[NUM_DDRACE_TEAMS]; uint64_t m_aLastSwap[MAX_CLIENTS]; // index is id of player who initiated swap - bool m_aTeamSentStartWarning[NUM_TEAMS]; + bool m_aTeamSentStartWarning[NUM_DDRACE_TEAMS]; // `m_aTeamUnfinishableKillTick` is -1 by default and gets set when a // team becomes unfinishable. If the team hasn't entered practice mode // by that time, it'll get killed to prevent people not understanding // the message from playing for a long time in an unfinishable team. - int m_aTeamUnfinishableKillTick[NUM_TEAMS]; + int m_aTeamUnfinishableKillTick[NUM_DDRACE_TEAMS]; class CGameContext *m_pGameContext; /** * Kill the whole team. * @param Team The team id to kill - * @param NewStrongID The player with that id will get strong hook on everyone else, -1 will set the normal spawning order - * @param ExceptID The player that should not get killed + * @param NewStrongId The player with that id will get strong hook on everyone else, -1 will set the normal spawning order + * @param ExceptId The player that should not get killed */ - void KillTeam(int Team, int NewStrongID, int ExceptID = -1); + void KillTeam(int Team, int NewStrongId, int ExceptId = -1); bool TeamFinished(int Team); - void OnTeamFinish(CPlayer **Players, unsigned int Size, float Time, const char *pTimestamp); - void OnFinish(CPlayer *Player, float Time, const char *pTimestamp); + void OnTeamFinish(int Team, CPlayer **Players, unsigned int Size, int TimeTicks, const char *pTimestamp); + void OnFinish(CPlayer *Player, int TimeTicks, const char *pTimestamp); public: enum @@ -66,13 +67,13 @@ class CGameTeams CGameTeams(CGameContext *pGameContext); // helper methods - CCharacter *Character(int ClientID) + CCharacter *Character(int ClientId) { - return GameServer()->GetPlayerChar(ClientID); + return GameServer()->GetPlayerChar(ClientId); } - CPlayer *GetPlayer(int ClientID) + CPlayer *GetPlayer(int ClientId) { - return GameServer()->m_apPlayers[ClientID]; + return GameServer()->m_apPlayers[ClientId]; } class CGameContext *GameServer() @@ -84,33 +85,34 @@ class CGameTeams return m_pGameContext->Server(); } - void OnCharacterStart(int ClientID); - void OnCharacterFinish(int ClientID); - void OnCharacterSpawn(int ClientID); - void OnCharacterDeath(int ClientID, int Weapon); + void OnCharacterStart(int ClientId); + void OnCharacterFinish(int ClientId); + void OnCharacterSpawn(int ClientId); + void OnCharacterDeath(int ClientId, int Weapon); void Tick(); // returns nullptr if successful, error string if failed - const char *SetCharacterTeam(int ClientID, int Team); + const char *SetCharacterTeam(int ClientId, int Team); void CheckTeamFinished(int Team); void ChangeTeamState(int Team, int State); - CClientMask TeamMask(int Team, int ExceptID = -1, int Asker = -1); + CClientMask TeamMask(int Team, int ExceptId = -1, int Asker = -1, int VersionFlags = CGameContext::FLAG_SIX | CGameContext::FLAG_SIXUP); int Count(int Team) const; // need to be very careful using this method. SERIOUSLY... - void SetForceCharacterTeam(int ClientID, int Team); + void SetForceCharacterTeam(int ClientId, int Team); void Reset(); void ResetRoundState(int Team); void ResetSwitchers(int Team); - void SendTeamsState(int ClientID); + void SendTeamsState(int ClientId); void SetTeamLock(int Team, bool Lock); + void SetTeamFlock(int Team, bool Mode); void ResetInvited(int Team); - void SetClientInvited(int Team, int ClientID, bool Invited); + void SetClientInvited(int Team, int ClientId, bool Invited); int GetDDRaceState(CPlayer *Player); int GetStartTime(CPlayer *Player); @@ -118,22 +120,22 @@ class CGameTeams void SetDDRaceState(CPlayer *Player, int DDRaceState); void SetStartTime(CPlayer *Player, int StartTime); void SetLastTimeCp(CPlayer *Player, int LastTimeCp); - void KillSavedTeam(int ClientID, int Team); - void ResetSavedTeam(int ClientID, int Team); + void KillSavedTeam(int ClientId, int Team); + void ResetSavedTeam(int ClientId, int Team); void RequestTeamSwap(CPlayer *pPlayer, CPlayer *pTargetPlayer, int Team); void SwapTeamCharacters(CPlayer *pPrimaryPlayer, CPlayer *pTargetPlayer, int Team); void ProcessSaveTeam(); int GetFirstEmptyTeam() const; - bool TeeStarted(int ClientID) + bool TeeStarted(int ClientId) { - return m_aTeeStarted[ClientID]; + return m_aTeeStarted[ClientId]; } - bool TeeFinished(int ClientID) + bool TeeFinished(int ClientId) { - return m_aTeeFinished[ClientID]; + return m_aTeeFinished[ClientId]; } int GetTeamState(int Team) @@ -149,9 +151,17 @@ class CGameTeams return m_aTeamLocked[Team]; } - bool IsInvited(int Team, int ClientID) + bool TeamFlock(int Team) { - return m_aInvited[Team].test(ClientID); + if(Team <= TEAM_FLOCK || Team >= TEAM_SUPER) + return false; + + return m_aTeamFlock[Team]; + } + + bool IsInvited(int Team, int ClientId) + { + return m_aInvited[Team].test(ClientId); } bool IsStarted(int Team) @@ -159,29 +169,29 @@ class CGameTeams return m_aTeamState[Team] == CGameTeams::TEAMSTATE_STARTED; } - void SetStarted(int ClientID, bool Started) + void SetStarted(int ClientId, bool Started) { - m_aTeeStarted[ClientID] = Started; + m_aTeeStarted[ClientId] = Started; } - void SetFinished(int ClientID, bool Finished) + void SetFinished(int ClientId, bool Finished) { - m_aTeeFinished[ClientID] = Finished; + m_aTeeFinished[ClientId] = Finished; } - void SetSaving(int TeamID, std::shared_ptr &SaveResult) + void SetSaving(int TeamId, std::shared_ptr &SaveResult) { - m_apSaveTeamResult[TeamID] = SaveResult; + m_apSaveTeamResult[TeamId] = SaveResult; } - bool GetSaving(int TeamID) + bool GetSaving(int TeamId) { - if(TeamID < TEAM_FLOCK || TeamID >= TEAM_SUPER) + if(TeamId < TEAM_FLOCK || TeamId >= TEAM_SUPER) return false; - if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && TeamID == TEAM_FLOCK) + if(g_Config.m_SvTeam != SV_TEAM_FORCED_SOLO && TeamId == TEAM_FLOCK) return false; - return m_apSaveTeamResult[TeamID] != nullptr; + return m_apSaveTeamResult[TeamId] != nullptr; } void SetPractice(int Team, bool Enabled) diff --git a/src/game/server/teehistorian.cpp b/src/game/server/teehistorian.cpp index 9fc9545c53..3316ae6d96 100644 --- a/src/game/server/teehistorian.cpp +++ b/src/game/server/teehistorian.cpp @@ -10,7 +10,7 @@ static const char TEEHISTORIAN_NAME[] = "teehistorian@ddnet.tw"; static const CUuid TEEHISTORIAN_UUID = CalculateUuid(TEEHISTORIAN_NAME); static const char TEEHISTORIAN_VERSION[] = "2"; -static const char TEEHISTORIAN_VERSION_MINOR[] = "6"; +static const char TEEHISTORIAN_VERSION_MINOR[] = "8"; #define UUID(id, name) static const CUuid UUID_##id = CalculateUuid(name); #include @@ -49,14 +49,14 @@ void CTeeHistorian::Reset(const CGameInfo *pGameInfo, WRITE_CALLBACK pfnWriteCal m_LastWrittenTick = 0; // Tick 0 is implicit at the start, game starts as tick 1. m_TickWritten = true; - m_MaxClientID = MAX_CLIENTS; + m_MaxClientId = MAX_CLIENTS; - // `m_PrevMaxClientID` is initialized in `BeginPlayers` + // `m_PrevMaxClientId` is initialized in `BeginPlayers` for(auto &PrevPlayer : m_aPrevPlayers) { PrevPlayer.m_Alive = false; // zero means no id - PrevPlayer.m_UniqueClientID = 0; + PrevPlayer.m_UniqueClientId = 0; PrevPlayer.m_Team = 0; } for(auto &PrevTeam : m_aPrevTeams) @@ -243,20 +243,20 @@ void CTeeHistorian::BeginPlayers() { dbg_assert(m_State == STATE_BEFORE_PLAYERS, "invalid teehistorian state"); - m_PrevMaxClientID = m_MaxClientID; + m_PrevMaxClientId = m_MaxClientId; // ensure that PLAYER_{DIFF, NEW, OLD} don't cause an implicit tick after a TICK_SKIP - // by not overwriting m_MaxClientID during RecordPlayer - m_MaxClientID = -1; + // by not overwriting m_MaxClientId during RecordPlayer + m_MaxClientId = -1; m_State = STATE_PLAYERS; } -void CTeeHistorian::EnsureTickWrittenPlayerData(int ClientID) +void CTeeHistorian::EnsureTickWrittenPlayerData(int ClientId) { - dbg_assert(ClientID > m_MaxClientID, "invalid player data order"); - m_MaxClientID = ClientID; + dbg_assert(ClientId > m_MaxClientId, "invalid player data order"); + m_MaxClientId = ClientId; - if(!m_TickWritten && (ClientID > m_PrevMaxClientID || m_LastWrittenTick + 1 != m_Tick)) + if(!m_TickWritten && (ClientId > m_PrevMaxClientId || m_LastWrittenTick + 1 != m_Tick)) { WriteTick(); } @@ -268,14 +268,14 @@ void CTeeHistorian::EnsureTickWrittenPlayerData(int ClientID) } } -void CTeeHistorian::RecordPlayer(int ClientID, const CNetObj_CharacterCore *pChar) +void CTeeHistorian::RecordPlayer(int ClientId, const CNetObj_CharacterCore *pChar) { dbg_assert(m_State == STATE_PLAYERS, "invalid teehistorian state"); - CTeehistorianPlayer *pPrev = &m_aPrevPlayers[ClientID]; + CTeehistorianPlayer *pPrev = &m_aPrevPlayers[ClientId]; if(!pPrev->m_Alive || pPrev->m_X != pChar->m_X || pPrev->m_Y != pChar->m_Y) { - EnsureTickWrittenPlayerData(ClientID); + EnsureTickWrittenPlayerData(ClientId); CPacker Buffer; Buffer.Reset(); @@ -283,12 +283,12 @@ void CTeeHistorian::RecordPlayer(int ClientID, const CNetObj_CharacterCore *pCha { int dx = pChar->m_X - pPrev->m_X; int dy = pChar->m_Y - pPrev->m_Y; - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); Buffer.AddInt(dx); Buffer.AddInt(dy); if(m_Debug) { - dbg_msg("teehistorian", "diff cid=%d dx=%d dy=%d", ClientID, dx, dy); + dbg_msg("teehistorian", "diff cid=%d dx=%d dy=%d", ClientId, dx, dy); } } else @@ -296,12 +296,12 @@ void CTeeHistorian::RecordPlayer(int ClientID, const CNetObj_CharacterCore *pCha int x = pChar->m_X; int y = pChar->m_Y; Buffer.AddInt(-TEEHISTORIAN_PLAYER_NEW); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); Buffer.AddInt(x); Buffer.AddInt(y); if(m_Debug) { - dbg_msg("teehistorian", "new cid=%d x=%d y=%d", ClientID, x, y); + dbg_msg("teehistorian", "new cid=%d x=%d y=%d", ClientId, x, y); } } Write(Buffer.Data(), Buffer.Size()); @@ -311,44 +311,44 @@ void CTeeHistorian::RecordPlayer(int ClientID, const CNetObj_CharacterCore *pCha pPrev->m_Alive = true; } -void CTeeHistorian::RecordDeadPlayer(int ClientID) +void CTeeHistorian::RecordDeadPlayer(int ClientId) { dbg_assert(m_State == STATE_PLAYERS, "invalid teehistorian state"); - CTeehistorianPlayer *pPrev = &m_aPrevPlayers[ClientID]; + CTeehistorianPlayer *pPrev = &m_aPrevPlayers[ClientId]; if(pPrev->m_Alive) { - EnsureTickWrittenPlayerData(ClientID); + EnsureTickWrittenPlayerData(ClientId); CPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_PLAYER_OLD); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); if(m_Debug) { - dbg_msg("teehistorian", "old cid=%d", ClientID); + dbg_msg("teehistorian", "old cid=%d", ClientId); } Write(Buffer.Data(), Buffer.Size()); } pPrev->m_Alive = false; } -void CTeeHistorian::RecordPlayerTeam(int ClientID, int Team) +void CTeeHistorian::RecordPlayerTeam(int ClientId, int Team) { - if(m_aPrevPlayers[ClientID].m_Team != Team) + if(m_aPrevPlayers[ClientId].m_Team != Team) { - m_aPrevPlayers[ClientID].m_Team = Team; + m_aPrevPlayers[ClientId].m_Team = Team; EnsureTickWritten(); CPacker Buffer; Buffer.Reset(); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); Buffer.AddInt(Team); if(m_Debug) { - dbg_msg("teehistorian", "player_team cid=%d team=%d", ClientID, Team); + dbg_msg("teehistorian", "player_team cid=%d team=%d", ClientId, Team); } WriteExtra(UUID_TEEHISTORIAN_PLAYER_TEAM, Buffer.Data(), Buffer.Size()); @@ -422,13 +422,13 @@ void CTeeHistorian::BeginInputs() m_State = STATE_INPUTS; } -void CTeeHistorian::RecordPlayerInput(int ClientID, uint32_t UniqueClientID, const CNetObj_PlayerInput *pInput) +void CTeeHistorian::RecordPlayerInput(int ClientId, uint32_t UniqueClientId, const CNetObj_PlayerInput *pInput) { CPacker Buffer; - CTeehistorianPlayer *pPrev = &m_aPrevPlayers[ClientID]; + CTeehistorianPlayer *pPrev = &m_aPrevPlayers[ClientId]; CNetObj_PlayerInput DiffInput; - if(pPrev->m_UniqueClientID == UniqueClientID) + if(pPrev->m_UniqueClientId == UniqueClientId) { if(mem_comp(&pPrev->m_Input, pInput, sizeof(pPrev->m_Input)) == 0) { @@ -442,7 +442,7 @@ void CTeeHistorian::RecordPlayerInput(int ClientID, uint32_t UniqueClientID, con if(m_Debug) { const int *pData = (const int *)&DiffInput; - dbg_msg("teehistorian", "diff_input cid=%d %d %d %d %d %d %d %d %d %d %d", ClientID, + dbg_msg("teehistorian", "diff_input cid=%d %d %d %d %d %d %d %d %d %d %d", ClientId, pData[0], pData[1], pData[2], pData[3], pData[4], pData[5], pData[6], pData[7], pData[8], pData[9]); } @@ -455,28 +455,28 @@ void CTeeHistorian::RecordPlayerInput(int ClientID, uint32_t UniqueClientID, con DiffInput = *pInput; if(m_Debug) { - dbg_msg("teehistorian", "new_input cid=%d", ClientID); + dbg_msg("teehistorian", "new_input cid=%d", ClientId); } } - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); for(size_t i = 0; i < sizeof(DiffInput) / sizeof(int32_t); i++) { Buffer.AddInt(((int *)&DiffInput)[i]); } - pPrev->m_UniqueClientID = UniqueClientID; + pPrev->m_UniqueClientId = UniqueClientId; pPrev->m_Input = *pInput; Write(Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordPlayerMessage(int ClientID, const void *pMsg, int MsgSize) +void CTeeHistorian::RecordPlayerMessage(int ClientId, const void *pMsg, int MsgSize) { EnsureTickWritten(); CPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_MESSAGE); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); Buffer.AddInt(MsgSize); Buffer.AddRaw(pMsg, MsgSize); @@ -484,16 +484,16 @@ void CTeeHistorian::RecordPlayerMessage(int ClientID, const void *pMsg, int MsgS { CUnpacker Unpacker; Unpacker.Reset(pMsg, MsgSize); - int MsgID = Unpacker.GetInt(); - int Sys = MsgID & 1; - MsgID >>= 1; - dbg_msg("teehistorian", "msg cid=%d sys=%d msgid=%d", ClientID, Sys, MsgID); + int MsgId = Unpacker.GetInt(); + int Sys = MsgId & 1; + MsgId >>= 1; + dbg_msg("teehistorian", "msg cid=%d sys=%d msgid=%d", ClientId, Sys, MsgId); } Write(Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordPlayerJoin(int ClientID, int Protocol) +void CTeeHistorian::RecordPlayerJoin(int ClientId, int Protocol) { dbg_assert(Protocol == PROTOCOL_6 || Protocol == PROTOCOL_7, "invalid version"); EnsureTickWritten(); @@ -501,10 +501,10 @@ void CTeeHistorian::RecordPlayerJoin(int ClientID, int Protocol) { CPacker Buffer; Buffer.Reset(); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); if(m_Debug) { - dbg_msg("teehistorian", "joinver%d cid=%d", Protocol == PROTOCOL_6 ? 6 : 7, ClientID); + dbg_msg("teehistorian", "joinver%d cid=%d", Protocol == PROTOCOL_6 ? 6 : 7, ClientId); } CUuid Uuid = Protocol == PROTOCOL_6 ? UUID_TEEHISTORIAN_JOINVER6 : UUID_TEEHISTORIAN_JOINVER7; WriteExtra(Uuid, Buffer.Data(), Buffer.Size()); @@ -513,74 +513,91 @@ void CTeeHistorian::RecordPlayerJoin(int ClientID, int Protocol) CPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_JOIN); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); if(m_Debug) { - dbg_msg("teehistorian", "join cid=%d", ClientID); + dbg_msg("teehistorian", "join cid=%d", ClientId); } Write(Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordPlayerRejoin(int ClientID) +void CTeeHistorian::RecordPlayerRejoin(int ClientId) { EnsureTickWritten(); CPacker Buffer; Buffer.Reset(); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); if(m_Debug) { - dbg_msg("teehistorian", "player_rejoin cid=%d", ClientID); + dbg_msg("teehistorian", "player_rejoin cid=%d", ClientId); } WriteExtra(UUID_TEEHISTORIAN_PLAYER_REJOIN, Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordPlayerReady(int ClientID) +void CTeeHistorian::RecordPlayerReady(int ClientId) { EnsureTickWritten(); CPacker Buffer; Buffer.Reset(); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); if(m_Debug) { - dbg_msg("teehistorian", "player_ready cid=%d", ClientID); + dbg_msg("teehistorian", "player_ready cid=%d", ClientId); } WriteExtra(UUID_TEEHISTORIAN_PLAYER_READY, Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordPlayerDrop(int ClientID, const char *pReason) +void CTeeHistorian::RecordPlayerDrop(int ClientId, const char *pReason) { EnsureTickWritten(); CPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_DROP); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); Buffer.AddString(pReason, 0); if(m_Debug) { - dbg_msg("teehistorian", "drop cid=%d reason='%s'", ClientID, pReason); + dbg_msg("teehistorian", "drop cid=%d reason='%s'", ClientId, pReason); } Write(Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordConsoleCommand(int ClientID, int FlagMask, const char *pCmd, IConsole::IResult *pResult) +void CTeeHistorian::RecordPlayerName(int ClientId, const char *pName) +{ + EnsureTickWritten(); + + CPacker Buffer; + Buffer.Reset(); + Buffer.AddInt(ClientId); + Buffer.AddString(pName, 0); + + if(m_Debug) + { + dbg_msg("teehistorian", "player_name cid=%d name='%s'", ClientId, pName); + } + + WriteExtra(UUID_TEEHISTORIAN_PLAYER_NAME, Buffer.Data(), Buffer.Size()); +} + +void CTeeHistorian::RecordConsoleCommand(int ClientId, int FlagMask, const char *pCmd, IConsole::IResult *pResult) { EnsureTickWritten(); CPacker Buffer; Buffer.Reset(); Buffer.AddInt(-TEEHISTORIAN_CONSOLE_COMMAND); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); Buffer.AddInt(FlagMask); Buffer.AddString(pCmd, 0); Buffer.AddInt(pResult->NumArguments()); @@ -591,7 +608,7 @@ void CTeeHistorian::RecordConsoleCommand(int ClientID, int FlagMask, const char if(m_Debug) { - dbg_msg("teehistorian", "ccmd cid=%d cmd='%s'", ClientID, pCmd); + dbg_msg("teehistorian", "ccmd cid=%d cmd='%s'", ClientId, pCmd); } Write(Buffer.Data(), Buffer.Size()); @@ -607,33 +624,33 @@ void CTeeHistorian::RecordTestExtra() WriteExtra(UUID_TEEHISTORIAN_TEST, "", 0); } -void CTeeHistorian::RecordPlayerSwap(int ClientID1, int ClientID2) +void CTeeHistorian::RecordPlayerSwap(int ClientId1, int ClientId2) { EnsureTickWritten(); CPacker Buffer; Buffer.Reset(); - Buffer.AddInt(ClientID1); - Buffer.AddInt(ClientID2); + Buffer.AddInt(ClientId1); + Buffer.AddInt(ClientId2); WriteExtra(UUID_TEEHISTORIAN_PLAYER_SWITCH, Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordTeamSaveSuccess(int Team, CUuid SaveID, const char *pTeamSave) +void CTeeHistorian::RecordTeamSaveSuccess(int Team, CUuid SaveId, const char *pTeamSave) { EnsureTickWritten(); CPacker Buffer; Buffer.Reset(); Buffer.AddInt(Team); - Buffer.AddRaw(&SaveID, sizeof(SaveID)); + Buffer.AddRaw(&SaveId, sizeof(SaveId)); Buffer.AddString(pTeamSave, 0); if(m_Debug) { - char aSaveID[UUID_MAXSTRSIZE]; - FormatUuid(SaveID, aSaveID, sizeof(aSaveID)); - dbg_msg("teehistorian", "save_success team=%d save_id=%s team_save='%s'", Team, aSaveID, pTeamSave); + char aSaveId[UUID_MAXSTRSIZE]; + FormatUuid(SaveId, aSaveId, sizeof(aSaveId)); + dbg_msg("teehistorian", "save_success team=%d save_id=%s team_save='%s'", Team, aSaveId, pTeamSave); } WriteExtra(UUID_TEEHISTORIAN_SAVE_SUCCESS, Buffer.Data(), Buffer.Size()); @@ -655,21 +672,21 @@ void CTeeHistorian::RecordTeamSaveFailure(int Team) WriteExtra(UUID_TEEHISTORIAN_SAVE_FAILURE, Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordTeamLoadSuccess(int Team, CUuid SaveID, const char *pTeamSave) +void CTeeHistorian::RecordTeamLoadSuccess(int Team, CUuid SaveId, const char *pTeamSave) { EnsureTickWritten(); CPacker Buffer; Buffer.Reset(); Buffer.AddInt(Team); - Buffer.AddRaw(&SaveID, sizeof(SaveID)); + Buffer.AddRaw(&SaveId, sizeof(SaveId)); Buffer.AddString(pTeamSave, 0); if(m_Debug) { - char aSaveID[UUID_MAXSTRSIZE]; - FormatUuid(SaveID, aSaveID, sizeof(aSaveID)); - dbg_msg("teehistorian", "load_success team=%d save_id=%s team_save='%s'", Team, aSaveID, pTeamSave); + char aSaveId[UUID_MAXSTRSIZE]; + FormatUuid(SaveId, aSaveId, sizeof(aSaveId)); + dbg_msg("teehistorian", "load_success team=%d save_id=%s team_save='%s'", Team, aSaveId, pTeamSave); } WriteExtra(UUID_TEEHISTORIAN_LOAD_SUCCESS, Buffer.Data(), Buffer.Size()); @@ -704,81 +721,81 @@ void CTeeHistorian::EndTick() m_State = STATE_BEFORE_TICK; } -void CTeeHistorian::RecordDDNetVersionOld(int ClientID, int DDNetVersion) +void CTeeHistorian::RecordDDNetVersionOld(int ClientId, int DDNetVersion) { CPacker Buffer; Buffer.Reset(); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); Buffer.AddInt(DDNetVersion); if(m_Debug) { - dbg_msg("teehistorian", "ddnetver_old cid=%d ddnet_version=%d", ClientID, DDNetVersion); + dbg_msg("teehistorian", "ddnetver_old cid=%d ddnet_version=%d", ClientId, DDNetVersion); } WriteExtra(UUID_TEEHISTORIAN_DDNETVER_OLD, Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordDDNetVersion(int ClientID, CUuid ConnectionID, int DDNetVersion, const char *pDDNetVersionStr) +void CTeeHistorian::RecordDDNetVersion(int ClientId, CUuid ConnectionId, int DDNetVersion, const char *pDDNetVersionStr) { CPacker Buffer; Buffer.Reset(); - Buffer.AddInt(ClientID); - Buffer.AddRaw(&ConnectionID, sizeof(ConnectionID)); + Buffer.AddInt(ClientId); + Buffer.AddRaw(&ConnectionId, sizeof(ConnectionId)); Buffer.AddInt(DDNetVersion); Buffer.AddString(pDDNetVersionStr, 0); if(m_Debug) { - char aConnnectionID[UUID_MAXSTRSIZE]; - FormatUuid(ConnectionID, aConnnectionID, sizeof(aConnnectionID)); - dbg_msg("teehistorian", "ddnetver cid=%d connection_id=%s ddnet_version=%d ddnet_version_str=%s", ClientID, aConnnectionID, DDNetVersion, pDDNetVersionStr); + char aConnnectionId[UUID_MAXSTRSIZE]; + FormatUuid(ConnectionId, aConnnectionId, sizeof(aConnnectionId)); + dbg_msg("teehistorian", "ddnetver cid=%d connection_id=%s ddnet_version=%d ddnet_version_str=%s", ClientId, aConnnectionId, DDNetVersion, pDDNetVersionStr); } WriteExtra(UUID_TEEHISTORIAN_DDNETVER, Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordAuthInitial(int ClientID, int Level, const char *pAuthName) +void CTeeHistorian::RecordAuthInitial(int ClientId, int Level, const char *pAuthName) { CPacker Buffer; Buffer.Reset(); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); Buffer.AddInt(Level); Buffer.AddString(pAuthName, 0); if(m_Debug) { - dbg_msg("teehistorian", "auth_init cid=%d level=%d auth_name=%s", ClientID, Level, pAuthName); + dbg_msg("teehistorian", "auth_init cid=%d level=%d auth_name=%s", ClientId, Level, pAuthName); } WriteExtra(UUID_TEEHISTORIAN_AUTH_INIT, Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordAuthLogin(int ClientID, int Level, const char *pAuthName) +void CTeeHistorian::RecordAuthLogin(int ClientId, int Level, const char *pAuthName) { CPacker Buffer; Buffer.Reset(); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); Buffer.AddInt(Level); Buffer.AddString(pAuthName, 0); if(m_Debug) { - dbg_msg("teehistorian", "auth_login cid=%d level=%d auth_name=%s", ClientID, Level, pAuthName); + dbg_msg("teehistorian", "auth_login cid=%d level=%d auth_name=%s", ClientId, Level, pAuthName); } WriteExtra(UUID_TEEHISTORIAN_AUTH_LOGIN, Buffer.Data(), Buffer.Size()); } -void CTeeHistorian::RecordAuthLogout(int ClientID) +void CTeeHistorian::RecordAuthLogout(int ClientId) { CPacker Buffer; Buffer.Reset(); - Buffer.AddInt(ClientID); + Buffer.AddInt(ClientId); if(m_Debug) { - dbg_msg("teehistorian", "auth_logout cid=%d", ClientID); + dbg_msg("teehistorian", "auth_logout cid=%d", ClientId); } WriteExtra(UUID_TEEHISTORIAN_AUTH_LOGOUT, Buffer.Data(), Buffer.Size()); @@ -794,6 +811,34 @@ void CTeeHistorian::RecordAntibot(const void *pData, int DataSize) WriteExtra(UUID_TEEHISTORIAN_ANTIBOT, pData, DataSize); } +void CTeeHistorian::RecordPlayerFinish(int ClientId, int TimeTicks) +{ + CPacker Buffer; + Buffer.Reset(); + Buffer.AddInt(ClientId); + Buffer.AddInt(TimeTicks); + if(m_Debug) + { + dbg_msg("teehistorian", "player_finish cid=%d time=%d", ClientId, TimeTicks); + } + + WriteExtra(UUID_TEEHISTORIAN_PLAYER_FINISH, Buffer.Data(), Buffer.Size()); +} + +void CTeeHistorian::RecordTeamFinish(int TeamId, int TimeTicks) +{ + CPacker Buffer; + Buffer.Reset(); + Buffer.AddInt(TeamId); + Buffer.AddInt(TimeTicks); + if(m_Debug) + { + dbg_msg("teehistorian", "team_finish cid=%d time=%d", TeamId, TimeTicks); + } + + WriteExtra(UUID_TEEHISTORIAN_TEAM_FINISH, Buffer.Data(), Buffer.Size()); +} + void CTeeHistorian::Finish() { dbg_assert(m_State == STATE_START || m_State == STATE_INPUTS || m_State == STATE_BEFORE_ENDTICK || m_State == STATE_BEFORE_TICK, "invalid teehistorian state"); diff --git a/src/game/server/teehistorian.h b/src/game/server/teehistorian.h index d4bc8d0275..46e97862f8 100644 --- a/src/game/server/teehistorian.h +++ b/src/game/server/teehistorian.h @@ -57,45 +57,49 @@ class CTeeHistorian void BeginTick(int Tick); void BeginPlayers(); - void RecordPlayer(int ClientID, const CNetObj_CharacterCore *pChar); - void RecordDeadPlayer(int ClientID); - void RecordPlayerTeam(int ClientID, int Team); + void RecordPlayer(int ClientId, const CNetObj_CharacterCore *pChar); + void RecordDeadPlayer(int ClientId); + void RecordPlayerTeam(int ClientId, int Team); void RecordTeamPractice(int Team, bool Practice); void EndPlayers(); void BeginInputs(); - void RecordPlayerInput(int ClientID, uint32_t UniqueClientID, const CNetObj_PlayerInput *pInput); - void RecordPlayerMessage(int ClientID, const void *pMsg, int MsgSize); - void RecordPlayerJoin(int ClientID, int Protocol); - void RecordPlayerRejoin(int ClientID); - void RecordPlayerReady(int ClientID); - void RecordPlayerDrop(int ClientID, const char *pReason); - void RecordConsoleCommand(int ClientID, int FlagMask, const char *pCmd, IConsole::IResult *pResult); + void RecordPlayerInput(int ClientId, uint32_t UniqueClientId, const CNetObj_PlayerInput *pInput); + void RecordPlayerMessage(int ClientId, const void *pMsg, int MsgSize); + void RecordPlayerJoin(int ClientId, int Protocol); + void RecordPlayerRejoin(int ClientId); + void RecordPlayerReady(int ClientId); + void RecordPlayerDrop(int ClientId, const char *pReason); + void RecordPlayerName(int ClientId, const char *pName); + void RecordConsoleCommand(int ClientId, int FlagMask, const char *pCmd, IConsole::IResult *pResult); void RecordTestExtra(); - void RecordPlayerSwap(int ClientID1, int ClientID2); - void RecordTeamSaveSuccess(int Team, CUuid SaveID, const char *pTeamSave); + void RecordPlayerSwap(int ClientId1, int ClientId2); + void RecordTeamSaveSuccess(int Team, CUuid SaveId, const char *pTeamSave); void RecordTeamSaveFailure(int Team); - void RecordTeamLoadSuccess(int Team, CUuid SaveID, const char *pTeamSave); + void RecordTeamLoadSuccess(int Team, CUuid SaveId, const char *pTeamSave); void RecordTeamLoadFailure(int Team); void EndInputs(); void EndTick(); - void RecordDDNetVersionOld(int ClientID, int DDNetVersion); - void RecordDDNetVersion(int ClientID, CUuid ConnectionID, int DDNetVersion, const char *pDDNetVersionStr); + void RecordDDNetVersionOld(int ClientId, int DDNetVersion); + void RecordDDNetVersion(int ClientId, CUuid ConnectionId, int DDNetVersion, const char *pDDNetVersionStr); - void RecordAuthInitial(int ClientID, int Level, const char *pAuthName); - void RecordAuthLogin(int ClientID, int Level, const char *pAuthName); - void RecordAuthLogout(int ClientID); + void RecordAuthInitial(int ClientId, int Level, const char *pAuthName); + void RecordAuthLogin(int ClientId, int Level, const char *pAuthName); + void RecordAuthLogout(int ClientId); void RecordAntibot(const void *pData, int DataSize); + void RecordPlayerFinish(int ClientId, int TimeTicks); + void RecordTeamFinish(int TeamId, int TimeTicks); + int m_Debug; // Possible values: 0, 1, 2. private: void WriteHeader(const CGameInfo *pGameInfo); void WriteExtra(CUuid Uuid, const void *pData, int DataSize); - void EnsureTickWrittenPlayerData(int ClientID); + void EnsureTickWrittenPlayerData(int ClientId); void EnsureTickWritten(); void WriteTick(); void Write(const void *pData, int DataSize); @@ -119,7 +123,7 @@ class CTeeHistorian int m_Y; CNetObj_PlayerInput m_Input; - uint32_t m_UniqueClientID; + uint32_t m_UniqueClientId; // DDNet team int m_Team; @@ -138,8 +142,8 @@ class CTeeHistorian int m_LastWrittenTick; bool m_TickWritten; int m_Tick; - int m_PrevMaxClientID; - int m_MaxClientID; + int m_PrevMaxClientId; + int m_MaxClientId; CTeehistorianPlayer m_aPrevPlayers[MAX_CLIENTS]; CTeam m_aPrevTeams[MAX_CLIENTS]; }; diff --git a/src/game/server/teeinfo.cpp b/src/game/server/teeinfo.cpp index 62e457c08d..5d4713f1ed 100644 --- a/src/game/server/teeinfo.cpp +++ b/src/game/server/teeinfo.cpp @@ -1,26 +1,27 @@ #include #include +#include #include "teeinfo.h" struct StdSkin { - char m_aSkinName[64]; + char m_aSkinName[24]; // body, marking, decoration, hands, feet, eyes - char m_apSkinPartNames[6][24]; - bool m_aUseCustomColors[6]; - int m_aSkinPartColors[6]; + char m_apSkinPartNames[protocol7::NUM_SKINPARTS][24]; + bool m_aUseCustomColors[protocol7::NUM_SKINPARTS]; + int m_aSkinPartColors[protocol7::NUM_SKINPARTS]; }; static StdSkin g_aStdSkins[] = { {"default", {"standard", "", "", "standard", "standard", "standard"}, {true, false, false, true, true, false}, {1798004, 0, 0, 1799582, 1869630, 0}}, - {"bluekitty", {"kitty", "whisker", "", "standard", "standard", "standard"}, {true, true, false, true, true, false}, {8681144, -8229413, 0, 7885547, 7885547, 0}}, + {"bluekitty", {"kitty", "whisker", "", "standard", "standard", "negative"}, {true, true, false, true, true, true}, {8681144, -8229413, 0, 7885547, 8868585, 9043712}}, {"bluestripe", {"standard", "stripes", "", "standard", "standard", "standard"}, {true, false, false, true, true, false}, {10187898, 0, 0, 750848, 1944919, 0}}, {"brownbear", {"bear", "bear", "hair", "standard", "standard", "standard"}, {true, true, false, true, true, false}, {1082745, -15634776, 0, 1082745, 1147174, 0}}, {"cammo", {"standard", "cammo2", "", "standard", "standard", "standard"}, {true, true, false, true, true, false}, {5334342, -11771603, 0, 750848, 1944919, 0}}, {"cammostripes", {"standard", "cammostripes", "", "standard", "standard", "standard"}, {true, true, false, true, true, false}, {5334342, -14840320, 0, 750848, 1944919, 0}}, {"coala", {"koala", "twinbelly", "", "standard", "standard", "standard"}, {true, true, false, true, true, false}, {184, -15397662, 0, 184, 9765959, 0}}, - {"limekitty", {"kitty", "whisker", "", "standard", "standard", "standard"}, {true, true, false, true, true, false}, {4612803, -12229920, 0, 3827951, 3827951, 0}}, + {"limekitty", {"kitty", "whisker", "", "standard", "standard", "negative"}, {true, true, false, true, true, true}, {4612803, -12229920, 0, 3827951, 3827951, 8256000}}, {"pinky", {"standard", "whisker", "", "standard", "standard", "standard"}, {true, true, false, true, true, false}, {15911355, -801066, 0, 15043034, 15043034, 0}}, {"redbopp", {"standard", "donny", "unibop", "standard", "standard", "standard"}, {true, true, true, true, true, false}, {16177260, -16590390, 16177260, 16177260, 7624169, 0}}, {"redstripe", {"standard", "stripe", "", "standard", "standard", "standard"}, {true, false, false, true, true, false}, {16307835, 0, 0, 184, 9765959, 0}}, @@ -38,9 +39,9 @@ CTeeInfo::CTeeInfo(const char *pSkinName, int UseCustomColor, int ColorBody, int m_ColorFeet = ColorFeet; } -CTeeInfo::CTeeInfo(const char *apSkinPartNames[6], const int *pUseCustomColors, const int *pSkinPartColors) +CTeeInfo::CTeeInfo(const char *apSkinPartNames[protocol7::NUM_SKINPARTS], const int *pUseCustomColors, const int *pSkinPartColors) { - for(int i = 0; i < 6; i++) + for(int i = 0; i < protocol7::NUM_SKINPARTS; i++) { str_copy(m_apSkinPartNames[i], apSkinPartNames[i], sizeof(m_apSkinPartNames[i])); m_aUseCustomColors[i] = pUseCustomColors[i]; @@ -51,7 +52,7 @@ CTeeInfo::CTeeInfo(const char *apSkinPartNames[6], const int *pUseCustomColors, void CTeeInfo::ToSixup() { // reset to default skin - for(int p = 0; p < 6; p++) + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) { str_copy(m_apSkinPartNames[p], g_aStdSkins[0].m_apSkinPartNames[p], 24); m_aUseCustomColors[p] = g_aStdSkins[0].m_aUseCustomColors[p]; @@ -63,7 +64,7 @@ void CTeeInfo::ToSixup() { if(!str_comp(m_aSkinName, StdSkin.m_aSkinName)) { - for(int p = 0; p < 6; p++) + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) { str_copy(m_apSkinPartNames[p], StdSkin.m_apSkinPartNames[p], 24); m_aUseCustomColors[p] = StdSkin.m_aUseCustomColors[p]; @@ -75,18 +76,18 @@ void CTeeInfo::ToSixup() if(m_UseCustomColor) { - int ColorBody = ColorHSLA(m_ColorBody).UnclampLighting().Pack(ms_DarkestLGT7); - int ColorFeet = ColorHSLA(m_ColorFeet).UnclampLighting().Pack(ms_DarkestLGT7); - m_aUseCustomColors[0] = true; - m_aUseCustomColors[1] = true; - m_aUseCustomColors[2] = true; - m_aUseCustomColors[3] = true; - m_aUseCustomColors[4] = true; - m_aSkinPartColors[0] = ColorBody; - m_aSkinPartColors[1] = 0x22FFFFFF; - m_aSkinPartColors[2] = ColorBody; - m_aSkinPartColors[3] = ColorBody; - m_aSkinPartColors[4] = ColorFeet; + int ColorBody = ColorHSLA(m_ColorBody).UnclampLighting(ColorHSLA::DARKEST_LGT).Pack(ColorHSLA::DARKEST_LGT7); + int ColorFeet = ColorHSLA(m_ColorFeet).UnclampLighting(ColorHSLA::DARKEST_LGT).Pack(ColorHSLA::DARKEST_LGT7); + m_aUseCustomColors[protocol7::SKINPART_BODY] = true; + m_aUseCustomColors[protocol7::SKINPART_MARKING] = true; + m_aUseCustomColors[protocol7::SKINPART_DECORATION] = true; + m_aUseCustomColors[protocol7::SKINPART_HANDS] = true; + m_aUseCustomColors[protocol7::SKINPART_FEET] = true; + m_aSkinPartColors[protocol7::SKINPART_BODY] = ColorBody; + m_aSkinPartColors[protocol7::SKINPART_MARKING] = 0x22FFFFFF; + m_aSkinPartColors[protocol7::SKINPART_DECORATION] = ColorBody; + m_aSkinPartColors[protocol7::SKINPART_HANDS] = ColorBody; + m_aSkinPartColors[protocol7::SKINPART_FEET] = ColorFeet; } } @@ -102,7 +103,7 @@ void CTeeInfo::FromSixup() for(auto &StdSkin : g_aStdSkins) { bool match = true; - for(int p = 0; p < 6; p++) + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) { if(str_comp(m_apSkinPartNames[p], StdSkin.m_apSkinPartNames[p]) || m_aUseCustomColors[p] != StdSkin.m_aUseCustomColors[p] || (m_aUseCustomColors[p] && m_aSkinPartColors[p] != StdSkin.m_aSkinPartColors[p])) { @@ -118,8 +119,8 @@ void CTeeInfo::FromSixup() } // find closest match - int best_skin = 0; - int best_matches = -1; + int BestSkin = 0; + int BestMatches = -1; for(int s = 0; s < 16; s++) { int matches = 0; @@ -127,15 +128,19 @@ void CTeeInfo::FromSixup() if(str_comp(m_apSkinPartNames[p], g_aStdSkins[s].m_apSkinPartNames[p]) == 0) matches++; - if(matches > best_matches) + if(matches > BestMatches) { - best_matches = matches; - best_skin = s; + BestMatches = matches; + BestSkin = s; } } - str_copy(m_aSkinName, g_aStdSkins[best_skin].m_aSkinName, sizeof(m_aSkinName)); + str_copy(m_aSkinName, g_aStdSkins[BestSkin].m_aSkinName, sizeof(m_aSkinName)); m_UseCustomColor = true; - m_ColorBody = ColorHSLA(m_aUseCustomColors[0] ? m_aSkinPartColors[0] : 255).UnclampLighting(ms_DarkestLGT7).Pack(ColorHSLA::DARKEST_LGT); - m_ColorFeet = ColorHSLA(m_aUseCustomColors[4] ? m_aSkinPartColors[4] : 255).UnclampLighting(ms_DarkestLGT7).Pack(ColorHSLA::DARKEST_LGT); + m_ColorBody = ColorHSLA(m_aUseCustomColors[protocol7::SKINPART_BODY] ? m_aSkinPartColors[protocol7::SKINPART_BODY] : 255) + .UnclampLighting(ColorHSLA::DARKEST_LGT7) + .Pack(ColorHSLA::DARKEST_LGT); + m_ColorFeet = ColorHSLA(m_aUseCustomColors[protocol7::SKINPART_FEET] ? m_aSkinPartColors[protocol7::SKINPART_FEET] : 255) + .UnclampLighting(ColorHSLA::DARKEST_LGT7) + .Pack(ColorHSLA::DARKEST_LGT); } diff --git a/src/game/server/teeinfo.h b/src/game/server/teeinfo.h index a2a72a46df..d3997cf420 100644 --- a/src/game/server/teeinfo.h +++ b/src/game/server/teeinfo.h @@ -4,9 +4,7 @@ class CTeeInfo { public: - constexpr static const float ms_DarkestLGT7 = 61 / 255.0f; - - char m_aSkinName[64] = {'\0'}; + char m_aSkinName[24] = {'\0'}; int m_UseCustomColor = 0; int m_ColorBody = 0; int m_ColorFeet = 0; diff --git a/src/game/teamscore.cpp b/src/game/teamscore.cpp index 0835a9814b..e4eed461a5 100644 --- a/src/game/teamscore.cpp +++ b/src/game/teamscore.cpp @@ -8,36 +8,36 @@ CTeamsCore::CTeamsCore() Reset(); } -bool CTeamsCore::SameTeam(int ClientID1, int ClientID2) const +bool CTeamsCore::SameTeam(int ClientId1, int ClientId2) const { - return m_aTeam[ClientID1] == TEAM_SUPER || m_aTeam[ClientID2] == TEAM_SUPER || m_aTeam[ClientID1] == m_aTeam[ClientID2]; + return m_aTeam[ClientId1] == TEAM_SUPER || m_aTeam[ClientId2] == TEAM_SUPER || m_aTeam[ClientId1] == m_aTeam[ClientId2]; } -int CTeamsCore::Team(int ClientID) const +int CTeamsCore::Team(int ClientId) const { - return m_aTeam[ClientID]; + return m_aTeam[ClientId]; } -void CTeamsCore::Team(int ClientID, int Team) +void CTeamsCore::Team(int ClientId, int Team) { dbg_assert(Team >= TEAM_FLOCK && Team <= TEAM_SUPER, "invalid team"); - m_aTeam[ClientID] = Team; + m_aTeam[ClientId] = Team; } -bool CTeamsCore::CanKeepHook(int ClientID1, int ClientID2) const +bool CTeamsCore::CanKeepHook(int ClientId1, int ClientId2) const { - if(m_aTeam[ClientID1] == (m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER) || m_aTeam[ClientID2] == (m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER) || ClientID1 == ClientID2) + if(m_aTeam[ClientId1] == (m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER) || m_aTeam[ClientId2] == (m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER) || ClientId1 == ClientId2) return true; - return m_aTeam[ClientID1] == m_aTeam[ClientID2]; + return m_aTeam[ClientId1] == m_aTeam[ClientId2]; } -bool CTeamsCore::CanCollide(int ClientID1, int ClientID2) const +bool CTeamsCore::CanCollide(int ClientId1, int ClientId2) const { - if(m_aTeam[ClientID1] == (m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER) || m_aTeam[ClientID2] == (m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER) || ClientID1 == ClientID2) + if(m_aTeam[ClientId1] == (m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER) || m_aTeam[ClientId2] == (m_IsDDRace16 ? VANILLA_TEAM_SUPER : TEAM_SUPER) || ClientId1 == ClientId2) return true; - if(m_aIsSolo[ClientID1] || m_aIsSolo[ClientID2]) + if(m_aIsSolo[ClientId1] || m_aIsSolo[ClientId2]) return false; - return m_aTeam[ClientID1] == m_aTeam[ClientID2]; + return m_aTeam[ClientId1] == m_aTeam[ClientId2]; } void CTeamsCore::Reset() @@ -54,15 +54,15 @@ void CTeamsCore::Reset() } } -void CTeamsCore::SetSolo(int ClientID, bool Value) +void CTeamsCore::SetSolo(int ClientId, bool Value) { - dbg_assert(ClientID >= 0 && ClientID < MAX_CLIENTS, "Invalid client id"); - m_aIsSolo[ClientID] = Value; + dbg_assert(ClientId >= 0 && ClientId < MAX_CLIENTS, "Invalid client id"); + m_aIsSolo[ClientId] = Value; } -bool CTeamsCore::GetSolo(int ClientID) const +bool CTeamsCore::GetSolo(int ClientId) const { - if(ClientID < 0 || ClientID >= MAX_CLIENTS) + if(ClientId < 0 || ClientId >= MAX_CLIENTS) return false; - return m_aIsSolo[ClientID]; + return m_aIsSolo[ClientId]; } diff --git a/src/game/teamscore.h b/src/game/teamscore.h index a37ded2ca8..4c82294824 100644 --- a/src/game/teamscore.h +++ b/src/game/teamscore.h @@ -8,7 +8,7 @@ enum { TEAM_FLOCK = 0, TEAM_SUPER = MAX_CLIENTS, - NUM_TEAMS = TEAM_SUPER + 1, + NUM_DDRACE_TEAMS = TEAM_SUPER + 1, VANILLA_TEAM_SUPER = VANILLA_MAX_CLIENTS }; @@ -31,17 +31,17 @@ class CTeamsCore CTeamsCore(); - bool SameTeam(int ClientID1, int ClientID2) const; + bool SameTeam(int ClientId1, int ClientId2) const; - bool CanKeepHook(int ClientID1, int ClientID2) const; - bool CanCollide(int ClientID1, int ClientID2) const; + bool CanKeepHook(int ClientId1, int ClientId2) const; + bool CanCollide(int ClientId1, int ClientId2) const; - int Team(int ClientID) const; - void Team(int ClientID, int Team); + int Team(int ClientId) const; + void Team(int ClientId, int Team); void Reset(); - void SetSolo(int ClientID, bool Value); - bool GetSolo(int ClientID) const; + void SetSolo(int ClientId, bool Value); + bool GetSolo(int ClientId) const; }; #endif diff --git a/src/game/version.h b/src/game/version.h index 195134e14a..61533632a2 100644 --- a/src/game/version.h +++ b/src/game/version.h @@ -3,14 +3,17 @@ #ifndef GAME_VERSION_H #define GAME_VERSION_H #ifndef GAME_RELEASE_VERSION -#define GAME_RELEASE_VERSION "17.4.2" +#define GAME_RELEASE_VERSION "18.7" #endif + +// teeworlds +#define CLIENT_VERSION7 0x0705 #define GAME_VERSION "0.6.4, " GAME_RELEASE_VERSION #define GAME_NETVERSION "0.6 626fce9a778df4d4" -#define DDNET_VERSION_NUMBER 17042 -#define GAME_STA_VERSION 001 -#define STA_VERSION "0.0.1" +#define GAME_NETVERSION7 "0.7 802f1be60a05665f" + +// ddnet +#define DDNET_VERSION_NUMBER 18070 extern const char *GIT_SHORTREV_HASH; #define GAME_NAME "DDNet" -#define STA_BUILD_DATE __DATE__ ", " __TIME__ #endif diff --git a/src/masterping/Cargo.toml b/src/masterping/Cargo.toml index d6bdc5dc21..0e1bf06123 100644 --- a/src/masterping/Cargo.toml +++ b/src/masterping/Cargo.toml @@ -2,7 +2,7 @@ name = "masterping" version = "0.0.1" authors = ["heinrich5991 "] -edition = "2018" +edition = "2021" publish = false license = "Zlib" diff --git a/src/mastersrv/Cargo.lock b/src/mastersrv/Cargo.lock index b5ba336209..ee42e554b3 100644 --- a/src/mastersrv/Cargo.lock +++ b/src/mastersrv/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "arrayvec" version = "0.5.2" @@ -59,16 +74,16 @@ dependencies = [ ] [[package]] -name = "bstr" -version = "0.2.17" +name = "bumpalo" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", - "serde", -] +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -76,12 +91,32 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cc" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "clap" version = "2.34.0" @@ -95,6 +130,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.2" @@ -114,28 +155,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "csv" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" -dependencies = [ - "bstr", - "csv-core", - "itoa 0.4.8", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" -dependencies = [ - "memchr", -] - [[package]] name = "digest" version = "0.10.3" @@ -310,7 +329,7 @@ checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" dependencies = [ "bytes", "fnv", - "itoa 1.0.1", + "itoa", ] [[package]] @@ -357,7 +376,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.1", + "itoa", "pin-project-lite", "socket2", "tokio", @@ -366,6 +385,29 @@ dependencies = [ "want", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.2.3" @@ -389,24 +431,24 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" -dependencies = [ - "serde", -] +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] -name = "itoa" -version = "1.0.1" +name = "js-sys" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] [[package]] name = "lazy_static" @@ -416,9 +458,24 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.125" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libloc" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" +checksum = "a2714dc2e1457b05760031b876cf46c7abf11b302278b59ebb7bbd37f8bc9145" +dependencies = [ + "chrono", + "ipnet", + "memmap2", + "yoke", + "yoke-derive", + "zerocopy", + "zerocopy-derive", +] [[package]] name = "log" @@ -437,12 +494,12 @@ dependencies = [ "base64", "bytes", "clap", - "csv", "env_logger", "headers", "hex", - "ipnet", + "libloc", "log", + "mime", "rand", "serde", "serde_json", @@ -465,6 +522,16 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" +dependencies = [ + "libc", + "stable_deref_trait", +] + [[package]] name = "mime" version = "0.3.16" @@ -512,6 +579,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.1" @@ -522,6 +598,12 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -545,7 +627,7 @@ checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -568,18 +650,18 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro2" -version = "1.0.37" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.18" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -625,12 +707,6 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - [[package]] name = "regex-syntax" version = "0.6.25" @@ -666,7 +742,7 @@ checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -676,7 +752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f972498cf015f7c0746cac89ebe1d6ef10c293b94175a243a2d9442c163d9944" dependencies = [ "indexmap", - "itoa 1.0.1", + "itoa", "ryu", "serde", ] @@ -688,7 +764,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.1", + "itoa", "ryu", "serde", ] @@ -731,6 +807,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.8.0" @@ -748,6 +830,28 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "term_size" version = "0.3.2" @@ -816,7 +920,7 @@ checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -885,7 +989,7 @@ checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.92", ] [[package]] @@ -924,6 +1028,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + [[package]] name = "unicode-normalization" version = "0.1.19" @@ -1008,6 +1118,60 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.68", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "winapi" version = "0.3.9" @@ -1038,3 +1202,126 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" diff --git a/src/mastersrv/Cargo.toml b/src/mastersrv/Cargo.toml index 6035363049..df249c2c54 100644 --- a/src/mastersrv/Cargo.toml +++ b/src/mastersrv/Cargo.toml @@ -2,7 +2,7 @@ name = "mastersrv" version = "0.0.1" authors = ["heinrich5991 "] -edition = "2018" +edition = "2021" publish = false license = "Zlib" @@ -16,12 +16,12 @@ clap = { version = "2.34.0", default-features = false, features = [ "suggestions", "wrap_help", ] } -csv = "1.1.6" env_logger = "0.8.3" headers = "0.3.7" hex = "0.4.3" -ipnet = { version = "2.5.0", features = ["serde"] } +libloc = "0.1.0" log = "0.4.17" +mime = "0.3.16" rand = "0.8.4" serde = { version = "1.0.126", features = ["derive"] } serde_json = { version = "1.0.64", features = [ diff --git a/src/mastersrv/src/locations.rs b/src/mastersrv/src/locations.rs index 269384bd69..99870e1308 100644 --- a/src/mastersrv/src/locations.rs +++ b/src/mastersrv/src/locations.rs @@ -1,49 +1,40 @@ use arrayvec::ArrayString; -use ipnet::Ipv4Net; -use serde::Deserialize; use std::net::IpAddr; use std::path::Path; pub type Location = ArrayString<[u8; 12]>; -#[derive(Deserialize)] -struct LocationRecord { - network: Ipv4Net, - location: Location, -} - +#[allow(dead_code)] // only used for `Debug` impl #[derive(Debug)] pub struct LocationsError(String); pub struct Locations { - locations: Vec, + inner: Option, } impl Locations { pub fn empty() -> Locations { Locations { - locations: Vec::new(), + inner: None, } } pub fn read(filename: &Path) -> Result { - let mut reader = csv::Reader::from_path(filename) + let inner = libloc::Locations::open(filename) .map_err(|e| LocationsError(format!("error opening {:?}: {}", filename, e)))?; - let locations: Result, _> = reader.deserialize().collect(); Ok(Locations { - locations: locations - .map_err(|e| LocationsError(format!("error deserializing: {}", e)))?, + inner: Some(inner), }) } pub fn lookup(&self, addr: IpAddr) -> Option { - let ipv4_addr = match addr { - IpAddr::V4(a) => a, - IpAddr::V6(_) => return None, // sad smiley - }; - for LocationRecord { network, location } in &self.locations { - if network.contains(&ipv4_addr) { - return Some(*location); - } - } - None + self.inner.as_ref().and_then(|inner| { + let country_code = inner.lookup(addr)?.country_code(); + let continent_code = inner.country(country_code)?.continent_code(); + let mut result = ArrayString::new(); + result.push_str(continent_code); + result.push_str(":"); + result.push_str(country_code); + result.make_ascii_lowercase(); + Some(result) + }) } } diff --git a/src/mastersrv/src/main.rs b/src/mastersrv/src/main.rs index d608fa4159..c16f065969 100644 --- a/src/mastersrv/src/main.rs +++ b/src/mastersrv/src/main.rs @@ -408,7 +408,7 @@ impl Servers { }); } hash_map::Entry::Occupied(mut o) => { - let mut server = &mut o.get_mut(); + let server = &mut o.get_mut(); if insert_addr { server.addresses.push(addr); server.addresses.sort_unstable(); @@ -823,8 +823,9 @@ fn register_from_headers( challenge_token: parse_opt(headers, "Challenge-Token")?, info_serial: parse(headers, "Info-Serial")?, info: if !info.is_empty() { - if headers.typed_get() != Some(headers::ContentType::json()) { - return Err(RegisterError::unsupported_media_type()); + match headers.typed_get::().map(mime::Mime::from) { + Some(mime) if mime.essence_str() == mime::APPLICATION_JSON => {} + _ => return Err(RegisterError::unsupported_media_type()), } Some(json::from_slice(info).map_err(|e| { RegisterError::new(format!("Request body deserialize error: {}", e)) @@ -897,7 +898,7 @@ async fn main() { .arg(Arg::with_name("locations") .long("locations") .value_name("LOCATIONS") - .help("IP to continent locations database filename (CSV file with network,continent_code header).") + .help("IP to continent locations database filename (libloc format, can be obtained from https://location.ipfire.org/databases/1/location.db.xz).") ) .arg(Arg::with_name("write-addresses") .long("write-addresses") @@ -1008,11 +1009,8 @@ async fn main() { addr.unwrap().ip() }; if let IpAddr::V6(v6) = addr { - if let Some(v4) = v6.to_ipv4() { - // TODO: switch to `to_ipv4_mapped` in the future. - if !v6.is_loopback() { - addr = IpAddr::from(v4); - } + if let Some(v4) = v6.to_ipv4_mapped() { + addr = IpAddr::from(v4); } } Ok(addr) diff --git a/src/rust-bridge/cpp/console.cpp b/src/rust-bridge/cpp/console.cpp index f4eeaac143..368ec06330 100644 --- a/src/rust-bridge/cpp/console.cpp +++ b/src/rust-bridge/cpp/console.cpp @@ -1,5 +1,5 @@ -#include "engine/console.h" #include "base/rust.h" +#include "engine/console.h" #include "engine/rust.h" #include #include @@ -16,14 +16,10 @@ inline namespace cxxbridge1 { #define CXXBRIDGE1_IS_COMPLETE namespace detail { namespace { -template -struct is_complete : std::false_type -{ -}; -template -struct is_complete : std::true_type -{ -}; +template +struct is_complete : std::false_type {}; +template +struct is_complete : std::true_type {}; } // namespace } // namespace detail #endif // CXXBRIDGE1_IS_COMPLETE @@ -31,67 +27,47 @@ struct is_complete : std::true_type #ifndef CXXBRIDGE1_RELOCATABLE #define CXXBRIDGE1_RELOCATABLE namespace detail { -template -struct make_void -{ - using type = void; +template +struct make_void { + using type = void; }; -template +template using void_t = typename make_void::type; -template class, typename...> -struct detect : std::false_type -{ -}; -template class T, typename... A> -struct detect>, T, A...> : std::true_type -{ -}; +template class, typename...> +struct detect : std::false_type {}; +template