diff --git a/.clang-tidy b/.clang-tidy index 536ea1ca463..d41c71378bc 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -81,7 +81,6 @@ Checks: > -readability-implicit-bool-conversion, -readability-isolate-declaration, -readability-magic-numbers, - -readability-make-member-function-const, -readability-named-parameter, -readability-non-const-parameter, -readability-simplify-boolean-expr, diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e09f0a7431..07e51c3c469 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1917,6 +1917,7 @@ set_src(ENGINE_INTERFACE GLOB src/engine keys.h map.h message.h + notifications.h rust.h server.h serverbrowser.h @@ -2190,6 +2191,7 @@ if(CLIENT) updater.h video.cpp video.h + warning.cpp ) set_src(GAME_CLIENT GLOB_RECURSE src/game/client animstate.cpp @@ -2366,13 +2368,22 @@ if(CLIENT) component.h editor.cpp editor.h + editor_action.h + editor_actions.cpp + editor_actions.h + editor_history.cpp + editor_history.h editor_object.cpp editor_object.h + editor_props.cpp + editor_trackers.cpp + editor_trackers.h explanations.cpp map_grid.cpp map_grid.h map_view.cpp map_view.h + mapitems.h mapitems/envelope.cpp mapitems/envelope.h mapitems/image.cpp @@ -3161,15 +3172,12 @@ if(NOT DEV) if(ANTIBOT) install(TARGETS ${TARGET_ANTIBOT} DESTINATION ${CMAKE_INSTALL_LIBDIR}/ddnet COMPONENT server) endif() - install(TARGETS ${TARGETS_TOOLS} DESTINATION ${CMAKE_INSTALL_LIBDIR}/chillerbot-ux COMPONENT tools) + install(TARGETS ${TARGETS_TOOLS} DESTINATION ${CMAKE_INSTALL_LIBDIR}/ddnet COMPONENT tools) install(FILES other/chillerbot-ux.desktop DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications COMPONENT client) - install(FILES other/chillerbot-ux.appdata.xml DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo COMPONENT client) - if(NOT CURSES_CLIENT) - foreach(SIZE 16 32 48 256) - install(FILES other/icons/chillerbot-ux_${SIZE}x${SIZE}x32.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${SIZE}x${SIZE}/apps RENAME chillerbot-ux.png COMPONENT client) - install(FILES other/icons/DDNet-Server_${SIZE}x${SIZE}x32.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${SIZE}x${SIZE}/apps RENAME ddnet-server.png COMPONENT server) - endforeach() - endif() + foreach(SIZE 16 32 48 256) + install(FILES other/icons/chillerbot-ux_${SIZE}x${SIZE}x32.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${SIZE}x${SIZE}/apps RENAME chillerbot-ux.png COMPONENT client) + install(FILES other/icons/DDNet-Server_${SIZE}x${SIZE}x32.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/${SIZE}x${SIZE}/apps RENAME ddnet-server.png COMPONENT server) + endforeach() foreach(file ${VULKAN_SHADER_FILE_LIST}) install(FILES ${PROJECT_BINARY_DIR}/${file} DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/ddnet/data/shader/vulkan COMPONENT client) endforeach() diff --git a/data/languages/brazilian_portuguese.txt b/data/languages/brazilian_portuguese.txt index 9f0a663caf5..5f34f8d9947 100644 --- a/data/languages/brazilian_portuguese.txt +++ b/data/languages/brazilian_portuguese.txt @@ -29,6 +29,7 @@ # Rafael Fontenelle 2023-07-07 12:11:00 # Rafael Fontenelle 2023-08-14 14:17:00 # Rafael Fontenelle 2023-09-25 14:23:00 +# Rafael Fontenelle 2023-12-05 17:01:00 ##### /authors ##### ##### translated strings ##### @@ -1723,56 +1724,56 @@ Render cut to video == Renderizar corte do vídeo Error playing demo -== +== Erro ao reproduzir demo Some map images could not be loaded. Check the local console for details. -== +== Algumas imagens de mapa não puderam ser carregadas. Verifique o console local para detalhes. Some map sounds could not be loaded. Check the local console for details. -== +== Alguns sons de mapa não puderam ser carregadas. Verifique o console local para detalhes. Loading menu themes -== +== Carregando temas de menu Render complete -== +== Renderização concluída Videos directory -== +== Diretório de vídeos Video was saved to '%s' -== +== O vídeo foi salvo em "%s" No demo selected -== +== Nenhuma demo selecionada Created -== +== Criada Netversion -== +== Netversion [Demo details] map not included -== +== mapa não incluído Ghosts directory -== +== Diretório de fantasmas Activate all -== +== Ativar tudo Deactivate all -== +== Desativar tudo Enable ghost -== +== Habilitar fantasma Only save improvements -== +== Salvar somente melhorias Regular background color -== +== Cor de fundo regular Entities background color -== +== Cor de fundo de entidades diff --git a/data/shader/tile_border.frag b/data/shader/tile_border.frag index a186901c317..2182dd97967 100644 --- a/data/shader/tile_border.frag +++ b/data/shader/tile_border.frag @@ -9,7 +9,7 @@ uniform sampler2DArray gTextureSampler; uniform vec4 gVertColor; #ifdef TW_TILE_TEXTURED -noperspective in vec3 TexCoord; +noperspective centroid in vec3 TexCoord; #endif out vec4 FragClr; diff --git a/data/shader/vulkan/tile_border.frag b/data/shader/vulkan/tile_border.frag index 31166b66155..841a3f4d4d8 100644 --- a/data/shader/vulkan/tile_border.frag +++ b/data/shader/vulkan/tile_border.frag @@ -10,7 +10,7 @@ layout(push_constant) uniform SVertexColorBO { } gColorBO; #ifdef TW_TILE_TEXTURED -layout (location = 0) noperspective in vec3 TexCoord; +layout (location = 0) noperspective centroid in vec3 TexCoord; #endif layout (location = 0) out vec4 FragClr; diff --git a/datasrc/compile.py b/datasrc/compile.py index 5e9a93b6c9b..80cd109242f 100644 --- a/datasrc/compile.py +++ b/datasrc/compile.py @@ -126,6 +126,7 @@ def gen_network_source(): print("""\ #include "protocol.h" +#include #include #include #include diff --git a/datasrc/network.py b/datasrc/network.py index 89855665182..7ad2c0244ea 100644 --- a/datasrc/network.py +++ b/datasrc/network.py @@ -78,10 +78,6 @@ }; ''' -RawSource = ''' -#include "protocol.h" -''' - Enums = [ Enum("EMOTE", Emotes), Enum("POWERUP", Powerups), @@ -439,7 +435,7 @@ ]), NetMessage("Sv_VoteSet", [ - NetIntRange("m_Timeout", 0, 60), + NetIntRange("m_Timeout", 0, 'max_int'), NetStringStrict("m_pDescription"), NetStringStrict("m_pReason"), ]), @@ -566,4 +562,14 @@ NetBool("m_RecordPersonal"), NetBool("m_RecordServer", default=False), ]), + + NetMessageEx("Sv_CommandInfo", "commandinfo@netmsg.ddnet.org", [ + NetStringStrict("m_pName"), + NetStringStrict("m_pArgsFormat"), + NetStringStrict("m_pHelpText") + ]), + + NetMessageEx("Sv_CommandInfoRemove", "commandinfo-remove@netmsg.ddnet.org", [ + NetStringStrict("m_pName") + ]), ] diff --git a/datasrc/seven/compile.py b/datasrc/seven/compile.py index 551901a1cd9..b24e2cdee5d 100644 --- a/datasrc/seven/compile.py +++ b/datasrc/seven/compile.py @@ -171,6 +171,7 @@ class CNetObjHandler lines = [] lines += ['#include "protocol7.h"'] + lines += ['#include '] lines += ['#include '] lines += ['#include '] diff --git a/datasrc/seven/network.py b/datasrc/seven/network.py index 8a041afba67..0a23dc6bf20 100644 --- a/datasrc/seven/network.py +++ b/datasrc/seven/network.py @@ -55,10 +55,6 @@ }; ''' -RawSource = ''' -#include "protocol.h" -''' - Enums = [ Pickups, Emotes, diff --git a/other/manifest/client.manifest.in b/other/manifest/client.manifest.in index 802656614e1..dca4cf522ee 100644 --- a/other/manifest/client.manifest.in +++ b/other/manifest/client.manifest.in @@ -11,16 +11,10 @@ - - - - - - - - - - + + + + diff --git a/src/antibot/antibot_data.h b/src/antibot/antibot_data.h index ea244aa194a..f65f6262878 100644 --- a/src/antibot/antibot_data.h +++ b/src/antibot/antibot_data.h @@ -1,7 +1,6 @@ #ifndef ANTIBOT_ANTIBOT_DATA_H #define ANTIBOT_ANTIBOT_DATA_H -#include #include enum diff --git a/src/base/detect.h b/src/base/detect.h index 90eeeae787a..8bceed08616 100644 --- a/src/base/detect.h +++ b/src/base/detect.h @@ -195,4 +195,12 @@ #define CONF_ARCH_STRING "unknown" #endif +#if defined(CONF_ARCH_ENDIAN_LITTLE) +#define CONF_ARCH_ENDIAN_STRING "little endian" +#elif defined(CONF_ARCH_ENDIAN_BIG) +#define CONF_ARCH_ENDIAN_STRING "big endian" +#else +#error "Unsupported endianness" +#endif + #endif diff --git a/src/base/hash_ctxt.h b/src/base/hash_ctxt.h index 2d382a75f38..e407f04c3f9 100644 --- a/src/base/hash_ctxt.h +++ b/src/base/hash_ctxt.h @@ -2,7 +2,6 @@ #define BASE_HASH_CTXT_H #include "hash.h" -#include "system.h" #include #if defined(CONF_OPENSSL) diff --git a/src/base/system.cpp b/src/base/system.cpp index 821febc89d3..f43f76e06b0 100644 --- a/src/base/system.cpp +++ b/src/base/system.cpp @@ -182,7 +182,7 @@ bool dbg_assert_has_failed() return dbg_assert_failing.load(std::memory_order_acquire); } -void dbg_assert_imp(const char *filename, int line, int test, const char *msg) +void dbg_assert_imp(const char *filename, int line, bool test, const char *msg) { if(!test) { @@ -1470,7 +1470,7 @@ std::string windows_format_system_message(unsigned long error) std::optional message = windows_wide_to_utf8(wide_message); LocalFree(wide_message); - return message.value_or("invalid error"); + return message.value_or("(invalid UTF-16 in error message)"); } #endif @@ -1560,14 +1560,17 @@ NETSOCKET net_udp_create(NETADDR bindaddr) /* set broadcast */ 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()); + } } } } @@ -1605,15 +1608,21 @@ NETSOCKET net_udp_create(NETADDR bindaddr) /* set broadcast */ 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 } } @@ -4439,7 +4448,8 @@ void os_locale_str(char *locale, size_t length) dbg_assert(GetUserDefaultLocaleName(wide_buffer, std::size(wide_buffer)) > 0, "GetUserDefaultLocaleName failure"); const std::optional buffer = windows_wide_to_utf8(wide_buffer); - str_copy(locale, buffer.value_or("en-US").c_str(), length); + dbg_assert(buffer.has_value(), "GetUserDefaultLocaleName returned invalid UTF-16"); + str_copy(locale, buffer.value().c_str(), length); #elif defined(CONF_PLATFORM_MACOS) CFLocaleRef locale_ref = CFLocaleCopyCurrent(); CFStringRef locale_identifier_ref = static_cast(CFLocaleGetValue(locale_ref, kCFLocaleIdentifier)); diff --git a/src/base/system.h b/src/base/system.h index 507ac749f84..76ba07cf90a 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -9,6 +9,7 @@ #define BASE_SYSTEM_H #include "detect.h" +#include "types.h" #ifndef __USE_GNU #define __USE_GNU @@ -73,7 +74,7 @@ * @see dbg_break */ #define dbg_assert(test, msg) dbg_assert_imp(__FILE__, __LINE__, test, msg) -void dbg_assert_imp(const char *filename, int line, int test, const char *msg); +void dbg_assert_imp(const char *filename, int line, bool test, const char *msg); #ifdef __clang_analyzer__ #include @@ -226,12 +227,8 @@ enum IOSEEK_START = 0, IOSEEK_CUR = 1, IOSEEK_END = 2, - - IO_MAX_PATH_LENGTH = 512, }; -typedef void *IOHANDLE; - /** * Opens a file. * @@ -741,43 +738,11 @@ ETimeSeason time_season(); * @defgroup Network-General */ -/** - * @ingroup Network-General - */ -typedef struct NETSOCKET_INTERNAL *NETSOCKET; - -/** - * @ingroup Network-General - */ -enum -{ - NETADDR_MAXSTRSIZE = 1 + (8 * 4 + 7) + 1 + 1 + 5 + 1, // [XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX]:XXXXX - - NETTYPE_LINK_BROADCAST = 4, - - NETTYPE_INVALID = 0, - NETTYPE_IPV4 = 1, - NETTYPE_IPV6 = 2, - NETTYPE_WEBSOCKET_IPV4 = 8, - - NETTYPE_ALL = NETTYPE_IPV4 | NETTYPE_IPV6 | NETTYPE_WEBSOCKET_IPV4, - NETTYPE_MASK = NETTYPE_ALL | NETTYPE_LINK_BROADCAST, -}; +extern const NETADDR NETADDR_ZEROED; /** * @ingroup Network-General */ -typedef struct NETADDR -{ - unsigned int type; - unsigned char ip[16]; - unsigned short port; - - bool operator==(const NETADDR &other) const; - bool operator!=(const NETADDR &other) const { return !(*this == other); } -} NETADDR; - -extern const NETADDR NETADDR_ZEROED; #ifdef CONF_FAMILY_UNIX /** @@ -1810,16 +1775,8 @@ void str_escape(char **dst, const char *src, const char *end); * * @remark The strings are treated as zero-terminated strings. */ -typedef int (*FS_LISTDIR_CALLBACK)(const char *name, int is_dir, int dir_type, void *user); void fs_listdir(const char *dir, FS_LISTDIR_CALLBACK cb, int type, void *user); -typedef struct -{ - const char *m_pName; - time_t m_TimeCreated; // seconds since UNIX Epoch - time_t m_TimeModified; // seconds since UNIX Epoch -} CFsFileInfo; - /** * Lists the files and folders in a directory and gets additional file information. * @@ -1832,7 +1789,6 @@ typedef struct * * @remark The strings are treated as zero-terminated strings. */ -typedef int (*FS_LISTDIR_CALLBACK_FILEINFO)(const CFsFileInfo *info, int is_dir, int dir_type, void *user); void fs_listdir_fileinfo(const char *dir, FS_LISTDIR_CALLBACK_FILEINFO cb, int type, void *user); /** @@ -2183,14 +2139,6 @@ int str_isallnum_hex(const char *str); unsigned str_quickhash(const char *str); -enum -{ - /** - * The maximum bytes necessary to encode one Unicode codepoint with UTF-8. - */ - UTF8_BYTE_LENGTH = 4, -}; - int str_utf8_to_skeleton(const char *str, int *buf, int buf_len); /* diff --git a/src/base/types.h b/src/base/types.h index ace9dc2f7c8..ffa1b47ed2c 100644 --- a/src/base/types.h +++ b/src/base/types.h @@ -1,6 +1,8 @@ #ifndef BASE_TYPES_H #define BASE_TYPES_H +#include + enum class TRISTATE { NONE, @@ -8,4 +10,56 @@ enum class TRISTATE ALL, }; +typedef void *IOHANDLE; + +typedef int (*FS_LISTDIR_CALLBACK)(const char *name, int is_dir, int dir_type, void *user); + +typedef struct +{ + const char *m_pName; + time_t m_TimeCreated; // seconds since UNIX Epoch + time_t m_TimeModified; // seconds since UNIX Epoch +} CFsFileInfo; + +typedef int (*FS_LISTDIR_CALLBACK_FILEINFO)(const CFsFileInfo *info, int is_dir, int dir_type, void *user); + +/** + * @ingroup Network-General + */ +typedef struct NETSOCKET_INTERNAL *NETSOCKET; + +enum +{ + /** + * The maximum bytes necessary to encode one Unicode codepoint with UTF-8. + */ + UTF8_BYTE_LENGTH = 4, + + IO_MAX_PATH_LENGTH = 512, + + NETADDR_MAXSTRSIZE = 1 + (8 * 4 + 7) + 1 + 1 + 5 + 1, // [XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX]:XXXXX + + NETTYPE_LINK_BROADCAST = 4, + + NETTYPE_INVALID = 0, + NETTYPE_IPV4 = 1, + NETTYPE_IPV6 = 2, + NETTYPE_WEBSOCKET_IPV4 = 8, + + NETTYPE_ALL = NETTYPE_IPV4 | NETTYPE_IPV6 | NETTYPE_WEBSOCKET_IPV4, + NETTYPE_MASK = NETTYPE_ALL | NETTYPE_LINK_BROADCAST, +}; + +/** + * @ingroup Network-General + */ +typedef struct NETADDR +{ + unsigned int type; + unsigned char ip[16]; + unsigned short port; + + bool operator==(const NETADDR &other) const; + bool operator!=(const NETADDR &other) const { return !(*this == other); } +} NETADDR; #endif // BASE_TYPES_H diff --git a/src/engine/client.h b/src/engine/client.h index a9b80c04dae..ef2fe4562cd 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -330,7 +330,7 @@ class IGameClient : public IInterface virtual int OnSnapInput(int *pData, bool Dummy, bool Force) = 0; virtual void OnDummySwap() = 0; virtual void SendDummyInfo(bool Start) = 0; - virtual int GetLastRaceTick() = 0; + virtual int GetLastRaceTick() const = 0; virtual const char *GetItemName(int Type) const = 0; virtual const char *Version() const = 0; @@ -341,8 +341,8 @@ class IGameClient : public IInterface virtual void OnDummyDisconnect() = 0; virtual void DummyResetInput() = 0; virtual void Echo(const char *pString) = 0; - virtual bool CanDisplayWarning() = 0; - virtual bool IsDisplayingWarning() = 0; + virtual bool CanDisplayWarning() const = 0; + virtual bool IsDisplayingWarning() const = 0; virtual CNetObjHandler *GetNetObjHandler() = 0; }; diff --git a/src/engine/client/backend/backend_base.cpp b/src/engine/client/backend/backend_base.cpp index 9e2ad5c4d3b..168598064fb 100644 --- a/src/engine/client/backend/backend_base.cpp +++ b/src/engine/client/backend/backend_base.cpp @@ -1,4 +1,5 @@ #include "backend_base.h" +#include #include void *CCommandProcessorFragment_GLBase::Resize(const unsigned char *pData, int Width, int Height, int NewWidth, int NewHeight, int BPP) diff --git a/src/engine/client/backend/backend_base.h b/src/engine/client/backend/backend_base.h index 25af6a68412..19b7eba6f27 100644 --- a/src/engine/client/backend/backend_base.h +++ b/src/engine/client/backend/backend_base.h @@ -15,7 +15,7 @@ struct SBackendCapabilites; -enum EDebugGFXModes +enum EDebugGfxModes { DEBUG_GFX_MODE_NONE = 0, DEBUG_GFX_MODE_MINIMUM, @@ -32,7 +32,7 @@ enum ERunCommandReturnTypes RUN_COMMAND_COMMAND_ERROR, }; -enum EGFXErrorType +enum EGfxErrorType { GFX_ERROR_TYPE_NONE = 0, GFX_ERROR_TYPE_INIT, @@ -46,7 +46,7 @@ enum EGFXErrorType GFX_ERROR_TYPE_UNKNOWN, }; -enum EGFXWarningType +enum EGfxWarningType { GFX_WARNING_TYPE_NONE = 0, GFX_WARNING_TYPE_INIT_FAILED, @@ -56,7 +56,7 @@ enum EGFXWarningType GFX_WARNING_TYPE_UNKNOWN, }; -struct SGFXErrorContainer +struct SGfxErrorContainer { struct SError { @@ -68,21 +68,21 @@ struct SGFXErrorContainer return m_RequiresTranslation == Other.m_RequiresTranslation && m_Err == Other.m_Err; } }; - EGFXErrorType m_ErrorType = EGFXErrorType::GFX_ERROR_TYPE_NONE; + EGfxErrorType m_ErrorType = EGfxErrorType::GFX_ERROR_TYPE_NONE; std::vector m_vErrors; }; -struct SGFXWarningContainer +struct SGfxWarningContainer { - EGFXWarningType m_WarningType = EGFXWarningType::GFX_WARNING_TYPE_NONE; + EGfxWarningType m_WarningType = EGfxWarningType::GFX_WARNING_TYPE_NONE; std::vector m_vWarnings; }; class CCommandProcessorFragment_GLBase { protected: - SGFXErrorContainer m_Error; - SGFXWarningContainer m_Warning; + SGfxErrorContainer m_Error; + SGfxWarningContainer m_Warning; static void *Resize(const unsigned char *pData, int Width, int Height, int NewWidth, int NewHeight, int BPP); @@ -97,10 +97,10 @@ class CCommandProcessorFragment_GLBase virtual void StartCommands(size_t CommandCount, size_t EstimatedRenderCallCount) {} virtual void EndCommands() {} - const SGFXErrorContainer &GetError() { return m_Error; } + const SGfxErrorContainer &GetError() { return m_Error; } virtual void ErroneousCleanup() {} - const SGFXWarningContainer &GetWarning() { return m_Warning; } + const SGfxWarningContainer &GetWarning() { return m_Warning; } enum { diff --git a/src/engine/client/backend/glsl_shader_compiler.cpp b/src/engine/client/backend/glsl_shader_compiler.cpp index 1077221acd3..f4c6fa095d8 100644 --- a/src/engine/client/backend/glsl_shader_compiler.cpp +++ b/src/engine/client/backend/glsl_shader_compiler.cpp @@ -84,6 +84,8 @@ void CGLSLCompiler::ParseLine(std::string &Line, const char *pReadLine, EGLSLSha //search for 'in' or 'out' while(*pBuff && ((*pBuff != 'i' || *(pBuff + 1) != 'n') && (*pBuff != 'o' || (*(pBuff + 1) && *(pBuff + 1) != 'u') || *(pBuff + 2) != 't'))) { + // append anything that is inbetween noperspective & in/out vars + Line.push_back(*pBuff); ++pBuff; } diff --git a/src/engine/client/backend/opengl/opengl_sl.cpp b/src/engine/client/backend/opengl/opengl_sl.cpp index a17335a29d3..046e4168d73 100644 --- a/src/engine/client/backend/opengl/opengl_sl.cpp +++ b/src/engine/client/backend/opengl/opengl_sl.cpp @@ -142,12 +142,12 @@ void CGLSL::DeleteShader() glDeleteShader(m_ShaderID); } -bool CGLSL::IsLoaded() +bool CGLSL::IsLoaded() const { return m_IsLoaded; } -TWGLuint CGLSL::GetShaderID() +TWGLuint CGLSL::GetShaderID() const { return m_ShaderID; } diff --git a/src/engine/client/backend/opengl/opengl_sl.h b/src/engine/client/backend/opengl/opengl_sl.h index 258c8cd3d38..3c2a6400237 100644 --- a/src/engine/client/backend/opengl/opengl_sl.h +++ b/src/engine/client/backend/opengl/opengl_sl.h @@ -20,8 +20,8 @@ class CGLSL bool LoadShader(CGLSLCompiler *pCompiler, class IStorage *pStorage, const char *pFile, int Type); void DeleteShader(); - bool IsLoaded(); - TWGLuint GetShaderID(); + bool IsLoaded() const; + TWGLuint GetShaderID() const; CGLSL(); virtual ~CGLSL(); diff --git a/src/engine/client/backend/opengl/opengl_sl_program.cpp b/src/engine/client/backend/opengl/opengl_sl_program.cpp index e14215a1d9b..2ba94325b15 100644 --- a/src/engine/client/backend/opengl/opengl_sl_program.cpp +++ b/src/engine/client/backend/opengl/opengl_sl_program.cpp @@ -25,7 +25,7 @@ void CGLSLProgram::DeleteProgram() glDeleteProgram(m_ProgramID); } -bool CGLSLProgram::AddShader(CGLSL *pShader) +bool CGLSLProgram::AddShader(CGLSL *pShader) const { if(pShader->IsLoaded()) { @@ -35,7 +35,7 @@ bool CGLSLProgram::AddShader(CGLSL *pShader) return false; } -void CGLSLProgram::DetachShader(CGLSL *pShader) +void CGLSLProgram::DetachShader(CGLSL *pShader) const { if(pShader->IsLoaded()) { @@ -43,7 +43,7 @@ void CGLSLProgram::DetachShader(CGLSL *pShader) } } -void CGLSLProgram::DetachShaderByID(TWGLuint ShaderID) +void CGLSLProgram::DetachShaderByID(TWGLuint ShaderID) const { glDetachShader(m_ProgramID, ShaderID); } @@ -68,7 +68,7 @@ void CGLSLProgram::LinkProgram() DetachAllShaders(); } -void CGLSLProgram::DetachAllShaders() +void CGLSLProgram::DetachAllShaders() const { TWGLuint aShaders[100]; GLsizei ReturnedCount = 0; @@ -119,18 +119,18 @@ void CGLSLProgram::SetUniform(int Loc, const bool Value) glUniform1i(Loc, (int)Value); } -int CGLSLProgram::GetUniformLoc(const char *pName) +int CGLSLProgram::GetUniformLoc(const char *pName) const { return glGetUniformLocation(m_ProgramID, pName); } -void CGLSLProgram::UseProgram() +void CGLSLProgram::UseProgram() const { if(m_IsLinked) glUseProgram(m_ProgramID); } -TWGLuint CGLSLProgram::GetProgramID() +TWGLuint CGLSLProgram::GetProgramID() const { return m_ProgramID; } diff --git a/src/engine/client/backend/opengl/opengl_sl_program.h b/src/engine/client/backend/opengl/opengl_sl_program.h index 75243b6e771..f278731c860 100644 --- a/src/engine/client/backend/opengl/opengl_sl_program.h +++ b/src/engine/client/backend/opengl/opengl_sl_program.h @@ -22,15 +22,15 @@ class CGLSLProgram void CreateProgram(); void DeleteProgram(); - bool AddShader(CGLSL *pShader); + bool AddShader(CGLSL *pShader) const; void LinkProgram(); - void UseProgram(); - TWGLuint GetProgramID(); + void UseProgram() const; + TWGLuint GetProgramID() const; - void DetachShader(CGLSL *pShader); - void DetachShaderByID(TWGLuint ShaderID); - void DetachAllShaders(); + void DetachShader(CGLSL *pShader) const; + void DetachShaderByID(TWGLuint ShaderID) const; + void DetachAllShaders() const; //Support various types void SetUniformVec2(int Loc, int Count, const float *pValue); @@ -41,7 +41,7 @@ class CGLSLProgram void SetUniform(int Loc, int Count, const float *pValues); //for performance reason we do not use SetUniform with using strings... save the Locations of the variables instead - int GetUniformLoc(const char *pName); + int GetUniformLoc(const char *pName) const; CGLSLProgram(); virtual ~CGLSLProgram(); diff --git a/src/engine/client/backend/vulkan/backend_vulkan.cpp b/src/engine/client/backend/vulkan/backend_vulkan.cpp index 63b32af2264..84a8b48d869 100644 --- a/src/engine/client/backend/vulkan/backend_vulkan.cpp +++ b/src/engine/client/backend/vulkan/backend_vulkan.cpp @@ -68,7 +68,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return g_Config.m_DbgGfx == DEBUG_GFX_MODE_VERBOSE || g_Config.m_DbgGfx == DEBUG_GFX_MODE_ALL; } - void VerboseAllocatedMemory(VkDeviceSize Size, size_t FrameImageIndex, EMemoryBlockUsage MemUsage) + void VerboseAllocatedMemory(VkDeviceSize Size, size_t FrameImageIndex, EMemoryBlockUsage MemUsage) const { const char *pUsage = "unknown"; switch(MemUsage) @@ -90,7 +90,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase dbg_msg("vulkan", "allocated chunk of memory with size: %" PRIzu " for frame %" PRIzu " (%s)", (size_t)Size, (size_t)m_CurImageIndex, pUsage); } - void VerboseDeallocatedMemory(VkDeviceSize Size, size_t FrameImageIndex, EMemoryBlockUsage MemUsage) + void VerboseDeallocatedMemory(VkDeviceSize Size, size_t FrameImageIndex, EMemoryBlockUsage MemUsage) const { const char *pUsage = "unknown"; switch(MemUsage) @@ -300,7 +300,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase } } - [[nodiscard]] bool IsUnused() + [[nodiscard]] bool IsUnused() const { return !m_Root.m_InUse; } @@ -829,7 +829,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase // the viewport of the resulting presented image on the screen // if there is a forced viewport the resulting image is smaller // than the full swap image size - VkExtent2D GetPresentedImageViewport() + VkExtent2D GetPresentedImageViewport() const { uint32_t ViewportWidth = m_SwapImageViewport.width; uint32_t ViewportHeight = m_SwapImageViewport.height; @@ -1099,15 +1099,15 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase * After an error occurred, the rendering stop as soon as possible * Always stop the current code execution after a call to this function (e.g. return false) */ - void SetError(EGFXErrorType ErrType, const char *pErr, const char *pErrStrExtra = nullptr) + void SetError(EGfxErrorType ErrType, const char *pErr, const char *pErrStrExtra = nullptr) { std::unique_lock Lock(m_ErrWarnMutex); - SGFXErrorContainer::SError Err = {false, pErr}; + SGfxErrorContainer::SError Err = {false, pErr}; if(std::find(m_Error.m_vErrors.begin(), m_Error.m_vErrors.end(), Err) == m_Error.m_vErrors.end()) m_Error.m_vErrors.emplace_back(Err); if(pErrStrExtra != nullptr) { - SGFXErrorContainer::SError ErrExtra = {false, pErrStrExtra}; + SGfxErrorContainer::SError ErrExtra = {false, pErrStrExtra}; if(std::find(m_Error.m_vErrors.begin(), m_Error.m_vErrors.end(), ErrExtra) == m_Error.m_vErrors.end()) m_Error.m_vErrors.emplace_back(ErrExtra); } @@ -1125,7 +1125,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase Lock.unlock(); // during initialization vulkan should not throw any errors but warnings instead // since most code in the swapchain is shared with runtime code, add this extra code path - SetWarning(EGFXWarningType::GFX_WARNING_TYPE_INIT_FAILED, pErr); + SetWarning(EGfxWarningType::GFX_WARNING_TYPE_INIT_FAILED, pErr); } } @@ -1136,7 +1136,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_Warning.m_vWarnings.emplace(m_Warning.m_vWarnings.begin(), pWarningPre); } - void SetWarning(EGFXWarningType WarningType, const char *pWarning) + void SetWarning(EGfxWarningType WarningType, const char *pWarning) { std::unique_lock Lock(m_ErrWarnMutex); dbg_msg("vulkan", "vulkan warning: %s", pWarning); @@ -1200,10 +1200,10 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase dbg_msg("vulkan", "%s", pCriticalError); break; case VK_ERROR_LAYER_NOT_PRESENT: - SetWarning(EGFXWarningType::GFX_WARNING_MISSING_EXTENSION, "One Vulkan layer was not present. (try to disable them)"); + SetWarning(EGfxWarningType::GFX_WARNING_MISSING_EXTENSION, "One Vulkan layer was not present. (try to disable them)"); break; case VK_ERROR_EXTENSION_NOT_PRESENT: - SetWarning(EGFXWarningType::GFX_WARNING_MISSING_EXTENSION, "One Vulkan extension was not present. (try to disable them)"); + SetWarning(EGfxWarningType::GFX_WARNING_MISSING_EXTENSION, "One Vulkan extension was not present. (try to disable them)"); break; case VK_ERROR_NATIVE_WINDOW_IN_USE_KHR: dbg_msg("vulkan", "native window in use"); @@ -1626,7 +1626,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase { if(vkMapMemory(m_VKDevice, TmpBufferMemory.m_Mem, 0, VK_WHOLE_SIZE, 0, &pMapData) != VK_SUCCESS) { - SetError(RequiresMapping ? EGFXErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_STAGING : EGFXErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER, "Failed to map buffer block memory.", + SetError(RequiresMapping ? EGfxErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_STAGING : EGfxErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER, "Failed to map buffer block memory.", GetMemoryUsageShort()); delete pNewHeap; return false; @@ -1643,7 +1643,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase Heaps.back()->m_Heap.Init(MemoryBlockSize * BlockCount, 0); if(!Heaps.back()->m_Heap.Allocate(RequiredSize, TargetAlignment, AllocatedMem)) { - SetError(RequiresMapping ? EGFXErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_STAGING : EGFXErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER, "Heap allocation failed directly after creating fresh heap.", + SetError(RequiresMapping ? EGfxErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_STAGING : EGfxErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER, "Heap allocation failed directly after creating fresh heap.", GetMemoryUsageShort()); return false; } @@ -1800,7 +1800,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(!AllocateVulkanMemory(&MemAllocInfo, &BufferMemory.m_Mem)) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_IMAGE, "Allocation for image memory failed.", + SetError(EGfxErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_IMAGE, "Allocation for image memory failed.", GetMemoryUsageShort()); return false; } @@ -2256,7 +2256,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkEndCommandBuffer(CommandBuffer) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_RENDER_RECORDING, "Command buffer cannot be ended anymore."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_RENDER_RECORDING, "Command buffer cannot be ended anymore."); return false; } @@ -2301,7 +2301,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase const char *pCritErrorMsg = CheckVulkanCriticalError(QueueSubmitRes); if(pCritErrorMsg != nullptr) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_RENDER_SUBMIT_FAILED, "Submitting to graphics queue failed.", pCritErrorMsg); + SetError(EGfxErrorType::GFX_ERROR_TYPE_RENDER_SUBMIT_FAILED, "Submitting to graphics queue failed.", pCritErrorMsg); return false; } } @@ -2328,7 +2328,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase const char *pCritErrorMsg = CheckVulkanCriticalError(QueuePresentRes); if(pCritErrorMsg != nullptr) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_SWAP_FAILED, "Presenting graphics queue failed.", pCritErrorMsg); + SetError(EGfxErrorType::GFX_ERROR_TYPE_SWAP_FAILED, "Presenting graphics queue failed.", pCritErrorMsg); return false; } } @@ -2370,7 +2370,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase const char *pCritErrorMsg = CheckVulkanCriticalError(AcqResult); if(pCritErrorMsg != nullptr) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_SWAP_FAILED, "Acquiring next image failed.", pCritErrorMsg); + SetError(EGfxErrorType::GFX_ERROR_TYPE_SWAP_FAILED, "Acquiring next image failed.", pCritErrorMsg); return false; } else if(AcqResult == VK_ERROR_SURFACE_LOST_KHR) @@ -2421,7 +2421,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkBeginCommandBuffer(CommandBuffer, &BeginInfo) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_RENDER_RECORDING, "Command buffer cannot be filled anymore."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_RENDER_RECORDING, "Command buffer cannot be filled anymore."); return false; } @@ -3168,7 +3168,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return State.m_BlendMode == CCommandBuffer::BLEND_ADDITIVE ? VULKAN_BACKEND_BLEND_MODE_ADDITATIVE : (State.m_BlendMode == CCommandBuffer::BLEND_NONE ? VULKAN_BACKEND_BLEND_MODE_NONE : VULKAN_BACKEND_BLEND_MODE_ALPHA); } - size_t GetDynamicModeIndexFromState(const CCommandBuffer::SState &State) + size_t GetDynamicModeIndexFromState(const CCommandBuffer::SState &State) const { return (State.m_ClipEnable || m_HasDynamicViewport || m_VKSwapImgAndViewportExtent.m_HasForcedViewport) ? VULKAN_BACKEND_CLIP_MODE_DYNAMIC_SCISSOR_AND_VIEWPORT : VULKAN_BACKEND_CLIP_MODE_NONE; } @@ -3485,14 +3485,14 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase unsigned int ExtCount = 0; if(!SDL_Vulkan_GetInstanceExtensions(pWindow, &ExtCount, nullptr)) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Could not get instance extensions from SDL."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Could not get instance extensions from SDL."); return false; } std::vector vExtensionList(ExtCount); if(!SDL_Vulkan_GetInstanceExtensions(pWindow, &ExtCount, vExtensionList.data())) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Could not get instance extensions from SDL."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Could not get instance extensions from SDL."); return false; } @@ -3541,7 +3541,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase VkResult Res = vkEnumerateInstanceLayerProperties(&LayerCount, NULL); if(Res != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Could not get vulkan layers."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Could not get vulkan layers."); return false; } @@ -3549,7 +3549,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase Res = vkEnumerateInstanceLayerProperties(&LayerCount, vVKInstanceLayers.data()); if(Res != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Could not get vulkan layers."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Could not get vulkan layers."); return false; } @@ -3626,7 +3626,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase const char *pCritErrorMsg = CheckVulkanCriticalError(Res); if(pCritErrorMsg != nullptr) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating instance failed.", pCritErrorMsg); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating instance failed.", pCritErrorMsg); return false; } else if(Res == VK_ERROR_LAYER_NOT_PRESENT || Res == VK_ERROR_EXTENSION_NOT_PRESENT) @@ -3693,12 +3693,12 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase auto Res = vkEnumeratePhysicalDevices(m_VKInstance, &DevicesCount, nullptr); if(Res != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, CheckVulkanCriticalError(Res)); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, CheckVulkanCriticalError(Res)); return false; } if(DevicesCount == 0) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "No vulkan compatible devices found."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "No vulkan compatible devices found."); return false; } @@ -3706,12 +3706,12 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase Res = vkEnumeratePhysicalDevices(m_VKInstance, &DevicesCount, vDeviceList.data()); if(Res != VK_SUCCESS && Res != VK_INCOMPLETE) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, CheckVulkanCriticalError(Res)); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, CheckVulkanCriticalError(Res)); return false; } if(DevicesCount == 0) { - SetWarning(EGFXWarningType::GFX_WARNING_TYPE_INIT_FAILED_MISSING_INTEGRATED_GPU_DRIVER, "No vulkan compatible devices found."); + SetWarning(EGfxWarningType::GFX_WARNING_TYPE_INIT_FAILED_MISSING_INTEGRATED_GPU_DRIVER, "No vulkan compatible devices found."); return false; } // make sure to use the correct amount of devices available @@ -3833,7 +3833,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase vkGetPhysicalDeviceQueueFamilyProperties(CurDevice, &FamQueueCount, nullptr); if(FamQueueCount == 0) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "No vulkan queue family properties found."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "No vulkan queue family properties found."); return false; } @@ -3855,7 +3855,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(QueueNodeIndex == std::numeric_limits::max()) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "No vulkan queue found that matches the requirements: graphics queue."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "No vulkan queue found that matches the requirements: graphics queue."); return false; } @@ -3874,14 +3874,14 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase uint32_t DevPropCount = 0; if(vkEnumerateDeviceExtensionProperties(m_VKGPU, NULL, &DevPropCount, NULL) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Querying logical device extension properties failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Querying logical device extension properties failed."); return false; } std::vector vDevPropList(DevPropCount); if(vkEnumerateDeviceExtensionProperties(m_VKGPU, NULL, &DevPropCount, vDevPropList.data()) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Querying logical device extension properties failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Querying logical device extension properties failed."); return false; } @@ -3921,7 +3921,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase VkResult res = vkCreateDevice(m_VKGPU, &VKCreateInfo, nullptr, &m_VKDevice); if(res != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Logical device could not be created."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Logical device could not be created."); return false; } @@ -3933,7 +3933,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(!SDL_Vulkan_CreateSurface(pWindow, m_VKInstance, &m_VKPresentSurface)) { dbg_msg("vulkan", "error from sdl: %s", SDL_GetError()); - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating a vulkan surface for the SDL window failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating a vulkan surface for the SDL window failed."); return false; } @@ -3941,7 +3941,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase vkGetPhysicalDeviceSurfaceSupportKHR(m_VKGPU, m_VKGraphicsQueueIndex, m_VKPresentSurface, &IsSupported); if(!IsSupported) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "The device surface does not support presenting the framebuffer to a screen. (maybe the wrong GPU was selected?)"); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "The device surface does not support presenting the framebuffer to a screen. (maybe the wrong GPU was selected?)"); return false; } @@ -3958,14 +3958,14 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase uint32_t PresentModeCount = 0; if(vkGetPhysicalDeviceSurfacePresentModesKHR(m_VKGPU, m_VKPresentSurface, &PresentModeCount, NULL) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "The device surface presentation modes could not be fetched."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "The device surface presentation modes could not be fetched."); return false; } std::vector vPresentModeList(PresentModeCount); if(vkGetPhysicalDeviceSurfacePresentModesKHR(m_VKGPU, m_VKPresentSurface, &PresentModeCount, vPresentModeList.data()) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "The device surface presentation modes could not be fetched."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "The device surface presentation modes could not be fetched."); return false; } @@ -3995,7 +3995,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase { if(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(m_VKGPU, m_VKPresentSurface, &VKSurfCapabilities) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "The device surface capabilities could not be fetched."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "The device surface capabilities could not be fetched."); return false; } return true; @@ -4047,7 +4047,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase std::vector vOurImgUsages = OurImageUsages(); if(vOurImgUsages.empty()) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Framebuffer image attachment types not supported."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Framebuffer image attachment types not supported."); return false; } @@ -4058,7 +4058,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase VkImageUsageFlags ImgUsageFlags = ImgUsage & VKCapabilities.supportedUsageFlags; if(ImgUsageFlags != ImgUsage) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Framebuffer image attachment types not supported."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Framebuffer image attachment types not supported."); return false; } @@ -4081,7 +4081,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase VkResult Res = vkGetPhysicalDeviceSurfaceFormatsKHR(m_VKGPU, m_VKPresentSurface, &SurfFormats, nullptr); if(Res != VK_SUCCESS && Res != VK_INCOMPLETE) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "The device surface format fetching failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "The device surface format fetching failed."); return false; } @@ -4089,7 +4089,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase Res = vkGetPhysicalDeviceSurfaceFormatsKHR(m_VKGPU, m_VKPresentSurface, &SurfFormats, vSurfFormatList.data()); if(Res != VK_SUCCESS && Res != VK_INCOMPLETE) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "The device surface format fetching failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "The device surface format fetching failed."); return false; } @@ -4175,7 +4175,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase const char *pCritErrorMsg = CheckVulkanCriticalError(SwapchainCreateRes); if(pCritErrorMsg != nullptr) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating the swap chain failed.", pCritErrorMsg); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating the swap chain failed.", pCritErrorMsg); return false; } else if(SwapchainCreateRes == VK_ERROR_NATIVE_WINDOW_IN_USE_KHR) @@ -4199,7 +4199,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase VkResult res = vkGetSwapchainImagesKHR(m_VKDevice, m_VKSwapChain, &ImgCount, nullptr); if(res != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Could not get swap chain images."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Could not get swap chain images."); return false; } @@ -4208,7 +4208,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase m_vSwapChainImages.resize(ImgCount); if(vkGetSwapchainImagesKHR(m_VKDevice, m_VKSwapChain, &ImgCount, m_vSwapChainImages.data()) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Could not get swap chain images."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Could not get swap chain images."); return false; } @@ -4316,7 +4316,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreateImageView(m_VKDevice, &CreateInfo, nullptr, &m_vSwapChainImageViewList[i]) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Could not create image views for the swap chain framebuffers."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Could not create image views for the swap chain framebuffers."); return false; } } @@ -4425,7 +4425,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreateRenderPass(m_VKDevice, &CreateRenderPassInfo, nullptr, &m_VKRenderPass) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating the render pass failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating the render pass failed."); return false; } @@ -4460,7 +4460,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreateFramebuffer(m_VKDevice, &FramebufferInfo, nullptr, &m_vFramebufferList[i]) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating the framebuffers failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating the framebuffers failed."); return false; } } @@ -4487,7 +4487,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreateShaderModule(m_VKDevice, &CreateInfo, nullptr, &ShaderModule) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Shader module was not created."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Shader module was not created."); return false; } @@ -4511,13 +4511,13 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreateDescriptorSetLayout(m_VKDevice, &LayoutInfo, nullptr, &m_StandardTexturedDescriptorSetLayout) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating descriptor layout failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating descriptor layout failed."); return false; } if(vkCreateDescriptorSetLayout(m_VKDevice, &LayoutInfo, nullptr, &m_Standard3DTexturedDescriptorSetLayout) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating descriptor layout failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating descriptor layout failed."); return false; } return true; @@ -4565,7 +4565,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(!ShaderLoaded) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "A shader file could not load correctly."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "A shader file could not load correctly."); return false; } @@ -4598,7 +4598,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase VkPipelineRasterizationStateCreateInfo &Rasterizer, VkPipelineMultisampleStateCreateInfo &Multisampling, VkPipelineColorBlendAttachmentState &ColorBlendAttachment, - VkPipelineColorBlendStateCreateInfo &ColorBlending) + VkPipelineColorBlendStateCreateInfo &ColorBlending) const { InputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; InputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; @@ -4705,7 +4705,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreatePipelineLayout(m_VKDevice, &PipelineLayoutInfo, nullptr, &PipeLayout) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating pipeline layout failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating pipeline layout failed."); return false; } @@ -4741,7 +4741,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreateGraphicsPipelines(m_VKDevice, VK_NULL_HANDLE, 1, &PipelineInfo, nullptr, &Pipeline) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating the graphic pipeline failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating the graphic pipeline failed."); return false; } @@ -4834,7 +4834,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreateDescriptorSetLayout(m_VKDevice, &LayoutInfo, nullptr, &m_TextDescriptorSetLayout) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating descriptor layout failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating descriptor layout failed."); return false; } @@ -4977,7 +4977,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreateDescriptorSetLayout(m_VKDevice, &LayoutInfo, nullptr, &SetLayout) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating descriptor layout failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating descriptor layout failed."); return false; } return true; @@ -5217,7 +5217,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase { if(vkCreateCommandPool(m_VKDevice, &CreatePoolInfo, nullptr, &m_vCommandPools[i]) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating the command pool failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating the command pool failed."); return false; } } @@ -5260,7 +5260,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkAllocateCommandBuffers(m_VKDevice, &AllocInfo, m_vMainDrawCommandBuffers.data()) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Allocating command buffers failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Allocating command buffers failed."); return false; } @@ -5268,7 +5268,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkAllocateCommandBuffers(m_VKDevice, &AllocInfo, m_vMemoryCommandBuffers.data()) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Allocating memory command buffers failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Allocating memory command buffers failed."); return false; } @@ -5283,7 +5283,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase AllocInfo.level = VK_COMMAND_BUFFER_LEVEL_SECONDARY; if(vkAllocateCommandBuffers(m_VKDevice, &AllocInfo, ThreadDrawCommandBuffers.data()) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Allocating thread command buffers failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Allocating thread command buffers failed."); return false; } } @@ -5340,7 +5340,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase 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."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating swap chain sync objects(fences, semaphores) failed."); return false; } } @@ -5635,7 +5635,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreateBuffer(m_VKDevice, &BufferInfo, nullptr, &VKBuffer) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER, "Buffer creation failed.", + SetError(EGfxErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER, "Buffer creation failed.", GetMemoryUsageShort()); return false; } @@ -5664,7 +5664,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(!AllocateVulkanMemory(&MemAllocInfo, &VKBufferMemory.m_Mem)) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER, "Allocation for buffer object failed.", + SetError(EGfxErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER, "Allocation for buffer object failed.", GetMemoryUsageShort()); return false; } @@ -5673,7 +5673,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkBindBufferMemory(m_VKDevice, VKBuffer, VKBufferMemory.m_Mem, 0) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER, "Binding memory to buffer failed.", + SetError(EGfxErrorType::GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER, "Binding memory to buffer failed.", GetMemoryUsageShort()); return false; } @@ -5702,7 +5702,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkCreateDescriptorPool(m_VKDevice, &PoolInfo, nullptr, &NewPool.m_Pool) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_INIT, "Creating the descriptor pool failed."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_INIT, "Creating the descriptor pool failed."); return false; } @@ -5967,12 +5967,12 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase FreeDescriptorSetFromPool(DescrSet); } - [[nodiscard]] bool HasMultiSampling() + [[nodiscard]] bool HasMultiSampling() const { return GetSampleCount() != VK_SAMPLE_COUNT_1_BIT; } - VkSampleCountFlagBits GetMaxSampleCount() + VkSampleCountFlagBits GetMaxSampleCount() const { if(m_MaxMultiSample & VK_SAMPLE_COUNT_64_BIT) return VK_SAMPLE_COUNT_64_BIT; @@ -5990,7 +5990,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase return VK_SAMPLE_COUNT_1_BIT; } - VkSampleCountFlagBits GetSampleCount() + VkSampleCountFlagBits GetSampleCount() const { auto MaxSampleCount = GetMaxSampleCount(); if(m_MultiSamplingCount >= 64 && MaxSampleCount >= VK_SAMPLE_COUNT_64_BIT) @@ -6205,7 +6205,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase BeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; if(vkBeginCommandBuffer(MemCommandBuffer, &BeginInfo) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_RENDER_RECORDING, "Command buffer cannot be filled anymore."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_RENDER_RECORDING, "Command buffer cannot be filled anymore."); return false; } } @@ -6244,7 +6244,7 @@ class CCommandProcessorFragment_Vulkan : public CCommandProcessorFragment_GLBase if(vkBeginCommandBuffer(DrawCommandBuffer, &BeginInfo) != VK_SUCCESS) { - SetError(EGFXErrorType::GFX_ERROR_TYPE_RENDER_RECORDING, "Thread draw command buffer cannot be filled anymore."); + SetError(EGfxErrorType::GFX_ERROR_TYPE_RENDER_RECORDING, "Thread draw command buffer cannot be filled anymore."); return false; } } diff --git a/src/engine/client/backend_sdl.cpp b/src/engine/client/backend_sdl.cpp index 142c7b82113..ab5c402d951 100644 --- a/src/engine/client/backend_sdl.cpp +++ b/src/engine/client/backend_sdl.cpp @@ -108,7 +108,7 @@ void CGraphicsBackend_Threaded::StopProcessor() void CGraphicsBackend_Threaded::RunBuffer(CCommandBuffer *pBuffer) { - SGFXErrorContainer Error; + SGfxErrorContainer Error; #ifdef CONF_WEBASM // run everything single threaded for now, context binding in a thread seems to not work as of now Error = m_pProcessor->GetError(); @@ -153,7 +153,7 @@ void CGraphicsBackend_Threaded::WaitForIdle() m_BufferSwapCond.wait(Lock, [this]() { return m_pBuffer == nullptr; }); } -void CGraphicsBackend_Threaded::ProcessError(const SGFXErrorContainer &Error) +void CGraphicsBackend_Threaded::ProcessError(const SGfxErrorContainer &Error) { std::string VerboseStr = "Graphics Assertion:"; for(const auto &ErrStr : Error.m_vErrors) @@ -274,31 +274,31 @@ void CCommandProcessor_SDL_GL::HandleError() switch(m_Error.m_ErrorType) { case GFX_ERROR_TYPE_INIT: - m_Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.", "Graphics error")}); + m_Error.m_vErrors.emplace_back(SGfxErrorContainer::SError{true, Localizable("Failed during initialization. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.", "Graphics error")}); break; case GFX_ERROR_TYPE_OUT_OF_MEMORY_IMAGE: [[fallthrough]]; case GFX_ERROR_TYPE_OUT_OF_MEMORY_BUFFER: [[fallthrough]]; case GFX_ERROR_TYPE_OUT_OF_MEMORY_STAGING: - m_Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.", "Graphics error")}); + m_Error.m_vErrors.emplace_back(SGfxErrorContainer::SError{true, Localizable("Out of VRAM. Try removing custom assets (skins, entities, etc.), especially those with high resolution.", "Graphics error")}); break; case GFX_ERROR_TYPE_RENDER_RECORDING: - m_Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("An error during command recording occurred. Try to update your GPU drivers.", "Graphics error")}); + m_Error.m_vErrors.emplace_back(SGfxErrorContainer::SError{true, Localizable("An error during command recording occurred. Try to update your GPU drivers.", "Graphics error")}); break; case GFX_ERROR_TYPE_RENDER_CMD_FAILED: - m_Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("A render command failed. Try to update your GPU drivers.", "Graphics error")}); + m_Error.m_vErrors.emplace_back(SGfxErrorContainer::SError{true, Localizable("A render command failed. Try to update your GPU drivers.", "Graphics error")}); break; case GFX_ERROR_TYPE_RENDER_SUBMIT_FAILED: - m_Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("Submitting the render commands failed. Try to update your GPU drivers.", "Graphics error")}); + m_Error.m_vErrors.emplace_back(SGfxErrorContainer::SError{true, Localizable("Submitting the render commands failed. Try to update your GPU drivers.", "Graphics error")}); break; case GFX_ERROR_TYPE_SWAP_FAILED: - m_Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("Failed to swap framebuffers. Try to update your GPU drivers.", "Graphics error")}); + m_Error.m_vErrors.emplace_back(SGfxErrorContainer::SError{true, Localizable("Failed to swap framebuffers. Try to update your GPU drivers.", "Graphics error")}); break; case GFX_ERROR_TYPE_UNKNOWN: [[fallthrough]]; default: - m_Error.m_vErrors.emplace_back(SGFXErrorContainer::SError{true, Localizable("Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.", "Graphics error")}); + m_Error.m_vErrors.emplace_back(SGfxErrorContainer::SError{true, Localizable("Unknown error. Try to change gfx_backend to OpenGL or Vulkan in settings_ddnet.cfg in the config directory and try again.", "Graphics error")}); break; } } @@ -416,7 +416,7 @@ CCommandProcessor_SDL_GL::~CCommandProcessor_SDL_GL() delete m_pGLBackend; } -const SGFXErrorContainer &CCommandProcessor_SDL_GL::GetError() const +const SGfxErrorContainer &CCommandProcessor_SDL_GL::GetError() const { return m_Error; } @@ -426,7 +426,7 @@ void CCommandProcessor_SDL_GL::ErroneousCleanup() return m_pGLBackend->ErroneousCleanup(); } -const SGFXWarningContainer &CCommandProcessor_SDL_GL::GetWarning() const +const SGfxWarningContainer &CCommandProcessor_SDL_GL::GetWarning() const { return m_Warning; } diff --git a/src/engine/client/backend_sdl.h b/src/engine/client/backend_sdl.h index 8a28dc496f1..952ba052c9c 100644 --- a/src/engine/client/backend_sdl.h +++ b/src/engine/client/backend_sdl.h @@ -48,7 +48,7 @@ class CGraphicsBackend_Threaded : public IGraphicsBackend { private: TTranslateFunc m_TranslateFunc; - SGFXWarningContainer m_Warning; + SGfxWarningContainer m_Warning; public: // constructed on the main thread, the rest of the functions is run on the render thread @@ -58,10 +58,10 @@ class CGraphicsBackend_Threaded : public IGraphicsBackend virtual ~ICommandProcessor() = default; virtual void RunBuffer(CCommandBuffer *pBuffer) = 0; - virtual const SGFXErrorContainer &GetError() const = 0; + virtual const SGfxErrorContainer &GetError() const = 0; virtual void ErroneousCleanup() = 0; - virtual const SGFXWarningContainer &GetWarning() const = 0; + virtual const SGfxWarningContainer &GetWarning() const = 0; }; CGraphicsBackend_Threaded(TTranslateFunc &&TranslateFunc); @@ -71,7 +71,7 @@ class CGraphicsBackend_Threaded : public IGraphicsBackend bool IsIdle() const override; void WaitForIdle() override; - void ProcessError(const SGFXErrorContainer &Error); + void ProcessError(const SGfxErrorContainer &Error); protected: void StartProcessor(ICommandProcessor *pProcessor); @@ -181,18 +181,18 @@ class CCommandProcessor_SDL_GL : public CGraphicsBackend_Threaded::ICommandProce EBackendType m_BackendType; - SGFXErrorContainer m_Error; - SGFXWarningContainer m_Warning; + SGfxErrorContainer m_Error; + SGfxWarningContainer m_Warning; public: CCommandProcessor_SDL_GL(EBackendType BackendType, int GLMajor, int GLMinor, int GLPatch); virtual ~CCommandProcessor_SDL_GL(); void RunBuffer(CCommandBuffer *pBuffer) override; - const SGFXErrorContainer &GetError() const override; + const SGfxErrorContainer &GetError() const override; void ErroneousCleanup() override; - const SGFXWarningContainer &GetWarning() const override; + const SGfxWarningContainer &GetWarning() const override; void HandleError(); void HandleWarning(); diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 64eaedc1b35..fcd2b60f5fe 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -730,9 +730,10 @@ void CClient::LoadDebugFont() void *CClient::SnapGetItem(int SnapID, int Index, CSnapItem *pItem) const { dbg_assert(SnapID >= 0 && SnapID < NUM_SNAPSHOT_TYPES, "invalid SnapID"); - const CSnapshotItem *pSnapshotItem = m_aapSnapshots[g_Config.m_ClDummy][SnapID]->m_pAltSnap->GetItem(Index); - pItem->m_DataSize = m_aapSnapshots[g_Config.m_ClDummy][SnapID]->m_pAltSnap->GetItemSize(Index); - pItem->m_Type = m_aapSnapshots[g_Config.m_ClDummy][SnapID]->m_pAltSnap->GetItemType(Index); + 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(); } @@ -746,7 +747,7 @@ int CClient::SnapItemSize(int SnapID, int Index) const const void *CClient::SnapFindItem(int SnapID, int Type, int ID) const { if(!m_aapSnapshots[g_Config.m_ClDummy][SnapID]) - return 0x0; + return nullptr; return m_aapSnapshots[g_Config.m_ClDummy][SnapID]->m_pAltSnap->FindItem(Type, ID); } @@ -1373,7 +1374,6 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) SHA256_DIGEST *pMapSha256 = (SHA256_DIGEST *)Unpacker.GetRaw(sizeof(*pMapSha256)); int MapCrc = Unpacker.GetInt(); int MapSize = Unpacker.GetInt(); - if(Unpacker.Error()) { return; @@ -1400,7 +1400,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) } int Version = Unpacker.GetInt(); int Flags = Unpacker.GetInt(); - if(Version <= 0) + if(Unpacker.Error() || Version <= 0) { return; } @@ -1421,85 +1421,82 @@ 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(); - const char *pError = 0; - - if(Unpacker.Error()) + if(Unpacker.Error() || MapSize < 0) + { return; - - if(m_DummyConnected) - DummyDisconnect(0); + } for(int i = 0; pMap[i]; i++) // protect the player from nasty map names { if(pMap[i] == '/' || pMap[i] == '\\') - pError = "strange character in map name"; + { + return; + } } - if(MapSize < 0) - pError = "invalid map size"; + if(m_DummyConnected) + { + DummyDisconnect(0); + } - if(pError) - DisconnectWithReason(pError); - else + SHA256_DIGEST *pMapSha256 = nullptr; + const char *pMapUrl = nullptr; + if(MapDetailsWerePresent && str_comp(m_aMapDetailsName, pMap) == 0 && m_MapDetailsCrc == MapCrc) { - SHA256_DIGEST *pMapSha256 = 0; - const char *pMapUrl = nullptr; - if(MapDetailsWerePresent && str_comp(m_aMapDetailsName, pMap) == 0 && m_MapDetailsCrc == MapCrc) - { - pMapSha256 = &m_MapDetailsSha256; - pMapUrl = m_aMapDetailsUrl[0] ? m_aMapDetailsUrl : nullptr; - } - pError = LoadMapSearch(pMap, pMapSha256, MapCrc); + pMapSha256 = &m_MapDetailsSha256; + pMapUrl = m_aMapDetailsUrl[0] ? m_aMapDetailsUrl : nullptr; + } - if(!pError) + if(LoadMapSearch(pMap, pMapSha256, MapCrc) == nullptr) + { + m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "loading done"); + SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_SENDING_READY); + SendReady(CONN_MAIN); + } + else + { + if(m_MapdownloadFileTemp) { - m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", "loading done"); - SetLoadingStateDetail(IClient::LOADING_STATE_DETAIL_SENDING_READY); - SendReady(CONN_MAIN); + io_close(m_MapdownloadFileTemp); + Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE); } - 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)); + // start map download + FormatMapDownloadFilename(pMap, pMapSha256, MapCrc, false, m_aMapdownloadFilename, sizeof(m_aMapdownloadFilename)); + FormatMapDownloadFilename(pMap, pMapSha256, MapCrc, true, m_aMapdownloadFilenameTemp, sizeof(m_aMapdownloadFilenameTemp)); - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "starting to download map to '%s'", m_aMapdownloadFilenameTemp); - m_pConsole->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client/network", aBuf); + char aBuf[256]; + 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_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; + m_MapdownloadSha256Present = (bool)pMapSha256; + m_MapdownloadSha256 = pMapSha256 ? *pMapSha256 : SHA256_ZEROED; + m_MapdownloadCrc = MapCrc; + m_MapdownloadTotalsize = MapSize; + m_MapdownloadAmount = 0; - ResetMapDownload(); + ResetMapDownload(); - if(pMapSha256) - { - char aUrl[256]; - char aEscaped[256]; - EscapeUrl(aEscaped, sizeof(aEscaped), m_aMapdownloadFilename + 15); // cut off downloadedmaps/ - bool UseConfigUrl = str_comp(g_Config.m_ClMapDownloadUrl, "https://maps.ddnet.org") != 0 || m_aMapDownloadUrl[0] == '\0'; - str_format(aUrl, sizeof(aUrl), "%s/%s", UseConfigUrl ? g_Config.m_ClMapDownloadUrl : m_aMapDownloadUrl, aEscaped); - - 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->ExpectSha256(*pMapSha256); - Engine()->AddJob(m_pMapdownloadTask); - } - else - SendMapRequest(); + if(pMapSha256) + { + char aUrl[256]; + char aEscaped[256]; + EscapeUrl(aEscaped, sizeof(aEscaped), m_aMapdownloadFilename + 15); // cut off downloadedmaps/ + bool UseConfigUrl = str_comp(g_Config.m_ClMapDownloadUrl, "https://maps.ddnet.org") != 0 || m_aMapDownloadUrl[0] == '\0'; + str_format(aUrl, sizeof(aUrl), "%s/%s", UseConfigUrl ? g_Config.m_ClMapDownloadUrl : m_aMapDownloadUrl, aEscaped); + + 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->ExpectSha256(*pMapSha256); + Engine()->AddJob(m_pMapdownloadTask); + } + else + { + SendMapRequest(); } } } @@ -1510,10 +1507,10 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) int Chunk = Unpacker.GetInt(); int Size = Unpacker.GetInt(); const unsigned char *pData = Unpacker.GetRaw(Size); - - // check for errors if(Unpacker.Error() || Size <= 0 || MapCRC != m_MapdownloadCrc || Chunk != m_MapdownloadChunk || !m_MapdownloadFileTemp) + { return; + } io_write(m_MapdownloadFileTemp, pData, Size); @@ -1610,6 +1607,10 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) else if(Msg == NETMSG_REDIRECT) { int RedirectPort = Unpacker.GetInt(); + if(Unpacker.Error()) + { + return; + } char aAddr[128]; char aIP[64]; NETADDR ServerAddr = ServerAddress(); @@ -1622,35 +1623,47 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) const char *pName = Unpacker.GetString(CUnpacker::SANITIZE_CC); const char *pHelp = Unpacker.GetString(CUnpacker::SANITIZE_CC); const char *pParams = Unpacker.GetString(CUnpacker::SANITIZE_CC); - if(Unpacker.Error() == 0) + if(!Unpacker.Error()) + { m_pConsole->RegisterTemp(pName, pParams, CFGFLAG_SERVER, pHelp); + } } else if(Conn == CONN_MAIN && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_CMD_REM) { const char *pName = Unpacker.GetString(CUnpacker::SANITIZE_CC); - if(Unpacker.Error() == 0) + if(!Unpacker.Error()) + { m_pConsole->DeregisterTemp(pName); + } } else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_AUTH_STATUS) { int ResultInt = Unpacker.GetInt(); - if(Unpacker.Error() == 0) + if(!Unpacker.Error()) + { m_aRconAuthed[Conn] = ResultInt; + } if(Conn == CONN_MAIN) { int Old = m_UseTempRconCommands; m_UseTempRconCommands = Unpacker.GetInt(); - if(Unpacker.Error() != 0) + if(Unpacker.Error()) + { m_UseTempRconCommands = 0; + } if(Old != 0 && m_UseTempRconCommands == 0) + { m_pConsole->DeregisterTempAll(); + } } } else if(!Dummy && (pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0 && Msg == NETMSG_RCON_LINE) { const char *pLine = Unpacker.GetString(); - if(Unpacker.Error() == 0) + if(!Unpacker.Error()) + { GameClient()->OnRconLine(pLine); + } } else if(Conn == CONN_MAIN && Msg == NETMSG_PING_REPLY) { @@ -1662,6 +1675,11 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) { int InputPredTick = Unpacker.GetInt(); int TimeLeft = Unpacker.GetInt(); + if(Unpacker.Error()) + { + return; + } + int64_t Now = time_get(); // adjust our prediction time @@ -1681,16 +1699,20 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) } else if(Msg == NETMSG_SNAP || Msg == NETMSG_SNAPSINGLE || Msg == NETMSG_SNAPEMPTY) { - int GameTick = Unpacker.GetInt(); - int DeltaTick = GameTick - Unpacker.GetInt(); - // 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) + { return; + } + + int GameTick = Unpacker.GetInt(); + int DeltaTick = GameTick - Unpacker.GetInt(); int NumParts = 1; int Part = 0; @@ -1709,9 +1731,10 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) } const char *pData = (const char *)Unpacker.GetRaw(PartSize); - if(Unpacker.Error() || NumParts < 1 || NumParts > CSnapshot::MAX_PARTS || Part < 0 || Part >= NumParts || PartSize < 0 || PartSize > MAX_SNAPSHOT_PACKSIZE) + { return; + } // Check m_aAckGameTick to see if we already got a snapshot for that tick if(GameTick >= m_aCurrentRecvTick[Conn] && GameTick > m_aAckGameTick[Conn]) @@ -1745,7 +1768,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) const CSnapshot *pDeltaShot = CSnapshot::EmptySnapshot(); if(DeltaTick >= 0) { - int DeltashotSize = m_aSnapshotStorage[Conn].Get(DeltaTick, 0, &pDeltaShot, 0); + int DeltashotSize = m_aSnapshotStorage[Conn].Get(DeltaTick, nullptr, &pDeltaShot, nullptr); if(DeltashotSize < 0) { @@ -1961,23 +1984,23 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) else if(Conn == CONN_MAIN && Msg == NETMSG_RCONTYPE) { bool UsernameReq = Unpacker.GetInt() & 1; - GameClient()->OnRconType(UsernameReq); + if(!Unpacker.Error()) + { + GameClient()->OnRconType(UsernameReq); + } } } - else + else if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0) { - if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) != 0) + // game message + if(!Dummy) { - // game message - if(!Dummy) - { - for(auto &DemoRecorder : m_aDemoRecorder) - if(DemoRecorder.IsRecording()) - DemoRecorder.RecordMessage(pPacket->m_pData, pPacket->m_DataSize); - } - - GameClient()->OnMessage(Msg, &Unpacker, Conn, Dummy); + for(auto &DemoRecorder : m_aDemoRecorder) + if(DemoRecorder.IsRecording()) + DemoRecorder.RecordMessage(pPacket->m_pData, pPacket->m_DataSize); } + + GameClient()->OnMessage(Msg, &Unpacker, Conn, Dummy); } } @@ -2235,6 +2258,7 @@ void CClient::LoadDDNetInfo() NETADDR Addr; if(!net_addr_from_str(&Addr, ConnectingIp)) { + m_HaveGlobalTcpAddr = true; m_GlobalTcpAddr = Addr; log_debug("info", "got global tcp ip address: %s", (const char *)ConnectingIp); } @@ -2693,7 +2717,6 @@ void CClient::InitInterfaces() m_pEngine = Kernel()->RequestInterface(); m_pEditor = Kernel()->RequestInterface(); m_pFavorites = Kernel()->RequestInterface(); - //m_pGraphics = Kernel()->RequestInterface(); m_pSound = Kernel()->RequestInterface(); m_pGameClient = Kernel()->RequestInterface(); m_pInput = Kernel()->RequestInterface(); @@ -2705,6 +2728,7 @@ void CClient::InitInterfaces() #endif m_pDiscord = Kernel()->RequestInterface(); m_pSteam = Kernel()->RequestInterface(); + m_pNotifications = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); m_DemoEditor.Init(m_pGameClient->NetVersion(), &m_SnapshotDelta, m_pConsole, m_pStorage); @@ -2749,19 +2773,14 @@ void CClient::Run() } // init graphics + m_pGraphics = CreateEngineGraphicsThreaded(); + Kernel()->RegisterInterface(m_pGraphics); // IEngineGraphics + Kernel()->RegisterInterface(static_cast(m_pGraphics), false); + if(m_pGraphics->Init() != 0) { - m_pGraphics = CreateEngineGraphicsThreaded(); - - bool RegisterFail = false; - RegisterFail = RegisterFail || !Kernel()->RegisterInterface(m_pGraphics); // IEngineGraphics - RegisterFail = RegisterFail || !Kernel()->RegisterInterface(static_cast(m_pGraphics), false); - - if(RegisterFail || m_pGraphics->Init() != 0) - { - dbg_msg("client", "couldn't init graphics"); - ShowMessageBox("Graphics Error", "The graphics could not be initialized."); - return; - } + dbg_msg("client", "couldn't init graphics"); + ShowMessageBox("Graphics Error", "The graphics could not be initialized."); + return; } // make sure the first frame just clears everything to prevent undesired colors when waiting for io @@ -3987,7 +4006,7 @@ void CClient::Notify(const char *pTitle, const char *pMessage) if(m_pGraphics->WindowActive() || !g_Config.m_ClShowNotifications) return; - NotificationsNotify(pTitle, pMessage); + Notifications()->Notify(pTitle, pMessage); Graphics()->NotifyWindow(); } @@ -4125,7 +4144,7 @@ void CClient::RegisterCommands() 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("save_replay", "?i[length] s[filename]", CFGFLAG_CLIENT, Con_SaveReplay, this, "Save a replay of the last defined amount of seconds"); + 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"); RustVersionRegister(*m_pConsole); @@ -4318,9 +4337,6 @@ int main(int argc, const char **argv) if(!RandInitFailed) CleanerFunctions.emplace([]() { secure_random_uninit(); }); - NotificationsInit(); - CleanerFunctions.emplace([]() { NotificationsUninit(); }); - // Register SDL for cleanup before creating the kernel and client, // so SDL is shutdown after kernel and client. Otherwise the client // may crash when shutting down after SDL is already shutdown. @@ -4364,17 +4380,17 @@ int main(int argc, const char **argv) // create the components IEngine *pEngine = CreateEngine(GAME_NAME, pFutureConsoleLogger, 2 * std::thread::hardware_concurrency() + 2); - IConsole *pConsole = CreateConsole(CFGFLAG_CLIENT).release(); + pKernel->RegisterInterface(pEngine, false); + CleanerFunctions.emplace([pEngine]() { + // Engine has to be destroyed before the graphics so that skin download thread can finish + delete pEngine; + }); + IStorage *pStorage = CreateStorage(IStorage::STORAGETYPE_CLIENT, argc, (const char **)argv); - IConfigManager *pConfigManager = CreateConfigManager(); - IEngineSound *pEngineSound = CreateEngineSound(); - IEngineInput *pEngineInput = CreateEngineInput(); - IEngineTextRender *pEngineTextRender = CreateEngineTextRender(); - IEngineMap *pEngineMap = CreateEngineMap(); - IDiscord *pDiscord = CreateDiscord(); - ISteam *pSteam = CreateSteam(); + pKernel->RegisterInterface(pStorage); pFutureAssertionLogger->Set(CreateAssertionLogger(pStorage, GAME_NAME)); + #if defined(CONF_EXCEPTION_HANDLING) char aBufPath[IO_MAX_PATH_LENGTH]; char aBufName[IO_MAX_PATH_LENGTH]; @@ -4394,51 +4410,45 @@ int main(int argc, const char **argv) return -1; } - { - bool RegisterFail = false; + IConsole *pConsole = CreateConsole(CFGFLAG_CLIENT).release(); + pKernel->RegisterInterface(pConsole); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngine, false); + IConfigManager *pConfigManager = CreateConfigManager(); + pKernel->RegisterInterface(pConfigManager); - CleanerFunctions.emplace([pEngine]() { - // Has to be before destroying graphics so that skin download thread can finish - delete pEngine; - }); + IEngineSound *pEngineSound = CreateEngineSound(); + pKernel->RegisterInterface(pEngineSound); // IEngineSound + pKernel->RegisterInterface(static_cast(pEngineSound), false); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pConsole); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pConfigManager); + IEngineInput *pEngineInput = CreateEngineInput(); + pKernel->RegisterInterface(pEngineInput); // IEngineInput + pKernel->RegisterInterface(static_cast(pEngineInput), false); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngineSound); // IEngineSound - RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast(pEngineSound), false); + IEngineTextRender *pEngineTextRender = CreateEngineTextRender(); + pKernel->RegisterInterface(pEngineTextRender); // IEngineTextRender + pKernel->RegisterInterface(static_cast(pEngineTextRender), false); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngineInput); // IEngineInput - RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast(pEngineInput), false); + IEngineMap *pEngineMap = CreateEngineMap(); + pKernel->RegisterInterface(pEngineMap); // IEngineMap + pKernel->RegisterInterface(static_cast(pEngineMap), false); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngineTextRender); // IEngineTextRender - RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast(pEngineTextRender), false); + IDiscord *pDiscord = CreateDiscord(); + pKernel->RegisterInterface(pDiscord); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pEngineMap); // IEngineMap - RegisterFail = RegisterFail || !pKernel->RegisterInterface(static_cast(pEngineMap), false); + ISteam *pSteam = CreateSteam(); + pKernel->RegisterInterface(pSteam); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(CreateEditor(), false); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(CreateFavorites().release()); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(CreateGameClient()); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pStorage); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pDiscord); - RegisterFail = RegisterFail || !pKernel->RegisterInterface(pSteam); + INotifications *pNotifications = CreateNotifications(); + pKernel->RegisterInterface(pNotifications); - if(RegisterFail) - { - const char *pError = "Failed to register an interface."; - dbg_msg("client", "%s", pError); - pClient->ShowMessageBox("Kernel Error", pError); - PerformAllCleanup(); - return -1; - } - } + pKernel->RegisterInterface(CreateEditor(), false); + pKernel->RegisterInterface(CreateFavorites().release()); + pKernel->RegisterInterface(CreateGameClient()); pEngine->Init(); pConsole->Init(); pConfigManager->Init(); + pNotifications->Init(GAME_NAME " Client"); // register all console commands pClient->RegisterCommands(); @@ -4697,7 +4707,7 @@ int CClient::UdpConnectivity(int NetType) break; case CONNECTIVITY::ADDRESS_KNOWN: GlobalUdpAddr.port = 0; - if(NetType == (int)m_GlobalTcpAddr.type && net_addr_comp(&m_GlobalTcpAddr, &GlobalUdpAddr) != 0) + if(m_HaveGlobalTcpAddr && NetType == (int)m_GlobalTcpAddr.type && net_addr_comp(&m_GlobalTcpAddr, &GlobalUdpAddr) != 0) { NewConnectivity = CONNECTIVITY_DIFFERING_UDP_TCP_IP_ADDRESSES; break; diff --git a/src/engine/client/client.h b/src/engine/client/client.h index a6941351ac2..14ffb588c97 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -39,6 +39,7 @@ class IEngineSound; class IFriends; class ILogger; class ISteam; +class INotifications; class IStorage; class IUpdater; @@ -71,6 +72,7 @@ class CClient : public IClient, public CDemoPlayer::IListener IEngineMap *m_pMap = nullptr; IEngineSound *m_pSound = nullptr; ISteam *m_pSteam = nullptr; + INotifications *m_pNotifications = nullptr; IStorage *m_pStorage = nullptr; IEngineTextRender *m_pTextRender = nullptr; IUpdater *m_pUpdater = nullptr; @@ -90,6 +92,7 @@ class CClient : public IClient, public CDemoPlayer::IListener CUuid m_ConnectionID = UUID_ZEROED; + bool m_HaveGlobalTcpAddr = false; NETADDR m_GlobalTcpAddr = NETADDR_ZEROED; uint64_t m_aSnapshotParts[NUM_DUMMIES] = {0, 0}; @@ -259,6 +262,7 @@ class CClient : public IClient, public CDemoPlayer::IListener IEngineInput *Input() { return m_pInput; } IEngineSound *Sound() { return m_pSound; } ISteam *Steam() { return m_pSteam; } + INotifications *Notifications() { return m_pNotifications; } IStorage *Storage() { return m_pStorage; } IEngineTextRender *TextRender() { return m_pTextRender; } IUpdater *Updater() { return m_pUpdater; } diff --git a/src/engine/client/discord.cpp b/src/engine/client/discord.cpp index bd83eb41e3e..bb9dc7b4c15 100644 --- a/src/engine/client/discord.cpp +++ b/src/engine/client/discord.cpp @@ -1,3 +1,4 @@ +#include #include // Hack for universal binary builds on macOS: Ignore arm64 until Discord diff --git a/src/engine/client/favorites.cpp b/src/engine/client/favorites.cpp index 39d015a75c6..9df4e42aff6 100644 --- a/src/engine/client/favorites.cpp +++ b/src/engine/client/favorites.cpp @@ -1,3 +1,4 @@ +#include #include #include #include diff --git a/src/engine/client/ghost.h b/src/engine/client/ghost.h index f555e1f2de5..544b7d04445 100644 --- a/src/engine/client/ghost.h +++ b/src/engine/client/ghost.h @@ -3,6 +3,8 @@ #include +#include + enum { MAX_ITEM_SIZE = 128, diff --git a/src/engine/client/graphics_threaded.cpp b/src/engine/client/graphics_threaded.cpp index a293e4a0fc6..48384a9641e 100644 --- a/src/engine/client/graphics_threaded.cpp +++ b/src/engine/client/graphics_threaded.cpp @@ -1074,26 +1074,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; } } diff --git a/src/engine/client/graphics_threaded.h b/src/engine/client/graphics_threaded.h index 514680749d9..eaf0570d66e 100644 --- a/src/engine/client/graphics_threaded.h +++ b/src/engine/client/graphics_threaded.h @@ -1,6 +1,7 @@ #ifndef ENGINE_CLIENT_GRAPHICS_THREADED_H #define ENGINE_CLIENT_GRAPHICS_THREADED_H +#include #include #include diff --git a/src/engine/client/notifications.cpp b/src/engine/client/notifications.cpp index 4cacd6519d7..577b4f4256b 100644 --- a/src/engine/client/notifications.cpp +++ b/src/engine/client/notifications.cpp @@ -4,35 +4,41 @@ #if defined(CONF_PLATFORM_MACOS) // Code is in src/macos/notification.mm. +void NotificationsNotifyMacOsInternal(const char *pTitle, const char *pMessage); #elif defined(CONF_FAMILY_UNIX) && !defined(CONF_PLATFORM_ANDROID) && !defined(CONF_PLATFORM_HAIKU) && !defined(CONF_WEBASM) #include -void NotificationsInit() +#define NOTIFICATIONS_USE_LIBNOTIFY +#endif + +void CNotifications::Init(const char *pAppname) { - notify_init("DDNet Client"); +#if defined(NOTIFICATIONS_USE_LIBNOTIFY) + notify_init(pAppname); +#endif } -void NotificationsUninit() + +void CNotifications::Shutdown() { +#if defined(NOTIFICATIONS_USE_LIBNOTIFY) notify_uninit(); +#endif } -void NotificationsNotify(const char *pTitle, const char *pMessage) + +void CNotifications::Notify(const char *pTitle, const char *pMessage) { +#if defined(CONF_PLATFORM_MACOS) + NotificationsNotifyMacOsInternal(pTitle, pMessage); +#elif defined(NOTIFICATIONS_USE_LIBNOTIFY) NotifyNotification *pNotif = notify_notification_new(pTitle, pMessage, "ddnet"); if(pNotif) { notify_notification_show(pNotif, NULL); g_object_unref(G_OBJECT(pNotif)); } +#endif } -#else -void NotificationsInit() -{ -} -void NotificationsUninit() -{ -} -void NotificationsNotify(const char *pTitle, const char *pMessage) + +INotifications *CreateNotifications() { - (void)pTitle; - (void)pMessage; + return new CNotifications(); } -#endif diff --git a/src/engine/client/notifications.h b/src/engine/client/notifications.h index 35d7b588a88..02463535e48 100644 --- a/src/engine/client/notifications.h +++ b/src/engine/client/notifications.h @@ -1,6 +1,14 @@ #ifndef ENGINE_CLIENT_NOTIFICATIONS_H #define ENGINE_CLIENT_NOTIFICATIONS_H -void NotificationsInit(); -void NotificationsUninit(); -void NotificationsNotify(const char *pTitle, const char *pMessage); + +#include + +class CNotifications : public INotifications +{ +public: + void Init(const char *pAppname) override; + void Shutdown() override; + void Notify(const char *pTitle, const char *pMessage) override; +}; + #endif // ENGINE_CLIENT_NOTIFICATIONS_H diff --git a/src/engine/client/serverbrowser.cpp b/src/engine/client/serverbrowser.cpp index 332bb7aff4b..22939fe1fad 100644 --- a/src/engine/client/serverbrowser.cpp +++ b/src/engine/client/serverbrowser.cpp @@ -122,7 +122,7 @@ void CServerBrowser::Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserDa { public: CServerBrowser *m_pThis; - bool operator()(int i, int j) + bool operator()(int i, int j) const { NETADDR Addr1 = m_pThis->m_ppServerlist[i]->m_Info.m_aAddresses[0]; NETADDR Addr2 = m_pThis->m_ppServerlist[j]->m_Info.m_aAddresses[0]; @@ -541,7 +541,7 @@ void ServerBrowserFormatAddresses(char *pBuffer, int BufferSize, NETADDR *pAddrs } } -void CServerBrowser::SetInfo(CServerEntry *pEntry, const CServerInfo &Info) +void CServerBrowser::SetInfo(CServerEntry *pEntry, const CServerInfo &Info) const { const CServerInfo TmpInfo = pEntry->m_Info; pEntry->m_Info = Info; @@ -575,7 +575,7 @@ void CServerBrowser::SetInfo(CServerEntry *pEntry, const CServerInfo &Info) { } - bool operator()(const CServerInfo::CClient &p0, const CServerInfo::CClient &p1) + bool operator()(const CServerInfo::CClient &p0, const CServerInfo::CClient &p1) const { // Sort players before non players if(p0.m_Player && !p1.m_Player) diff --git a/src/engine/client/serverbrowser.h b/src/engine/client/serverbrowser.h index 854907c910d..1920b6c7444 100644 --- a/src/engine/client/serverbrowser.h +++ b/src/engine/client/serverbrowser.h @@ -217,7 +217,7 @@ class CServerBrowser : public IServerBrowser void RegisterCommands(); static void Con_LeakIpAddress(IConsole::IResult *pResult, void *pUserData); - void SetInfo(CServerEntry *pEntry, const CServerInfo &Info); + void SetInfo(CServerEntry *pEntry, const CServerInfo &Info) const; void SetLatency(NETADDR Addr, int Latency); static bool ParseCommunityFinishes(CCommunity *pCommunity, const json_value &Finishes); diff --git a/src/engine/client/serverbrowser_http.h b/src/engine/client/serverbrowser_http.h index d75b3d157df..b6884f869a3 100644 --- a/src/engine/client/serverbrowser_http.h +++ b/src/engine/client/serverbrowser_http.h @@ -1,6 +1,6 @@ #ifndef ENGINE_CLIENT_SERVERBROWSER_HTTP_H #define ENGINE_CLIENT_SERVERBROWSER_HTTP_H -#include +#include class CServerInfo; class IConsole; diff --git a/src/engine/client/serverbrowser_ping_cache.cpp b/src/engine/client/serverbrowser_ping_cache.cpp index ff46adf3141..d59f1bd8347 100644 --- a/src/engine/client/serverbrowser_ping_cache.cpp +++ b/src/engine/client/serverbrowser_ping_cache.cpp @@ -1,5 +1,6 @@ #include "serverbrowser_ping_cache.h" +#include #include #include diff --git a/src/engine/client/serverbrowser_ping_cache.h b/src/engine/client/serverbrowser_ping_cache.h index bacd8378220..5c81e939f8e 100644 --- a/src/engine/client/serverbrowser_ping_cache.h +++ b/src/engine/client/serverbrowser_ping_cache.h @@ -1,6 +1,6 @@ #ifndef ENGINE_CLIENT_SERVERBROWSER_PING_CACHE_H #define ENGINE_CLIENT_SERVERBROWSER_PING_CACHE_H -#include +#include class IConsole; class IStorage; diff --git a/src/engine/client/sound.cpp b/src/engine/client/sound.cpp index 7784e162b56..86540773051 100644 --- a/src/engine/client/sound.cpp +++ b/src/engine/client/sound.cpp @@ -286,7 +286,7 @@ int CSound::AllocID() return -1; } -void CSound::RateConvert(CSample &Sample) +void CSound::RateConvert(CSample &Sample) const { // make sure that we need to convert this sound if(!Sample.m_pData || Sample.m_Rate == m_MixingRate) @@ -321,7 +321,7 @@ void CSound::RateConvert(CSample &Sample) Sample.m_Rate = m_MixingRate; } -bool CSound::DecodeOpus(CSample &Sample, const void *pData, unsigned DataSize) +bool CSound::DecodeOpus(CSample &Sample, const void *pData, unsigned DataSize) const { OggOpusFile *pOpusFile = op_open_memory((const unsigned char *)pData, DataSize, nullptr); if(pOpusFile) @@ -414,7 +414,7 @@ static int PushBackByte(void *pId, int Char) } #endif -bool CSound::DecodeWV(CSample &Sample, const void *pData, unsigned DataSize) +bool CSound::DecodeWV(CSample &Sample, const void *pData, unsigned DataSize) const { char aError[100]; diff --git a/src/engine/client/sound.h b/src/engine/client/sound.h index 3780e192202..ea5eb5766f3 100644 --- a/src/engine/client/sound.h +++ b/src/engine/client/sound.h @@ -77,10 +77,10 @@ class CSound : public IEngineSound int *m_pMixBuffer = nullptr; int AllocID(); - void RateConvert(CSample &Sample); + void RateConvert(CSample &Sample) const; - bool DecodeOpus(CSample &Sample, const void *pData, unsigned DataSize); - bool DecodeWV(CSample &Sample, const void *pData, unsigned DataSize); + bool DecodeOpus(CSample &Sample, const void *pData, unsigned DataSize) const; + bool DecodeWV(CSample &Sample, const void *pData, unsigned DataSize) const; void UpdateVolume(); diff --git a/src/engine/client/steam.cpp b/src/engine/client/steam.cpp index f5b3609cd0e..6c1cd20ea1d 100644 --- a/src/engine/client/steam.cpp +++ b/src/engine/client/steam.cpp @@ -1,5 +1,6 @@ #include +#include #include #include diff --git a/src/engine/client/text.cpp b/src/engine/client/text.cpp index 15a734aa310..7aa3b666ad1 100644 --- a/src/engine/client/text.cpp +++ b/src/engine/client/text.cpp @@ -1268,6 +1268,7 @@ class CTextRender : public IEngineTextRender pCursor->m_GlyphCount = 0; pCursor->m_CharCount = 0; pCursor->m_MaxLines = 0; + pCursor->m_LineSpacing = 0; pCursor->m_StartX = x; pCursor->m_StartY = y; @@ -1291,6 +1292,8 @@ class CTextRender : public IEngineTextRender pCursor->m_ForceCursorRendering = false; pCursor->m_CursorCharacter = -1; pCursor->m_CursorRenderedPosition = vec2(-1.0f, -1.0f); + + pCursor->m_vColorSplits = {}; } void MoveCursor(CTextCursor *pCursor, float x, float y) const override @@ -1330,11 +1333,12 @@ class CTextRender : public IEngineTextRender return Cursor.m_LongestLineWidth; } - STextBoundingBox TextBoundingBox(float Size, const char *pText, int StrLength = -1, float LineWidth = -1.0f, int Flags = 0) override + STextBoundingBox TextBoundingBox(float Size, const char *pText, int StrLength = -1, float LineWidth = -1.0f, float LineSpacing = 0.0f, int Flags = 0) override { CTextCursor Cursor; SetCursor(&Cursor, 0, 0, Size, Flags); Cursor.m_LineWidth = LineWidth; + Cursor.m_LineSpacing = LineSpacing; TextEx(&Cursor, pText, StrLength); return Cursor.BoundingBox(); } @@ -1477,6 +1481,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; // string length if(Length < 0) @@ -1534,10 +1539,10 @@ 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 <= CursorPos.y && - CharY > CursorPos.y) || + CharY - pCursor->m_AlignedFontSize - LineSpacing <= CursorPos.y && + CharY + LineSpacing > CursorPos.y) || (CheckOuter && - CharY - pCursor->m_AlignedFontSize > CursorPos.y); + CharY - pCursor->m_AlignedFontSize + LineSpacing > CursorPos.y); }; const auto &&CheckSelectionStart = [&](bool CheckOuter, vec2 CursorPos, int &SelectionChar, bool &SelectionUsedCase, float LastCharX, float LastCharWidth, float CharX, float CharWidth, float CharY) { if(!SelectionStarted && !SelectionUsedCase) @@ -1552,10 +1557,10 @@ class CTextRender : public IEngineTextRender }; const auto &&CheckOutsideChar = [&](bool CheckOuter, vec2 CursorPos, float CharX, float CharWidth, float CharY) -> bool { return (CharX + CharWidth / 2 > CursorPos.x && - CharY - pCursor->m_AlignedFontSize <= CursorPos.y && - CharY > CursorPos.y) || + CharY - pCursor->m_AlignedFontSize - LineSpacing <= CursorPos.y && + CharY + LineSpacing > CursorPos.y) || (CheckOuter && - CharY <= CursorPos.y); + CharY - LineSpacing <= CursorPos.y); }; const auto &&CheckSelectionEnd = [&](bool CheckOuter, vec2 CursorPos, int &SelectionChar, bool &SelectionUsedCase, float CharX, float CharWidth, float CharY) { if(SelectionStarted && !SelectionUsedCase) @@ -1580,7 +1585,7 @@ class CTextRender : public IEngineTextRender return false; DrawX = pCursor->m_StartX; - DrawY += pCursor->m_AlignedFontSize; + DrawY += pCursor->m_AlignedFontSize + pCursor->m_LineSpacing; if((RenderFlags & TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT) == 0) { DrawX = round_to_int(DrawX * FakeToScreen.x) / FakeToScreen.x; // realign @@ -1611,6 +1616,8 @@ class CTextRender : public IEngineTextRender bool GotNewLine = false; bool GotNewLineLast = false; + int ColorOption = 0; + while(pCurrent < pEnd && pCurrent != pEllipsis) { bool NewLine = false; @@ -1662,6 +1669,7 @@ class CTextRender : public IEngineTextRender while(pCurrent < pBatchEnd && pCurrent != pEllipsis) { + const int PrevCharCount = pCursor->m_CharCount; pCursor->m_CharCount += pTmp - pCurrent; pCurrent = pTmp; int Character = NextCharacter; @@ -1742,8 +1750,19 @@ class CTextRender : public IEngineTextRender const float CharX = (DrawX + CharKerning) + BearingX; const float CharY = TmpY - BearingY; + // Check if we have any color split + ColorRGBA Color = m_Color; + 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) + Color = Split.m_Color; + if(PrevCharCount >= (Split.m_CharIndex + Split.m_Length - 1)) + ColorOption++; + } + // don't add text that isn't drawn, the color overwrite is used for that - if(m_Color.a != 0.f && IsRendered) + if(Color.a != 0.f && IsRendered) { TextContainer.m_StringInfo.m_vCharacterQuads.emplace_back(); STextCharQuad &TextCharQuad = TextContainer.m_StringInfo.m_vCharacterQuads.back(); @@ -1752,37 +1771,37 @@ class CTextRender : public IEngineTextRender TextCharQuad.m_aVertices[0].m_Y = CharY; TextCharQuad.m_aVertices[0].m_U = pGlyph->m_aUVs[0]; TextCharQuad.m_aVertices[0].m_V = pGlyph->m_aUVs[3]; - TextCharQuad.m_aVertices[0].m_Color.r = (unsigned char)(m_Color.r * 255.f); - TextCharQuad.m_aVertices[0].m_Color.g = (unsigned char)(m_Color.g * 255.f); - TextCharQuad.m_aVertices[0].m_Color.b = (unsigned char)(m_Color.b * 255.f); - TextCharQuad.m_aVertices[0].m_Color.a = (unsigned char)(m_Color.a * 255.f); + TextCharQuad.m_aVertices[0].m_Color.r = (unsigned char)(Color.r * 255.f); + TextCharQuad.m_aVertices[0].m_Color.g = (unsigned char)(Color.g * 255.f); + TextCharQuad.m_aVertices[0].m_Color.b = (unsigned char)(Color.b * 255.f); + TextCharQuad.m_aVertices[0].m_Color.a = (unsigned char)(Color.a * 255.f); TextCharQuad.m_aVertices[1].m_X = CharX + CharWidth; TextCharQuad.m_aVertices[1].m_Y = CharY; TextCharQuad.m_aVertices[1].m_U = pGlyph->m_aUVs[2]; TextCharQuad.m_aVertices[1].m_V = pGlyph->m_aUVs[3]; - TextCharQuad.m_aVertices[1].m_Color.r = (unsigned char)(m_Color.r * 255.f); - TextCharQuad.m_aVertices[1].m_Color.g = (unsigned char)(m_Color.g * 255.f); - TextCharQuad.m_aVertices[1].m_Color.b = (unsigned char)(m_Color.b * 255.f); - TextCharQuad.m_aVertices[1].m_Color.a = (unsigned char)(m_Color.a * 255.f); + TextCharQuad.m_aVertices[1].m_Color.r = (unsigned char)(Color.r * 255.f); + TextCharQuad.m_aVertices[1].m_Color.g = (unsigned char)(Color.g * 255.f); + TextCharQuad.m_aVertices[1].m_Color.b = (unsigned char)(Color.b * 255.f); + TextCharQuad.m_aVertices[1].m_Color.a = (unsigned char)(Color.a * 255.f); TextCharQuad.m_aVertices[2].m_X = CharX + CharWidth; TextCharQuad.m_aVertices[2].m_Y = CharY - CharHeight; TextCharQuad.m_aVertices[2].m_U = pGlyph->m_aUVs[2]; TextCharQuad.m_aVertices[2].m_V = pGlyph->m_aUVs[1]; - TextCharQuad.m_aVertices[2].m_Color.r = (unsigned char)(m_Color.r * 255.f); - TextCharQuad.m_aVertices[2].m_Color.g = (unsigned char)(m_Color.g * 255.f); - TextCharQuad.m_aVertices[2].m_Color.b = (unsigned char)(m_Color.b * 255.f); - TextCharQuad.m_aVertices[2].m_Color.a = (unsigned char)(m_Color.a * 255.f); + TextCharQuad.m_aVertices[2].m_Color.r = (unsigned char)(Color.r * 255.f); + TextCharQuad.m_aVertices[2].m_Color.g = (unsigned char)(Color.g * 255.f); + TextCharQuad.m_aVertices[2].m_Color.b = (unsigned char)(Color.b * 255.f); + TextCharQuad.m_aVertices[2].m_Color.a = (unsigned char)(Color.a * 255.f); TextCharQuad.m_aVertices[3].m_X = CharX; TextCharQuad.m_aVertices[3].m_Y = CharY - CharHeight; TextCharQuad.m_aVertices[3].m_U = pGlyph->m_aUVs[0]; TextCharQuad.m_aVertices[3].m_V = pGlyph->m_aUVs[1]; - TextCharQuad.m_aVertices[3].m_Color.r = (unsigned char)(m_Color.r * 255.f); - TextCharQuad.m_aVertices[3].m_Color.g = (unsigned char)(m_Color.g * 255.f); - TextCharQuad.m_aVertices[3].m_Color.b = (unsigned char)(m_Color.b * 255.f); - TextCharQuad.m_aVertices[3].m_Color.a = (unsigned char)(m_Color.a * 255.f); + TextCharQuad.m_aVertices[3].m_Color.r = (unsigned char)(Color.r * 255.f); + TextCharQuad.m_aVertices[3].m_Color.g = (unsigned char)(Color.g * 255.f); + TextCharQuad.m_aVertices[3].m_Color.b = (unsigned char)(Color.b * 255.f); + TextCharQuad.m_aVertices[3].m_Color.a = (unsigned char)(Color.a * 255.f); } // calculate the full width from the last selection point to the end of this selection draw on screen diff --git a/src/engine/client/video.cpp b/src/engine/client/video.cpp index c8efc788049..62eeaf85980 100644 --- a/src/engine/client/video.cpp +++ b/src/engine/client/video.cpp @@ -809,7 +809,7 @@ bool CVideo::OpenAudio() } /* Add an output stream. */ -bool CVideo::AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCodec **ppCodec, enum AVCodecID CodecId) +bool CVideo::AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCodec **ppCodec, enum AVCodecID CodecId) const { AVCodecContext *pContext; diff --git a/src/engine/client/video.h b/src/engine/client/video.h index 849a0b72535..0575cdee7ee 100644 --- a/src/engine/client/video.h +++ b/src/engine/client/video.h @@ -2,7 +2,6 @@ #define ENGINE_CLIENT_VIDEO_H #include -#include extern "C" { #include @@ -80,7 +79,7 @@ class CVideo : public IVideo void FinishFrames(OutputStream *pStream); void CloseStream(OutputStream *pStream); - bool AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCodec **ppCodec, enum AVCodecID CodecId); + bool AddStream(OutputStream *pStream, AVFormatContext *pOC, const AVCodec **ppCodec, enum AVCodecID CodecId) const; CGraphics_Threaded *m_pGraphics; IStorage *m_pStorage; diff --git a/src/engine/client/warning.cpp b/src/engine/client/warning.cpp new file mode 100644 index 00000000000..0ed4a9daae7 --- /dev/null +++ b/src/engine/client/warning.cpp @@ -0,0 +1,14 @@ +#include + +#include + +SWarning::SWarning(const char *pMsg) +{ + str_copy(m_aWarningTitle, ""); + str_copy(m_aWarningMsg, pMsg); +} +SWarning::SWarning(const char *pTitle, const char *pMsg) +{ + str_copy(m_aWarningTitle, pTitle); + str_copy(m_aWarningMsg, pMsg); +} diff --git a/src/engine/demo.h b/src/engine/demo.h index 240551de26d..f42193e9b03 100644 --- a/src/engine/demo.h +++ b/src/engine/demo.h @@ -8,6 +8,8 @@ #include #include +#include + enum { MAX_TIMELINE_MARKERS = 64 @@ -37,15 +39,7 @@ struct CDemoHeader unsigned char m_aLength[sizeof(int32_t)]; char m_aTimestamp[20]; - bool Valid() const - { - // Check marker and ensure that strings are zero-terminated and valid UTF-8. - return mem_comp(m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) == 0 && - mem_has_null(m_aNetversion, sizeof(m_aNetversion)) && str_utf8_check(m_aNetversion) && - mem_has_null(m_aMapName, sizeof(m_aMapName)) && str_utf8_check(m_aMapName) && - mem_has_null(m_aType, sizeof(m_aType)) && str_utf8_check(m_aType) && - mem_has_null(m_aTimestamp, sizeof(m_aTimestamp)) && str_utf8_check(m_aTimestamp); - } + bool Valid() const; }; struct CTimelineMarkers diff --git a/src/engine/discord.h b/src/engine/discord.h index 92e4401fad2..5b5f0a82698 100644 --- a/src/engine/discord.h +++ b/src/engine/discord.h @@ -2,6 +2,7 @@ #define ENGINE_DISCORD_H #include "kernel.h" +#include class IDiscord : public IInterface { diff --git a/src/engine/graphics.h b/src/engine/graphics.h index 5021422618b..951748b1170 100644 --- a/src/engine/graphics.h +++ b/src/engine/graphics.h @@ -7,6 +7,7 @@ #include "warning.h" #include +#include #include #include diff --git a/src/engine/input.h b/src/engine/input.h index cbf693faa13..87740c2fa2a 100644 --- a/src/engine/input.h +++ b/src/engine/input.h @@ -4,6 +4,7 @@ #define ENGINE_INPUT_H #include "kernel.h" +#include const int g_MaxKeys = 512; extern const char g_aaKeyStrings[g_MaxKeys][20]; diff --git a/src/engine/kernel.h b/src/engine/kernel.h index 03ef8f070f4..7412142a0d0 100644 --- a/src/engine/kernel.h +++ b/src/engine/kernel.h @@ -3,8 +3,6 @@ #ifndef ENGINE_KERNEL_H #define ENGINE_KERNEL_H -#include - class IKernel; class IInterface; @@ -35,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: @@ -46,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/map.h b/src/engine/map.h index 5f5f4c67a6c..47c2c5ed8a1 100644 --- a/src/engine/map.h +++ b/src/engine/map.h @@ -5,6 +5,7 @@ #include "kernel.h" #include +#include enum { diff --git a/src/engine/notifications.h b/src/engine/notifications.h new file mode 100644 index 00000000000..38cfe88b0a6 --- /dev/null +++ b/src/engine/notifications.h @@ -0,0 +1,17 @@ +#ifndef ENGINE_NOTIFICATIONS_H +#define ENGINE_NOTIFICATIONS_H + +#include "kernel.h" + +class INotifications : public IInterface +{ + MACRO_INTERFACE("notifications") +public: + virtual void Init(const char *pAppname) = 0; + virtual void Shutdown() override = 0; + virtual void Notify(const char *pTitle, const char *pMessage) = 0; +}; + +INotifications *CreateNotifications(); + +#endif // ENGINE_NOTIFICATIONS_H diff --git a/src/engine/server.h b/src/engine/server.h index 7f8e1306b04..1e81f77397b 100644 --- a/src/engine/server.h +++ b/src/engine/server.h @@ -8,6 +8,7 @@ #include #include +#include #include "kernel.h" #include "message.h" diff --git a/src/engine/server/authmanager.cpp b/src/engine/server/authmanager.cpp index 6014af70fe1..59f1bc3c5a8 100644 --- a/src/engine/server/authmanager.cpp +++ b/src/engine/server/authmanager.cpp @@ -1,5 +1,6 @@ #include "authmanager.h" #include +#include #include #include diff --git a/src/engine/server/databases/connection.cpp b/src/engine/server/databases/connection.cpp index 1609b85a9ff..ac124baa49f 100644 --- a/src/engine/server/databases/connection.cpp +++ b/src/engine/server/databases/connection.cpp @@ -2,7 +2,12 @@ #include -void IDbConnection::FormatCreateRace(char *aBuf, unsigned int BufferSize, bool Backup) +IDbConnection::IDbConnection(const char *pPrefix) +{ + str_copy(m_aPrefix, pPrefix); +} + +void IDbConnection::FormatCreateRace(char *aBuf, unsigned int BufferSize, bool Backup) const { str_format(aBuf, BufferSize, "CREATE TABLE IF NOT EXISTS %s_race%s (" @@ -28,7 +33,7 @@ void IDbConnection::FormatCreateRace(char *aBuf, unsigned int BufferSize, bool B BinaryCollate(), MAX_NAME_LENGTH, BinaryCollate()); } -void IDbConnection::FormatCreateTeamrace(char *aBuf, unsigned int BufferSize, const char *pIdType, bool Backup) +void IDbConnection::FormatCreateTeamrace(char *aBuf, unsigned int BufferSize, const char *pIdType, bool Backup) const { str_format(aBuf, BufferSize, "CREATE TABLE IF NOT EXISTS %s_teamrace%s (" @@ -45,7 +50,7 @@ void IDbConnection::FormatCreateTeamrace(char *aBuf, unsigned int BufferSize, co BinaryCollate(), MAX_NAME_LENGTH, BinaryCollate(), pIdType); } -void IDbConnection::FormatCreateMaps(char *aBuf, unsigned int BufferSize) +void IDbConnection::FormatCreateMaps(char *aBuf, unsigned int BufferSize) const { str_format(aBuf, BufferSize, "CREATE TABLE IF NOT EXISTS %s_maps (" @@ -60,7 +65,7 @@ void IDbConnection::FormatCreateMaps(char *aBuf, unsigned int BufferSize) GetPrefix(), BinaryCollate(), BinaryCollate(), BinaryCollate()); } -void IDbConnection::FormatCreateSaves(char *aBuf, unsigned int BufferSize, bool Backup) +void IDbConnection::FormatCreateSaves(char *aBuf, unsigned int BufferSize, bool Backup) const { str_format(aBuf, BufferSize, "CREATE TABLE IF NOT EXISTS %s_saves%s (" @@ -77,7 +82,7 @@ void IDbConnection::FormatCreateSaves(char *aBuf, unsigned int BufferSize, bool BinaryCollate(), BinaryCollate(), BinaryCollate()); } -void IDbConnection::FormatCreatePoints(char *aBuf, unsigned int BufferSize) +void IDbConnection::FormatCreatePoints(char *aBuf, unsigned int BufferSize) const { str_format(aBuf, BufferSize, "CREATE TABLE IF NOT EXISTS %s_points (" diff --git a/src/engine/server/databases/connection.h b/src/engine/server/databases/connection.h index 332e278a4b4..a2da8e0dafd 100644 --- a/src/engine/server/databases/connection.h +++ b/src/engine/server/databases/connection.h @@ -2,7 +2,6 @@ #define ENGINE_SERVER_DATABASES_CONNECTION_H #include "connection_pool.h" -#include #include @@ -12,10 +11,7 @@ class IConsole; class IDbConnection { public: - IDbConnection(const char *pPrefix) - { - str_copy(m_aPrefix, pPrefix); - } + IDbConnection(const char *pPrefix); virtual ~IDbConnection() {} IDbConnection &operator=(const IDbConnection &) = delete; virtual void Print(IConsole *pConsole, const char *pMode) = 0; @@ -88,11 +84,11 @@ class IDbConnection char m_aPrefix[64]; protected: - void FormatCreateRace(char *aBuf, unsigned int BufferSize, bool Backup); - void FormatCreateTeamrace(char *aBuf, unsigned int BufferSize, const char *pIdType, bool Backup); - void FormatCreateMaps(char *aBuf, unsigned int BufferSize); - void FormatCreateSaves(char *aBuf, unsigned int BufferSize, bool Backup); - void FormatCreatePoints(char *aBuf, unsigned int BufferSize); + void FormatCreateRace(char *aBuf, unsigned int BufferSize, bool Backup) const; + void FormatCreateTeamrace(char *aBuf, unsigned int BufferSize, const char *pIdType, bool Backup) const; + void FormatCreateMaps(char *aBuf, unsigned int BufferSize) const; + void FormatCreateSaves(char *aBuf, unsigned int BufferSize, bool Backup) const; + void FormatCreatePoints(char *aBuf, unsigned int BufferSize) const; }; bool MysqlAvailable(); diff --git a/src/engine/server/main.cpp b/src/engine/server/main.cpp index ea19875316f..a50b0d81a9b 100644 --- a/src/engine/server/main.cpp +++ b/src/engine/server/main.cpp @@ -110,17 +110,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 +131,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(); diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 80cb9672612..753faf03b7a 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -1020,7 +1020,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 @@ -1540,7 +1540,7 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) 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; } @@ -1557,7 +1557,7 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) 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; } @@ -1571,7 +1571,7 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) } const char *pPassword = Unpacker.GetString(CUnpacker::SANITIZE_CC); - if(!str_utf8_check(pPassword)) + if(Unpacker.Error()) { return; } @@ -1610,6 +1610,10 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) } int Chunk = Unpacker.GetInt(); + if(Unpacker.Error()) + { + return; + } if(Chunk != m_aClients[ClientID].m_NextMapChunk || !Config()->m_SvFastDownload) { SendMapData(ClientID, Chunk); @@ -1675,19 +1679,20 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) } 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; + } + 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) + 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 @@ -1712,7 +1717,13 @@ 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)); @@ -1727,11 +1738,11 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) 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) @@ -1739,7 +1750,7 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) 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)) { @@ -1762,108 +1773,113 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) } else if(Msg == NETMSG_RCON_AUTH) { + if((pPacket->m_Flags & NET_CHUNKFLAG_VITAL) == 0) + { + return; + } const char *pName = ""; 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: - { - 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); + 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); + } - // 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"); } } + else + { + SendRconLine(ClientID, "Wrong password."); + } } else if(Msg == NETMSG_PING) { @@ -1896,11 +1912,10 @@ void CServer::ProcessClientPacket(CNetChunk *pPacket) } } } - 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); } } @@ -2491,13 +2506,14 @@ 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()); + CNetChunk Response; Response.m_ClientID = -1; Response.m_Address = Packet.m_Address; Response.m_Flags = NETSENDFLAG_CONNLESS; diff --git a/src/engine/server/upnp.h b/src/engine/server/upnp.h index f5c6a7597ed..6b35db268c4 100644 --- a/src/engine/server/upnp.h +++ b/src/engine/server/upnp.h @@ -1,7 +1,7 @@ #ifndef ENGINE_SERVER_UPNP_H #define ENGINE_SERVER_UPNP_H -#include +#include class CUPnP { NETADDR m_Addr; diff --git a/src/engine/serverbrowser.h b/src/engine/serverbrowser.h index 1d01d3e8fb8..c3e83a20032 100644 --- a/src/engine/serverbrowser.h +++ b/src/engine/serverbrowser.h @@ -4,7 +4,7 @@ #define ENGINE_SERVERBROWSER_H #include -#include +#include #include #include diff --git a/src/engine/shared/config.cpp b/src/engine/shared/config.cpp index b262c493a42..3c0f61e23b3 100644 --- a/src/engine/shared/config.cpp +++ b/src/engine/shared/config.cpp @@ -51,12 +51,12 @@ struct SConfigVariable virtual void ResetToOld() = 0; protected: - void ExecuteLine(const char *pLine) + void ExecuteLine(const char *pLine) const { m_pConsole->ExecuteLine(pLine, (m_Flags & CFGFLAG_GAME) != 0 ? IConsole::CLIENT_ID_GAME : -1); } - bool CheckReadOnly() + bool CheckReadOnly() const { if(!m_ReadOnly) return false; @@ -156,7 +156,7 @@ struct SIntConfigVariable : public SConfigVariable void ResetToOld() override { - SetValue(m_OldValue); + *m_pVariable = m_OldValue; } }; @@ -257,7 +257,7 @@ struct SColorConfigVariable : public SConfigVariable void ResetToOld() override { - SetValue(m_OldValue); + *m_pVariable = m_OldValue; } }; @@ -362,7 +362,7 @@ struct SStringConfigVariable : public SConfigVariable void ResetToOld() override { - SetValue(m_pOldValue); + str_copy(m_pStr, m_pOldValue, m_MaxSize); } }; diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index 2d879389514..4ea7f9b0ffe 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -101,6 +101,8 @@ MACRO_CONFIG_INT(EdSmoothZoomTime, ed_smooth_zoom_time, 250, 0, 5000, CFGFLAG_CL MACRO_CONFIG_INT(EdLimitMaxZoomLevel, ed_limit_max_zoom_level, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Specifies, if zooming in the editor should be limited or not (0 = no limit)") MACRO_CONFIG_INT(EdZoomTarget, ed_zoom_target, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Zoom to the current mouse target") 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(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") @@ -178,7 +180,7 @@ MACRO_CONFIG_INT(ClSkipStartMenu, cl_skip_start_menu, 0, 0, 1, CFGFLAG_CLIENT | // server MACRO_CONFIG_INT(SvWarmup, sv_warmup, 0, 0, 0, CFGFLAG_SERVER, "Number of seconds to do warmup before round starts") 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_SAVE | CFGFLAG_SERVER, "Game type (ddnet, mod)") +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") @@ -248,6 +250,7 @@ MACRO_CONFIG_INT(ClRefreshRateInactive, cl_refresh_rate_inactive, 120, 0, 10000, MACRO_CONFIG_INT(ClEditor, cl_editor, 0, 0, 1, CFGFLAG_CLIENT, "Open the map editor") MACRO_CONFIG_INT(ClEditorDilate, cl_editor_dilate, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Automatically dilates embedded images") MACRO_CONFIG_STR(ClSkinFilterString, cl_skin_filter_string, 25, "", CFGFLAG_SAVE | CFGFLAG_CLIENT, "Skin filtering string") +MACRO_CONFIG_INT(ClEditorMaxHistory, cl_editor_max_history, 50, 1, 500, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Maximum number of undo actions in the editor history (not shared between editor, envelope editor and server settings editor)") MACRO_CONFIG_INT(ClAutoDemoRecord, cl_auto_demo_record, 1, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Automatically record demos") MACRO_CONFIG_INT(ClAutoDemoOnConnect, cl_auto_demo_on_connect, 0, 0, 1, CFGFLAG_SAVE | CFGFLAG_CLIENT, "Only start a new demo when connect while automatically record demos") @@ -370,7 +373,7 @@ MACRO_CONFIG_STR(SvName, sv_name, 128, "unnamed server", CFGFLAG_SERVER, "Server 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_STR(SvHostname, sv_hostname, 128, "", CFGFLAG_SAVE | CFGFLAG_SERVER, "Server hostname (0.7 only)") +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") @@ -449,13 +452,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") diff --git a/src/engine/shared/console.cpp b/src/engine/shared/console.cpp index f89bf6810aa..e20e2dae282 100644 --- a/src/engine/shared/console.cpp +++ b/src/engine/shared/console.cpp @@ -1034,7 +1034,7 @@ void CConsole::CResult::ResetVictim() m_Victim = VICTIM_NONE; } -bool CConsole::CResult::HasVictim() +bool CConsole::CResult::HasVictim() const { return m_Victim != VICTIM_NONE; } diff --git a/src/engine/shared/console.h b/src/engine/shared/console.h index f5c4c97eae1..339b521ce6b 100644 --- a/src/engine/shared/console.h +++ b/src/engine/shared/console.h @@ -5,6 +5,7 @@ #include "memheap.h" #include +#include #include #include @@ -136,7 +137,7 @@ class CConsole : public IConsole int m_Victim; void ResetVictim(); - bool HasVictim(); + bool HasVictim() const; void SetVictim(int Victim); void SetVictim(const char *pVictim); int GetVictim() const override; diff --git a/src/engine/shared/csv.cpp b/src/engine/shared/csv.cpp index 0fc90a87948..056091e8c86 100644 --- a/src/engine/shared/csv.cpp +++ b/src/engine/shared/csv.cpp @@ -1,5 +1,7 @@ #include "csv.h" +#include + void CsvWrite(IOHANDLE File, int NumColumns, const char *const *ppColumns) { for(int i = 0; i < NumColumns; i++) diff --git a/src/engine/shared/csv.h b/src/engine/shared/csv.h index 9d3de0e047e..0417e454883 100644 --- a/src/engine/shared/csv.h +++ b/src/engine/shared/csv.h @@ -1,7 +1,7 @@ #ifndef ENGINE_SHARED_CSV_H #define ENGINE_SHARED_CSV_H -#include +#include void CsvWrite(IOHANDLE File, int NumColumns, const char *const *ppColumns); diff --git a/src/engine/shared/datafile.h b/src/engine/shared/datafile.h index 0d8dd6a39c9..db77ca41e46 100644 --- a/src/engine/shared/datafile.h +++ b/src/engine/shared/datafile.h @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include diff --git a/src/engine/shared/demo.cpp b/src/engine/shared/demo.cpp index 2b10b555495..2eefe2b824d 100644 --- a/src/engine/shared/demo.cpp +++ b/src/engine/shared/demo.cpp @@ -31,6 +31,16 @@ static const int gs_NumMarkersOffset = 176; static const ColorRGBA gs_DemoPrintColor{0.75f, 0.7f, 0.7f, 1.0f}; +bool CDemoHeader::Valid() const +{ + // Check marker and ensure that strings are zero-terminated and valid UTF-8. + return mem_comp(m_aMarker, gs_aHeaderMarker, sizeof(gs_aHeaderMarker)) == 0 && + mem_has_null(m_aNetversion, sizeof(m_aNetversion)) && str_utf8_check(m_aNetversion) && + mem_has_null(m_aMapName, sizeof(m_aMapName)) && str_utf8_check(m_aMapName) && + mem_has_null(m_aType, sizeof(m_aType)) && str_utf8_check(m_aType) && + mem_has_null(m_aTimestamp, sizeof(m_aTimestamp)) && str_utf8_check(m_aTimestamp); +} + CDemoRecorder::CDemoRecorder(class CSnapshotDelta *pSnapshotDelta, bool NoMapData) { m_File = 0; diff --git a/src/engine/shared/engine.cpp b/src/engine/shared/engine.cpp index 259c28523fc..9f0469e98de 100644 --- a/src/engine/shared/engine.cpp +++ b/src/engine/shared/engine.cpp @@ -49,15 +49,8 @@ class CEngine : public IEngine str_copy(m_aAppName, pAppname); if(!Test) { - // dbg_msg("engine", "running on %s-%s-%s", CONF_FAMILY_STRING, CONF_PLATFORM_STRING, CONF_ARCH_STRING); -#ifdef CONF_ARCH_ENDIAN_LITTLE - dbg_msg("engine", "arch is little endian"); -#elif defined(CONF_ARCH_ENDIAN_BIG) - dbg_msg("engine", "arch is big endian"); -#else - dbg_msg("engine", "unknown endian"); -#endif + dbg_msg("engine", "arch is %s", CONF_ARCH_ENDIAN_STRING); char aVersionStr[128]; if(os_version_str(aVersionStr, sizeof(aVersionStr))) @@ -78,6 +71,7 @@ class CEngine : public IEngine ~CEngine() override { m_JobPool.Destroy(); + CNetBase::CloseLog(); } void Init() override @@ -88,8 +82,6 @@ class CEngine : public IEngine if(!m_pConsole || !m_pStorage) return; - char aFullPath[IO_MAX_PATH_LENGTH]; - m_pStorage->GetCompletePath(IStorage::TYPE_SAVE, "dumps/", aFullPath, sizeof(aFullPath)); m_pConsole->Register("dbg_lognetwork", "", CFGFLAG_SERVER | CFGFLAG_CLIENT, Con_DbgLognetwork, this, "Log the network"); } diff --git a/src/engine/shared/fifo.h b/src/engine/shared/fifo.h index b844de0738f..ad431b9bbab 100644 --- a/src/engine/shared/fifo.h +++ b/src/engine/shared/fifo.h @@ -1,6 +1,7 @@ #ifndef ENGINE_SHARED_FIFO_H #define ENGINE_SHARED_FIFO_H +#include #include class CFifo diff --git a/src/engine/shared/filecollection.cpp b/src/engine/shared/filecollection.cpp index 8125e8ab91e..0dab1c258f4 100644 --- a/src/engine/shared/filecollection.cpp +++ b/src/engine/shared/filecollection.cpp @@ -2,6 +2,7 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include +#include #include diff --git a/src/engine/shared/filecollection.h b/src/engine/shared/filecollection.h index d284685ac80..9a4b561c65a 100644 --- a/src/engine/shared/filecollection.h +++ b/src/engine/shared/filecollection.h @@ -3,7 +3,7 @@ #ifndef ENGINE_SHARED_FILECOLLECTION_H #define ENGINE_SHARED_FILECOLLECTION_H -#include +#include #include diff --git a/src/engine/shared/http.cpp b/src/engine/shared/http.cpp index 090e9aabd55..259ff8d0446 100644 --- a/src/engine/shared/http.cpp +++ b/src/engine/shared/http.cpp @@ -90,7 +90,6 @@ bool HttpInit(IStorage *pStorage) 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_SHARE, CURL_LOCK_DATA_CONNECT); curl_share_setopt(gs_pShare, CURLSHOPT_LOCKFUNC, CurlLock); curl_share_setopt(gs_pShare, CURLSHOPT_UNLOCKFUNC, CurlUnlock); diff --git a/src/engine/shared/jsonwriter.cpp b/src/engine/shared/jsonwriter.cpp index 5c870e4f5aa..ca703bf49c9 100644 --- a/src/engine/shared/jsonwriter.cpp +++ b/src/engine/shared/jsonwriter.cpp @@ -3,6 +3,8 @@ #include "jsonwriter.h" +#include + static char EscapeJsonChar(char c) { switch(c) diff --git a/src/engine/shared/jsonwriter.h b/src/engine/shared/jsonwriter.h index 2365afe2f96..83086055a0c 100644 --- a/src/engine/shared/jsonwriter.h +++ b/src/engine/shared/jsonwriter.h @@ -3,9 +3,10 @@ #ifndef ENGINE_SHARED_JSONWRITER_H #define ENGINE_SHARED_JSONWRITER_H -#include +#include #include +#include /** * JSON writer with abstract writing function. diff --git a/src/engine/shared/kernel.cpp b/src/engine/shared/kernel.cpp index f578cb68505..bdeb46ee3a0 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 8cc02a59ce8..d322ee27926 100644 --- a/src/engine/shared/linereader.cpp +++ b/src/engine/shared/linereader.cpp @@ -2,6 +2,8 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include "linereader.h" +#include + void CLineReader::Init(IOHANDLE File) { m_BufferMaxSize = sizeof(m_aBuffer) - 1; diff --git a/src/engine/shared/linereader.h b/src/engine/shared/linereader.h index 6d4aa07e202..7d5f1c5ab45 100644 --- a/src/engine/shared/linereader.h +++ b/src/engine/shared/linereader.h @@ -2,7 +2,7 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #ifndef ENGINE_SHARED_LINEREADER_H #define ENGINE_SHARED_LINEREADER_H -#include +#include // buffered stream for reading lines, should perhaps be something smaller class CLineReader diff --git a/src/engine/shared/map.h b/src/engine/shared/map.h index ebb3a8591cb..c104c6c0a6a 100644 --- a/src/engine/shared/map.h +++ b/src/engine/shared/map.h @@ -3,7 +3,7 @@ #ifndef ENGINE_SHARED_MAP_H #define ENGINE_SHARED_MAP_H -#include +#include #include "datafile.h" #include diff --git a/src/engine/shared/netban.h b/src/engine/shared/netban.h index fd7dbb8a857..6de37e51d90 100644 --- a/src/engine/shared/netban.h +++ b/src/engine/shared/netban.h @@ -1,9 +1,8 @@ #ifndef ENGINE_SHARED_NETBAN_H #define ENGINE_SHARED_NETBAN_H -#include - #include +#include inline int NetComp(const NETADDR *pAddr1, const NETADDR *pAddr2) { diff --git a/src/engine/shared/network.cpp b/src/engine/shared/network.cpp index d7afd49b384..92e87ce8a08 100644 --- a/src/engine/shared/network.cpp +++ b/src/engine/shared/network.cpp @@ -317,7 +317,7 @@ void CNetBase::SendControlMsg(NETSOCKET Socket, NETADDR *pAddr, int Ack, int Con CNetBase::SendPacket(Socket, pAddr, &Construct, SecurityToken, Sixup, true); } -unsigned char *CNetChunkHeader::Pack(unsigned char *pData, int Split) +unsigned char *CNetChunkHeader::Pack(unsigned char *pData, int Split) const { pData[0] = ((m_Flags & 3) << 6) | ((m_Size >> Split) & 0x3f); pData[1] = (m_Size & ((1 << Split) - 1)); diff --git a/src/engine/shared/network.h b/src/engine/shared/network.h index 8dc323f2690..7ab0de5b5f4 100644 --- a/src/engine/shared/network.h +++ b/src/engine/shared/network.h @@ -133,7 +133,7 @@ class CNetChunkHeader int m_Size; int m_Sequence; - unsigned char *Pack(unsigned char *pData, int Split = 4); + unsigned char *Pack(unsigned char *pData, int Split = 4) const; unsigned char *Unpack(unsigned char *pData, int Split = 4); }; diff --git a/src/engine/shared/packer.cpp b/src/engine/shared/packer.cpp index 2b183113b16..3593929ea68 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_ex.cpp b/src/engine/shared/protocol_ex.cpp index 4d44ca7bed7..51f4b286ada 100644 --- a/src/engine/shared/protocol_ex.cpp +++ b/src/engine/shared/protocol_ex.cpp @@ -3,6 +3,7 @@ #include "config.h" #include "uuid_manager.h" +#include #include #include diff --git a/src/engine/shared/serverinfo.cpp b/src/engine/shared/serverinfo.cpp index 9f0f4a1b42f..ecccfa5994e 100644 --- a/src/engine/shared/serverinfo.cpp +++ b/src/engine/shared/serverinfo.cpp @@ -2,6 +2,7 @@ #include "json.h" #include +#include #include #include diff --git a/src/engine/shared/snapshot.cpp b/src/engine/shared/snapshot.cpp index 5449a071692..7481eb63e4a 100644 --- a/src/engine/shared/snapshot.cpp +++ b/src/engine/shared/snapshot.cpp @@ -4,8 +4,8 @@ #include "compression.h" #include "uuid_manager.h" -#include #include +#include #include #include @@ -98,7 +98,7 @@ const void *CSnapshot::FindItem(int Type, int ID) const return Index < 0 ? nullptr : GetItem(Index)->Data(); } -unsigned CSnapshot::Crc() +unsigned CSnapshot::Crc() const { unsigned int Crc = 0; @@ -113,7 +113,7 @@ unsigned CSnapshot::Crc() return Crc; } -void CSnapshot::DebugDump() +void CSnapshot::DebugDump() const { dbg_msg("snapshot", "data_size=%d num_items=%d", m_DataSize, m_NumItems); for(int i = 0; i < m_NumItems; i++) @@ -256,10 +256,10 @@ CSnapshotDelta::CSnapshotDelta(const CSnapshotDelta &Old) mem_zero(&m_Empty, sizeof(m_Empty)); } -void CSnapshotDelta::SetStaticsize(int ItemType, int Size) +void CSnapshotDelta::SetStaticsize(int ItemType, size_t Size) { - if(ItemType < 0 || ItemType >= MAX_NETOBJSIZES) - return; + dbg_assert(ItemType >= 0 && ItemType < MAX_NETOBJSIZES, "ItemType invalid"); + dbg_assert(Size <= (size_t)std::numeric_limits::max(), "Size invalid"); m_aItemSizes[ItemType] = Size; } @@ -269,7 +269,7 @@ const CSnapshotDelta::CData *CSnapshotDelta::EmptyDelta() const } // TODO: OPT: this should be made much faster -int CSnapshotDelta::CreateDelta(const CSnapshot *pFrom, CSnapshot *pTo, void *pDstData) +int CSnapshotDelta::CreateDelta(const CSnapshot *pFrom, const CSnapshot *pTo, void *pDstData) { CData *pDelta = (CData *)pDstData; int *pData = (int *)pDelta->m_aData; @@ -352,13 +352,6 @@ int CSnapshotDelta::CreateDelta(const CSnapshot *pFrom, CSnapshot *pTo, void *pD return (int)((char *)pData - (char *)pDstData); } -static int RangeCheck(void *pEnd, void *pPtr, int Size) -{ - if((const char *)pPtr + Size > (const char *)pEnd) - return -1; - return 0; -} - int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const void *pSrcData, int DataSize) { CData *pDelta = (CData *)pSrcData; @@ -423,12 +416,12 @@ int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const vo { if(pData + 1 > pEnd) return -103; - if(*pData < 0 || (size_t)*pData > INT_MAX / sizeof(int32_t)) + if(*pData < 0 || (size_t)*pData > std::numeric_limits::max() / sizeof(int32_t)) return -204; ItemSize = (*pData++) * sizeof(int32_t); } - if(ItemSize < 0 || RangeCheck(pEnd, pData, ItemSize)) + if(ItemSize < 0 || (const char *)pEnd - (const char *)pData < ItemSize) return -205; const int Key = (Type << 16) | ID; @@ -465,24 +458,21 @@ int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const vo void CSnapshotStorage::Init() { - m_pFirst = 0; - m_pLast = 0; + m_pFirst = nullptr; + m_pLast = nullptr; } void CSnapshotStorage::PurgeAll() { - CHolder *pHolder = m_pFirst; - - while(pHolder) + while(m_pFirst) { - CHolder *pNext = pHolder->m_pNext; - free(pHolder); - pHolder = pNext; + CHolder *pNext = m_pFirst->m_pNext; + free(m_pFirst->m_pSnap); + free(m_pFirst->m_pAltSnap); + free(m_pFirst); + m_pFirst = pNext; } - - // no more snapshots in storage - m_pFirst = 0; - m_pLast = 0; + m_pLast = nullptr; } void CSnapshotStorage::PurgeUntil(int Tick) @@ -494,6 +484,8 @@ void CSnapshotStorage::PurgeUntil(int Tick) CHolder *pNext = pHolder->m_pNext; if(pHolder->m_Tick >= Tick) return; // no more to remove + free(pHolder->m_pSnap); + free(pHolder->m_pAltSnap); free(pHolder); // did we come to the end of the list? @@ -501,49 +493,42 @@ void CSnapshotStorage::PurgeUntil(int Tick) break; m_pFirst = pNext; - pNext->m_pPrev = 0x0; - + pNext->m_pPrev = nullptr; pHolder = pNext; } // no more snapshots in storage - m_pFirst = 0; - m_pLast = 0; + m_pFirst = nullptr; + m_pLast = nullptr; } -void CSnapshotStorage::Add(int Tick, int64_t Tagtime, int DataSize, const void *pData, int AltDataSize, const void *pAltData) +void CSnapshotStorage::Add(int Tick, int64_t Tagtime, size_t DataSize, const void *pData, size_t AltDataSize, const void *pAltData) { - // allocate memory for holder + snapshot_data - int TotalSize = sizeof(CHolder) + DataSize; - - if(AltDataSize > 0) - { - TotalSize += AltDataSize; - } - - CHolder *pHolder = (CHolder *)malloc(TotalSize); + dbg_assert(DataSize <= (size_t)CSnapshot::MAX_SIZE, "Snapshot data size invalid"); + dbg_assert(AltDataSize <= (size_t)CSnapshot::MAX_SIZE, "Alt snapshot data size invalid"); - // set data + CHolder *pHolder = static_cast(malloc(sizeof(CHolder))); pHolder->m_Tick = Tick; pHolder->m_Tagtime = Tagtime; - pHolder->m_SnapSize = DataSize; - pHolder->m_pSnap = (CSnapshot *)(pHolder + 1); + + pHolder->m_pSnap = static_cast(malloc(DataSize)); mem_copy(pHolder->m_pSnap, pData, DataSize); + pHolder->m_SnapSize = DataSize; - if(AltDataSize > 0) // create alternative if wanted + if(AltDataSize) // create alternative if wanted { - pHolder->m_pAltSnap = (CSnapshot *)(((char *)pHolder->m_pSnap) + DataSize); + pHolder->m_pAltSnap = static_cast(malloc(AltDataSize)); mem_copy(pHolder->m_pAltSnap, pAltData, AltDataSize); pHolder->m_AltSnapSize = AltDataSize; } else { - pHolder->m_pAltSnap = 0; + pHolder->m_pAltSnap = nullptr; pHolder->m_AltSnapSize = 0; } // link - pHolder->m_pNext = 0; + pHolder->m_pNext = nullptr; pHolder->m_pPrev = m_pLast; if(m_pLast) m_pLast->m_pNext = pHolder; @@ -552,7 +537,7 @@ void CSnapshotStorage::Add(int Tick, int64_t Tagtime, int DataSize, const void * m_pLast = pHolder; } -int CSnapshotStorage::Get(int Tick, int64_t *pTagtime, const CSnapshot **ppData, const CSnapshot **ppAltData) +int CSnapshotStorage::Get(int Tick, int64_t *pTagtime, const CSnapshot **ppData, const CSnapshot **ppAltData) const { CHolder *pHolder = m_pFirst; @@ -605,7 +590,7 @@ int *CSnapshotBuilder::GetItemData(int Key) if(GetItem(i)->Key() == Key) return GetItem(i)->Data(); } - return 0; + return nullptr; } int CSnapshotBuilder::Finish(void *pSnapData) @@ -619,7 +604,7 @@ int CSnapshotBuilder::Finish(void *pSnapData) return pSnap->TotalSize(); } -int CSnapshotBuilder::GetTypeFromIndex(int Index) +int CSnapshotBuilder::GetTypeFromIndex(int Index) const { return CSnapshot::MAX_TYPE - Index; } @@ -657,7 +642,7 @@ void *CSnapshotBuilder::NewItem(int Type, int ID, int Size) { if(ID == -1) { - return 0; + return nullptr; } if(m_DataSize + sizeof(CSnapshotItem) + Size >= CSnapshot::MAX_SIZE || @@ -665,7 +650,7 @@ void *CSnapshotBuilder::NewItem(int Type, int ID, int Size) { dbg_assert(m_DataSize < CSnapshot::MAX_SIZE, "too much data"); dbg_assert(m_NumItems < CSnapshot::MAX_ITEMS, "too many items"); - return 0; + return nullptr; } bool Extended = false; diff --git a/src/engine/shared/snapshot.h b/src/engine/shared/snapshot.h index 0388e533f9e..6947db1bce3 100644 --- a/src/engine/shared/snapshot.h +++ b/src/engine/shared/snapshot.h @@ -56,8 +56,8 @@ class CSnapshot int GetExternalItemType(int InternalType) const; const void *FindItem(int Type, int ID) const; - unsigned Crc(); - void DebugDump(); + unsigned Crc() const; + void DebugDump() const; bool IsValid(size_t ActualSize) const; static const CSnapshot *EmptySnapshot() { return &ms_EmptySnapshot; } @@ -95,10 +95,10 @@ class CSnapshotDelta CSnapshotDelta(const CSnapshotDelta &Old); int GetDataRate(int Index) const { return m_aSnapshotDataRate[Index]; } int GetDataUpdates(int Index) const { return m_aSnapshotDataUpdates[Index]; } - void SetStaticsize(int ItemType, int Size); + void SetStaticsize(int ItemType, size_t Size); const CData *EmptyDelta() const; - int CreateDelta(const class CSnapshot *pFrom, class CSnapshot *pTo, void *pDstData); - int UnpackDelta(const class CSnapshot *pFrom, class CSnapshot *pTo, const void *pSrcData, int DataSize); + int CreateDelta(const CSnapshot *pFrom, const CSnapshot *pTo, void *pDstData); + int UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const void *pSrcData, int DataSize); }; // CSnapshotStorage @@ -130,8 +130,8 @@ class CSnapshotStorage void Init(); void PurgeAll(); void PurgeUntil(int Tick); - void Add(int Tick, int64_t Tagtime, int DataSize, const void *pData, int AltDataSize, const void *pAltData); - int Get(int Tick, int64_t *pTagtime, const CSnapshot **ppData, const CSnapshot **ppAltData); + void Add(int Tick, int64_t Tagtime, size_t DataSize, const void *pData, size_t AltDataSize, const void *pAltData); + int Get(int Tick, int64_t *pTagtime, const CSnapshot **ppData, const CSnapshot **ppAltData) const; }; class CSnapshotBuilder @@ -152,7 +152,7 @@ class CSnapshotBuilder void AddExtendedItemType(int Index); int GetExtendedItemTypeIndex(int TypeID); - int GetTypeFromIndex(int Index); + int GetTypeFromIndex(int Index) const; bool m_Sixup; diff --git a/src/engine/shared/uuid_manager.cpp b/src/engine/shared/uuid_manager.cpp index 4dfb9010060..3cff589d608 100644 --- a/src/engine/shared/uuid_manager.cpp +++ b/src/engine/shared/uuid_manager.cpp @@ -1,6 +1,7 @@ #include "uuid_manager.h" #include +#include #include #include @@ -100,6 +101,11 @@ bool CUuid::operator!=(const CUuid &Other) const return !(*this == Other); } +bool CUuid::operator<(const CUuid &Other) const +{ + return mem_comp(this, &Other, sizeof(*this)) < 0; +} + static int GetIndex(int ID) { return ID - OFFSET_UUID; diff --git a/src/engine/shared/uuid_manager.h b/src/engine/shared/uuid_manager.h index c1c607e750f..00d1a4822f5 100644 --- a/src/engine/shared/uuid_manager.h +++ b/src/engine/shared/uuid_manager.h @@ -3,8 +3,6 @@ #include -#include - enum { UUID_MAXSTRSIZE = 37, // 12345678-0123-5678-0123-567890123456 @@ -21,7 +19,7 @@ struct CUuid bool operator==(const CUuid &Other) const; bool operator!=(const CUuid &Other) const; - bool operator<(const CUuid &Other) const { return mem_comp(m_aData, Other.m_aData, sizeof(m_aData)) < 0; } + bool operator<(const CUuid &Other) const; }; extern const CUuid UUID_ZEROED; diff --git a/src/engine/steam.h b/src/engine/steam.h index baf9661175b..20341e2b048 100644 --- a/src/engine/steam.h +++ b/src/engine/steam.h @@ -1,6 +1,8 @@ #ifndef ENGINE_STEAM_H #define ENGINE_STEAM_H +#include + #include "kernel.h" class ISteam : public IInterface diff --git a/src/engine/storage.h b/src/engine/storage.h index 38d441f3955..9380d0dd5e2 100644 --- a/src/engine/storage.h +++ b/src/engine/storage.h @@ -4,6 +4,7 @@ #define ENGINE_STORAGE_H #include +#include #include "kernel.h" diff --git a/src/engine/textrender.h b/src/engine/textrender.h index 9862d7a88b6..0d1287c3d4e 100644 --- a/src/engine/textrender.h +++ b/src/engine/textrender.h @@ -140,6 +140,8 @@ MAYBE_UNUSED static const char *FONT_ICON_DICE_FIVE = "\xEF\x94\xA3"; 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"; } // end namespace FontIcons enum ETextCursorSelectionMode @@ -179,6 +181,18 @@ struct STextBoundingBox } }; +// Allow to render multi colored text in one go without having to call TextEx() multiple times. +// Needed to allow multi colored multi line texts +struct STextColorSplit +{ + int m_CharIndex; // Which index within the text should the split occur + int m_Length; // How long is the split + ColorRGBA m_Color; // The color the text should be starting from m_CharIndex + + STextColorSplit(int CharIndex, int Length, const ColorRGBA &Color) : + m_CharIndex(CharIndex), m_Length(Length), m_Color(Color) {} +}; + class CTextCursor { public: @@ -197,6 +211,7 @@ class CTextCursor float m_FontSize; float m_AlignedFontSize; + float m_LineSpacing; ETextCursorSelectionMode m_CalculateSelectionMode; float m_SelectionHeightFactor; @@ -217,15 +232,48 @@ class CTextCursor int m_CursorCharacter; vec2 m_CursorRenderedPosition; + // Color splits of the cursor to allow multicolored text + std::vector m_vColorSplits; + float Height() const { - return m_LineCount * m_AlignedFontSize; + return m_LineCount * m_AlignedFontSize + std::max(0, m_LineCount - 1) * m_LineSpacing; } STextBoundingBox BoundingBox() const { return {m_StartX, m_StartY, m_LongestLineWidth, Height()}; } + + void Reset() + { + m_Flags = 0; + m_LineCount = 0; + m_GlyphCount = 0; + m_CharCount = 0; + m_MaxLines = 0; + m_StartX = 0; + m_StartY = 0; + m_LineWidth = 0; + m_X = 0; + m_Y = 0; + m_MaxCharacterHeight = 0; + m_LongestLineWidth = 0; + m_FontSize = 0; + m_AlignedFontSize = 0; + m_LineSpacing = 0; + m_CalculateSelectionMode = TEXT_CURSOR_SELECTION_MODE_NONE; + m_SelectionHeightFactor = 0; + m_PressMouse = vec2(); + m_ReleaseMouse = vec2(); + m_SelectionStart = 0; + m_SelectionEnd = 0; + m_CursorMode = TEXT_CURSOR_CURSOR_MODE_NONE; + m_ForceCursorRendering = false; + m_CursorCharacter = 0; + m_CursorRenderedPosition = vec2(); + m_vColorSplits.clear(); + } }; struct STextContainerUsages @@ -303,7 +351,7 @@ class ITextRender : public IInterface virtual void TextSelectionColor(ColorRGBA rgb) = 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, int Flags = 0) = 0; + virtual STextBoundingBox TextBoundingBox(float Size, const char *pText, int StrLength = -1, float LineWidth = -1.0f, float LineSpacing = 0.0f, int Flags = 0) = 0; virtual ColorRGBA GetTextColor() const = 0; virtual ColorRGBA GetTextOutlineColor() const = 0; diff --git a/src/engine/warning.h b/src/engine/warning.h index 0214a6b0fbe..bd1e45e7528 100644 --- a/src/engine/warning.h +++ b/src/engine/warning.h @@ -3,17 +3,10 @@ struct SWarning { - SWarning() {} - SWarning(const char *pMsg) - { - str_copy(m_aWarningTitle, ""); - str_copy(m_aWarningMsg, pMsg); - } - SWarning(const char *pTitle, const char *pMsg) - { - str_copy(m_aWarningTitle, pTitle); - str_copy(m_aWarningMsg, pMsg); - } + SWarning() = default; + SWarning(const char *pMsg); + SWarning(const char *pTitle, const char *pMsg); + char m_aWarningTitle[128]; char m_aWarningMsg[256]; bool m_WasShown = false; diff --git a/src/game/client/components/chat.cpp b/src/game/client/components/chat.cpp index 3b8d1cfee54..079538fb178 100644 --- a/src/game/client/components/chat.cpp +++ b/src/game/client/components/chat.cpp @@ -35,11 +35,11 @@ CChat::CChat() Line.m_QuadContainerIndex = -1; } -#define CHAT_COMMAND(name, params, flags, callback, userdata, help) RegisterCommand(name, params, flags, help); +#define CHAT_COMMAND(name, params, flags, callback, userdata, help) m_vDefaultCommands.emplace_back(name, params, help); #include #undef CHAT_COMMAND - std::sort(m_vCommands.begin(), m_vCommands.end()); + std::sort(m_vDefaultCommands.begin(), m_vDefaultCommands.end()); m_Mode = MODE_NONE; m_Input.SetClipboardLineCallback([this](const char *pStr) { SayChat(pStr); }); @@ -76,9 +76,20 @@ CChat::CChat() }); } -void CChat::RegisterCommand(const char *pName, const char *pParams, int flags, const char *pHelp) +void CChat::RegisterCommand(const char *pName, const char *pParams, const char *pHelpText) { - m_vCommands.emplace_back(pName, pParams); + // Don't allow duplicate commands. + for(const auto &Command : m_vCommands) + if(str_comp(Command.m_aName, pName) == 0) + return; + + m_vCommands.emplace_back(pName, pParams, pHelpText); + m_CommandsNeedSorting = true; +} + +void CChat::UnregisterCommand(const char *pName) +{ + m_vCommands.erase(std::remove_if(m_vCommands.begin(), m_vCommands.end(), [pName](const CCommand &Command) { return str_comp(Command.m_aName, pName) == 0; }), m_vCommands.end()); } void CChat::RebuildChat() @@ -126,6 +137,8 @@ void CChat::Reset() m_CurrentLine = 0; m_IsInputCensored = false; m_EditingNewLine = true; + m_ServerSupportsCommandInfo = false; + m_CommandsNeedSorting = false; mem_zero(m_aCurrentInputText, sizeof(m_aCurrentInputText)); DisableMode(); @@ -222,6 +235,11 @@ 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) @@ -232,10 +250,19 @@ bool CChat::OnInput(const IInput::CEvent &Event) DisableMode(); m_pClient->OnRelease(); if(g_Config.m_ClChatReset) + { m_Input.Clear(); + m_pHistoryEntry = nullptr; + } } else if(Event.m_Flags & IInput::FLAG_PRESS && (Event.m_Key == KEY_RETURN || Event.m_Key == KEY_KP_ENTER)) { + if(m_CommandsNeedSorting) + { + std::sort(m_vCommands.begin(), m_vCommands.end()); + m_CommandsNeedSorting = false; + } + if(m_Input.GetString()[0]) { bool AddEntry = false; @@ -258,7 +285,7 @@ bool CChat::OnInput(const IInput::CEvent &Event) mem_copy(pEntry->m_aText, m_Input.GetString(), m_Input.GetLength() + 1); } } - m_pHistoryEntry = 0x0; + m_pHistoryEntry = nullptr; DisableMode(); m_pClient->OnRelease(); m_Input.Clear(); @@ -307,7 +334,7 @@ bool CChat::OnInput(const IInput::CEvent &Event) }); } - if(m_aCompletionBuffer[0] == '/') + if(m_aCompletionBuffer[0] == '/' && !m_vCommands.empty()) { CCommand *pCompletionCommand = 0; @@ -340,7 +367,7 @@ bool CChat::OnInput(const IInput::CEvent &Event) auto &Command = m_vCommands[Index]; - if(str_startswith(Command.m_pName, pCommandStart)) + if(str_startswith(Command.m_aName, pCommandStart)) { pCompletionCommand = &Command; m_CompletionChosen = Index + SearchType * NumCommands; @@ -357,10 +384,10 @@ bool CChat::OnInput(const IInput::CEvent &Event) // add the command str_append(aBuf, "/"); - str_append(aBuf, pCompletionCommand->m_pName); + str_append(aBuf, pCompletionCommand->m_aName); // add separator - const char *pSeparator = pCompletionCommand->m_pParams[0] == '\0' ? "" : " "; + const char *pSeparator = pCompletionCommand->m_aParams[0] == '\0' ? "" : " "; str_append(aBuf, pSeparator); if(*pSeparator) str_append(aBuf, pSeparator); @@ -368,7 +395,7 @@ bool CChat::OnInput(const IInput::CEvent &Event) // add part after the name str_append(aBuf, m_Input.GetString() + m_PlaceholderOffset + m_PlaceholderLength); - m_PlaceholderLength = str_length(pSeparator) + str_length(pCompletionCommand->m_pName) + 1; + m_PlaceholderLength = str_length(pSeparator) + str_length(pCompletionCommand->m_aName) + 1; m_Input.Set(aBuf); m_Input.SetCursorOffset(m_PlaceholderOffset + m_PlaceholderLength); } @@ -541,6 +568,21 @@ void CChat::OnMessage(int MsgType, void *pRawMsg) if(!m_pClient->m_ChatHelper.FilterChat(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) + { + CNetMsg_Sv_CommandInfo *pMsg = (CNetMsg_Sv_CommandInfo *)pRawMsg; + if(!m_ServerSupportsCommandInfo) + { + m_vCommands.clear(); + m_ServerSupportsCommandInfo = true; + } + RegisterCommand(pMsg->m_pName, pMsg->m_pArgsFormat, pMsg->m_pHelpText); + } + else if(MsgType == NETMSGTYPE_SV_COMMANDINFOREMOVE) + { + CNetMsg_Sv_CommandInfoRemove *pMsg = (CNetMsg_Sv_CommandInfoRemove *)pRawMsg; + UnregisterCommand(pMsg->m_pName); + } } bool CChat::LineShouldHighlight(const char *pLine, const char *pName) @@ -1181,7 +1223,10 @@ void CChat::OnRender() m_Input.Activate(EInputPriority::CHAT); // Ensure that the input is active const CUIRect InputCursorRect = {Cursor.m_X, Cursor.m_Y - ScrollOffset, 0.0f, 0.0f}; - const STextBoundingBox BoundingBox = m_Input.Render(&InputCursorRect, Cursor.m_FontSize, TEXTALIGN_TL, m_Input.WasChanged(), MessageMaxWidth); + const bool WasChanged = m_Input.WasChanged(); + const bool WasCursorChanged = m_Input.WasCursorChanged(); + const bool Changed = WasChanged || WasCursorChanged; + const STextBoundingBox BoundingBox = m_Input.Render(&InputCursorRect, Cursor.m_FontSize, TEXTALIGN_TL, Changed, MessageMaxWidth, 0.0f); Graphics()->ClipDisable(); diff --git a/src/game/client/components/chat.h b/src/game/client/components/chat.h index 62ee924390e..cf23efc935a 100644 --- a/src/game/client/components/chat.h +++ b/src/game/client/components/chat.h @@ -96,21 +96,26 @@ class CChat : public CComponent struct CCommand { - const char *m_pName; - const char *m_pParams; + char m_aName[IConsole::TEMPCMD_NAME_LENGTH]; + char m_aParams[IConsole::TEMPCMD_PARAMS_LENGTH]; + char m_aHelpText[IConsole::TEMPCMD_HELP_LENGTH]; CCommand() = default; - CCommand(const char *pName, const char *pParams) : - m_pName(pName), m_pParams(pParams) + CCommand(const char *pName, const char *pParams, const char *pHelpText) { + str_copy(m_aName, pName); + str_copy(m_aParams, pParams); + str_copy(m_aHelpText, pHelpText); } - bool operator<(const CCommand &Other) const { return str_comp(m_pName, Other.m_pName) < 0; } - bool operator<=(const CCommand &Other) const { return str_comp(m_pName, Other.m_pName) <= 0; } - bool operator==(const CCommand &Other) const { return str_comp(m_pName, Other.m_pName) == 0; } + bool operator<(const CCommand &Other) const { return str_comp(m_aName, Other.m_aName) < 0; } + bool operator<=(const CCommand &Other) const { return str_comp(m_aName, Other.m_aName) <= 0; } + bool operator==(const CCommand &Other) const { return str_comp(m_aName, Other.m_aName) == 0; } }; std::vector m_vCommands; + std::vector m_vDefaultCommands; + bool m_CommandsNeedSorting; struct CHistoryEntry { @@ -126,6 +131,8 @@ class CChat : public CComponent char m_aCurrentInputText[MAX_LINE_LENGTH]; bool m_EditingNewLine; + bool m_ServerSupportsCommandInfo; + static void ConSay(IConsole::IResult *pResult, void *pUserData); static void ConSayTeam(IConsole::IResult *pResult, void *pUserData); static void ConChat(IConsole::IResult *pResult, void *pUserData); @@ -151,7 +158,8 @@ class CChat : public CComponent void DisableMode(); void Say(int Team, const char *pLine); void SayChat(const char *pLine); - void RegisterCommand(const char *pName, const char *pParams, int flags, const char *pHelp); + void RegisterCommand(const char *pName, const char *pParams, const char *pHelpText); + void UnregisterCommand(const char *pName); void Echo(const char *pString); void OnWindowResize() override; @@ -165,6 +173,7 @@ class CChat : public CComponent void OnMessage(int MsgType, void *pRawMsg) override; bool OnInput(const IInput::CEvent &Event) override; void OnInit() override; + void OnMapLoad() override; void RebuildChat(); diff --git a/src/game/client/components/chillerbot/chillerbotux.cpp b/src/game/client/components/chillerbot/chillerbotux.cpp index a701a3193c3..e51453622e3 100644 --- a/src/game/client/components/chillerbot/chillerbotux.cpp +++ b/src/game/client/components/chillerbot/chillerbotux.cpp @@ -208,7 +208,7 @@ void CChillerBotUX::ChangeTileNotifyTick() IEngineGraphics *pGraphics = ((IEngineGraphics *)Kernel()->RequestInterface()); if(pGraphics && !pGraphics->WindowActive() && Graphics()) { - NotificationsNotify("chillerbot-ux", "current tile changed"); + Client()->Notify("chillerbot-ux", "current tile changed"); Graphics()->NotifyWindow(); } m_LastNotification = time_get(); diff --git a/src/game/client/components/console.cpp b/src/game/client/components/console.cpp index 7c2ae047078..0721ebe501c 100644 --- a/src/game/client/components/console.cpp +++ b/src/game/client/components/console.cpp @@ -24,8 +24,13 @@ #include #include +#include + #include "console.h" +static constexpr float FONT_SIZE = 10.0f; +static constexpr float LINE_SPACING = 1.0f; + class CConsoleLogger : public ILogger { CGameConsole *m_pConsole; @@ -95,6 +100,9 @@ static bool IsSettingCommandPrefix(const char *pStr) return std::any_of(std::begin(gs_apSettingCommands), std::end(gs_apSettingCommands), [pStr](auto *pCmd) { return str_startswith_nocase(pStr, pCmd); }); } +const ColorRGBA CGameConsole::ms_SearchHighlightColor = ColorRGBA(1.0f, 0.0f, 0.0f, 1.0f); +const ColorRGBA CGameConsole::ms_SearchSelectedColor = ColorRGBA(1.0f, 1.0f, 0.0f, 1.0f); + CGameConsole::CInstance::CInstance(int Type) { m_pHistoryEntry = 0x0; @@ -125,6 +133,9 @@ CGameConsole::CInstance::CInstance(int Type) m_IsCommand = false; m_Input.SetClipboardLineCallback([this](const char *pStr) { ExecuteLine(pStr); }); + + m_CurrentMatchIndex = -1; + m_aCurrentSearchString[0] = '\0'; } void CGameConsole::CInstance::Init(CGameConsole *pGameConsole) @@ -142,31 +153,43 @@ void CGameConsole::CInstance::ClearBacklog() } m_Backlog.Init(); - m_BacklogCurPage = 0; + m_BacklogCurLine = 0; + UpdateSearch(); } -void CGameConsole::CInstance::ClearBacklogYOffsets() +void CGameConsole::CInstance::UpdateBacklogTextAttributes() { - // Pending backlog entries are not handled because they don't have a Y offset yet. + // 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)) { - pEntry->m_YOffset = -1.0f; + UpdateEntryTextAttributes(pEntry); } } void CGameConsole::CInstance::PumpBacklogPending() { - // 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)) + 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)) + { + 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) { - const size_t EntrySize = sizeof(CBacklogEntry) + pPendingEntry->m_Length; - CBacklogEntry *pEntry = m_Backlog.Allocate(EntrySize); - mem_copy(pEntry, pPendingEntry, EntrySize); - ++m_NewLineCounter; + UpdateEntryTextAttributes(pEntry); + m_NewLineCounter += pEntry->m_LineCount; } - m_BacklogPending.Init(); } void CGameConsole::CInstance::ClearHistory() @@ -241,134 +264,201 @@ bool CGameConsole::CInstance::OnInput(const IInput::CEvent &Event) { bool Handled = false; + auto &&SelectNextSearchMatch = [&](int Direction) { + if(!m_vSearchMatches.empty()) + { + m_CurrentMatchIndex += Direction; + if(m_CurrentMatchIndex >= (int)m_vSearchMatches.size()) + m_CurrentMatchIndex = 0; + if(m_CurrentMatchIndex < 0) + m_CurrentMatchIndex = (int)m_vSearchMatches.size() - 1; + m_HasSelection = false; + // Also scroll to the correct line + ScrollToCenter(m_vSearchMatches[m_CurrentMatchIndex].m_StartLine, m_vSearchMatches[m_CurrentMatchIndex].m_EndLine); + } + }; + + const int BacklogPrevLine = m_BacklogCurLine; if(Event.m_Flags & IInput::FLAG_PRESS) { if(Event.m_Key == KEY_RETURN || Event.m_Key == KEY_KP_ENTER) { - if(!m_Input.IsEmpty() || (m_UsernameReq && !m_pGameConsole->Client()->RconAuthed() && !m_UserGot)) + if(!m_Searching) { - if(m_Type == CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed()) + if(!m_Input.IsEmpty() || (m_UsernameReq && !m_pGameConsole->Client()->RconAuthed() && !m_UserGot)) { - const char *pPrevEntry = m_History.Last(); - if(pPrevEntry == nullptr || str_comp(pPrevEntry, m_Input.GetString()) != 0) + if(m_Type == CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed()) { - char *pEntry = m_History.Allocate(m_Input.GetLength() + 1); - str_copy(pEntry, m_Input.GetString(), m_Input.GetLength() + 1); + const char *pPrevEntry = m_History.Last(); + if(pPrevEntry == nullptr || str_comp(pPrevEntry, m_Input.GetString()) != 0) + { + char *pEntry = m_History.Allocate(m_Input.GetLength() + 1); + str_copy(pEntry, m_Input.GetString(), m_Input.GetLength() + 1); + } + // print out the user's commands before they get run + char aBuf[IConsole::CMDLINE_LENGTH + 3]; + str_format(aBuf, sizeof(aBuf), "> %s", m_Input.GetString()); + m_pGameConsole->PrintLine(m_Type, aBuf); } - // print out the user's commands before they get run - char aBuf[IConsole::CMDLINE_LENGTH + 3]; - str_format(aBuf, sizeof(aBuf), "> %s", m_Input.GetString()); - m_pGameConsole->PrintLine(m_Type, aBuf); + ExecuteLine(m_Input.GetString()); + m_Input.Clear(); + m_pHistoryEntry = 0x0; } - ExecuteLine(m_Input.GetString()); - m_Input.Clear(); - m_pHistoryEntry = 0x0; + } + else + { + SelectNextSearchMatch(m_pGameConsole->m_pClient->Input()->ShiftIsPressed() ? -1 : 1); } Handled = true; } else if(Event.m_Key == KEY_UP) { - if(m_pHistoryEntry) + if(m_Searching) { - char *pTest = m_History.Prev(m_pHistoryEntry); - - if(pTest) - m_pHistoryEntry = pTest; + SelectNextSearchMatch(-1); } - else - m_pHistoryEntry = m_History.Last(); + else if(m_Type == CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed()) + { + if(m_pHistoryEntry) + { + char *pTest = m_History.Prev(m_pHistoryEntry); - if(m_pHistoryEntry) - m_Input.Set(m_pHistoryEntry); + if(pTest) + m_pHistoryEntry = pTest; + } + else + m_pHistoryEntry = m_History.Last(); + + if(m_pHistoryEntry) + m_Input.Set(m_pHistoryEntry); + } Handled = true; } else if(Event.m_Key == KEY_DOWN) { - if(m_pHistoryEntry) - m_pHistoryEntry = m_History.Next(m_pHistoryEntry); + if(m_Searching) + { + SelectNextSearchMatch(1); + } + else if(m_Type == CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed()) + { + if(m_pHistoryEntry) + m_pHistoryEntry = m_History.Next(m_pHistoryEntry); - if(m_pHistoryEntry) - m_Input.Set(m_pHistoryEntry); - else - m_Input.Clear(); + if(m_pHistoryEntry) + m_Input.Set(m_pHistoryEntry); + else + m_Input.Clear(); + } Handled = true; } else if(Event.m_Key == KEY_TAB) { const int Direction = m_pGameConsole->m_pClient->Input()->ShiftIsPressed() ? -1 : 1; - // 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); - if(m_Type == CGameConsole::CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed()) + if(!m_Searching) { + // 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); + if(m_Type == CGameConsole::CONSOLETYPE_LOCAL || m_pGameConsole->Client()->RconAuthed()) + { + if(CompletionEnumerationCount) + { + 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); + } + else if(m_CompletionChosen != -1) + { + m_CompletionChosen = -1; + Reset(); + } + } + + // Argument completion + const bool TuningCompletion = IsTuningCommandPrefix(GetString()); + const bool SettingCompletion = IsSettingCommandPrefix(GetString()); + if(TuningCompletion) + CompletionEnumerationCount = PossibleTunings(m_aCompletionBufferArgument); + else if(SettingCompletion) + CompletionEnumerationCount = m_pGameConsole->m_pConsole->PossibleCommands(m_aCompletionBufferArgument, m_CompletionFlagmask, UseTempCommands); + if(CompletionEnumerationCount) { - 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); + 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) + PossibleTunings(m_aCompletionBufferArgument, PossibleArgumentsCompleteCallback, this); + else if(SettingCompletion) + m_pGameConsole->m_pConsole->PossibleCommands(m_aCompletionBufferArgument, m_CompletionFlagmask, UseTempCommands, PossibleArgumentsCompleteCallback, this); } - else if(m_CompletionChosen != -1) + else if(m_CompletionChosenArgument != -1) { - m_CompletionChosen = -1; + m_CompletionChosenArgument = -1; Reset(); } } - - // Argument completion - const bool TuningCompletion = IsTuningCommandPrefix(GetString()); - const bool SettingCompletion = IsSettingCommandPrefix(GetString()); - if(TuningCompletion) - CompletionEnumerationCount = PossibleTunings(m_aCompletionBufferArgument); - else if(SettingCompletion) - CompletionEnumerationCount = m_pGameConsole->m_pConsole->PossibleCommands(m_aCompletionBufferArgument, m_CompletionFlagmask, UseTempCommands); - - 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) - PossibleTunings(m_aCompletionBufferArgument, PossibleArgumentsCompleteCallback, this); - else if(SettingCompletion) - m_pGameConsole->m_pConsole->PossibleCommands(m_aCompletionBufferArgument, m_CompletionFlagmask, UseTempCommands, PossibleArgumentsCompleteCallback, this); - } - else if(m_CompletionChosenArgument != -1) + else { - m_CompletionChosenArgument = -1; - Reset(); + // Use Tab / Shift-Tab to cycle through search matches + SelectNextSearchMatch(Direction); } } else if(Event.m_Key == KEY_PAGEUP) { - ++m_BacklogCurPage; - m_HasSelection = false; + m_BacklogCurLine += GetLinesToScroll(-1, m_LinesRendered); } else if(Event.m_Key == KEY_PAGEDOWN) { - m_HasSelection = false; - --m_BacklogCurPage; - if(m_BacklogCurPage < 0) - m_BacklogCurPage = 0; + m_BacklogCurLine -= GetLinesToScroll(1, m_LinesRendered); + if(m_BacklogCurLine < 0) + m_BacklogCurLine = 0; + } + else if(Event.m_Key == KEY_MOUSE_WHEEL_UP) + { + m_BacklogCurLine += GetLinesToScroll(-1, 1); + } + else if(Event.m_Key == KEY_MOUSE_WHEEL_DOWN) + { + --m_BacklogCurLine; + if(m_BacklogCurLine < 0) + m_BacklogCurLine = 0; } // 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()) { - m_BacklogCurPage = INT_MAX; - m_HasSelection = false; + m_BacklogCurLine += GetLinesToScroll(-1, -1); + m_BacklogLastActiveLine = m_BacklogCurLine; } else if(Event.m_Key == KEY_END && m_Input.IsEmpty()) { - m_BacklogCurPage = 0; - m_HasSelection = false; + m_BacklogCurLine = 0; + } + else if(Event.m_Key == KEY_F && m_pGameConsole->Input()->ModifierIsPressed() && Event.m_Flags & IInput::FLAG_PRESS) + { + m_Searching = !m_Searching; + ClearSearch(); + + Handled = true; } } + if(m_BacklogCurLine != BacklogPrevLine) + { + m_HasSelection = false; + } + if(!Handled) + { Handled = m_Input.ProcessInput(Event); + if(Handled) + UpdateSearch(); + } if(Event.m_Flags & (IInput::FLAG_PRESS | IInput::FLAG_TEXT)) { @@ -428,9 +518,180 @@ void CGameConsole::CInstance::PrintLine(const char *pLine, int Len, ColorRGBA Pr pEntry->m_YOffset = -1.0f; pEntry->m_PrintColor = PrintColor; pEntry->m_Length = Len; + pEntry->m_LineCount = -1; str_copy(pEntry->m_aText, pLine, Len + 1); } +int CGameConsole::CInstance::GetLinesToScroll(int Direction, int LinesToScroll) +{ + auto *pEntry = m_Backlog.Last(); + int Line = 0; + int LinesToSkip = (Direction == -1 ? m_BacklogCurLine + m_LinesRendered : m_BacklogCurLine - 1); + while(Line < LinesToSkip && pEntry) + { + if(pEntry->m_LineCount == -1) + UpdateEntryTextAttributes(pEntry); + Line += pEntry->m_LineCount; + pEntry = m_Backlog.Prev(pEntry); + } + + int Amount = maximum(0, Line - LinesToSkip); + while(pEntry && (LinesToScroll > 0 ? Amount < LinesToScroll : true)) + { + if(pEntry->m_LineCount == -1) + UpdateEntryTextAttributes(pEntry); + Amount += pEntry->m_LineCount; + pEntry = Direction == -1 ? m_Backlog.Prev(pEntry) : m_Backlog.Next(pEntry); + } + + return LinesToScroll > 0 ? minimum(Amount, LinesToScroll) : Amount; +} + +void CGameConsole::CInstance::ScrollToCenter(int StartLine, int EndLine) +{ + // This method is used to scroll lines from `StartLine` to `EndLine` to the center of the screen, if possible. + + // Find target line + int Target = maximum(0, (int)ceil(StartLine - minimum(StartLine - EndLine, m_LinesRendered) / 2) - m_LinesRendered / 2); + if(m_BacklogCurLine == Target) + return; + + // Compute acutal amount of lines to scroll to make sure lines fit in viewport and we don't have empty space + int Direction = m_BacklogCurLine - Target < 0 ? -1 : 1; + int LinesToScroll = absolute(Target - m_BacklogCurLine); + int ComputedLines = GetLinesToScroll(Direction, LinesToScroll); + + if(Direction == -1) + m_BacklogCurLine += ComputedLines; + else + m_BacklogCurLine -= ComputedLines; +} + +void CGameConsole::CInstance::UpdateEntryTextAttributes(CBacklogEntry *pEntry) const +{ + 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_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_LineCount = Cursor.m_LineCount; +} + +void CGameConsole::CInstance::ClearSearch() +{ + m_vSearchMatches.clear(); + m_CurrentMatchIndex = -1; + m_Input.Clear(); + m_aCurrentSearchString[0] = '\0'; +} + +void CGameConsole::CInstance::UpdateSearch() +{ + if(!m_Searching) + return; + + const char *pSearchText = m_Input.GetString(); + bool SearchChanged = str_utf8_comp_nocase(pSearchText, m_aCurrentSearchString) != 0; + + int SearchLength = m_Input.GetLength(); + str_copy(m_aCurrentSearchString, pSearchText); + + m_vSearchMatches.clear(); + if(pSearchText[0] == '\0') + { + m_CurrentMatchIndex = -1; + return; + } + + if(SearchChanged) + { + m_CurrentMatchIndex = -1; + m_HasSelection = false; + } + + 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; + + for(; pEntry; EntryLine += pEntry->m_LineCount, pEntry = m_Backlog.Prev(pEntry)) + { + const char *pSearchPos = str_utf8_find_nocase(pEntry->m_aText, pSearchText); + if(!pSearchPos) + continue; + + int EntryLineCount = pEntry->m_LineCount; + + // Find all occurences of the search string and save their positions + while(pSearchPos) + { + int Pos = pSearchPos - pEntry->m_aText; + + if(EntryLineCount == 1) + { + m_vSearchMatches.emplace_back(Pos, EntryLine, EntryLine, EntryLine); + if(EntryLine > LineToScrollStart) + { + LineToScrollStart = EntryLine; + LineToScrollEnd = EntryLine; + } + } + else + { + // A match can span multiple lines in case of a multiline entry, so we need to know which line the match starts at + // and which line it ends at in order to put it in viewport properly + STextSizeProperties Props; + int LineCount; + Props.m_pLineCount = &LineCount; + + // Compute line of end match + pTextRender->TextWidth(FONT_SIZE, pEntry->m_aText, Pos + SearchLength, LineWidth, 0, Props); + int EndLine = (EntryLineCount - LineCount); + int MatchEndLine = EntryLine + EndLine; + + // Compute line of start of match + int MatchStartLine = MatchEndLine; + if(LineCount > 1) + { + pTextRender->TextWidth(FONT_SIZE, pEntry->m_aText, Pos, LineWidth, 0, Props); + int StartLine = (EntryLineCount - LineCount); + MatchStartLine = EntryLine + StartLine; + } + + if(MatchStartLine > LineToScrollStart) + { + LineToScrollStart = MatchStartLine; + LineToScrollEnd = MatchEndLine; + } + + m_vSearchMatches.emplace_back(Pos, MatchStartLine, MatchEndLine, EntryLine); + } + + pSearchPos = str_utf8_find_nocase(pEntry->m_aText + Pos + SearchLength, pSearchText); + } + } + + if(!m_vSearchMatches.empty() && SearchChanged) + m_CurrentMatchIndex = 0; + else + m_CurrentMatchIndex = std::clamp(m_CurrentMatchIndex, -1, (int)m_vSearchMatches.size() - 1); + + // Reverse order of lines by sorting so we have matches from top to bottom instead of bottom to top + std::sort(m_vSearchMatches.begin(), m_vSearchMatches.end(), [](const SSearchMatch &MatchA, const SSearchMatch &MatchB) { + if(MatchA.m_StartLine == MatchB.m_StartLine) + return MatchA.m_Pos < MatchB.m_Pos; // Make sure to keep position order + return MatchA.m_StartLine > MatchB.m_StartLine; + }); + + if(!m_vSearchMatches.empty() && SearchChanged) + { + ScrollToCenter(LineToScrollStart, LineToScrollEnd); + } +} + CGameConsole::CGameConsole() : m_LocalConsole(CONSOLETYPE_LOCAL), m_RemoteConsole(CONSOLETYPE_REMOTE) { @@ -523,12 +784,39 @@ void CGameConsole::PossibleCommandsRenderCallback(int Index, const char *pStr, v pInfo->m_TotalWidth = pInfo->m_Cursor.m_X + pInfo->m_Offset; } +void CGameConsole::Prompt(char (&aPrompt)[32]) +{ + CInstance *pConsole = CurrentConsole(); + if(pConsole->m_Searching) + { + str_format(aPrompt, sizeof(aPrompt), "%s: ", Localize("Searching")); + } + else if(m_ConsoleType == CONSOLETYPE_REMOTE) + { + if(Client()->State() == IClient::STATE_LOADING || Client()->State() == IClient::STATE_ONLINE) + { + if(Client()->RconAuthed()) + str_copy(aPrompt, "rcon> "); + else if(pConsole->m_UsernameReq && !pConsole->m_UserGot) + str_format(aPrompt, sizeof(aPrompt), "%s> ", Localize("Enter Username")); + else + str_format(aPrompt, sizeof(aPrompt), "%s> ", Localize("Enter Password")); + } + else + str_format(aPrompt, sizeof(aPrompt), "%s> ", Localize("NOT CONNECTED")); + } + else + { + str_copy(aPrompt, "> "); + } +} + void CGameConsole::OnRender() { CUIRect Screen = *UI()->Screen(); CInstance *pConsole = CurrentConsole(); - float ConsoleMaxHeight = Screen.h * 3 / 5.0f; + float MaxConsoleHeight = Screen.h * 3 / 5.0f; float ConsoleHeight; float Progress = (Client()->GlobalTime() - (m_StateChangeEnd - m_StateChangeDuration)) / m_StateChangeDuration; @@ -539,7 +827,7 @@ void CGameConsole::OnRender() { m_ConsoleState = CONSOLE_CLOSED; pConsole->m_Input.Deactivate(); - pConsole->m_BacklogLastActivePage = -1; + pConsole->m_BacklogLastActiveLine = -1; } else if(m_ConsoleState == CONSOLE_OPENING) { @@ -565,10 +853,10 @@ void CGameConsole::OnRender() ConsoleHeightScale = ConsoleScaleFunc(Progress); else if(m_ConsoleState == CONSOLE_CLOSING) ConsoleHeightScale = ConsoleScaleFunc(1.0f - Progress); - else //if (console_state == CONSOLE_OPEN) + else // if (console_state == CONSOLE_OPEN) ConsoleHeightScale = ConsoleScaleFunc(1.0f); - ConsoleHeight = ConsoleHeightScale * ConsoleMaxHeight; + ConsoleHeight = ConsoleHeightScale * MaxConsoleHeight; UI()->MapScreen(); @@ -620,8 +908,10 @@ void CGameConsole::OnRender() ConsoleHeight -= 22.0f; { - float FontSize = 10.0f; - float RowHeight = FontSize * 1.25f; + // Get height of 1 line + float LineHeight = TextRender()->TextBoundingBox(FONT_SIZE, " ", -1, -1, LINE_SPACING).m_H + LINE_SPACING; + + float RowHeight = FONT_SIZE * 1.25f; float x = 3; float y = ConsoleHeight - RowHeight - 5.0f; @@ -630,31 +920,11 @@ void CGameConsole::OnRender() // render prompt CTextCursor Cursor; - TextRender()->SetCursor(&Cursor, x, y, FontSize, TEXTFLAG_RENDER); - const char *pPrompt = "> "; - if(m_ConsoleType == CONSOLETYPE_REMOTE) - { - if(Client()->State() == IClient::STATE_LOADING || Client()->State() == IClient::STATE_ONLINE) - { - if(Client()->RconAuthed()) - pPrompt = "rcon> "; - else - { - if(pConsole->m_UsernameReq) - { - if(!pConsole->m_UserGot) - pPrompt = "Enter Username> "; - else - pPrompt = "Enter Password> "; - } - else - pPrompt = "Enter Password> "; - } - } - else - pPrompt = "NOT CONNECTED> "; - } - TextRender()->TextEx(&Cursor, pPrompt, -1); + TextRender()->SetCursor(&Cursor, x, y, FONT_SIZE, TEXTFLAG_RENDER); + + char aPrompt[32]; + Prompt(aPrompt); + TextRender()->TextEx(&Cursor, aPrompt); // check if mouse is pressed if(!pConsole->m_MouseIsPress && Input()->NativeMousePressed(1)) @@ -697,16 +967,20 @@ 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 - const CUIRect InputCursorRect = {x, y + FontSize, 0.0f, 0.0f}; - pConsole->m_BoundingBox = pConsole->m_Input.Render(&InputCursorRect, FontSize, TEXTALIGN_BL, pConsole->m_Input.WasChanged(), Screen.w - 10.0f - x); + 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(); + const bool Changed = WasChanged || WasCursorChanged; + pConsole->m_BoundingBox = pConsole->m_Input.Render(&InputCursorRect, FONT_SIZE, TEXTALIGN_BL, Changed, Screen.w - 10.0f - x, LINE_SPACING); if(pConsole->m_LastInputHeight == 0.0f && pConsole->m_BoundingBox.m_H != 0.0f) pConsole->m_LastInputHeight = pConsole->m_BoundingBox.m_H; if(pConsole->m_Input.HasSelection()) pConsole->m_HasSelection = false; // Clear console selection if we have a line input selection - y -= pConsole->m_BoundingBox.m_H - FontSize; - TextRender()->SetCursor(&Cursor, x, y, FontSize, TEXTFLAG_RENDER); + y -= pConsole->m_BoundingBox.m_H - FONT_SIZE; + TextRender()->SetCursor(&Cursor, x, y, FONT_SIZE, TEXTFLAG_RENDER); Cursor.m_LineWidth = Screen.w - 10.0f - x; + Cursor.m_LineSpacing = LINE_SPACING; if(pConsole->m_LastInputHeight != pConsole->m_BoundingBox.m_H) { @@ -716,7 +990,7 @@ void CGameConsole::OnRender() } // render possible commands - if((m_ConsoleType == CONSOLETYPE_LOCAL || Client()->RconAuthed()) && !pConsole->m_Input.IsEmpty()) + if(!pConsole->m_Searching && (m_ConsoleType == CONSOLETYPE_LOCAL || Client()->RconAuthed()) && !pConsole->m_Input.IsEmpty()) { CCompletionOptionRenderInfo Info; Info.m_pSelf = this; @@ -726,7 +1000,7 @@ void CGameConsole::OnRender() Info.m_Width = Screen.w; Info.m_TotalWidth = 0.0f; Info.m_pCurrentCmd = pConsole->m_aCompletionBuffer; - TextRender()->SetCursor(&Info.m_Cursor, InitialX - Info.m_Offset, InitialY + RowHeight + 2.0f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_STOP_AT_END); + 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); pConsole->m_CompletionRenderOffset = Info.m_Offset; @@ -761,89 +1035,164 @@ void CGameConsole::OnRender() 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 + CTextCursor MatchInfoCursor; + TextRender()->SetCursor(&MatchInfoCursor, InitialX, InitialY + RowHeight + 2.0f, FONT_SIZE, TEXTFLAG_RENDER); + TextRender()->TextColor(0.8f, 0.8f, 0.8f, 1.0f); + if(!pConsole->m_vSearchMatches.empty()) + { + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), Localize("Match %d of %d"), pConsole->m_CurrentMatchIndex + 1, (int)pConsole->m_vSearchMatches.size()); + TextRender()->TextEx(&MatchInfoCursor, aBuf, -1); + } + else + { + TextRender()->TextEx(&MatchInfoCursor, Localize("No results"), -1); + } + } pConsole->PumpBacklogPending(); + if(pConsole->m_NewLineCounter > 0) + { + pConsole->UpdateSearch(); - // render log (current page, wrap lines) + // keep scroll position when new entries are printed. + if(pConsole->m_BacklogCurLine != 0) + { + pConsole->m_BacklogCurLine += pConsole->m_NewLineCounter; + pConsole->m_BacklogLastActiveLine += pConsole->m_NewLineCounter; + } + } + + // render console log (current entry, status, wrap lines) CInstance::CBacklogEntry *pEntry = pConsole->m_Backlog.Last(); float OffsetY = 0.0f; - float LineOffset = 1.0f; std::string SelectionString; - if(pConsole->m_BacklogLastActivePage < 0) - pConsole->m_BacklogLastActivePage = pConsole->m_BacklogCurPage; - int TotalPages = 1; - for(int Page = 0; Page <= maximum(pConsole->m_BacklogLastActivePage, pConsole->m_BacklogCurPage); ++Page, OffsetY = 0.0f) + if(pConsole->m_BacklogLastActiveLine < 0) + pConsole->m_BacklogLastActiveLine = pConsole->m_BacklogCurLine; + + int LineNum = -1; + pConsole->m_LinesRendered = 0; + + int SkippedLines = 0; + bool First = true; + + 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); + + while(pEntry) { - while(pEntry) + if(pEntry->m_LineCount == -1) + pConsole->UpdateEntryTextAttributes(pEntry); + + LineNum += pEntry->m_LineCount; + if(LineNum < pConsole->m_BacklogLastActiveLine) { - TextRender()->TextColor(pEntry->m_PrintColor); + SkippedLines += pEntry->m_LineCount; + pEntry = pConsole->m_Backlog.Prev(pEntry); + continue; + } + TextRender()->TextColor(pEntry->m_PrintColor); - // get y offset (calculate it if we haven't yet) - if(pEntry->m_YOffset < 0.0f) - { - TextRender()->SetCursor(&Cursor, 0.0f, 0.0f, FontSize, 0); - Cursor.m_LineWidth = Screen.w - 10; - Cursor.m_MaxLines = 10; - TextRender()->TextEx(&Cursor, pEntry->m_aText, -1); - pEntry->m_YOffset = Cursor.Height() + LineOffset; - } - OffsetY += pEntry->m_YOffset; + if(First) + { + int Diff = pConsole->m_BacklogLastActiveLine - SkippedLines; + OffsetY -= Diff * LineHeight - LINE_SPACING; + } + + float LocalOffsetY = OffsetY + pEntry->m_YOffset / (float)pEntry->m_LineCount; + OffsetY += pEntry->m_YOffset; + + // 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; + if(!pConsole->m_MouseIsPress) + pConsole->m_MouseRelease.y -= MouseExtraOff; + } + + // stop rendering when lines reach the top + bool Outside = y - OffsetY <= RowHeight; + int CanRenderOneLine = y - LocalOffsetY > RowHeight; + if(Outside && !CanRenderOneLine) + break; + + 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); + Cursor.m_LineWidth = Screen.w - 10.0f; + Cursor.m_MaxLines = pEntry->m_LineCount; + Cursor.m_LineSpacing = LINE_SPACING; + Cursor.m_CalculateSelectionMode = (m_ConsoleState == CONSOLE_OPEN && pConsole->m_MousePress.y < pConsole->m_BoundingBox.m_Y && (pConsole->m_MouseIsPress || (pConsole->m_CurSelStart != pConsole->m_CurSelEnd) || pConsole->m_HasSelection)) ? TEXT_CURSOR_SELECTION_MODE_CALCULATE : TEXT_CURSOR_SELECTION_MODE_NONE; + Cursor.m_PressMouse = pConsole->m_MousePress; + Cursor.m_ReleaseMouse = pConsole->m_MouseRelease; + + if(pConsole->m_Searching && pConsole->m_CurrentMatchIndex != -1) + { + std::vector vMatches; + std::copy_if(pConsole->m_vSearchMatches.begin(), pConsole->m_vSearchMatches.end(), std::back_inserter(vMatches), [&](const CInstance::SSearchMatch &Match) { return Match.m_EntryLine == LineNum + 1 - pEntry->m_LineCount; }); - if((pConsole->m_HasSelection || pConsole->m_MouseIsPress) && pConsole->m_NewLineCounter > 0) + auto CurrentSelectedOccurrence = pConsole->m_vSearchMatches[pConsole->m_CurrentMatchIndex]; + + std::vector vColorSplits; + for(const auto &Match : vMatches) { - float MouseExtraOff = pEntry->m_YOffset; - pConsole->m_MousePress.y -= MouseExtraOff; - if(!pConsole->m_MouseIsPress) - pConsole->m_MouseRelease.y -= MouseExtraOff; + bool IsSelected = CurrentSelectedOccurrence.m_EntryLine == Match.m_EntryLine && CurrentSelectedOccurrence.m_Pos == Match.m_Pos; + Cursor.m_vColorSplits.emplace_back( + Match.m_Pos, + pConsole->m_Input.GetLength(), + IsSelected ? ms_SearchSelectedColor : ms_SearchHighlightColor); } + } - // next page when lines reach the top - if(y - OffsetY <= RowHeight) - break; + TextRender()->TextEx(&Cursor, pEntry->m_aText, -1); + Cursor.m_vColorSplits = {}; - // just render output from current backlog page (render bottom up) - if(Page == pConsole->m_BacklogLastActivePage) + if(Cursor.m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_CALCULATE) + { + pConsole->m_CurSelStart = minimum(Cursor.m_SelectionStart, Cursor.m_SelectionEnd); + pConsole->m_CurSelEnd = maximum(Cursor.m_SelectionStart, Cursor.m_SelectionEnd); + } + pConsole->m_LinesRendered += First ? pEntry->m_LineCount - (pConsole->m_BacklogLastActiveLine - SkippedLines) : pEntry->m_LineCount; + + if(pConsole->m_CurSelStart != pConsole->m_CurSelEnd) + { + if(m_WantsSelectionCopy) { - TextRender()->SetCursor(&Cursor, 0.0f, y - OffsetY, FontSize, TEXTFLAG_RENDER); - Cursor.m_LineWidth = Screen.w - 10.0f; - Cursor.m_MaxLines = 10; - Cursor.m_CalculateSelectionMode = (m_ConsoleState == CONSOLE_OPEN && pConsole->m_MousePress.y < pConsole->m_BoundingBox.m_Y && (pConsole->m_MouseIsPress || (pConsole->m_CurSelStart != pConsole->m_CurSelEnd) || pConsole->m_HasSelection)) ? TEXT_CURSOR_SELECTION_MODE_CALCULATE : TEXT_CURSOR_SELECTION_MODE_NONE; - Cursor.m_PressMouse = pConsole->m_MousePress; - Cursor.m_ReleaseMouse = pConsole->m_MouseRelease; - TextRender()->TextEx(&Cursor, pEntry->m_aText, -1); - if(Cursor.m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_CALCULATE) - { - pConsole->m_CurSelStart = minimum(Cursor.m_SelectionStart, Cursor.m_SelectionEnd); - pConsole->m_CurSelEnd = maximum(Cursor.m_SelectionStart, Cursor.m_SelectionEnd); - } - if(pConsole->m_CurSelStart != pConsole->m_CurSelEnd) - { - if(m_WantsSelectionCopy) - { - const bool HasNewLine = !SelectionString.empty(); - const size_t OffUTF8Start = str_utf8_offset_chars_to_bytes(pEntry->m_aText, pConsole->m_CurSelStart); - const size_t OffUTF8End = str_utf8_offset_chars_to_bytes(pEntry->m_aText, pConsole->m_CurSelEnd); - SelectionString.insert(0, (std::string(&pEntry->m_aText[OffUTF8Start], OffUTF8End - OffUTF8Start) + (HasNewLine ? "\n" : ""))); - } - pConsole->m_HasSelection = true; - } + const bool HasNewLine = !SelectionString.empty(); + const size_t OffUTF8Start = str_utf8_offset_chars_to_bytes(pEntry->m_aText, pConsole->m_CurSelStart); + const size_t OffUTF8End = str_utf8_offset_chars_to_bytes(pEntry->m_aText, pConsole->m_CurSelEnd); + SelectionString.insert(0, (std::string(&pEntry->m_aText[OffUTF8Start], OffUTF8End - OffUTF8Start) + (HasNewLine ? "\n" : ""))); } - pEntry = pConsole->m_Backlog.Prev(pEntry); - - // reset color - TextRender()->TextColor(1, 1, 1, 1); - if(pConsole->m_NewLineCounter > 0) - --pConsole->m_NewLineCounter; + 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 + TextRender()->TextColor(TextRender()->DefaultTextColor()); + First = false; + if(!pEntry) break; - TotalPages++; } - pConsole->m_BacklogCurPage = clamp(pConsole->m_BacklogCurPage, 0, TotalPages - 1); - pConsole->m_BacklogLastActivePage = pConsole->m_BacklogCurPage; + + Graphics()->ClipDisable(); + + pConsole->m_BacklogLastActiveLine = pConsole->m_BacklogCurLine; if(m_WantsSelectionCopy && !SelectionString.empty()) { @@ -854,16 +1203,17 @@ void CGameConsole::OnRender() m_WantsSelectionCopy = false; } - // render page + TextRender()->TextColor(TextRender()->DefaultTextColor()); + + // render current lines and status (locked, following) char aBuf[128]; - TextRender()->TextColor(1, 1, 1, 1); - str_format(aBuf, sizeof(aBuf), Localize("-Page %d-"), pConsole->m_BacklogCurPage + 1); - TextRender()->Text(10.0f, FontSize / 2.f, FontSize, aBuf, -1.0f); + 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); // render version str_copy(aBuf, "v" GAME_VERSION " on " CONF_PLATFORM_STRING " " CONF_ARCH_STRING); - float Width = TextRender()->TextWidth(FontSize, aBuf, -1, -1.0f); - TextRender()->Text(Screen.w - Width - 10.0f, FontSize / 2.f, FontSize, aBuf, -1.0f); + float Width = TextRender()->TextWidth(FONT_SIZE, aBuf, -1, -1.0f); + TextRender()->Text(Screen.w - Width - 10.0f, FONT_SIZE / 2.f, FONT_SIZE, aBuf); } } @@ -985,15 +1335,17 @@ void CGameConsole::ConDumpRemoteConsole(IConsole::IResult *pResult, void *pUserD void CGameConsole::ConConsolePageUp(IConsole::IResult *pResult, void *pUserData) { CInstance *pConsole = ((CGameConsole *)pUserData)->CurrentConsole(); - pConsole->m_BacklogCurPage++; + pConsole->m_BacklogCurLine += pConsole->GetLinesToScroll(-1, pConsole->m_LinesRendered); + pConsole->m_HasSelection = false; } void CGameConsole::ConConsolePageDown(IConsole::IResult *pResult, void *pUserData) { CInstance *pConsole = ((CGameConsole *)pUserData)->CurrentConsole(); - --pConsole->m_BacklogCurPage; - if(pConsole->m_BacklogCurPage < 0) - pConsole->m_BacklogCurPage = 0; + pConsole->m_BacklogCurLine -= pConsole->GetLinesToScroll(1, pConsole->m_LinesRendered); + pConsole->m_HasSelection = false; + if(pConsole->m_BacklogCurLine < 0) + pConsole->m_BacklogCurLine = 0; } void CGameConsole::ConchainConsoleOutputLevel(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) @@ -1018,9 +1370,9 @@ void CGameConsole::RequireUsername(bool UsernameReq) void CGameConsole::PrintLine(int Type, const char *pLine) { if(Type == CONSOLETYPE_LOCAL) - m_LocalConsole.PrintLine(pLine, str_length(pLine), ColorRGBA{1, 1, 1, 1}); + m_LocalConsole.PrintLine(pLine, str_length(pLine), TextRender()->DefaultTextColor()); else if(Type == CONSOLETYPE_REMOTE) - m_RemoteConsole.PrintLine(pLine, str_length(pLine), ColorRGBA{1, 1, 1, 1}); + m_RemoteConsole.PrintLine(pLine, str_length(pLine), TextRender()->DefaultTextColor()); } void CGameConsole::OnConsoleInit() @@ -1048,9 +1400,9 @@ void CGameConsole::OnInit() Engine()->SetAdditionalLogger(std::unique_ptr(m_pConsoleLogger)); // add resize event Graphics()->AddWindowResizeListener([this]() { - m_LocalConsole.ClearBacklogYOffsets(); + m_LocalConsole.UpdateBacklogTextAttributes(); m_LocalConsole.m_HasSelection = false; - m_RemoteConsole.ClearBacklogYOffsets(); + m_RemoteConsole.UpdateBacklogTextAttributes(); m_RemoteConsole.m_HasSelection = false; }); } diff --git a/src/game/client/components/console.h b/src/game/client/components/console.h index cadaefd95aa..66e3c90e0bf 100644 --- a/src/game/client/components/console.h +++ b/src/game/client/components/console.h @@ -30,6 +30,7 @@ class CGameConsole : public CComponent struct CBacklogEntry { float m_YOffset; + int m_LineCount; ColorRGBA m_PrintColor; size_t m_Length; char m_aText[1]; @@ -43,8 +44,9 @@ class CGameConsole : public CComponent CLineInputBuffered m_Input; const char *m_pName; int m_Type; - int m_BacklogCurPage; - int m_BacklogLastActivePage = -1; + int m_BacklogCurLine; + int m_BacklogLastActiveLine = -1; + int m_LinesRendered; STextBoundingBox m_BoundingBox = {0.0f, 0.0f, 0.0f, 0.0f}; float m_LastInputHeight = 0.0f; @@ -76,11 +78,26 @@ class CGameConsole : public CComponent const char *m_pCommandHelp; const char *m_pCommandParams; + bool m_Searching = false; + struct SSearchMatch + { + int m_Pos; + int m_StartLine; + int m_EndLine; + int m_EntryLine; + + SSearchMatch(int Pos, int StartLine, int EndLine, int EntryLine) : + m_Pos(Pos), m_StartLine(StartLine), m_EndLine(EndLine), m_EntryLine(EntryLine) {} + }; + int m_CurrentMatchIndex; + char m_aCurrentSearchString[IConsole::CMDLINE_LENGTH]; + std::vector m_vSearchMatches; + CInstance(int t); void Init(CGameConsole *pGameConsole); void ClearBacklog() REQUIRES(!m_BacklogPendingLock); - void ClearBacklogYOffsets(); + void UpdateBacklogTextAttributes(); void PumpBacklogPending() REQUIRES(!m_BacklogPendingLock); void ClearHistory(); void Reset(); @@ -89,10 +106,20 @@ class CGameConsole : public CComponent bool OnInput(const IInput::CEvent &Event); void PrintLine(const char *pLine, int Len, ColorRGBA PrintColor) REQUIRES(!m_BacklogPendingLock); + int GetLinesToScroll(int Direction, int LinesToScroll); + void ScrollToCenter(int StartLine, int EndLine); + void ClearSearch(); const char *GetString() const { return m_Input.GetString(); } static void PossibleCommandsCompleteCallback(int Index, const char *pStr, void *pUser); static void PossibleArgumentsCompleteCallback(int Index, const char *pStr, void *pUser); + + void UpdateEntryTextAttributes(CBacklogEntry *pEntry) const; + + private: + void UpdateSearch(); + + friend class CGameConsole; }; class IConsole *m_pConsole; @@ -110,6 +137,9 @@ class CGameConsole : public CComponent bool m_WantsSelectionCopy = false; + static const ColorRGBA ms_SearchHighlightColor; + static const ColorRGBA ms_SearchSelectedColor; + void Toggle(int Type); void Dump(int Type); @@ -145,6 +175,7 @@ class CGameConsole : public CComponent virtual void OnRender() override; virtual void OnMessage(int MsgType, void *pRawMsg) override; virtual bool OnInput(const IInput::CEvent &Event) override; + void Prompt(char (&aPrompt)[32]); bool IsClosed() { return m_ConsoleState == CONSOLE_CLOSED; } }; diff --git a/src/game/client/components/controls.cpp b/src/game/client/components/controls.cpp index 81e415d5e3c..f72cce98f70 100644 --- a/src/game/client/components/controls.cpp +++ b/src/game/client/components/controls.cpp @@ -267,8 +267,11 @@ int CControls::SnapInput(int *pData) m_aInputData[g_Config.m_ClDummy].m_Direction = 1; // scale TargetX, TargetY by zoom. - 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; + 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; + } // dummy copy moves if(g_Config.m_ClDummyCopyMoves) diff --git a/src/game/client/components/ghost.cpp b/src/game/client/components/ghost.cpp index d15fa52a22b..2a55688f15d 100644 --- a/src/game/client/components/ghost.cpp +++ b/src/game/client/components/ghost.cpp @@ -679,7 +679,7 @@ void CGhost::OnMapLoad() m_AllowRestart = false; } -int CGhost::GetLastRaceTick() +int CGhost::GetLastRaceTick() const { return m_LastRaceTick; } diff --git a/src/game/client/components/ghost.h b/src/game/client/components/ghost.h index 9a6d3867791..c7582523e8c 100644 --- a/src/game/client/components/ghost.h +++ b/src/game/client/components/ghost.h @@ -174,7 +174,7 @@ class CGhost : public CComponent class IGhostLoader *GhostLoader() const { return m_pGhostLoader; } class IGhostRecorder *GhostRecorder() const { return m_pGhostRecorder; } - int GetLastRaceTick(); + int GetLastRaceTick() const; void RefindSkins(); }; diff --git a/src/game/client/components/mapimages.cpp b/src/game/client/components/mapimages.cpp index 089303bf544..9a4733f1612 100644 --- a/src/game/client/components/mapimages.cpp +++ b/src/game/client/components/mapimages.cpp @@ -427,7 +427,7 @@ void CMapImages::SetTextureScale(int Scale) } } -int CMapImages::GetTextureScale() +int CMapImages::GetTextureScale() const { return m_TextureScale; } diff --git a/src/game/client/components/mapimages.h b/src/game/client/components/mapimages.h index c511b04113b..66bd83df566 100644 --- a/src/game/client/components/mapimages.h +++ b/src/game/client/components/mapimages.h @@ -70,7 +70,7 @@ class CMapImages : public CComponent IGraphics::CTextureHandle GetOverlayCenter(); void SetTextureScale(int Scale); - int GetTextureScale(); + int GetTextureScale() const; void ChangeEntitiesPath(const char *pPath); diff --git a/src/game/client/components/mapsounds.cpp b/src/game/client/components/mapsounds.cpp index 9f7c0b93e3b..3fbba3139b8 100644 --- a/src/game/client/components/mapsounds.cpp +++ b/src/game/client/components/mapsounds.cpp @@ -51,8 +51,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; diff --git a/src/game/client/components/menus.cpp b/src/game/client/components/menus.cpp index f3c2cbf9f8e..34f853db28b 100644 --- a/src/game/client/components/menus.cpp +++ b/src/game/client/components/menus.cpp @@ -120,7 +120,7 @@ int CMenus::DoButton_Toggle(const void *pID, int Checked, const CUIRect *pRect, 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 r, float FontFactor, vec4 ColorHot, vec4 Color) +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) { CUIRect Text = *pRect; @@ -128,7 +128,7 @@ int CMenus::DoButton_Menu(CButtonContainer *pButtonContainer, const char *pText, Color = ColorRGBA(0.6f, 0.6f, 0.6f, 0.5f); Color.a *= UI()->ButtonColorMul(pButtonContainer); - pRect->Draw(Color, Corners, r); + pRect->Draw(Color, Corners, Rounding); if(pImageName) { @@ -157,14 +157,6 @@ int CMenus::DoButton_Menu(CButtonContainer *pButtonContainer, const char *pText, return UI()->DoButtonLogic(pButtonContainer, Checked, pRect); } -void CMenus::DoButton_KeySelect(const void *pID, const char *pText, const CUIRect *pRect) -{ - pRect->Draw(ColorRGBA(1, 1, 1, 0.5f * UI()->ButtonColorMul(pID)), IGraphics::CORNER_ALL, 5.0f); - CUIRect Temp; - pRect->HMargin(1.0f, &Temp); - UI()->DoLabel(&Temp, pText, Temp.h * CUI::ms_FontmodHeight, TEXTALIGN_MC); -} - 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 bool MouseInside = UI()->HotItem() == pButtonContainer; @@ -385,7 +377,7 @@ ColorHSLA CMenus::DoLine_ColorPicker(CButtonContainer *pResetID, const float Lin 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, vec4(1, 1, 1, 0.5f), vec4(1, 1, 1, 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); } @@ -452,15 +444,15 @@ int CMenus::DoButton_CheckBox_Number(const void *pID, const char *pText, int Che int CMenus::DoKeyReader(const void *pID, const CUIRect *pRect, int Key, int ModifierCombination, int *pNewModifierCombination) { // process - static const void *pGrabbedID = 0; - static bool MouseReleased = true; + 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) && pGrabbedID == pID) - MouseReleased = true; + if(!UI()->MouseButton(0) && !UI()->MouseButton(1) && s_pGrabbedID == pID) + s_MouseReleased = true; if(UI()->CheckActiveItem(pID)) { @@ -474,8 +466,8 @@ int CMenus::DoKeyReader(const void *pID, const CUIRect *pRect, int Key, int Modi } m_Binder.m_GotKey = false; UI()->SetActiveItem(nullptr); - MouseReleased = false; - pGrabbedID = pID; + s_MouseReleased = false; + s_pGrabbedID = pID; } if(s_ButtonUsed == 1 && !UI()->MouseButton(1)) @@ -487,7 +479,7 @@ int CMenus::DoKeyReader(const void *pID, const CUIRect *pRect, int Key, int Modi } else if(UI()->HotItem() == pID) { - if(MouseReleased) + if(s_MouseReleased) { if(UI()->MouseButton(0)) { @@ -508,17 +500,19 @@ int CMenus::DoKeyReader(const void *pID, const CUIRect *pRect, int Key, int Modi if(Inside) UI()->SetHotItem(pID); - // draw + char aBuf[64]; if(UI()->CheckActiveItem(pID) && s_ButtonUsed == 0) - DoButton_KeySelect(pID, "???", pRect); + str_copy(aBuf, Localize("Press a key…")); else if(NewKey == 0) - DoButton_KeySelect(pID, "", pRect); + aBuf[0] = '\0'; else - { - char aBuf[64]; str_format(aBuf, sizeof(aBuf), "%s%s", CBinds::GetKeyBindModifiersName(*pNewModifierCombination), Input()->KeyName(NewKey)); - DoButton_KeySelect(pID, aBuf, pRect); - } + + 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)); + 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); return NewKey; } @@ -969,7 +963,7 @@ void CMenus::PopupWarning(const char *pTopic, const char *pBody, const char *pBu m_PopupWarningLastTime = time_get_nanoseconds(); } -bool CMenus::CanDisplayWarning() +bool CMenus::CanDisplayWarning() const { return m_Popup == POPUP_NONE; } @@ -1661,7 +1655,7 @@ int CMenus::Render() char aFilePath[IO_MAX_PATH_LENGTH]; char aSaveFolder[IO_MAX_PATH_LENGTH]; - Storage()->GetCompletePath(Storage()->TYPE_SAVE, "videos", aSaveFolder, sizeof(aSaveFolder)); + 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); @@ -2035,7 +2029,7 @@ void CMenus::OnRender() RenderDemoPlayer(*UI()->Screen()); } - if(Client()->State() == IClient::STATE_ONLINE && m_pClient->m_ServerMode == m_pClient->SERVERMODE_PUREMOD) + if(Client()->State() == IClient::STATE_ONLINE && m_pClient->m_ServerMode == CGameClient::SERVERMODE_PUREMOD) { Client()->Disconnect(); SetActive(true); diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index 60d5c3ef2fe..a5e134782c1 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -59,7 +59,7 @@ class CMenus : public CComponent 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_Menu(CButtonContainer *pButtonContainer, const char *pText, int Checked, const CUIRect *pRect, const char *pImageName = nullptr, int Corners = IGraphics::CORNER_ALL, float r = 5.0f, float FontFactor = 0.0f, vec4 ColorHot = vec4(1.0f, 1.0f, 1.0f, 0.75f), vec4 Color = vec4(1, 1, 1, 0.5f)); + 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_CheckBox_Common(const void *pID, const char *pText, const char *pBoxText, const CUIRect *pRect); @@ -72,7 +72,6 @@ class CMenus : public CComponent 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); - void DoButton_KeySelect(const void *pID, const char *pText, const CUIRect *pRect); int DoKeyReader(const void *pID, const CUIRect *pRect, int Key, int ModifierCombination, int *pNewModifierCombination); void DoSettingsControlsButtons(int Start, int Stop, CUIRect View); @@ -722,8 +721,8 @@ class CMenus : public CComponent void UpdateOwnGhost(CGhostItem Item); void DeleteGhostItem(int Index); - int GetCurPopup() { return m_Popup; } - bool CanDisplayWarning(); + int GetCurPopup() const { return m_Popup; } + bool CanDisplayWarning() const; void PopupWarning(const char *pTopic, const char *pBody, const char *pButton, std::chrono::nanoseconds Duration); diff --git a/src/game/client/components/menus_ingame.cpp b/src/game/client/components/menus_ingame.cpp index 106d6511007..1834c3d5aa3 100644 --- a/src/game/client/components/menus_ingame.cpp +++ b/src/game/client/components/menus_ingame.cpp @@ -75,7 +75,7 @@ void CMenus::RenderGame(CUIRect MainView) static CButtonContainer s_DummyButton; if(!Client()->DummyAllowed()) { - DoButton_Menu(&s_DummyButton, Localize("Connect Dummy"), 1, &Button, nullptr, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(1.0f, 0.5f, 0.5f, 0.75f), vec4(1, 0.5f, 0.5f, 0.5f)); + DoButton_Menu(&s_DummyButton, Localize("Connect Dummy"), 1, &Button); } else if(DummyConnecting) { @@ -554,7 +554,9 @@ bool CMenus::RenderServerControlServer(CUIRect MainView) if(!Item.m_Visible) continue; - UI()->DoLabel(&Item.m_Rect, pOption->m_aDescription, 13.0f, TEXTALIGN_ML); + CUIRect Label; + Item.m_Rect.VMargin(2.0f, &Label); + UI()->DoLabel(&Label, pOption->m_aDescription, 13.0f, TEXTALIGN_ML); } s_CurVoteOption = s_ListBox.DoEnd(); @@ -1115,7 +1117,7 @@ void CMenus::RenderGhost(CUIRect MainView) static CButtonContainer s_DirectoryButton; static CButtonContainer s_ActivateAll; - if(DoButton_FontIcon(&s_ReloadButton, FONT_ICON_ARROW_ROTATE_RIGHT, 0, &Button) || Input()->KeyPress(KEY_F5)) + if(DoButton_FontIcon(&s_ReloadButton, FONT_ICON_ARROW_ROTATE_RIGHT, 0, &Button) || Input()->KeyPress(KEY_F5) || (Input()->KeyPress(KEY_R) && Input()->ModifierIsPressed())) { m_pClient->m_Ghost.UnloadAll(); GhostlistPopulate(); diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp index 0e18687a0f6..1890ccf5310 100644 --- a/src/game/client/components/menus_settings.cpp +++ b/src/game/client/components/menus_settings.cpp @@ -625,7 +625,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView) 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, vec4(1, 1, 1, 0.5f + Highlight * 0.25f), vec4(1, 1, 1, 0.25f + Highlight * 0.25f))) + 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) { @@ -924,7 +924,7 @@ void CMenus::RenderSettingsTee(CUIRect MainView) 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, nullptr, IGraphics::CORNER_ALL, 5, 0, vec4(1.0f, 1.0f, 1.0f, 0.75f), vec4(1, 1, 1, 0.5f))) + if(DoButton_Menu(&s_SkinRefreshButtonID, 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); @@ -2495,7 +2495,7 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) DoButton_CheckBoxAutoVMarginAndSet(&g_Config.m_ClShowhud, Localize("Show ingame HUD"), &g_Config.m_ClShowhud, &Section, LineSize); // Switches of the various normal HUD elements - LeftView.HSplitTop(SectionTotalMargin + 5 * LineSize, &Section, &LeftView); + 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); @@ -2503,6 +2503,7 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) 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); // Settings of the HUD element for votes LeftView.HSplitTop(SectionTotalMargin + LineSize, &Section, &LeftView); @@ -2694,9 +2695,17 @@ 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]; + 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; @@ -2810,21 +2819,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")); @@ -2972,6 +2980,13 @@ void CMenus::RenderSettingsAppearance(CUIRect MainView) break; } } + + 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); } else if(s_CurTab == APPEARANCE_TAB_HOOK_COLLISION) { diff --git a/src/game/client/components/menus_settings_assets.cpp b/src/game/client/components/menus_settings_assets.cpp index e1db2fe072c..e0ff5a40819 100644 --- a/src/game/client/components/menus_settings_assets.cpp +++ b/src/game/client/components/menus_settings_assets.cpp @@ -659,7 +659,7 @@ void CMenus::RenderSettingsCustom(CUIRect MainView) 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, nullptr, IGraphics::CORNER_ALL, 5, 0, vec4(1.0f, 1.0f, 1.0f, 0.75f), vec4(1, 1, 1, 0.5f))) + 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 b0a4c143dfb..3e4c4a7fd97 100644 --- a/src/game/client/components/menus_start.cpp +++ b/src/game/client/components/menus_start.cpp @@ -39,7 +39,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button); static CButtonContainer s_DiscordButton; - if(DoButton_Menu(&s_DiscordButton, Localize("Discord"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f))) + 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)) @@ -51,7 +51,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button); static CButtonContainer s_LearnButton; - if(DoButton_Menu(&s_LearnButton, Localize("Learn"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f))) + 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)) @@ -64,7 +64,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button); static CButtonContainer s_TutorialButton; static float s_JoinTutorialTime = 0.0f; - if(DoButton_Menu(&s_TutorialButton, Localize("Tutorial"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f)) || + 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)) { const char *pAddr = ServerBrowser()->GetTutorialServer(); @@ -88,7 +88,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button); static CButtonContainer s_WebsiteButton; - if(DoButton_Menu(&s_WebsiteButton, Localize("Website"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f))) + 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)) @@ -100,7 +100,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) ExtMenu.HSplitBottom(5.0f, &ExtMenu, 0); // little space ExtMenu.HSplitBottom(20.0f, &ExtMenu, &Button); static CButtonContainer s_NewsButton; - if(DoButton_Menu(&s_NewsButton, Localize("News"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, vec4(0.0f, 0.0f, 0.0f, 0.5f), g_Config.m_UiUnreadNews ? vec4(0.0f, 1.0f, 0.0f, 0.25f) : vec4(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(KEY_N)) + if(DoButton_Menu(&s_NewsButton, Localize("News"), 0, &Button, 0, IGraphics::CORNER_ALL, 5.0f, 0.0f, g_Config.m_UiUnreadNews ? ColorRGBA(0.0f, 1.0f, 0.0f, 0.25f) : ColorRGBA(0.0f, 0.0f, 0.0f, 0.25f)) || CheckHotKey(KEY_N)) NewPage = PAGE_NEWS; CUIRect Menu; @@ -110,7 +110,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) 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, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(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)) { @@ -125,7 +125,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) Menu.HSplitBottom(100.0f, &Menu, 0); Menu.HSplitBottom(40.0f, &Menu, &Button); static CButtonContainer s_SettingsButton; - if(DoButton_Menu(&s_SettingsButton, Localize("Settings"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "settings" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(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; Menu.HSplitBottom(5.0f, &Menu, 0); // little space @@ -135,7 +135,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) if(!is_process_alive(m_ServerProcess.m_Process)) KillServer(); - 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, vec4(0.0f, 0.0f, 0.0f, 0.5f), m_ServerProcess.m_Process ? vec4(0.0f, 1.0f, 0.0f, 0.25f) : vec4(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) { @@ -166,7 +166,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) Menu.HSplitBottom(5.0f, &Menu, 0); // little space Menu.HSplitBottom(40.0f, &Menu, &Button); static CButtonContainer s_MapEditorButton; - if(DoButton_Menu(&s_MapEditorButton, Localize("Editor"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "editor" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, vec4(0.0f, 0.0f, 0.0f, 0.5f), m_pClient->Editor()->HasUnsavedData() ? vec4(0.0f, 1.0f, 0.0f, 0.25f) : vec4(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)) || (!EditorHotkeyWasPressed && Client()->LocalTime() - EditorHotKeyChecktime < 0.1f && CheckHotKey(KEY_E))) { g_Config.m_ClEditor = 1; Input()->MouseModeRelative(); @@ -181,7 +181,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) Menu.HSplitBottom(5.0f, &Menu, 0); // little space Menu.HSplitBottom(40.0f, &Menu, &Button); static CButtonContainer s_DemoButton; - if(DoButton_Menu(&s_DemoButton, Localize("Demos"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "demos" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(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; } @@ -189,7 +189,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) Menu.HSplitBottom(5.0f, &Menu, 0); // little space Menu.HSplitBottom(40.0f, &Menu, &Button); static CButtonContainer s_PlayButton; - if(DoButton_Menu(&s_PlayButton, Localize("Play", "Start menu"), 0, &Button, g_Config.m_ClShowStartMenuImages ? "play_game" : 0, IGraphics::CORNER_ALL, Rounding, 0.5f, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(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; } @@ -243,7 +243,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) 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, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f))) + 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(); } @@ -254,7 +254,7 @@ void CMenus::RenderStartMenu(CUIRect MainView) 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, vec4(0.0f, 0.0f, 0.0f, 0.5f), vec4(0.0f, 0.0f, 0.0f, 0.25f))) + 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(); } @@ -265,9 +265,9 @@ void CMenus::RenderStartMenu(CUIRect MainView) Part.VSplitLeft(100.0f, &ProgressBar, &Percent); ProgressBar.y += 2.0f; ProgressBar.HMargin(1.0f, &ProgressBar); - ProgressBar.Draw(vec4(1.0f, 1.0f, 1.0f, 0.25f), IGraphics::CORNER_ALL, 5.0f); + 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(vec4(1.0f, 1.0f, 1.0f, 0.5f), IGraphics::CORNER_ALL, 5.0f); + ProgressBar.Draw(ColorRGBA(1.0f, 1.0f, 1.0f, 0.5f), IGraphics::CORNER_ALL, 5.0f); } #elif defined(CONF_INFORM_UPDATE) if(str_comp(Client()->LatestVersion(), "0") != 0) diff --git a/src/game/client/components/players.cpp b/src/game/client/components/players.cpp index 87cd222a047..cf69fc34d5b 100644 --- a/src/game/client/components/players.cpp +++ b/src/game/client/components/players.cpp @@ -202,8 +202,7 @@ void CPlayers::RenderHookCollLine( DoBreak = true; } - int TeleNr = 0; - int Hit = Collision()->IntersectLineTeleHook(OldPos, NewPos, &FinishPos, 0x0, &TeleNr); + int Hit = Collision()->IntersectLineTeleHook(OldPos, NewPos, &FinishPos, 0x0); if(!DoBreak && Hit) { @@ -837,10 +836,12 @@ void CPlayers::OnRender() RenderTools()->RenderTee(CAnimState::GetIdle(), &RenderInfoSpec, EMOTE_BLINK, vec2(1, 0), m_aClient.m_SpecChar); } - // render everyone else's tee, then our own + // 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; + for(int ClientID = 0; ClientID < MAX_CLIENTS; ClientID++) { - if(ClientID == LocalClientID || !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; } @@ -855,11 +856,11 @@ void CPlayers::OnRender() } RenderPlayer(&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(RenderLastID != -1 && m_pClient->m_Snap.m_aCharacters[RenderLastID].m_Active && IsPlayerInfoAvailable(RenderLastID)) { - const CGameClient::CClientData *pLocalClientData = &m_pClient->m_aClients[LocalClientID]; - RenderHookCollLine(&pLocalClientData->m_RenderPrev, &pLocalClientData->m_RenderCur, LocalClientID); - RenderPlayer(&pLocalClientData->m_RenderPrev, &pLocalClientData->m_RenderCur, &aRenderInfo[LocalClientID], LocalClientID); + 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); } } diff --git a/src/game/client/components/statboard.cpp b/src/game/client/components/statboard.cpp index 71b35a1b42a..be9bed21950 100644 --- a/src/game/client/components/statboard.cpp +++ b/src/game/client/components/statboard.cpp @@ -41,7 +41,7 @@ void CStatboard::OnConsoleInit() Console()->Register("+statboard", "", CFGFLAG_CLIENT, ConKeyStats, this, "Show stats"); } -bool CStatboard::IsActive() +bool CStatboard::IsActive() const { return m_Active; } diff --git a/src/game/client/components/statboard.h b/src/game/client/components/statboard.h index 8026c56a08c..e5d2a88067d 100644 --- a/src/game/client/components/statboard.h +++ b/src/game/client/components/statboard.h @@ -28,7 +28,7 @@ class CStatboard : public CComponent virtual void OnRender() override; virtual void OnRelease() override; virtual void OnMessage(int MsgType, void *pRawMsg) override; - bool IsActive(); + bool IsActive() const; }; #endif // GAME_CLIENT_COMPONENTS_STATBOARD_H diff --git a/src/game/client/components/voting.h b/src/game/client/components/voting.h index 077edcb8162..f81959a68ed 100644 --- a/src/game/client/components/voting.h +++ b/src/game/client/components/voting.h @@ -3,8 +3,6 @@ #ifndef GAME_CLIENT_COMPONENTS_VOTING_H #define GAME_CLIENT_COMPONENTS_VOTING_H -#include - #include #include diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 8ded2a9a215..863b74f1d46 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -802,7 +802,7 @@ void CGameClient::OnDummyDisconnect() m_PredictedDummyID = -1; } -int CGameClient::GetLastRaceTick() +int CGameClient::GetLastRaceTick() const { return m_Ghost.GetLastRaceTick(); } @@ -2421,7 +2421,7 @@ void CGameClient::SendDummyInfo(bool Start) } } -void CGameClient::SendKill(int ClientID) +void CGameClient::SendKill(int ClientID) const { CNetMsg_Cl_Kill Msg; Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); @@ -2894,7 +2894,7 @@ void CGameClient::Echo(const char *pString) m_Chat.Echo(pString); } -bool CGameClient::IsOtherTeam(int ClientID) +bool CGameClient::IsOtherTeam(int ClientID) const { bool Local = m_Snap.m_LocalClientID == ClientID; @@ -2917,7 +2917,7 @@ bool CGameClient::IsOtherTeam(int ClientID) return m_Teams.Team(ClientID) != m_Teams.Team(m_Snap.m_LocalClientID); } -int CGameClient::SwitchStateTeam() +int CGameClient::SwitchStateTeam() const { if(m_aSwitchStateTeam[g_Config.m_ClDummy] >= 0) return m_aSwitchStateTeam[g_Config.m_ClDummy]; @@ -2928,7 +2928,7 @@ int CGameClient::SwitchStateTeam() return m_Teams.Team(m_Snap.m_LocalClientID); } -bool CGameClient::IsLocalCharSuper() +bool CGameClient::IsLocalCharSuper() const { if(m_Snap.m_LocalClientID < 0) return false; @@ -3567,12 +3567,12 @@ void CGameClient::DummyResetInput() m_DummyInput = m_Controls.m_aInputData[!g_Config.m_ClDummy]; } -bool CGameClient::CanDisplayWarning() +bool CGameClient::CanDisplayWarning() const { return m_Menus.CanDisplayWarning(); } -bool CGameClient::IsDisplayingWarning() +bool CGameClient::IsDisplayingWarning() const { return m_Menus.GetCurPopup() == CMenus::POPUP_WARNING; } diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 8eaeb0c0191..1d315c41d43 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -540,7 +540,7 @@ class CGameClient : public IGameClient void SendSwitchTeam(int Team); void SendInfo(bool Start); void SendDummyInfo(bool Start) override; - void SendKill(int ClientID); + void SendKill(int ClientID) const; // DDRace @@ -554,7 +554,7 @@ class CGameClient : public IGameClient int IntersectCharacter(vec2 HookPos, vec2 NewPos, vec2 &NewPos2, int ownID); - int GetLastRaceTick() override; + int GetLastRaceTick() const override; bool IsTeamPlay() { return m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS; } @@ -576,11 +576,11 @@ class CGameClient : public IGameClient void DummyResetInput() override; void Echo(const char *pString) override; - bool IsOtherTeam(int ClientID); - int SwitchStateTeam(); - bool IsLocalCharSuper(); - bool CanDisplayWarning() override; - bool IsDisplayingWarning() override; + bool IsOtherTeam(int ClientID) const; + int SwitchStateTeam() const; + bool IsLocalCharSuper() const; + bool CanDisplayWarning() const override; + bool IsDisplayingWarning() const override; CNetObjHandler *GetNetObjHandler() override; void LoadGameSkin(const char *pPath, bool AsDir = false); diff --git a/src/game/client/lineinput.cpp b/src/game/client/lineinput.cpp index e6cbf781068..7af53941a0b 100644 --- a/src/game/client/lineinput.cpp +++ b/src/game/client/lineinput.cpp @@ -86,6 +86,7 @@ void CLineInput::SetRange(const char *pString, size_t Begin, size_t End) m_Len += AddedCharSize - RemovedCharSize; m_NumChars += AddedCharCount - RemovedCharCount; m_WasChanged = true; + m_WasCursorChanged = true; m_pStr[m_Len] = '\0'; m_SelectionStart = m_SelectionEnd = m_CursorPos; } @@ -158,7 +159,7 @@ void CLineInput::MoveCursor(EMoveDirection Direction, bool MoveWord, const char void CLineInput::SetCursorOffset(size_t Offset) { m_SelectionStart = m_SelectionEnd = m_LastCompositionCursorPos = m_CursorPos = clamp(Offset, 0, m_Len); - m_WasChanged = true; + m_WasCursorChanged = true; } void CLineInput::SetSelection(size_t Start, size_t End) @@ -168,7 +169,7 @@ void CLineInput::SetSelection(size_t Start, size_t End) std::swap(Start, End); m_SelectionStart = clamp(Start, 0, m_Len); m_SelectionEnd = clamp(End, 0, m_Len); - m_WasChanged = true; + m_WasCursorChanged = true; } size_t CLineInput::OffsetFromActualToDisplay(size_t ActualOffset) @@ -383,12 +384,12 @@ bool CLineInput::ProcessInput(const IInput::CEvent &Event) } } - m_WasChanged |= OldCursorPos != m_CursorPos; - m_WasChanged |= SelectionLength != GetSelectionLength(); - return m_WasChanged || KeyHandled; + m_WasCursorChanged |= OldCursorPos != m_CursorPos; + m_WasCursorChanged |= SelectionLength != GetSelectionLength(); + return m_WasChanged || m_WasCursorChanged || KeyHandled; } -STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth) +STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing) { // update derived attributes to handle external changes to the buffer UpdateStrData(); @@ -422,12 +423,13 @@ STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Al pDisplayStr = DisplayStrBuffer.c_str(); } - const STextBoundingBox BoundingBox = TextRender()->TextBoundingBox(FontSize, pDisplayStr, -1, LineWidth); + const STextBoundingBox BoundingBox = TextRender()->TextBoundingBox(FontSize, pDisplayStr, -1, LineWidth, LineSpacing); 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_ForceCursorRendering = Changed; + Cursor.m_LineSpacing = LineSpacing; Cursor.m_PressMouse.x = m_MouseSelection.m_PressMouse.x; Cursor.m_ReleaseMouse.x = m_MouseSelection.m_ReleaseMouse.x; if(LineWidth < 0.0f) @@ -497,6 +499,7 @@ STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Al CTextCursor CaretCursor; TextRender()->SetCursor(&CaretCursor, CursorPos.x, CursorPos.y, FontSize, 0); CaretCursor.m_LineWidth = LineWidth; + CaretCursor.m_LineSpacing = LineSpacing; CaretCursor.m_CursorMode = TEXT_CURSOR_CURSOR_MODE_SET; CaretCursor.m_CursorCharacter = str_utf8_offset_bytes_to_chars(pDisplayStr, DisplayCursorOffset); TextRender()->TextEx(&CaretCursor, pDisplayStr); @@ -504,10 +507,11 @@ STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Al } else { - const STextBoundingBox BoundingBox = TextRender()->TextBoundingBox(FontSize, pDisplayStr, -1, LineWidth); + const STextBoundingBox BoundingBox = TextRender()->TextBoundingBox(FontSize, pDisplayStr, -1, LineWidth, LineSpacing); 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; TextRender()->TextEx(&Cursor, pDisplayStr); } @@ -629,7 +633,7 @@ void CLineInput::Activate(EInputPriority Priority) ms_ActiveInputPriority = Priority; } -void CLineInput::Deactivate() +void CLineInput::Deactivate() const { if(!IsActive()) return; diff --git a/src/game/client/lineinput.h b/src/game/client/lineinput.h index 54f196e6121..0bd405a0412 100644 --- a/src/game/client/lineinput.h +++ b/src/game/client/lineinput.h @@ -77,6 +77,7 @@ class CLineInput FDisplayTextCallback m_pfnDisplayTextCallback; FCalculateOffsetCallback m_pfnCalculateOffsetCallback; bool m_WasChanged; + bool m_WasCursorChanged; bool m_WasRendered; char m_ClearButtonId; @@ -179,15 +180,21 @@ class CLineInput m_WasChanged = false; return Changed; } + bool WasCursorChanged() + { + const bool Changed = m_WasCursorChanged; + m_WasCursorChanged = false; + return Changed; + } - STextBoundingBox Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth); + STextBoundingBox Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing); SMouseSelection *GetMouseSelection() { return &m_MouseSelection; } const void *GetClearButtonId() const { return &m_ClearButtonId; } bool IsActive() const { return GetActiveInput() == this; } void Activate(EInputPriority Priority); - void Deactivate(); + void Deactivate() const; }; template diff --git a/src/game/client/prediction/entities/character.cpp b/src/game/client/prediction/entities/character.cpp index d73e9940355..d1c1570f136 100644 --- a/src/game/client/prediction/entities/character.cpp +++ b/src/game/client/prediction/entities/character.cpp @@ -1358,7 +1358,7 @@ void CCharacter::SetCoreWorld(CGameWorld *pGameWorld) m_Core.SetCoreWorld(&pGameWorld->m_Core, pGameWorld->Collision(), pGameWorld->Teams()); } -bool CCharacter::Match(CCharacter *pChar) +bool CCharacter::Match(CCharacter *pChar) const { return distance(pChar->m_Core.m_Pos, m_Core.m_Pos) <= 32.f; } diff --git a/src/game/client/prediction/entities/character.h b/src/game/client/prediction/entities/character.h index 206212413a4..b424a8c9b73 100644 --- a/src/game/client/prediction/entities/character.h +++ b/src/game/client/prediction/entities/character.h @@ -123,7 +123,7 @@ class CCharacter : public CEntity int m_GameTeam; bool m_CanMoveInFreeze; - bool Match(CCharacter *pChar); + bool Match(CCharacter *pChar) const; void ResetPrediction(); void SetTuneZone(int Zone); diff --git a/src/game/client/prediction/entities/laser.cpp b/src/game/client/prediction/entities/laser.cpp index 1f377192a18..6b0fb9247d3 100644 --- a/src/game/client/prediction/entities/laser.cpp +++ b/src/game/client/prediction/entities/laser.cpp @@ -99,11 +99,9 @@ void CLaser::DoBounce() vec2 Coltile; int Res; - int z; - vec2 To = m_Pos + m_Dir * m_Energy; - Res = Collision()->IntersectLineTeleWeapon(m_Pos, To, &Coltile, &To, &z); + Res = Collision()->IntersectLineTeleWeapon(m_Pos, To, &Coltile, &To); if(Res) { diff --git a/src/game/client/prediction/gameworld.cpp b/src/game/client/prediction/gameworld.cpp index 58f9eeb6979..a2fb7012367 100644 --- a/src/game/client/prediction/gameworld.cpp +++ b/src/game/client/prediction/gameworld.cpp @@ -372,7 +372,7 @@ void CGameWorld::CreateExplosion(vec2 Pos, int Owner, int Weapon, bool NoDamage, } } -bool CGameWorld::IsLocalTeam(int OwnerID) +bool CGameWorld::IsLocalTeam(int OwnerID) const { return OwnerID < 0 || m_Teams.CanCollide(m_LocalClientID, OwnerID); } @@ -698,7 +698,7 @@ CEntity *CGameWorld::FindMatch(int ObjID, int ObjType, const void *pObjData) return 0; } -void CGameWorld::OnModified() +void CGameWorld::OnModified() const { if(m_pChild) m_pChild->m_IsValidCopy = false; diff --git a/src/game/client/prediction/gameworld.h b/src/game/client/prediction/gameworld.h index fbb63ad9b05..18be1fc2586 100644 --- a/src/game/client/prediction/gameworld.h +++ b/src/game/client/prediction/gameworld.h @@ -89,8 +89,8 @@ class CGameWorld int m_LocalClientID; - bool IsLocalTeam(int OwnerID); - void OnModified(); + 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); diff --git a/src/game/client/render.cpp b/src/game/client/render.cpp index 948fdc800da..0e081c27812 100644 --- a/src/game/client/render.cpp +++ b/src/game/client/render.cpp @@ -57,7 +57,7 @@ void CRenderTools::Init(IGraphics *pGraphics, ITextRender *pTextRender) Graphics()->QuadContainerUpload(m_TeeQuadContainerIndex); } -void CRenderTools::SelectSprite(CDataSprite *pSpr, int Flags, int sx, int sy) +void CRenderTools::SelectSprite(CDataSprite *pSpr, int Flags, int sx, int sy) const { int x = pSpr->m_X + sx; int y = pSpr->m_Y + sy; @@ -82,45 +82,45 @@ void CRenderTools::SelectSprite(CDataSprite *pSpr, int Flags, int sx, int sy) Graphics()->QuadsSetSubset(x1, y1, x2, y2); } -void CRenderTools::SelectSprite(int Id, int Flags, int sx, int sy) +void CRenderTools::SelectSprite(int Id, int Flags, int sx, int sy) const { if(Id < 0 || Id >= g_pData->m_NumSprites) return; SelectSprite(&g_pData->m_aSprites[Id], Flags, sx, sy); } -void CRenderTools::GetSpriteScale(struct CDataSprite *pSprite, float &ScaleX, float &ScaleY) +void CRenderTools::GetSpriteScale(const CDataSprite *pSprite, float &ScaleX, float &ScaleY) const { int w = pSprite->m_W; int h = pSprite->m_H; GetSpriteScaleImpl(w, h, ScaleX, ScaleY); } -void CRenderTools::GetSpriteScale(int Id, float &ScaleX, float &ScaleY) +void CRenderTools::GetSpriteScale(int Id, float &ScaleX, float &ScaleY) const { GetSpriteScale(&g_pData->m_aSprites[Id], ScaleX, ScaleY); } -void CRenderTools::GetSpriteScaleImpl(int Width, int Height, float &ScaleX, float &ScaleY) +void CRenderTools::GetSpriteScaleImpl(int Width, int Height, float &ScaleX, float &ScaleY) const { const float f = length(vec2(Width, Height)); ScaleX = Width / f; ScaleY = Height / f; } -void CRenderTools::DrawSprite(float x, float y, float Size) +void CRenderTools::DrawSprite(float x, float y, float Size) const { IGraphics::CQuadItem QuadItem(x, y, Size * gs_SpriteWScale, Size * gs_SpriteHScale); Graphics()->QuadsDraw(&QuadItem, 1); } -void CRenderTools::DrawSprite(float x, float y, float ScaledWidth, float ScaledHeight) +void CRenderTools::DrawSprite(float x, float y, float ScaledWidth, float ScaledHeight) const { IGraphics::CQuadItem QuadItem(x, y, ScaledWidth, ScaledHeight); Graphics()->QuadsDraw(&QuadItem, 1); } -void CRenderTools::RenderCursor(vec2 Center, float Size) +void CRenderTools::RenderCursor(vec2 Center, float Size) const { Graphics()->WrapClamp(); Graphics()->TextureSet(g_pData->m_aImages[IMAGE_CURSOR].m_Id); @@ -132,7 +132,7 @@ void CRenderTools::RenderCursor(vec2 Center, float Size) Graphics()->WrapNormal(); } -void CRenderTools::RenderIcon(int ImageId, int SpriteId, const CUIRect *pRect, const ColorRGBA *pColor) +void CRenderTools::RenderIcon(int ImageId, int SpriteId, const CUIRect *pRect, const ColorRGBA *pColor) const { Graphics()->TextureSet(g_pData->m_aImages[ImageId].m_Id); Graphics()->QuadsBegin(); @@ -144,25 +144,25 @@ void CRenderTools::RenderIcon(int ImageId, int SpriteId, const CUIRect *pRect, c Graphics()->QuadsEnd(); } -int CRenderTools::QuadContainerAddSprite(int QuadContainerIndex, float x, float y, float Size) +int CRenderTools::QuadContainerAddSprite(int QuadContainerIndex, float x, float y, float Size) const { IGraphics::CQuadItem QuadItem(x, y, Size, Size); return Graphics()->QuadContainerAddQuads(QuadContainerIndex, &QuadItem, 1); } -int CRenderTools::QuadContainerAddSprite(int QuadContainerIndex, float Size) +int CRenderTools::QuadContainerAddSprite(int QuadContainerIndex, float Size) const { IGraphics::CQuadItem QuadItem(-(Size) / 2.f, -(Size) / 2.f, (Size), (Size)); return Graphics()->QuadContainerAddQuads(QuadContainerIndex, &QuadItem, 1); } -int CRenderTools::QuadContainerAddSprite(int QuadContainerIndex, float Width, float Height) +int CRenderTools::QuadContainerAddSprite(int QuadContainerIndex, float Width, float Height) const { IGraphics::CQuadItem QuadItem(-(Width) / 2.f, -(Height) / 2.f, (Width), (Height)); return Graphics()->QuadContainerAddQuads(QuadContainerIndex, &QuadItem, 1); } -int CRenderTools::QuadContainerAddSprite(int QuadContainerIndex, float X, float Y, float Width, float Height) +int CRenderTools::QuadContainerAddSprite(int QuadContainerIndex, float X, float Y, float Width, float Height) const { IGraphics::CQuadItem QuadItem(X, Y, Width, Height); return Graphics()->QuadContainerAddQuads(QuadContainerIndex, &QuadItem, 1); diff --git a/src/game/client/render.h b/src/game/client/render.h index 9538abdaac2..247bd518e0e 100644 --- a/src/game/client/render.h +++ b/src/game/client/render.h @@ -132,21 +132,21 @@ class CRenderTools void Init(class IGraphics *pGraphics, class ITextRender *pTextRender); - void SelectSprite(CDataSprite *pSprite, int Flags = 0, int sx = 0, int sy = 0); - void SelectSprite(int Id, int Flags = 0, int sx = 0, int sy = 0); - - void GetSpriteScale(CDataSprite *pSprite, float &ScaleX, float &ScaleY); - void GetSpriteScale(int Id, float &ScaleX, float &ScaleY); - void GetSpriteScaleImpl(int Width, int Height, float &ScaleX, float &ScaleY); - - void DrawSprite(float x, float y, float size); - void DrawSprite(float x, float y, float ScaledWidth, float ScaledHeight); - void RenderCursor(vec2 Center, float Size); - void RenderIcon(int ImageId, int SpriteId, const CUIRect *pRect, const ColorRGBA *pColor = nullptr); - int QuadContainerAddSprite(int QuadContainerIndex, float x, float y, float size); - int QuadContainerAddSprite(int QuadContainerIndex, float size); - int QuadContainerAddSprite(int QuadContainerIndex, float Width, float Height); - int QuadContainerAddSprite(int QuadContainerIndex, float X, float Y, float Width, float Height); + 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 GetSpriteScale(const CDataSprite *pSprite, float &ScaleX, float &ScaleY) const; + void GetSpriteScale(int Id, float &ScaleX, float &ScaleY) const; + void GetSpriteScaleImpl(int Width, int Height, float &ScaleX, float &ScaleY) const; + + void DrawSprite(float x, float y, float Size) const; + void DrawSprite(float x, float y, float ScaledWidth, float ScaledHeight) const; + void RenderCursor(vec2 Center, float Size) const; + void RenderIcon(int ImageId, int SpriteId, const CUIRect *pRect, const ColorRGBA *pColor = nullptr) const; + int QuadContainerAddSprite(int QuadContainerIndex, float x, float y, float Size) const; + int QuadContainerAddSprite(int QuadContainerIndex, float Size) const; + int QuadContainerAddSprite(int QuadContainerIndex, float Width, float Height) const; + 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); @@ -160,13 +160,13 @@ class CRenderTools // map render methods (render_map.cpp) static void RenderEvalEnvelope(const IEnvelopePointAccess *pPoints, int Channels, std::chrono::nanoseconds TimeNanos, ColorRGBA &Result); - void RenderQuads(CQuad *pQuads, int NumQuads, int Flags, ENVELOPE_EVAL pfnEval, void *pUser); - void ForceRenderQuads(CQuad *pQuads, int NumQuads, int Flags, ENVELOPE_EVAL pfnEval, void *pUser, float Alpha = 1.0f); - void RenderTilemap(CTile *pTiles, int w, int h, float Scale, ColorRGBA Color, int RenderFlags, ENVELOPE_EVAL pfnEval, void *pUser, int ColorEnv, int ColorEnvOffset); + 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; // 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); + 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; // helpers void CalcScreenParams(float Aspect, float Zoom, float *pWidth, float *pHeight); @@ -177,14 +177,14 @@ class CRenderTools // DDRace - 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 5e858292d4c..8241a3d22e2 100644 --- a/src/game/client/render_map.cpp +++ b/src/game/client/render_map.cpp @@ -351,7 +351,7 @@ static void Rotate(CPoint *pCenter, CPoint *pPoint, float Rotation) pPoint->y = (int)(x * std::sin(Rotation) + y * std::cos(Rotation) + pCenter->y); } -void CRenderTools::RenderQuads(CQuad *pQuads, int NumQuads, int RenderFlags, ENVELOPE_EVAL pfnEval, void *pUser) +void CRenderTools::RenderQuads(CQuad *pQuads, int NumQuads, int RenderFlags, ENVELOPE_EVAL pfnEval, void *pUser) const { if(!g_Config.m_ClShowQuads || g_Config.m_ClOverlayEntities == 100) return; @@ -359,7 +359,7 @@ void CRenderTools::RenderQuads(CQuad *pQuads, int NumQuads, int RenderFlags, ENV ForceRenderQuads(pQuads, NumQuads, RenderFlags, pfnEval, pUser, (100 - g_Config.m_ClOverlayEntities) / 100.0f); } -void CRenderTools::ForceRenderQuads(CQuad *pQuads, int NumQuads, int RenderFlags, ENVELOPE_EVAL pfnEval, void *pUser, float Alpha) +void CRenderTools::ForceRenderQuads(CQuad *pQuads, int NumQuads, int RenderFlags, ENVELOPE_EVAL pfnEval, void *pUser, float Alpha) const { Graphics()->TrianglesBegin(); float Conv = 1 / 255.0f; @@ -443,7 +443,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) + ENVELOPE_EVAL pfnEval, void *pUser, int ColorEnv, int ColorEnvOffset) const { float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); @@ -541,7 +541,7 @@ void CRenderTools::RenderTileRectangle(int RectX, int RectY, int RectW, int Rect } void CRenderTools::RenderTilemap(CTile *pTiles, int w, int h, float Scale, ColorRGBA Color, int RenderFlags, - ENVELOPE_EVAL pfnEval, void *pUser, int ColorEnv, int ColorEnvOffset) + ENVELOPE_EVAL pfnEval, void *pUser, int ColorEnv, int ColorEnvOffset) const { float ScreenX0, ScreenY0, ScreenX1, ScreenY1; Graphics()->GetScreen(&ScreenX0, &ScreenY0, &ScreenX1, &ScreenY1); @@ -707,7 +707,7 @@ void CRenderTools::RenderTilemap(CTile *pTiles, int w, int h, float Scale, Color 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; @@ -757,7 +757,7 @@ void CRenderTools::RenderTeleOverlay(CTeleTile *pTele, int w, int h, float Scale 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); @@ -826,7 +826,7 @@ void CRenderTools::RenderSpeedupOverlay(CSpeedupTile *pSpeedup, int w, int h, fl 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; @@ -886,7 +886,7 @@ void CRenderTools::RenderSwitchOverlay(CSwitchTile *pSwitch, int w, int h, float 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; @@ -935,7 +935,7 @@ void CRenderTools::RenderTuneOverlay(CTuneTile *pTune, int w, int h, float Scale 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); @@ -1052,7 +1052,7 @@ 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; @@ -1171,7 +1171,7 @@ 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; @@ -1333,7 +1333,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/ui.cpp b/src/game/client/ui.cpp index a02b4d238e6..65618adc512 100644 --- a/src/game/client/ui.cpp +++ b/src/game/client/ui.cpp @@ -47,7 +47,7 @@ void CUIElement::SUIElementRect::Reset() m_Rounding = -1.0f; m_Corners = -1; m_Text.clear(); - mem_zero(&m_Cursor, sizeof(m_Cursor)); + m_Cursor.Reset(); m_TextColor = ColorRGBA(-1, -1, -1, -1); m_TextOutlineColor = ColorRGBA(-1, -1, -1, -1); m_QuadColor = ColorRGBA(-1, -1, -1, -1); @@ -152,7 +152,7 @@ void CUI::AddUIElement(CUIElement *pElement) m_vpUIElements.push_back(pElement); } -void CUI::ResetUIElement(CUIElement &UIElement) +void CUI::ResetUIElement(CUIElement &UIElement) const { for(CUIElement::SUIElementRect &Rect : UIElement.m_vUIRects) { @@ -490,19 +490,37 @@ int CUI::DoDraggableButtonLogic(const void *pID, int Checked, const CUIRect *pRe return ReturnValue; } -int CUI::DoPickerLogic(const void *pID, const CUIRect *pRect, float *pX, float *pY) +EEditState CUI::DoPickerLogic(const void *pID, const CUIRect *pRect, float *pX, float *pY) { + static const void *s_pEditing = nullptr; + if(MouseHovered(pRect)) SetHotItem(pID); + EEditState Res = EEditState::EDITING; + if(HotItem() == pID && MouseButtonClicked(0)) + { SetActiveItem(pID); + if(!s_pEditing) + { + s_pEditing = pID; + Res = EEditState::START; + } + } if(CheckActiveItem(pID) && !MouseButton(0)) + { SetActiveItem(nullptr); + if(s_pEditing == pID) + { + s_pEditing = nullptr; + Res = EEditState::END; + } + } - if(!CheckActiveItem(pID)) - return 0; + if(!CheckActiveItem(pID) && Res == EEditState::EDITING) + return EEditState::NONE; if(Input()->ShiftIsPressed()) m_MouseSlow = true; @@ -512,10 +530,10 @@ int CUI::DoPickerLogic(const void *pID, const CUIRect *pRect, float *pX, float * if(pY) *pY = clamp(m_MouseY - pRect->y, 0.0f, pRect->h); - return 1; + return Res; } -void CUI::DoSmoothScrollLogic(float *pScrollOffset, float *pScrollOffsetChange, float ViewPortSize, float TotalSize, bool SmoothClamp, float ScrollSpeed) +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) @@ -648,7 +666,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) +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); @@ -660,7 +678,7 @@ void CUI::DoLabel(const CUIRect *pRect, const char *pText, float Size, int Align 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) +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); @@ -687,7 +705,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) +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; @@ -748,6 +766,7 @@ bool CUI::DoEditBox(CLineInput *pLineInput, const CUIRect *pRect, float FontSize const bool Inside = MouseHovered(pRect); const bool Active = m_pLastActiveItem == pLineInput; const bool Changed = pLineInput->WasChanged(); + const bool CursorChanged = pLineInput->WasCursorChanged(); const float VSpacing = 2.0f; CUIRect Textbox; @@ -824,11 +843,11 @@ 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, -1.0f); + const STextBoundingBox BoundingBox = pLineInput->Render(&Textbox, FontSize, TEXTALIGN_ML, Changed || CursorChanged, -1.0f, 0.0f); ClipDisable(); // Scroll left or right if necessary - if(Active && !JustGotActive && (Changed || Input()->HasComposition())) + if(Active && !JustGotActive && (Changed || CursorChanged || Input()->HasComposition())) { const float CaretPositionX = pLineInput->GetCaretPosition().x - Textbox.x - ScrollOffset - ScrollOffsetChange; if(CaretPositionX > Textbox.w) @@ -981,12 +1000,19 @@ int CUI::DoButton_PopupMenu(CButtonContainer *pButtonContainer, const char *pTex } 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; +} + +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); @@ -1091,7 +1117,23 @@ int64_t CUI::DoValueSelector(const void *pID, const CUIRect *pRect, const char * if(!m_ValueSelectorTextMode) s_NumberInput.Clear(); - return Current; + if(s_pEditing == pID) + State = EEditState::EDITING; + + bool MouseLocked = CheckMouseLock(); + if((MouseLocked || m_ValueSelectorTextMode) && !s_pEditing) + { + State = EEditState::START; + s_pEditing = pID; + } + + if(!CheckMouseLock() && !m_ValueSelectorTextMode && s_pEditing == pID) + { + State = EEditState::END; + s_pEditing = nullptr; + } + + return SEditResult{State, Current}; } float CUI::DoScrollbarV(const void *pID, const CUIRect *pRect, float Current) @@ -1294,7 +1336,7 @@ void CUI::DoScrollbarOption(const void *pID, int *pOption, const CUIRect *pRect, *pOption = Value; } -void CUI::RenderProgressSpinner(vec2 Center, float OuterRadius, const SProgressSpinnerProperties &Props) +void CUI::RenderProgressSpinner(vec2 Center, float OuterRadius, const SProgressSpinnerProperties &Props) const { static float s_SpinnerOffset = 0.0f; static float s_LastRender = Client()->LocalTime(); @@ -1368,27 +1410,30 @@ 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); + 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); } CUIRect PopupRect = PopupMenu.m_Rect; @@ -1397,9 +1442,11 @@ 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); } } @@ -1686,6 +1733,7 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View { SColorPickerPopupContext *pColorPicker = static_cast(pContext); CUI *pUI = pColorPicker->m_pUI; + pColorPicker->m_State = EEditState::NONE; CUIRect ColorsArea, HueArea, BottomArea, ModeButtonArea, HueRect, SatRect, ValueRect, HexRect, AlphaRect; @@ -1759,10 +1807,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View HuePartialArea.Draw4(TL, TL, BL, BL, IGraphics::CORNER_NONE, 0.0f); } - const auto &&RenderAlphaSelector = [&](unsigned OldA) -> unsigned { + const auto &&RenderAlphaSelector = [&](unsigned OldA) -> SEditResult { if(pColorPicker->m_Alpha) { - return pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[3], &AlphaRect, "A:", OldA, 0, 255); + return pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[3], &AlphaRect, "A:", OldA, 0, 255); } else { @@ -1770,7 +1818,7 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View str_format(aBuf, sizeof(aBuf), "A: %d", OldA); pUI->DoLabel(&AlphaRect, aBuf, 10.0f, TEXTALIGN_MC); AlphaRect.Draw(ColorRGBA(0.0f, 0.0f, 0.0f, 0.65f), IGraphics::CORNER_ALL, 3.0f); - return OldA; + return {EEditState::NONE, OldA}; } }; @@ -1782,10 +1830,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View const unsigned OldV = (unsigned)(PickerColorHSV.v * 255.0f); const unsigned OldA = (unsigned)(PickerColorHSV.a * 255.0f); - const unsigned H = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "H:", OldH, 0, 255); - const unsigned S = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "S:", OldS, 0, 255); - const unsigned V = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "V:", OldV, 0, 255); - const unsigned A = RenderAlphaSelector(OldA); + 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); + const auto [StateV, V] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "V:", OldV, 0, 255); + const auto [StateA, A] = RenderAlphaSelector(OldA); if(OldH != H || OldS != S || OldV != V || OldA != A) { @@ -1793,6 +1841,15 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View PickerColorHSL = color_cast(PickerColorHSV); PickerColorRGB = color_cast(PickerColorHSL); } + + for(auto State : {StateH, StateS, StateV, StateA}) + { + if(State != EEditState::NONE) + { + pColorPicker->m_State = State; + break; + } + } } else if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_RGBA) { @@ -1801,10 +1858,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View const unsigned OldB = (unsigned)(PickerColorRGB.b * 255.0f); const unsigned OldA = (unsigned)(PickerColorRGB.a * 255.0f); - const unsigned R = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "R:", OldR, 0, 255); - const unsigned G = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "G:", OldG, 0, 255); - const unsigned B = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "B:", OldB, 0, 255); - const unsigned A = RenderAlphaSelector(OldA); + 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); + const auto [StateB, B] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "B:", OldB, 0, 255); + const auto [StateA, A] = RenderAlphaSelector(OldA); if(OldR != R || OldG != G || OldB != B || OldA != A) { @@ -1812,6 +1869,15 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View PickerColorHSL = color_cast(PickerColorRGB); PickerColorHSV = color_cast(PickerColorHSL); } + + for(auto State : {StateR, StateG, StateB, StateA}) + { + if(State != EEditState::NONE) + { + pColorPicker->m_State = State; + break; + } + } } else if(pColorPicker->m_ColorMode == SColorPickerPopupContext::MODE_HSLA) { @@ -1820,10 +1886,10 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View const unsigned OldL = (unsigned)(PickerColorHSL.l * 255.0f); const unsigned OldA = (unsigned)(PickerColorHSL.a * 255.0f); - const unsigned H = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[0], &HueRect, "H:", OldH, 0, 255); - const unsigned S = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[1], &SatRect, "S:", OldS, 0, 255); - const unsigned L = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "L:", OldL, 0, 255); - const unsigned A = RenderAlphaSelector(OldA); + 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); + const auto [StateL, L] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[2], &ValueRect, "L:", OldL, 0, 255); + const auto [StateA, A] = RenderAlphaSelector(OldA); if(OldH != H || OldS != S || OldL != L || OldA != A) { @@ -1831,6 +1897,15 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View PickerColorHSV = color_cast(PickerColorHSL); PickerColorRGB = color_cast(PickerColorHSL); } + + for(auto State : {StateH, StateS, StateL, StateA}) + { + if(State != EEditState::NONE) + { + pColorPicker->m_State = State; + break; + } + } } else { @@ -1842,7 +1917,7 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View Props.m_IsHex = true; Props.m_HexPrefix = pColorPicker->m_Alpha ? 8 : 6; const unsigned OldHex = PickerColorRGB.PackAlphaLast(pColorPicker->m_Alpha); - const unsigned Hex = pUI->DoValueSelector(&pColorPicker->m_aValueSelectorIds[4], &HexRect, "Hex:", OldHex, 0, pColorPicker->m_Alpha ? 0xFFFFFFFFll : 0xFFFFFFll, Props); + auto [HexState, Hex] = pUI->DoValueSelectorWithState(&pColorPicker->m_aValueSelectorIds[4], &HexRect, "Hex:", OldHex, 0, pColorPicker->m_Alpha ? 0xFFFFFFFFll : 0xFFFFFFll, Props); if(OldHex != Hex) { const float OldAlpha = PickerColorRGB.a; @@ -1853,21 +1928,28 @@ CUI::EPopupMenuFunctionResult CUI::PopupColorPicker(void *pContext, CUIRect View PickerColorHSV = color_cast(PickerColorHSL); } + if(HexState != EEditState::NONE) + pColorPicker->m_State = HexState; + // Logic float PickerX, PickerY; - if(pUI->DoPickerLogic(&pColorPicker->m_ColorPickerId, &ColorsArea, &PickerX, &PickerY)) + EEditState ColorPickerRes = pUI->DoPickerLogic(&pColorPicker->m_ColorPickerId, &ColorsArea, &PickerX, &PickerY); + if(ColorPickerRes != EEditState::NONE) { PickerColorHSV.y = PickerX / ColorsArea.w; PickerColorHSV.z = 1.0f - PickerY / ColorsArea.h; PickerColorHSL = color_cast(PickerColorHSV); PickerColorRGB = color_cast(PickerColorHSL); + pColorPicker->m_State = ColorPickerRes; } - if(pUI->DoPickerLogic(&pColorPicker->m_HuePickerId, &HueArea, &PickerX, &PickerY)) + EEditState HuePickerRes = pUI->DoPickerLogic(&pColorPicker->m_HuePickerId, &HueArea, &PickerX, &PickerY); + if(HuePickerRes != EEditState::NONE) { PickerColorHSV.x = 1.0f - PickerY / HueArea.h; PickerColorHSL = color_cast(PickerColorHSV); PickerColorRGB = color_cast(PickerColorHSL); + pColorPicker->m_State = HuePickerRes; } // Marker Color Area diff --git a/src/game/client/ui.h b/src/game/client/ui.h index 80a7e26dfe6..6c3c5d9596d 100644 --- a/src/game/client/ui.h +++ b/src/game/client/ui.h @@ -18,6 +18,22 @@ class IClient; class IGraphics; class IKernel; +enum class EEditState +{ + NONE, + START, + EDITING, + END, + ONE_GO +}; + +template +struct SEditResult +{ + EEditState m_State; + T m_Value; +}; + struct SUIAnimator { bool m_Active; @@ -392,7 +408,7 @@ class CUI HOTKEY_END = 1 << 11, }; - void ResetUIElement(CUIElement &UIElement); + void ResetUIElement(CUIElement &UIElement) const; CUIElement *GetNewUIElement(int RequestedRectCount); @@ -488,14 +504,14 @@ class CUI int DoButtonLogic(const void *pID, int Checked, const CUIRect *pRect); int DoDraggableButtonLogic(const void *pID, int Checked, const CUIRect *pRect, bool *pClicked, bool *pAbrupted); - int 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); + 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 = {}); + 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); - 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); + 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); @@ -505,6 +521,7 @@ class CUI 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; } @@ -521,7 +538,7 @@ class CUI 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 = ""); // progress spinner - void RenderProgressSpinner(vec2 Center, float OuterRadius, const SProgressSpinnerProperties &Props = {}); + 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 = {}); @@ -620,6 +637,7 @@ class CUI const char m_ColorPickerId = 0; const char m_aValueSelectorIds[5] = {0}; CButtonContainer m_aModeButtons[(int)MODE_HSLA + 1]; + EEditState m_State; }; void ShowPopupColorPicker(float X, float Y, SColorPickerPopupContext *pContext); diff --git a/src/game/collision.cpp b/src/game/collision.cpp index ef5edb1171c..8612ad7dc26 100644 --- a/src/game/collision.cpp +++ b/src/game/collision.cpp @@ -225,7 +225,7 @@ static int GetMoveRestrictions(int Direction, int Tile, int Flags) return Result & GetMoveRestrictionsMask(Direction); } -int CCollision::GetMoveRestrictions(CALLBACK_SWITCHACTIVE pfnSwitchActive, void *pUser, vec2 Pos, float Distance, int OverrideCenterTileIndex) +int CCollision::GetMoveRestrictions(CALLBACK_SWITCHACTIVE pfnSwitchActive, void *pUser, vec2 Pos, float Distance, int OverrideCenterTileIndex) const { static const vec2 DIRECTIONS[NUM_MR_DIRS] = { @@ -336,11 +336,14 @@ int CCollision::IntersectLineTeleHook(vec2 Pos0, vec2 Pos1, vec2 *pOutCollision, int iy = round_to_int(Pos.y); int Index = GetPureMapIndex(Pos); - if(g_Config.m_SvOldTeleportHook) - *pTeleNr = IsTeleport(Index); - else - *pTeleNr = IsTeleportHook(Index); - if(*pTeleNr) + if(pTeleNr) + { + if(g_Config.m_SvOldTeleportHook) + *pTeleNr = IsTeleport(Index); + else + *pTeleNr = IsTeleportHook(Index); + } + if(pTeleNr && *pTeleNr) { if(pOutCollision) *pOutCollision = Pos; @@ -391,11 +394,14 @@ int CCollision::IntersectLineTeleWeapon(vec2 Pos0, vec2 Pos1, vec2 *pOutCollisio int iy = round_to_int(Pos.y); int Index = GetPureMapIndex(Pos); - if(g_Config.m_SvOldTeleportWeapons) - *pTeleNr = IsTeleport(Index); - else - *pTeleNr = IsTeleportWeapon(Index); - if(*pTeleNr) + if(pTeleNr) + { + if(g_Config.m_SvOldTeleportWeapons) + *pTeleNr = IsTeleport(Index); + else + *pTeleNr = IsTeleportWeapon(Index); + } + if(pTeleNr && *pTeleNr) { if(pOutCollision) *pOutCollision = Pos; diff --git a/src/game/collision.h b/src/game/collision.h index fa697f6ed33..351ac05dd2c 100644 --- a/src/game/collision.h +++ b/src/game/collision.h @@ -39,8 +39,8 @@ class CCollision int GetWidth() const { return m_Width; } int GetHeight() const { return m_Height; } int IntersectLine(vec2 Pos0, vec2 Pos1, vec2 *pOutCollision, vec2 *pOutBeforeCollision) const; - int IntersectLineTeleWeapon(vec2 Pos0, vec2 Pos1, vec2 *pOutCollision, vec2 *pOutBeforeCollision, int *pTeleNr) const; - int IntersectLineTeleHook(vec2 Pos0, vec2 Pos1, vec2 *pOutCollision, vec2 *pOutBeforeCollision, int *pTeleNr) const; + int IntersectLineTeleWeapon(vec2 Pos0, vec2 Pos1, vec2 *pOutCollision, vec2 *pOutBeforeCollision, int *pTeleNr = nullptr) const; + int IntersectLineTeleHook(vec2 Pos0, vec2 Pos1, vec2 *pOutCollision, vec2 *pOutBeforeCollision, int *pTeleNr = nullptr) const; void MovePoint(vec2 *pInoutPos, vec2 *pInoutVel, float Elasticity, int *pBounces) const; void MoveBox(vec2 *pInoutPos, vec2 *pInoutVel, vec2 Size, vec2 Elasticity, bool *pGrounded = nullptr) const; bool TestBox(vec2 Pos, vec2 Size) const; @@ -62,7 +62,7 @@ class CCollision int GetIndex(vec2 PrevPos, vec2 Pos) const; int GetFIndex(int x, int y) const; - int GetMoveRestrictions(CALLBACK_SWITCHACTIVE pfnSwitchActive, void *pUser, vec2 Pos, float Distance = 18.0f, int OverrideCenterTileIndex = -1); + 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) { return GetMoveRestrictions(nullptr, nullptr, Pos, Distance); diff --git a/src/game/ddracechat.h b/src/game/ddracechat.h index 16f86fc106e..b26dceac216 100644 --- a/src/game/ddracechat.h +++ b/src/game/ddracechat.h @@ -45,10 +45,11 @@ CHAT_COMMAND("points", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConPoin CHAT_COMMAND("top5points", "?i[number]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTopPoints, this, "Shows five points of the global point ladder beginning with rank i (1 by default)") CHAT_COMMAND("timecp", "?r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTimeCP, this, "Set your checkpoints based on another player") -CHAT_COMMAND("team", "?i[id]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConJoinTeam, this, "Lets you join team i (shows your team if left blank)") -CHAT_COMMAND("lock", "?i['0'|'1']", CFGFLAG_CHAT | CFGFLAG_SERVER, ConLockTeam, 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.") -CHAT_COMMAND("unlock", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConUnlockTeam, this, "Unlock a team") -CHAT_COMMAND("invite", "r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConInviteTeam, this, "Invite a person to a locked team") +CHAT_COMMAND("team", "?i[id]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConTeam, this, "Lets you join team i (shows your team if left blank)") +CHAT_COMMAND("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.") +CHAT_COMMAND("unlock", "", CFGFLAG_CHAT | CFGFLAG_SERVER, ConUnlock, this, "Unlock a team") +CHAT_COMMAND("invite", "r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConInvite, this, "Invite a person to a locked team") +CHAT_COMMAND("join", "r[player name]", CFGFLAG_CHAT | CFGFLAG_SERVER, ConJoin, this, "Join the team of the specified player") CHAT_COMMAND("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") CHAT_COMMAND("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") diff --git a/src/game/editor/auto_map.cpp b/src/game/editor/auto_map.cpp index f8cf86ed0c9..4f58f8e0fda 100644 --- a/src/game/editor/auto_map.cpp +++ b/src/game/editor/auto_map.cpp @@ -9,6 +9,7 @@ #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 static uint32_t HashUInt32(uint32_t Num) @@ -173,9 +174,9 @@ void CAutoMapper::Load(const char *pTileName) { Value = CPosRule::NOTINDEX; CIndexInfo NewIndexInfo1 = {0, 0, false}; - //CIndexInfo NewIndexInfo2 = {-1, 0}; + // CIndexInfo NewIndexInfo2 = {-1, 0}; vNewIndexList.push_back(NewIndexInfo1); - //vNewIndexList.push_back(NewIndexInfo2); + // vNewIndexList.push_back(NewIndexInfo2); } else if(!str_comp(aValue, "INDEX") || !str_comp(aValue, "NOTINDEX")) { @@ -425,8 +426,10 @@ void CAutoMapper::ProceedLocalized(CLayerTiles *pLayer, int ConfigID, int Seed, { CTile *pIn = &pUpdateLayer->m_pTiles[(y - UpdateFromY) * pUpdateLayer->m_Width + x - UpdateFromX]; CTile *pOut = &pLayer->m_pTiles[y * pLayer->m_Width + x]; + CTile Previous = *pOut; pOut->m_Index = pIn->m_Index; pOut->m_Flags = pIn->m_Flags; + pLayer->RecordStateChange(x, y, Previous, *pOut); } } @@ -442,6 +445,7 @@ void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigID, int Seed, int SeedO Seed = rand(); CConfiguration *pConf = &m_vConfigs[ConfigID]; + pLayer->ClearHistory(); // for every run: copy tiles, automap, overwrite tiles for(size_t h = 0; h < pConf->m_vRuns.size(); ++h) @@ -534,8 +538,10 @@ void CAutoMapper::Proceed(CLayerTiles *pLayer, int ConfigID, int Seed, int SeedO if(RespectRules && (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_Flags = pIndexRule->m_Flag; + pLayer->RecordStateChange(x, y, Previous, *pTile); } } } diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 5888c7fa4af..183c8453720 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -32,11 +32,13 @@ #include #include +#include #include #include #include "auto_map.h" #include "editor.h" +#include "editor_actions.h" #include #include @@ -328,7 +330,7 @@ void CEditor::RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, Graphics()->QuadsEnd(); } -int 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, ColorRGBA *pColor, bool ShowValue) { // logic static float s_Value; @@ -337,6 +339,8 @@ int CEditor::UiDoValueSelector(void *pID, CUIRect *pRect, const char *pLabel, in 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; if(UI()->MouseButton(1) && UI()->HotItem() == pID) { @@ -438,14 +442,27 @@ int CEditor::UiDoValueSelector(void *pID, CUIRect *pRect, const char *pLabel, in str_format(aBuf, sizeof(aBuf), "#%06X", Current); else str_from_int(Current, aBuf); - pRect->Draw(pColor ? *pColor : GetButtonColor(pID, 0), Corners, 5.0f); + pRect->Draw(pColor ? *pColor : GetButtonColor(pID, 0), Corners, 3.0f); UI()->DoLabel(pRect, aBuf, 10, TEXTALIGN_MC); } if(!s_TextMode) s_NumberInput.Clear(); - return Current; + bool MouseLocked = UI()->CheckMouseLock(); + if((MouseLocked || s_TextMode) && !s_Editing) + { + State = EEditState::START; + s_Editing = true; + } + + if(!MouseLocked && !s_TextMode && s_Editing) + { + State = EEditState::END; + s_Editing = false; + } + + return SEditResult{State, Current}; } std::shared_ptr CEditor::GetSelectedGroup() const @@ -491,7 +508,7 @@ std::vector CEditor::GetSelectedQuads() return vpQuads; } -CSoundSource *CEditor::GetSelectedSource() +CSoundSource *CEditor::GetSelectedSource() const { std::shared_ptr pSounds = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_SOUNDS)); if(!pSounds) @@ -576,8 +593,14 @@ void CEditor::DeleteSelectedQuads() if(!pLayer) return; + std::vector vSelectedQuads(m_vSelectedQuads); + std::vector vDeletedQuads; + for(int i = 0; i < (int)m_vSelectedQuads.size(); ++i) { + auto const &Quad = pLayer->m_vQuads[m_vSelectedQuads[i]]; + vDeletedQuads.push_back(Quad); + pLayer->m_vQuads.erase(pLayer->m_vQuads.begin() + m_vSelectedQuads[i]); for(int j = i + 1; j < (int)m_vSelectedQuads.size(); ++j) if(m_vSelectedQuads[j] > m_vSelectedQuads[i]) @@ -586,6 +609,8 @@ void CEditor::DeleteSelectedQuads() m_vSelectedQuads.erase(m_vSelectedQuads.begin() + i); i--; } + + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0], vSelectedQuads, vDeletedQuads)); } bool CEditor::IsQuadSelected(int Index) const @@ -714,6 +739,39 @@ bool CEditor::IsTangentSelected() const return IsTangentInSelected() || IsTangentOutSelected(); } +std::pair CEditor::EnvGetSelectedTimeAndValue() const +{ + if(m_SelectedEnvelope < 0 || m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size()) + return {}; + + std::shared_ptr pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope]; + int CurrentTime; + int CurrentValue; + if(IsTangentInSelected()) + { + auto [SelectedIndex, SelectedChannel] = m_SelectedTangentInPoint; + + CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel]; + CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaY[SelectedChannel]; + } + else if(IsTangentOutSelected()) + { + auto [SelectedIndex, SelectedChannel] = m_SelectedTangentOutPoint; + + CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel]; + CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaY[SelectedChannel]; + } + else + { + auto [SelectedIndex, SelectedChannel] = m_vSelectedEnvelopePoints.front(); + + CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time; + CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]; + } + + return std::pair{CurrentTime, CurrentValue}; +} + bool CEditor::CallbackOpenMap(const char *pFileName, int StorageType, void *pUser) { CEditor *pEditor = (CEditor *)pUser; @@ -907,7 +965,7 @@ void CEditor::DoAudioPreview(CUIRect View, const void *pPlayPauseButtonID, const } 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); @@ -1101,6 +1159,23 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) TB_Top.VSplitLeft(5.0f, nullptr, &TB_Top); + // undo/redo group + TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); + static int s_UndoButton = 0; + if(DoButton_FontIcon(&s_UndoButton, FONT_ICON_UNDO, m_EditorHistory.CanUndo() - 1, &Button, 0, "[ctrl+z] Undo last action", IGraphics::CORNER_L)) + { + m_EditorHistory.Undo(); + } + + TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); + static int s_RedoButton = 0; + if(DoButton_FontIcon(&s_RedoButton, FONT_ICON_REDO, m_EditorHistory.CanRedo() - 1, &Button, 0, "[ctrl+y] Redo last action", IGraphics::CORNER_R)) + { + m_EditorHistory.Redo(); + } + + TB_Top.VSplitLeft(5.0f, nullptr, &TB_Top); + // brush manipulation { int Enabled = m_pBrush->IsEmpty() ? -1 : 0; @@ -1144,7 +1219,8 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) } TB_Top.VSplitLeft(30.0f, &Button, &TB_Top); - s_RotationAmount = UiDoValueSelector(&s_RotationAmount, &Button, "", s_RotationAmount, TileLayer ? 90 : 1, 359, TileLayer ? 90 : 1, TileLayer ? 10.0f : 2.0f, "Rotation of the brush in degrees. Use left mouse button to drag and change the value. Hold shift to be more precise.", true, false, IGraphics::CORNER_NONE); + auto RotationAmountRes = UiDoValueSelector(&s_RotationAmount, &Button, "", s_RotationAmount, TileLayer ? 90 : 1, 359, TileLayer ? 90 : 1, TileLayer ? 10.0f : 2.0f, "Rotation of the brush in degrees. Use left mouse button to drag and change the value. Hold shift to be more precise.", true, false, IGraphics::CORNER_NONE); + s_RotationAmount = RotationAmountRes.m_Value; TB_Top.VSplitLeft(25.0f, &Button, &TB_Top); static int s_CwButton = 0; @@ -1309,24 +1385,9 @@ void CEditor::DoToolbarLayers(CUIRect ToolBar) } if(pLayer->m_Type == LAYERTYPE_QUADS) - { - std::shared_ptr pLayerQuads = std::static_pointer_cast(pLayer); - - int Width = 64; - int Height = 64; - if(pLayerQuads->m_Image >= 0) - { - Width = m_Map.m_vpImages[pLayerQuads->m_Image]->m_Width; - Height = m_Map.m_vpImages[pLayerQuads->m_Image]->m_Height; - } - - pLayerQuads->NewQuad(x, y, Width, Height); - } + m_EditorHistory.Execute(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0], x, y)); else if(pLayer->m_Type == LAYERTYPE_SOUNDS) - { - std::shared_ptr pLayerSounds = std::static_pointer_cast(pLayer); - pLayerSounds->NewSource(x, y); - } + m_EditorHistory.Execute(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0], x, y)); } TB_Bottom.VSplitLeft(5.0f, &Button, &TB_Bottom); } @@ -1351,7 +1412,7 @@ 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; @@ -1468,7 +1529,462 @@ void CEditor::DoSoundSource(CSoundSource *pSource, int Index) Graphics()->QuadsDraw(&QuadItem, 1); } -void CEditor::DoQuad(CQuad *pQuad, int Index) +void CEditor::PreparePointDrag(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex) +{ + m_QuadDragOriginalPoints[QuadIndex][PointIndex] = pQuad->m_aPoints[PointIndex]; +} + +void CEditor::DoPointDrag(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY) +{ + pQuad->m_aPoints[PointIndex].x = m_QuadDragOriginalPoints[QuadIndex][PointIndex].x + OffsetX; + pQuad->m_aPoints[PointIndex].y = m_QuadDragOriginalPoints[QuadIndex][PointIndex].y + OffsetY; +} + +CEditor::EAxis CEditor::GetDragAxis(int OffsetX, int OffsetY) +{ + if(Input()->ShiftIsPressed()) + if(absolute(OffsetX) < absolute(OffsetY)) + return EAxis::AXIS_Y; + else + return EAxis::AXIS_X; + else + return EAxis::AXIS_NONE; +} + +void CEditor::DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point) +{ + if(Axis == EAxis::AXIS_NONE) + return; + + 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); + 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)); + 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); + Graphics()->QuadsDraw(&QuadItem, 1); +} + +void CEditor::ComputePointAlignments(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY, std::vector &vAlignments, bool Append) const +{ + if(!Append) + vAlignments.clear(); + if(!g_Config.m_EdAlignQuads) + return; + + // Perform computation from the original position of this point + int Threshold = f2fx(maximum(10.0f, 10.0f * m_MouseWScale)); + CPoint OrigPoint = m_QuadDragOriginalPoints.at(QuadIndex)[PointIndex]; + // Get the "current" point by applying the offset + CPoint Point = OrigPoint + ivec2(OffsetX, OffsetY); + + // Save smallest diff on both axis to only keep closest alignments + int SmallestDiffX = Threshold + 1, SmallestDiffY = Threshold + 1; + // Store both axis alignments in separate vectors + std::vector vAlignmentsX, vAlignmentsY; + + // Check if we can align/snap to a specific point + auto &&CheckAlignment = [&](CPoint *pQuadPoint) { + int DX = pQuadPoint->x - Point.x; + int DY = pQuadPoint->y - Point.y; + int DiffX = absolute(DX); + int DiffY = absolute(DY); + + // Check the X axis + if(DiffX <= Threshold) + { + // Only store alignments that have the smallest difference + if(DiffX < SmallestDiffX) + { + vAlignmentsX.clear(); + SmallestDiffX = DiffX; + } + + // We can have multiple alignments having the same difference/distance + if(DiffX == SmallestDiffX) + { + vAlignmentsX.push_back(SAlignmentInfo{ + *pQuadPoint, // Aligned point + {OrigPoint.y}, // Value that can change (which is not snapped), original position + EAxis::AXIS_Y, // The alignment axis + PointIndex, // The index of the point + DX, + }); + } + } + if(DiffY <= Threshold) + { + // Only store alignments that have the smallest difference + if(DiffY < SmallestDiffY) + { + vAlignmentsY.clear(); + SmallestDiffY = DiffY; + } + + if(DiffY == SmallestDiffY) + { + vAlignmentsY.push_back(SAlignmentInfo{ + *pQuadPoint, + {OrigPoint.x}, + EAxis::AXIS_X, + PointIndex, + DY, + }); + } + } + }; + + // Iterate through all the quads of the current layer + // Check alignment with each point of the quad (corners & pivot) + // Compute an AABB (Axis Aligned Bounding Box) to get the center of the quad + // Check alignment with the center of the quad + for(size_t i = 0; i < pLayer->m_vQuads.size(); i++) + { + auto *pCurrentQuad = &pLayer->m_vQuads[i]; + CPoint Min = pCurrentQuad->m_aPoints[0]; + CPoint Max = pCurrentQuad->m_aPoints[0]; + + for(int v = 0; v < 5; v++) + { + CPoint *pQuadPoint = &pCurrentQuad->m_aPoints[v]; + + if(v != 4) + { // Don't use pivot to compute AABB + if(pQuadPoint->x < Min.x) + Min.x = pQuadPoint->x; + if(pQuadPoint->y < Min.y) + Min.y = pQuadPoint->y; + if(pQuadPoint->x > Max.x) + Max.x = pQuadPoint->x; + if(pQuadPoint->y > Max.y) + Max.y = pQuadPoint->y; + } + + // Don't check alignment with current point + if(pQuadPoint == &pQuad->m_aPoints[PointIndex]) + continue; + + // Don't check alignment with other selected points + bool IsCurrentPointSelected = IsQuadSelected(i) && (IsQuadCornerSelected(v) || (v == PointIndex && PointIndex == 4)); + if(IsCurrentPointSelected) + continue; + + CheckAlignment(pQuadPoint); + } + + CPoint Center = (Min + Max) / 2.0f; + CheckAlignment(&Center); + } + + // Finally concatenate both alignment vectors into the output + vAlignments.reserve(vAlignmentsX.size() + vAlignmentsY.size()); + vAlignments.insert(vAlignments.end(), vAlignmentsX.begin(), vAlignmentsX.end()); + vAlignments.insert(vAlignments.end(), vAlignmentsY.begin(), vAlignmentsY.end()); +} + +void CEditor::ComputePointsAlignments(const std::shared_ptr &pLayer, bool Pivot, int OffsetX, int OffsetY, std::vector &vAlignments) const +{ + // This method is used to compute alignments from selected points + // and only apply the closest alignment on X and Y to the offset. + + vAlignments.clear(); + std::vector vAllAlignments; + + for(int Selected : m_vSelectedQuads) + { + CQuad *pQuad = &pLayer->m_vQuads[Selected]; + + if(!Pivot) + { + for(int m = 0; m < 4; m++) + { + if(IsQuadPointSelected(Selected, m)) + { + ComputePointAlignments(pLayer, pQuad, Selected, m, OffsetX, OffsetY, vAllAlignments, true); + } + } + } + else + { + ComputePointAlignments(pLayer, pQuad, Selected, 4, OffsetX, OffsetY, vAllAlignments, true); + } + } + + int SmallestDiffX, SmallestDiffY; + SmallestDiffX = SmallestDiffY = std::numeric_limits::max(); + + std::vector vAlignmentsX, vAlignmentsY; + + for(const auto &Alignment : vAllAlignments) + { + int AbsDiff = absolute(Alignment.m_Diff); + if(Alignment.m_Axis == EAxis::AXIS_X) + { + if(AbsDiff < SmallestDiffY) + { + SmallestDiffY = AbsDiff; + vAlignmentsY.clear(); + } + if(AbsDiff == SmallestDiffY) + vAlignmentsY.emplace_back(Alignment); + } + else if(Alignment.m_Axis == EAxis::AXIS_Y) + { + if(AbsDiff < SmallestDiffX) + { + SmallestDiffX = AbsDiff; + vAlignmentsX.clear(); + } + if(AbsDiff == SmallestDiffX) + vAlignmentsX.emplace_back(Alignment); + } + } + + vAlignments.reserve(vAlignmentsX.size() + vAlignmentsY.size()); + vAlignments.insert(vAlignments.end(), vAlignmentsX.begin(), vAlignmentsX.end()); + vAlignments.insert(vAlignments.end(), vAlignmentsY.begin(), vAlignmentsY.end()); +} + +void CEditor::ComputeAABBAlignments(const std::shared_ptr &pLayer, const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY, std::vector &vAlignments) const +{ + vAlignments.clear(); + if(!g_Config.m_EdAlignQuads) + return; + + // 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 SmallestDiffX = Threshold + 1, SmallestDiffY = Threshold + 1; + std::vector vAlignmentsX, vAlignmentsY; + + auto &&CheckAlignment = [&](CPoint &Aligned, int Point) { + CPoint ToCheck = AABB.m_aPoints[Point] + ivec2(OffsetX, OffsetY); + int DX = Aligned.x - ToCheck.x; + int DY = Aligned.y - ToCheck.y; + int DiffX = absolute(DX); + int DiffY = absolute(DY); + + if(DiffX <= Threshold) + { + if(DiffX < SmallestDiffX) + { + SmallestDiffX = DiffX; + vAlignmentsX.clear(); + } + + if(DiffX == SmallestDiffX) + { + vAlignmentsX.push_back(SAlignmentInfo{ + Aligned, + {AABB.m_aPoints[Point].y}, + EAxis::AXIS_Y, + Point, + DX, + }); + } + } + if(DiffY <= Threshold) + { + if(DiffY < SmallestDiffY) + { + SmallestDiffY = DiffY; + vAlignmentsY.clear(); + } + + if(DiffY == SmallestDiffY) + { + vAlignmentsY.push_back(SAlignmentInfo{ + Aligned, + {AABB.m_aPoints[Point].x}, + EAxis::AXIS_X, + Point, + DY, + }); + } + } + }; + + auto &&CheckAABBAlignment = [&](CPoint &QuadMin, CPoint &QuadMax) { + CPoint QuadCenter = (QuadMin + QuadMax) / 2.0f; + CPoint aQuadPoints[5] = { + QuadMin, // Top left + {QuadMax.x, QuadMin.y}, // Top right + {QuadMin.x, QuadMax.y}, // Bottom left + QuadMax, // Bottom right + QuadCenter, + }; + + // Check all points with all the other points + for(auto &QuadPoint : aQuadPoints) + { + // i is the quad point which is "aligned" and that we want to compare with + for(int j = 0; j < 5; j++) + { + // j is the point we try to align + CheckAlignment(QuadPoint, j); + } + } + }; + + // Iterate through all quads of the current layer + // Compute AABB of all quads and check if the dragged AABB can be aligned to this AABB. + for(size_t i = 0; i < pLayer->m_vQuads.size(); i++) + { + auto *pCurrentQuad = &pLayer->m_vQuads[i]; + if(IsQuadSelected(i)) // Don't check with other selected quads + continue; + + // Get AABB of this quad + CPoint QuadMin = pCurrentQuad->m_aPoints[0], QuadMax = pCurrentQuad->m_aPoints[0]; + for(int v = 1; v < 4; v++) + { + QuadMin.x = minimum(QuadMin.x, pCurrentQuad->m_aPoints[v].x); + QuadMin.y = minimum(QuadMin.y, pCurrentQuad->m_aPoints[v].y); + QuadMax.x = maximum(QuadMax.x, pCurrentQuad->m_aPoints[v].x); + QuadMax.y = maximum(QuadMax.y, pCurrentQuad->m_aPoints[v].y); + } + + CheckAABBAlignment(QuadMin, QuadMax); + } + + // Finally, concatenate both alignment vectors into the output + vAlignments.reserve(vAlignmentsX.size() + vAlignmentsY.size()); + vAlignments.insert(vAlignments.end(), vAlignmentsX.begin(), vAlignmentsX.end()); + vAlignments.insert(vAlignments.end(), vAlignmentsY.begin(), vAlignmentsY.end()); +} + +void CEditor::DrawPointAlignments(const std::vector &vAlignments, int OffsetX, int OffsetY) +{ + if(!g_Config.m_EdAlignQuads) + return; + + // Drawing an alignment is easy, we convert fixed to float for the aligned point coords + // and we also convert the "changing" value after applying the offset (which might be edited to actually align the value with the alignment). + Graphics()->SetColor(1, 0, 0.1f, 1); + for(const SAlignmentInfo &Alignment : 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); + 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)); + Graphics()->QuadsDrawTL(&Line, 1); + } + } +} + +void CEditor::DrawAABB(const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY) +{ + // Drawing an AABB is simply converting the points from fixed to float + // Then making lines out of quads and drawing them + vec2 TL = {fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].x + OffsetX), fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].y + OffsetY)}; + vec2 TR = {fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].x + OffsetX), fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].y + OffsetY)}; + vec2 BL = {fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].x + OffsetX), fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].y + OffsetY)}; + vec2 BR = {fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].x + OffsetX), fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].y + OffsetY)}; + vec2 Center = {fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].x + OffsetX), fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].y + OffsetY)}; + + // 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}, + }; + 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); + Graphics()->QuadsDraw(&CenterQuad, 1); +} + +void CEditor::QuadSelectionAABB(const std::shared_ptr &pLayer, SAxisAlignedBoundingBox &OutAABB) +{ + // Compute an englobing AABB of the current selection of quads + CPoint Min{ + std::numeric_limits::max(), + std::numeric_limits::max(), + }; + CPoint Max{ + std::numeric_limits::min(), + std::numeric_limits::min(), + }; + for(int Selected : m_vSelectedQuads) + { + CQuad *pQuad = &pLayer->m_vQuads[Selected]; + for(auto &Point : pQuad->m_aPoints) + { + 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); + } + } + CPoint Center = (Min + Max) / 2.0f; + CPoint aPoints[SAxisAlignedBoundingBox::NUM_POINTS] = { + Min, // Top left + {Max.x, Min.y}, // Top right + {Min.x, Max.y}, // Bottom left + Max, // Bottom right + Center, + }; + mem_copy(OutAABB.m_aPoints, aPoints, sizeof(CPoint) * SAxisAlignedBoundingBox::NUM_POINTS); +} + +void CEditor::ApplyAlignments(const std::vector &vAlignments, int &OffsetX, int &OffsetY) +{ + if(vAlignments.empty()) + return; + + // Find X and Y aligment + const int *pAlignedX = nullptr; + const int *pAlignedY = nullptr; + + // To Find the alignments we simply iterate through the vector of alignments and find the first + // X and Y alignments. + // Then, we use the saved m_Diff to adjust the offset + int AdjustX = 0, AdjustY = 0; + for(const SAlignmentInfo &Alignment : vAlignments) + { + if(Alignment.m_Axis == EAxis::AXIS_X && !pAlignedY) + { + pAlignedY = &Alignment.m_AlignedPoint.y; + AdjustY = Alignment.m_Diff; + } + else if(Alignment.m_Axis == EAxis::AXIS_Y && !pAlignedX) + { + pAlignedX = &Alignment.m_AlignedPoint.x; + AdjustX = Alignment.m_Diff; + } + } + + // Adjust offset + OffsetX += AdjustX; + OffsetY += AdjustY; +} + +void CEditor::ApplyAxisAlignment(int &OffsetX, int &OffsetY) +{ + // This is used to preserve axis alignment when pressing `Shift` + // Should be called before any other computation + EAxis Axis = GetDragAxis(OffsetX, OffsetY); + OffsetX = ((Axis == EAxis::AXIS_NONE || Axis == EAxis::AXIS_X) ? OffsetX : 0); + OffsetY = ((Axis == EAxis::AXIS_NONE || Axis == EAxis::AXIS_Y) ? OffsetY : 0); +} + +void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, int Index) { enum { @@ -1490,6 +2006,11 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) static float s_RotateAngle = 0; 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) + static SAxisAlignedBoundingBox s_SelectionAABB; // Selection AABB + static ivec2 s_LastOffset; // Last offset, stored as static so we can use it to draw every frame // get pivot float CenterX = fx2f(pQuad->m_aPoints[4].x); @@ -1497,6 +2018,18 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) const bool IgnoreGrid = Input()->AltIsPressed(); + auto &&GetDragOffset = [&]() -> ivec2 { + float x = wx; + float y = wy; + if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) + MapView()->MapGrid()->SnapToGrid(x, y); + + int OffsetX = f2fx(x) - s_OriginalPosition.x; + int OffsetY = f2fx(y) - s_OriginalPosition.y; + + return {OffsetX, OffsetY}; + }; + // draw selection background if(IsQuadSelected(Index)) { @@ -1519,57 +2052,76 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) if(!IsQuadSelected(Index)) SelectQuad(Index); + s_OriginalPosition = pQuad->m_aPoints[4]; + if(Input()->ShiftIsPressed()) + { s_Operation = OP_MOVE_PIVOT; + // When moving, we need to save the original position of all selected pivots + for(int Selected : m_vSelectedQuads) + { + CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected]; + PreparePointDrag(pLayer, pCurrentQuad, Selected, 4); + } + } else + { s_Operation = OP_MOVE_ALL; + // When moving, we need to save the original position of all selected quads points + for(int Selected : m_vSelectedQuads) + { + CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected]; + for(size_t v = 0; v < 5; v++) + PreparePointDrag(pLayer, pCurrentQuad, Selected, v); + } + // And precompute AABB of selection since it will not change during drag + if(g_Config.m_EdAlignQuads) + QuadSelectionAABB(pLayer, s_SelectionAABB); + } } } // check if we only should move pivot if(s_Operation == OP_MOVE_PIVOT) { - float x = wx; - float y = wy; - if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) - MapView()->MapGrid()->SnapToGrid(x, y); + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); + + s_LastOffset = GetDragOffset(); // Update offset + ApplyAxisAlignment(s_LastOffset.x, s_LastOffset.y); // Apply axis alignment to the offset - int OffsetX = f2fx(x) - pQuad->m_aPoints[4].x; - int OffsetY = f2fx(y) - pQuad->m_aPoints[4].y; + ComputePointsAlignments(pLayer, true, s_LastOffset.x, s_LastOffset.y, s_PivotAlignments); + ApplyAlignments(s_PivotAlignments, s_LastOffset.x, s_LastOffset.y); - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); for(auto &Selected : m_vSelectedQuads) { CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected]; - pCurrentQuad->m_aPoints[4].x += OffsetX; - pCurrentQuad->m_aPoints[4].y += OffsetY; + DoPointDrag(pLayer, pCurrentQuad, Selected, 4, s_LastOffset.x, s_LastOffset.y); } } else if(s_Operation == OP_MOVE_ALL) { - // move all points including pivot - float x = wx; - float y = wy; - if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) - MapView()->MapGrid()->SnapToGrid(x, y); + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); - int OffsetX = f2fx(x) - pQuad->m_aPoints[4].x; - int OffsetY = f2fx(y) - pQuad->m_aPoints[4].y; + // Compute drag offset + s_LastOffset = GetDragOffset(); + ApplyAxisAlignment(s_LastOffset.x, s_LastOffset.y); - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); - for(auto &Selected : m_vSelectedQuads) + // Then compute possible alignments with the selection AABB + ComputeAABBAlignments(pLayer, s_SelectionAABB, s_LastOffset.x, s_LastOffset.y, s_vAABBAlignments); + // Apply alignments before drag + ApplyAlignments(s_vAABBAlignments, s_LastOffset.x, s_LastOffset.y); + // Then do the drag + for(int Selected : m_vSelectedQuads) { CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected]; - for(auto &Point : pCurrentQuad->m_aPoints) - { - Point.x += OffsetX; - Point.y += OffsetY; - } + for(int v = 0; v < 5; v++) + DoPointDrag(pLayer, pCurrentQuad, Selected, v, s_LastOffset.x, s_LastOffset.y); } } else if(s_Operation == OP_ROTATE) { - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); + for(size_t i = 0; i < m_vSelectedQuads.size(); ++i) { CQuad *pCurrentQuad = &pLayer->m_vQuads[m_vSelectedQuads[i]]; @@ -1587,6 +2139,24 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) } } + // Draw axis and aligments when moving + if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL) + { + EAxis Axis = GetDragAxis(s_LastOffset.x, s_LastOffset.y); + DrawAxis(Axis, s_OriginalPosition, pQuad->m_aPoints[4]); + } + + if(s_Operation == OP_MOVE_PIVOT) + DrawPointAlignments(s_PivotAlignments, s_LastOffset.x, s_LastOffset.y); + + if(s_Operation == OP_MOVE_ALL) + { + DrawPointAlignments(s_vAABBAlignments, s_LastOffset.x, s_LastOffset.y); + + if(g_Config.m_EdShowQuadsRect) + DrawAABB(s_SelectionAABB, s_LastOffset.x, s_LastOffset.y); + } + if(s_Operation == OP_CONTEXT_MENU) { if(!UI()->MouseButton(1)) @@ -1624,6 +2194,7 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) UI()->DisableMouseLock(); s_Operation = OP_NONE; UI()->SetActiveItem(nullptr); + m_QuadTracker.EndQuadTrack(); } else if(UI()->MouseButton(1)) { @@ -1632,7 +2203,6 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) UI()->SetActiveItem(nullptr); // Reset points to old position - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); for(size_t i = 0; i < m_vSelectedQuads.size(); ++i) { CQuad *pCurrentQuad = &pLayer->m_vQuads[m_vSelectedQuads[i]]; @@ -1652,10 +2222,19 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) else SelectQuad(Index); } + else if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL) + { + m_QuadTracker.EndQuadTrack(); + } UI()->DisableMouseLock(); s_Operation = OP_NONE; UI()->SetActiveItem(nullptr); + + s_LastOffset = ivec2(); + s_OriginalPosition = ivec2(); + s_vAABBAlignments.clear(); + s_PivotAlignments.clear(); } } @@ -1668,7 +2247,6 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) s_Operation = OP_ROTATE; s_RotateAngle = 0; - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_QUADS)); s_vvRotatePoints.clear(); s_vvRotatePoints.resize(m_vSelectedQuads.size()); for(size_t i = 0; i < m_vSelectedQuads.size(); ++i) @@ -1727,7 +2305,7 @@ void CEditor::DoQuad(CQuad *pQuad, int Index) Graphics()->QuadsDraw(&QuadItem, 1); } -void CEditor::DoQuadPoint(CQuad *pQuad, int QuadIndex, int V) +void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int V) { void *pID = &pQuad->m_aPoints[V]; @@ -1757,9 +2335,24 @@ void CEditor::DoQuadPoint(CQuad *pQuad, int QuadIndex, int V) static int s_Operation = OP_NONE; static float s_MouseXStart = 0.0f; static float s_MouseYStart = 0.0f; + static CPoint s_OriginalPoint; + static std::vector s_Alignments; // Alignments + static ivec2 s_LastOffset; const bool IgnoreGrid = Input()->AltIsPressed(); + auto &&GetDragOffset = [&]() -> ivec2 { + float x = wx; + float y = wy; + if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) + MapView()->MapGrid()->SnapToGrid(x, y); + + int OffsetX = f2fx(x) - s_OriginalPoint.x; + int OffsetY = f2fx(y) - s_OriginalPoint.y; + + return {OffsetX, OffsetY}; + }; + if(UI()->CheckActiveItem(pID)) { if(m_MouseDeltaWx * m_MouseDeltaWx + m_MouseDeltaWy * m_MouseDeltaWy > 0.0f) @@ -1780,31 +2373,43 @@ void CEditor::DoQuadPoint(CQuad *pQuad, int QuadIndex, int V) 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); + } } } if(s_Operation == OP_MOVEPOINT) { - float x = wx; - float y = wy; - if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) - MapView()->MapGrid()->SnapToGrid(x, y); + m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); - int OffsetX = f2fx(x) - pQuad->m_aPoints[V].x; - int OffsetY = f2fx(y) - pQuad->m_aPoints[V].y; + s_LastOffset = GetDragOffset(); // Update offset + ApplyAxisAlignment(s_LastOffset.x, s_LastOffset.y); // Apply axis alignment to offset + + 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++) { if(IsQuadPointSelected(QuadIndex, m)) { - pQuad->m_aPoints[m].x += OffsetX; - pQuad->m_aPoints[m].y += OffsetY; + DoPointDrag(pLayer, pQuad, QuadIndex, m, s_LastOffset.x, s_LastOffset.y); } } } else if(s_Operation == OP_MOVEUV) { + int SelectedPoints = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3); + + m_QuadTracker.BeginQuadPointPropTrack(pLayer, m_vSelectedQuads, SelectedPoints); + m_QuadTracker.AddQuadPointPropTrack(EQuadPointProp::PROP_TEX_U); + m_QuadTracker.AddQuadPointPropTrack(EQuadPointProp::PROP_TEX_V); + for(int m = 0; m < 4; m++) { if(IsQuadPointSelected(QuadIndex, m)) @@ -1822,6 +2427,19 @@ void CEditor::DoQuadPoint(CQuad *pQuad, int QuadIndex, int V) } } + // Draw axis and alignments when dragging + if(s_Operation == OP_MOVEPOINT) + { + Graphics()->SetColor(1, 0, 0.1f, 1); + + // Axis + EAxis Axis = GetDragAxis(s_LastOffset.x, s_LastOffset.y); + DrawAxis(Axis, s_OriginalPoint, pQuad->m_aPoints[V]); + + // Alignments + DrawPointAlignments(s_Alignments, s_LastOffset.x, s_LastOffset.y); + } + if(s_Operation == OP_CONTEXT_MENU) { if(!UI()->MouseButton(1)) @@ -1852,6 +2470,15 @@ void CEditor::DoQuadPoint(CQuad *pQuad, int QuadIndex, int V) SelectQuadPoint(QuadIndex, V); } + if(s_Operation == OP_MOVEPOINT) + { + m_QuadTracker.EndQuadTrack(); + } + else if(s_Operation == OP_MOVEUV) + { + m_QuadTracker.EndQuadPointPropTrackAll(); + } + UI()->DisableMouseLock(); UI()->SetActiveItem(nullptr); } @@ -2081,6 +2708,7 @@ void CEditor::DoQuadKnife(int QuadIndex) pResult->m_aPoints[4].y = ((pResult->m_aPoints[0].y + pResult->m_aPoints[3].y) / 2 + (pResult->m_aPoints[1].y + pResult->m_aPoints[2].y) / 2) / 2; m_QuadKnifeCount = 0; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, m_vSelectedLayers[0])); } // Render @@ -2677,6 +3305,8 @@ void CEditor::DoMapEditor(CUIRect View) std::shared_ptr pBrush = m_pBrush->IsEmpty() ? nullptr : m_pBrush->m_vpLayers[BrushIndex]; apEditLayers[k]->FillSelection(m_pBrush->IsEmpty(), pBrush, r); } + std::shared_ptr Action = std::make_shared(this, m_SelectedGroup); + m_EditorHistory.RecordAction(Action); } else { @@ -2707,6 +3337,7 @@ void CEditor::DoMapEditor(CUIRect View) size_t BrushIndex = k; 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); } @@ -2785,9 +3416,9 @@ 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->m_vQuads[i], i, v); + DoQuadPoint(pLayer, &pLayer->m_vQuads[i], i, v); - DoQuad(&pLayer->m_vQuads[i], i); + DoQuad(pLayer, &pLayer->m_vQuads[i], i); } Graphics()->QuadsEnd(); } @@ -2883,9 +3514,9 @@ void CEditor::DoMapEditor(CUIRect View) } } - // do panning if(UI()->CheckActiveItem(s_pEditorID)) { + // do panning if(s_Operation == OP_PAN_WORLD) MapView()->OffsetWorld(-vec2(m_MouseDeltaX, m_MouseDeltaY) * m_MouseWScale); else if(s_Operation == OP_PAN_EDITOR) @@ -2894,6 +3525,14 @@ void CEditor::DoMapEditor(CUIRect View) // release mouse if(!UI()->MouseButton(0)) { + if(s_Operation == OP_BRUSH_DRAW) + { + std::shared_ptr Action = std::make_shared(this, m_SelectedGroup); + + if(!Action->IsEmpty()) // Avoid recording tile draw action when placing quads only + m_EditorHistory.RecordAction(Action); + } + s_Operation = OP_NONE; UI()->SetActiveItem(nullptr); } @@ -2952,303 +3591,71 @@ void CEditor::DoMapEditor(CUIRect View) { GetSelectedGroup()->MapScreen(); - std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayer(0)); - IGraphics::CTextureHandle Texture; - if(pLayer->m_Image >= 0 && pLayer->m_Image < (int)m_Map.m_vpImages.size()) - Texture = m_Map.m_vpImages[pLayer->m_Image]->m_Texture; - - DoQuadEnvelopes(pLayer->m_vQuads, Texture); - m_ShowEnvelopePreview = SHOWENV_NONE; - } - - UI()->MapScreen(); - //UI()->ClipDisable(); - m_ChillerEditor.DoMapEditor(); -} - -void CEditor::SetHotQuadPoint(const std::shared_ptr &pLayer) -{ - 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; - - float CurrDist = dx * dx + dy * dy; - if(CurrDist < MinDist) - { - MinDist = CurrDist; - pMinPoint = pID; - return true; - } - return false; - }; - - for(size_t i = 0; i < pLayer->m_vQuads.size(); i++) - { - CQuad &Quad = pLayer->m_vQuads.at(i); - - if(m_ShowTileInfo != SHOW_TILE_OFF && m_ShowEnvelopePreview != SHOWENV_NONE && Quad.m_PosEnv >= 0) - { - for(auto &EnvPoint : m_Map.m_vpEnvelopes[Quad.m_PosEnv]->m_vPoints) - { - float px = fx2f(Quad.m_aPoints[4].x) + fx2f(EnvPoint.m_aValues[0]); - float py = fx2f(Quad.m_aPoints[4].y) + fx2f(EnvPoint.m_aValues[1]); - if(UpdateMinimum(px, py, &EnvPoint)) - m_CurrentQuadIndex = i; - } - } - - for(auto &Point : Quad.m_aPoints) - UpdateMinimum(fx2f(Point.x), fx2f(Point.y), &Point); - } - - if(pMinPoint != nullptr) - UI()->SetHotItem(pMinPoint); -} - -int CEditor::DoProperties(CUIRect *pToolBox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color) -{ - int Change = -1; - - for(int i = 0; pProps[i].m_pName; 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); - - if(pProps[i].m_Type == PROPTYPE_INT_STEP) - { - 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); - int NewValue = 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); - if(NewValue != pProps[i].m_Value) - { - *pNewVal = NewValue; - Change = i; - } - if(DoButton_ButtonDec((char *)&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Decrease")) - { - *pNewVal = pProps[i].m_Value - 1; - Change = i; - } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, nullptr, 0, &Inc, 0, "Increase")) - { - *pNewVal = pProps[i].m_Value + 1; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_BOOL) - { - CUIRect No, Yes; - Shifter.VSplitMid(&No, &Yes); - if(DoButton_ButtonDec(&pIDs[i], "No", !pProps[i].m_Value, &No, 0, "")) - { - *pNewVal = 0; - Change = i; - } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 1, "Yes", pProps[i].m_Value, &Yes, 0, "")) - { - *pNewVal = 1; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_INT_SCROLL) - { - int NewValue = 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."); - if(NewValue != pProps[i].m_Value) - { - *pNewVal = NewValue; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_ANGLE_SCROLL) - { - CUIRect Inc, Dec; - Shifter.VSplitRight(10.0f, &Shifter, &Inc); - Shifter.VSplitLeft(10.0f, &Dec, &Shifter); - const bool Shift = Input()->ShiftIsPressed(); - int Step = Shift ? 1 : 45; - int Value = pProps[i].m_Value; - - int NewValue = 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); - if(DoButton_ButtonDec(&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Decrease")) - { - NewValue = (std::ceil((pProps[i].m_Value / (float)Step)) - 1) * Step; - if(NewValue < 0) - NewValue += 360; - } - if(DoButton_ButtonInc(&pIDs[i] + 2, nullptr, 0, &Inc, 0, "Increase")) - NewValue = (pProps[i].m_Value + Step) / Step * Step; - - if(NewValue != pProps[i].m_Value) - { - *pNewVal = NewValue % 360; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_COLOR) - { - const auto &&SetColor = [&](ColorRGBA NewColor) { - const int NewValue = NewColor.PackAlphaLast(); - if(NewValue != pProps[i].m_Value) - { - *pNewVal = NewValue; - Change = i; - } - }; - DoColorPickerButton(&pIDs[i], &Shifter, ColorRGBA::UnpackAlphaLast(pProps[i].m_Value), SetColor); - } - else if(pProps[i].m_Type == PROPTYPE_IMAGE) - { - const char *pName; - if(pProps[i].m_Value < 0) - pName = "None"; - 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()); - - int r = PopupSelectImageResult(); - if(r >= -1) - { - *pNewVal = r; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_SHIFT) - { - CUIRect Left, Right, Up, Down; - 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); - 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")) - { - *pNewVal = DIRECTION_LEFT; - Change = i; - } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 3, "+", 0, &Right, 0, "Right")) - { - *pNewVal = DIRECTION_RIGHT; - Change = i; - } - if(DoButton_ButtonDec(((char *)&pIDs[i]) + 1, "-", 0, &Up, 0, "Up")) - { - *pNewVal = DIRECTION_UP; - Change = i; - } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, "+", 0, &Down, 0, "Down")) - { - *pNewVal = DIRECTION_DOWN; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_SOUND) - { - const char *pName; - if(pProps[i].m_Value < 0) - pName = "None"; - else - pName = m_Map.m_vpSounds[pProps[i].m_Value]->m_aName; + std::shared_ptr pLayer = std::static_pointer_cast(GetSelectedLayer(0)); + IGraphics::CTextureHandle Texture; + if(pLayer->m_Image >= 0 && pLayer->m_Image < (int)m_Map.m_vpImages.size()) + Texture = m_Map.m_vpImages[pLayer->m_Image]->m_Texture; - if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) - PopupSelectSoundInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); + DoQuadEnvelopes(pLayer->m_vQuads, Texture); + m_ShowEnvelopePreview = SHOWENV_NONE; + } - int r = PopupSelectSoundResult(); - if(r >= -1) - { - *pNewVal = r; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_AUTOMAPPER) - { - const char *pName; - if(pProps[i].m_Value < 0 || pProps[i].m_Min < 0 || pProps[i].m_Min >= (int)m_Map.m_vpImages.size()) - pName = "None"; - else - pName = m_Map.m_vpImages[pProps[i].m_Min]->m_AutoMapper.GetConfigName(pProps[i].m_Value); + UI()->MapScreen(); + //UI()->ClipDisable(); + m_ChillerEditor.DoMapEditor(); +} - if(DoButton_Ex(&pIDs[i], pName, 0, &Shifter, 0, nullptr, IGraphics::CORNER_ALL)) - PopupSelectConfigAutoMapInvoke(pProps[i].m_Value, UI()->MouseX(), UI()->MouseY()); +void CEditor::SetHotQuadPoint(const std::shared_ptr &pLayer) +{ + float wx = UI()->MouseWorldX(); + float wy = UI()->MouseWorldY(); - int r = PopupSelectConfigAutoMapResult(); - if(r >= -1) - { - *pNewVal = r; - Change = i; - } - } - else if(pProps[i].m_Type == PROPTYPE_ENVELOPE) - { - CUIRect Inc, Dec; - char aBuf[8]; - int CurValue = pProps[i].m_Value; + float MinDist = 500.0f; + void *pMinPoint = nullptr; - Shifter.VSplitRight(10.0f, &Shifter, &Inc); - Shifter.VSplitLeft(10.0f, &Dec, &Shifter); + auto UpdateMinimum = [&](float px, float py, void *pID) { + float dx = (px - wx) / m_MouseWScale; + float dy = (py - wy) / m_MouseWScale; - if(CurValue <= 0) - str_copy(aBuf, "None:"); - else if(m_Map.m_vpEnvelopes[CurValue - 1]->m_aName[0]) - { - str_format(aBuf, sizeof(aBuf), "%s:", m_Map.m_vpEnvelopes[CurValue - 1]->m_aName); - if(!str_endswith(aBuf, ":")) - { - aBuf[sizeof(aBuf) - 2] = ':'; - aBuf[sizeof(aBuf) - 1] = '\0'; - } - } - else - aBuf[0] = '\0'; + float CurrDist = dx * dx + dy * dy; + if(CurrDist < MinDist) + { + MinDist = CurrDist; + pMinPoint = pID; + return true; + } + return false; + }; - int NewVal = UiDoValueSelector((char *)&pIDs[i], &Shifter, aBuf, CurValue, 0, m_Map.m_vpEnvelopes.size(), 1, 1.0f, "Set Envelope", false, false, IGraphics::CORNER_NONE); - if(NewVal != CurValue) - { - *pNewVal = NewVal; - Change = i; - } + for(size_t i = 0; i < pLayer->m_vQuads.size(); i++) + { + CQuad &Quad = pLayer->m_vQuads.at(i); - if(DoButton_ButtonDec((char *)&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Previous Envelope")) - { - *pNewVal = pProps[i].m_Value - 1; - Change = i; - } - if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, nullptr, 0, &Inc, 0, "Next Envelope")) + if(m_ShowTileInfo != SHOW_TILE_OFF && m_ShowEnvelopePreview != SHOWENV_NONE && Quad.m_PosEnv >= 0) + { + for(auto &EnvPoint : m_Map.m_vpEnvelopes[Quad.m_PosEnv]->m_vPoints) { - *pNewVal = pProps[i].m_Value + 1; - Change = i; + float px = fx2f(Quad.m_aPoints[4].x) + fx2f(EnvPoint.m_aValues[0]); + float py = fx2f(Quad.m_aPoints[4].y) + fx2f(EnvPoint.m_aValues[1]); + if(UpdateMinimum(px, py, &EnvPoint)) + m_CurrentQuadIndex = i; } } + + for(auto &Point : Quad.m_aPoints) + UpdateMinimum(fx2f(Point.x), fx2f(Point.y), &Point); } - return Change; + if(pMinPoint != nullptr) + UI()->SetHotItem(pMinPoint); } 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, 5.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, 5.0f); + 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."); if(Input()->ShiftIsPressed()) @@ -3263,6 +3670,7 @@ void CEditor::DoColorPickerButton(const void *pID, const CUIRect *pRect, ColorRG std::optional ParsedColor = color_parse(pClipboard); if(ParsedColor) { + m_ColorPickerPopupContext.m_State = EEditState::ONE_GO; SetColor(ParsedColor.value()); } } @@ -3294,6 +3702,13 @@ void CEditor::DoColorPickerButton(const void *pID, const CUIRect *pRect, ColorRG else { m_pColorPickerPopupActiveID = nullptr; + if(m_ColorPickerPopupContext.m_State == EEditState::EDITING) + { + ColorRGBA c = color_cast(m_ColorPickerPopupContext.m_HsvaColor); + m_ColorPickerPopupContext.m_State = EEditState::END; + SetColor(c); + m_ColorPickerPopupContext.m_State = EEditState::NONE; + } } } @@ -3321,6 +3736,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) OP_GROUP_DRAG }; static int s_Operation = OP_NONE; + static int s_PreviousOperation = OP_NONE; static const void *s_pDraggedButton = 0; static float s_InitialMouseY = 0; static float s_InitialCutHeight = 0; @@ -3334,6 +3750,14 @@ void CEditor::RenderLayers(CUIRect LayersBox) bool AnyButtonActive = false; std::vector vButtonsPerGroup; + auto SetOperation = [](int Operation) { + if(Operation != s_Operation) + { + s_PreviousOperation = s_Operation; + s_Operation = Operation; + } + }; + vButtonsPerGroup.reserve(m_Map.m_vpGroups.size()); for(const std::shared_ptr &pGroup : m_Map.m_vpGroups) { @@ -3341,7 +3765,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) } if(!UI()->CheckActiveItem(s_pDraggedButton)) - s_Operation = OP_NONE; + SetOperation(OP_NONE); if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG) { @@ -3439,14 +3863,14 @@ void CEditor::RenderLayers(CUIRect LayersBox) { s_InitialMouseY = UI()->MouseY(); s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y; - s_Operation = OP_CLICK; + SetOperation(OP_CLICK); if(g != m_SelectedGroup) SelectLayer(0, g); } if(Abrupted) - s_Operation = OP_NONE; + SetOperation(OP_NONE); if(s_Operation == OP_CLICK) { @@ -3479,7 +3903,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) if(!m_Map.m_vpGroups[g]->m_vpLayers.empty() && Input()->MouseDoubleClick()) m_Map.m_vpGroups[g]->m_Collapse ^= 1; - s_Operation = OP_NONE; + SetOperation(OP_NONE); } if(s_Operation == OP_GROUP_DRAG && Clicked) @@ -3487,7 +3911,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) } else if(s_pDraggedButton == &m_Map.m_vpGroups[g]) { - s_Operation = OP_NONE; + SetOperation(OP_NONE); } } @@ -3591,7 +4015,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) { s_InitialMouseY = UI()->MouseY(); s_InitialCutHeight = s_InitialMouseY - UnscrolledLayersBox.y; - s_Operation = OP_CLICK; + SetOperation(OP_CLICK); if(!Input()->ShiftIsPressed() && !IsLayerSelected) { @@ -3600,7 +4024,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) } if(Abrupted) - s_Operation = OP_NONE; + SetOperation(OP_NONE); if(s_Operation == OP_CLICK) { @@ -3651,23 +4075,33 @@ void CEditor::RenderLayers(CUIRect LayersBox) bool AllTile = true; for(size_t j = 0; AllTile && j < m_vSelectedLayers.size(); j++) { - if(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[m_vSelectedLayers[j]]->m_Type == LAYERTYPE_TILES) + int LayerIndex = m_vSelectedLayers[j]; + if(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[LayerIndex]->m_Type == LAYERTYPE_TILES) + { s_LayerPopupContext.m_vpLayers.push_back(std::static_pointer_cast(m_Map.m_vpGroups[m_SelectedGroup]->m_vpLayers[m_vSelectedLayers[j]])); + s_LayerPopupContext.m_vLayerIndices.push_back(LayerIndex); + } else AllTile = false; } - // Don't allow editing if all selected layers are tile layers + // Don't allow editing if all selected layers are not tile layers if(!AllTile) + { s_LayerPopupContext.m_vpLayers.clear(); + 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); } - s_Operation = OP_NONE; + SetOperation(OP_NONE); } if(s_Operation == OP_LAYER_DRAG && Clicked) @@ -3677,7 +4111,7 @@ void CEditor::RenderLayers(CUIRect LayersBox) } else if(s_pDraggedButton == m_Map.m_vpGroups[g]->m_vpLayers[i].get()) { - s_Operation = OP_NONE; + SetOperation(OP_NONE); } } @@ -3757,16 +4191,28 @@ void CEditor::RenderLayers(CUIRect LayersBox) auto InsertPosition = std::find(m_Map.m_vpGroups.begin(), m_Map.m_vpGroups.end(), pNextGroup); m_Map.m_vpGroups.insert(InsertPosition, pSelectedGroup); - m_SelectedGroup = InsertPosition - m_Map.m_vpGroups.begin(); + auto Pos = std::find(m_Map.m_vpGroups.begin(), m_Map.m_vpGroups.end(), pSelectedGroup); + m_SelectedGroup = Pos - m_Map.m_vpGroups.begin(); + m_Map.OnModify(); } + static int s_InitialGroupIndex; + static std::vector s_vInitialLayerIndices; + if(MoveLayers || MoveGroup) - s_Operation = OP_NONE; + SetOperation(OP_NONE); if(StartDragLayer) - s_Operation = OP_LAYER_DRAG; + { + SetOperation(OP_LAYER_DRAG); + s_InitialGroupIndex = m_SelectedGroup; + s_vInitialLayerIndices = std::vector(m_vSelectedLayers); + } if(StartDragGroup) - s_Operation = OP_GROUP_DRAG; + { + s_InitialGroupIndex = m_SelectedGroup; + SetOperation(OP_GROUP_DRAG); + } if(s_Operation == OP_LAYER_DRAG || s_Operation == OP_GROUP_DRAG) { @@ -3849,13 +4295,41 @@ void CEditor::RenderLayers(CUIRect LayersBox) { m_Map.NewGroup(); m_SelectedGroup = m_Map.m_vpGroups.size() - 1; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, false)); } } s_ScrollRegion.End(); if(!AnyButtonActive) - s_Operation = OP_NONE; + SetOperation(OP_NONE); + + if(s_Operation == OP_NONE) + { + if(s_PreviousOperation == OP_GROUP_DRAG) + { + s_PreviousOperation = OP_NONE; + m_EditorHistory.RecordAction(std::make_shared(this, m_SelectedGroup, EGroupProp::PROP_ORDER, s_InitialGroupIndex, m_SelectedGroup)); + } + else if(s_PreviousOperation == OP_LAYER_DRAG) + { + if(s_InitialGroupIndex != m_SelectedGroup) + { + m_EditorHistory.RecordAction(std::make_shared(this, s_InitialGroupIndex, s_vInitialLayerIndices, m_SelectedGroup, m_vSelectedLayers)); + } + else + { + std::vector> vpActions; + for(int k = 0; k < (int)m_vSelectedLayers.size(); k++) + { + int LayerIndex = m_vSelectedLayers[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))); + } + s_PreviousOperation = OP_NONE; + } + } } bool CEditor::SelectLayerByTile() @@ -4174,7 +4648,7 @@ void CEditor::SelectGameLayer() } } -void CEditor::SortImages() +std::vector CEditor::SortImages() { static const auto &&s_ImageNameComparator = [](const std::shared_ptr &pLhs, const std::shared_ptr &pRhs) { return str_comp(pLhs->m_aName, pRhs->m_aName) < 0; @@ -4201,7 +4675,11 @@ void CEditor::SortImages() if(*pIndex >= 0) *pIndex = vSortedIndex[*pIndex]; }); + + return vSortedIndex; } + + return std::vector(); } void CEditor::RenderImagesList(CUIRect ToolBox) @@ -4322,7 +4800,7 @@ 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 && IsVanillaImage(pImg->m_aName) ? 107 : (pImg->m_External ? 73 : 90); static SPopupMenuId s_PopupImageId; UI()->DoPopupMenu(&s_PopupImageId, UI()->MouseX(), UI()->MouseY(), 140, Height, this, PopupImage); } @@ -5302,6 +5780,14 @@ void CEditor::RenderStatusbar(CUIRect View, CUIRect *pTooltipRect) m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS ? EXTRAEDITOR_NONE : EXTRAEDITOR_SERVER_SETTINGS; } + 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) + { + m_ActiveExtraEditor = m_ActiveExtraEditor == EXTRAEDITOR_HISTORY ? EXTRAEDITOR_NONE : EXTRAEDITOR_HISTORY; + } + View.VSplitRight(10.0f, pTooltipRect, nullptr); } @@ -5654,23 +6140,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(m_SelectedEnvelope >= 0 && m_SelectedEnvelope < (int)m_Map.m_vpEnvelopes.size()) pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope]; - enum - { - OP_NONE, - OP_SELECT, - OP_DRAG_POINT, - OP_DRAG_POINT_X, - OP_DRAG_POINT_Y, - OP_CONTEXT_MENU, - OP_BOX_SELECT, - OP_SCALE - }; - static int s_Operation = OP_NONE; + static EEnvelopeEditorOp s_Operation = EEnvelopeEditorOp::OP_NONE; static std::vector s_vAccurateDragValuesX = {}; static std::vector s_vAccurateDragValuesY = {}; static float s_MouseXStart = 0.0f; static float s_MouseYStart = 0.0f; + static CLineInput s_NameInput; + CUIRect ToolBar, CurveBar, ColorBar, DragBar; View.HSplitTop(30.0f, &DragBar, nullptr); DragBar.y -= 2.0f; @@ -5690,6 +6167,23 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) CUIRect Button; std::shared_ptr pNewEnv = nullptr; + // redo button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + static int s_RedoButton = 0; + if(DoButton_FontIcon(&s_RedoButton, FONT_ICON_REDO, m_EnvelopeEditorHistory.CanRedo() ? 0 : -1, &Button, 0, "[Ctrl+Y] Redo last action", IGraphics::CORNER_R, 11.0f) == 1) + { + m_EnvelopeEditorHistory.Redo(); + } + + // undo button + ToolBar.VSplitRight(25.0f, &ToolBar, &Button); + ToolBar.VSplitRight(10.0f, &ToolBar, nullptr); + static int s_UndoButton = 0; + if(DoButton_FontIcon(&s_UndoButton, FONT_ICON_UNDO, m_EnvelopeEditorHistory.CanUndo() ? 0 : -1, &Button, 0, "[Ctrl+Z] Undo last action", IGraphics::CORNER_L, 11.0f) == 1) + { + m_EnvelopeEditorHistory.Undo(); + } + ToolBar.VSplitRight(50.0f, &ToolBar, &Button); static int s_NewSoundButton = 0; if(DoButton_Editor(&s_NewSoundButton, "Sound+", 0, &Button, 0, "Creates a new sound envelope")) @@ -5724,10 +6218,12 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static int s_DeleteButton = 0; if(DoButton_Editor(&s_DeleteButton, "✗", 0, &Button, 0, "Delete this envelope")) { + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, m_SelectedEnvelope)); m_Map.DeleteEnvelope(m_SelectedEnvelope); if(m_SelectedEnvelope >= (int)m_Map.m_vpEnvelopes.size()) m_SelectedEnvelope = m_Map.m_vpEnvelopes.size() - 1; pEnvelope = m_SelectedEnvelope >= 0 ? m_Map.m_vpEnvelopes[m_SelectedEnvelope] : nullptr; + m_Map.OnModify(); } // Move right button @@ -5736,9 +6232,11 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static int s_MoveRightButton = 0; if(DoButton_Ex(&s_MoveRightButton, "→", 0, &Button, 0, "Move this envelope to the right", IGraphics::CORNER_R)) { + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, m_SelectedEnvelope, CEditorActionEnvelopeEdit::EEditType::ORDER, m_SelectedEnvelope, m_SelectedEnvelope + 1)); m_Map.SwapEnvelopes(m_SelectedEnvelope, m_SelectedEnvelope + 1); m_SelectedEnvelope = clamp(m_SelectedEnvelope + 1, 0, m_Map.m_vpEnvelopes.size() - 1); pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope]; + m_Map.OnModify(); } // Move left button @@ -5746,9 +6244,11 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static int s_MoveLeftButton = 0; if(DoButton_Ex(&s_MoveLeftButton, "←", 0, &Button, 0, "Move this envelope to the left", IGraphics::CORNER_L)) { + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, m_SelectedEnvelope, CEditorActionEnvelopeEdit::EEditType::ORDER, m_SelectedEnvelope, m_SelectedEnvelope - 1)); m_Map.SwapEnvelopes(m_SelectedEnvelope - 1, m_SelectedEnvelope); m_SelectedEnvelope = clamp(m_SelectedEnvelope - 1, 0, m_Map.m_vpEnvelopes.size() - 1); pEnvelope = m_Map.m_vpEnvelopes[m_SelectedEnvelope]; + m_Map.OnModify(); } if(pEnvelope) @@ -5796,6 +6296,8 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) pNewEnv->AddPoint(0, 0); pNewEnv->AddPoint(1000, 0); } + + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, pNewEnv)); } CUIRect Shifter, Inc, Dec; @@ -5808,13 +6310,12 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) ColorRGBA EnvColor = ColorRGBA(1, 1, 1, 0.5f); if(!m_Map.m_vpEnvelopes.empty()) { - EnvColor = IsEnvelopeUsed(m_SelectedEnvelope) ? - ColorRGBA(1, 0.7f, 0.7f, 0.5f) : - ColorRGBA(0.7f, 1, 0.7f, 0.5f); + EnvColor = IsEnvelopeUsed(m_SelectedEnvelope) ? ColorRGBA(1, 0.7f, 0.7f, 0.5f) : ColorRGBA(0.7f, 1, 0.7f, 0.5f); } static int s_EnvelopeSelector = 0; - int NewValue = UiDoValueSelector(&s_EnvelopeSelector, &Shifter, aBuf, m_SelectedEnvelope + 1, 1, m_Map.m_vpEnvelopes.size(), 1, 1.0f, "Select Envelope", false, false, IGraphics::CORNER_NONE, &EnvColor, false); + auto NewValueRes = UiDoValueSelector(&s_EnvelopeSelector, &Shifter, aBuf, m_SelectedEnvelope + 1, 1, m_Map.m_vpEnvelopes.size(), 1, 1.0f, "Select Envelope", false, false, IGraphics::CORNER_NONE, &EnvColor, false); + int NewValue = NewValueRes.m_Value; if(NewValue - 1 != m_SelectedEnvelope) { m_SelectedEnvelope = NewValue - 1; @@ -5848,7 +6349,6 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) ToolBar.VSplitLeft(3.0f, nullptr, &ToolBar); ToolBar.VSplitLeft(ToolBar.w > ToolBar.h * 40 ? 80.0f : 60.0f, &Button, &ToolBar); - static CLineInput s_NameInput; s_NameInput.SetBuffer(pEnvelope->m_aName, sizeof(pEnvelope->m_aName)); if(DoEditBox(&s_NameInput, &Button, 10.0f, IGraphics::CORNER_ALL, "The name of the selected envelope")) { @@ -5923,7 +6423,11 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) ToolBar.VSplitLeft(12.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)")) + { + 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); @@ -5933,7 +6437,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { UI()->SetHotItem(&s_EnvelopeEditorID); - if(s_Operation == OP_NONE && (UI()->MouseButton(2) || (UI()->MouseButton(0) && Input()->ModifierIsPressed()))) + if(s_Operation == EEnvelopeEditorOp::OP_NONE && (UI()->MouseButton(2) || (UI()->MouseButton(0) && Input()->ModifierIsPressed()))) { m_OffsetEnvelopeX += UI()->MouseDeltaX() / Graphics()->ScreenWidth() * UI()->Screen()->w / View.w; m_OffsetEnvelopeY -= UI()->MouseDeltaY() / Graphics()->ScreenHeight() * UI()->Screen()->h / View.h; @@ -5988,19 +6492,17 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } if(!TimeFound) - pEnvelope->AddPoint(FixedTime, - f2fx(Channels.r), f2fx(Channels.g), - f2fx(Channels.b), f2fx(Channels.a)); + m_EnvelopeEditorHistory.Execute(std::make_shared(this, m_SelectedEnvelope, FixedTime, Channels)); if(FixedTime < 0) RemoveTimeOffsetEnvelope(pEnvelope); m_Map.OnModify(); } - else if(s_Operation != OP_BOX_SELECT && !Input()->ModifierIsPressed()) + else if(s_Operation != EEnvelopeEditorOp::OP_BOX_SELECT && !Input()->ModifierIsPressed()) { static int s_BoxSelectID = 0; UI()->SetActiveItem(&s_BoxSelectID); - s_Operation = OP_BOX_SELECT; + s_Operation = EEnvelopeEditorOp::OP_BOX_SELECT; s_MouseXStart = UI()->MouseX(); s_MouseYStart = UI()->MouseY(); } @@ -6216,7 +6718,14 @@ 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)")) + { + int PrevCurve = pEnvelope->m_vPoints[i].m_Curvetype; pEnvelope->m_vPoints[i].m_Curvetype = (pEnvelope->m_vPoints[i].m_Curvetype + 1) % NUM_CURVETYPES; + + m_EnvelopeEditorHistory.RecordAction(std::make_shared(this, + m_SelectedEnvelope, i, 0, CEditorActionEnvelopeEditPoint::EEditType::CURVE_TYPE, PrevCurve, pEnvelope->m_vPoints[i].m_Curvetype)); + m_Map.OnModify(); + } } } } @@ -6277,8 +6786,16 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->DoPopupMenu(&s_PopupEnvPointId, UI()->MouseX(), UI()->MouseY(), 150, 56 + (pEnvelope->GetChannels() == 4 ? 16.0f : 0.0f), this, PopupEnvPoint); }; - if(s_Operation == OP_NONE) + if(s_Operation == EEnvelopeEditorOp::OP_NONE) + { SetHotEnvelopePoint(View, pEnvelope, s_ActiveChannels); + if(!UI()->MouseButton(0)) + m_EnvOpTracker.Stop(false); + } + else + { + m_EnvOpTracker.Begin(s_Operation); + } UI()->ClipEnable(&View); Graphics()->TextureClear(); @@ -6288,7 +6805,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(!(s_ActiveChannels & (1 << c))) continue; - for(size_t i = 0; i < pEnvelope->m_vPoints.size(); i++) + for(int i = 0; i < (int)pEnvelope->m_vPoints.size(); i++) { // point handle { @@ -6318,27 +6835,27 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { m_ShowEnvelopePreview = SHOWENV_SELECTED; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { float dx = s_MouseXStart - UI()->MouseX(); float dy = s_MouseYStart - UI()->MouseY(); if(dx * dx + dy * dy > 20.0f) { - s_Operation = OP_DRAG_POINT; + s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT; if(!IsEnvPointSelected(i, c)) SelectEnvPoint(i, c); } } - if(s_Operation == OP_DRAG_POINT || s_Operation == OP_DRAG_POINT_X || s_Operation == OP_DRAG_POINT_Y) + if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_X || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_Y) { if(Input()->ShiftIsPressed()) { - if(s_Operation == OP_DRAG_POINT || s_Operation == OP_DRAG_POINT_Y) + if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_Y) { - s_Operation = OP_DRAG_POINT_X; + s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT_X; s_vAccurateDragValuesX.clear(); for(auto [SelectedIndex, _] : m_vSelectedEnvelopePoints) s_vAccurateDragValuesX.push_back(pEnvelope->m_vPoints[SelectedIndex].m_Time); @@ -6387,9 +6904,9 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } else { - if(s_Operation == OP_DRAG_POINT || s_Operation == OP_DRAG_POINT_X) + if(s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT || s_Operation == EEnvelopeEditorOp::OP_DRAG_POINT_X) { - s_Operation = OP_DRAG_POINT_Y; + s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT_Y; s_vAccurateDragValuesY.clear(); for(auto [SelectedIndex, SelectedChannel] : m_vSelectedEnvelopePoints) s_vAccurateDragValuesY.push_back(pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]); @@ -6403,7 +6920,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) s_vAccurateDragValuesY[k] -= DeltaY; pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(s_vAccurateDragValuesY[k]); - if(pEnvelope->GetChannels() == 4) + if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4) { pEnvelope->m_vPoints[i].m_aValues[c] = clamp(pEnvelope->m_vPoints[i].m_aValues[c], 0, 1024); s_vAccurateDragValuesY[k] = clamp(s_vAccurateDragValuesY[k], 0, 1024); @@ -6413,7 +6930,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } - if(s_Operation == OP_CONTEXT_MENU) + if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU) { if(!UI()->MouseButton(1)) { @@ -6428,7 +6945,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->DoPopupMenu(&s_PopupEnvPointMultiId, UI()->MouseX(), UI()->MouseY(), 80, 22, this, PopupEnvPointMulti); } UI()->SetActiveItem(nullptr); - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; } } else if(!UI()->MouseButton(0)) @@ -6436,7 +6953,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->SetActiveItem(nullptr); m_SelectedQuadEnvelope = -1; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { if(Input()->ShiftIsPressed()) ToggleEnvPoint(i, c); @@ -6444,7 +6961,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) SelectEnvPoint(i, c); } - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; m_Map.OnModify(); } @@ -6455,7 +6972,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(UI()->MouseButton(0)) { UI()->SetActiveItem(pID); - s_Operation = OP_SELECT; + s_Operation = EEnvelopeEditorOp::OP_SELECT; m_SelectedQuadEnvelope = m_SelectedEnvelope; s_MouseXStart = UI()->MouseX(); @@ -6465,12 +6982,11 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { if(Input()->ShiftIsPressed()) { - pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + i); - m_Map.OnModify(); + m_EnvelopeEditorHistory.Execute(std::make_shared(this, m_SelectedEnvelope, i)); } else { - s_Operation = OP_CONTEXT_MENU; + s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU; if(!IsEnvPointSelected(i, c)) SelectEnvPoint(i, c); UI()->SetActiveItem(pID); @@ -6490,6 +7006,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } // tangent handles for bezier curves + if(i >= 0 && i < (int)pEnvelope->m_vPoints.size()) { // Out-Tangent handle if(pEnvelope->m_vPoints[i].m_Curvetype == CURVETYPE_BEZIER) @@ -6524,14 +7041,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { m_ShowEnvelopePreview = SHOWENV_SELECTED; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { float dx = s_MouseXStart - UI()->MouseX(); float dy = s_MouseYStart - UI()->MouseY(); if(dx * dx + dy * dy > 20.0f) { - s_Operation = OP_DRAG_POINT; + s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT; s_vAccurateDragValuesX = {static_cast(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaX[c])}; s_vAccurateDragValuesY = {static_cast(pEnvelope->m_vPoints[i].m_Bezier.m_aOutTangentDeltaY[c])}; @@ -6541,7 +7058,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } - if(s_Operation == OP_DRAG_POINT) + 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); @@ -6556,7 +7073,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) s_vAccurateDragValuesX[0] = clamp(s_vAccurateDragValuesX[0], 0, f2fxt(ScreenToEnvelopeX(View, View.x + View.w)) - pEnvelope->m_vPoints[i].m_Time); } - if(s_Operation == OP_CONTEXT_MENU) + if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU) { if(!UI()->MouseButton(1)) { @@ -6573,10 +7090,10 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->SetActiveItem(nullptr); m_SelectedQuadEnvelope = -1; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) SelectTangentOutPoint(i, c); - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; m_Map.OnModify(); } @@ -6587,7 +7104,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(UI()->MouseButton(0)) { UI()->SetActiveItem(pID); - s_Operation = OP_SELECT; + s_Operation = EEnvelopeEditorOp::OP_SELECT; m_SelectedQuadEnvelope = m_SelectedEnvelope; s_MouseXStart = UI()->MouseX(); @@ -6604,7 +7121,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } else { - s_Operation = OP_CONTEXT_MENU; + s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU; SelectTangentOutPoint(i, c); UI()->SetActiveItem(pID); } @@ -6656,14 +7173,14 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) { m_ShowEnvelopePreview = SHOWENV_SELECTED; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) { float dx = s_MouseXStart - UI()->MouseX(); float dy = s_MouseYStart - UI()->MouseY(); if(dx * dx + dy * dy > 20.0f) { - s_Operation = OP_DRAG_POINT; + s_Operation = EEnvelopeEditorOp::OP_DRAG_POINT; s_vAccurateDragValuesX = {static_cast(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaX[c])}; s_vAccurateDragValuesY = {static_cast(pEnvelope->m_vPoints[i].m_Bezier.m_aInTangentDeltaY[c])}; @@ -6673,7 +7190,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } } - if(s_Operation == OP_DRAG_POINT) + 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); @@ -6688,7 +7205,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) s_vAccurateDragValuesX[0] = clamp(s_vAccurateDragValuesX[0], f2fxt(ScreenToEnvelopeX(View, View.x)) - pEnvelope->m_vPoints[i].m_Time, 0); } - if(s_Operation == OP_CONTEXT_MENU) + if(s_Operation == EEnvelopeEditorOp::OP_CONTEXT_MENU) { if(!UI()->MouseButton(1)) { @@ -6705,10 +7222,10 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) UI()->SetActiveItem(nullptr); m_SelectedQuadEnvelope = -1; - if(s_Operation == OP_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_SELECT) SelectTangentInPoint(i, c); - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; m_Map.OnModify(); } @@ -6719,7 +7236,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(UI()->MouseButton(0)) { UI()->SetActiveItem(pID); - s_Operation = OP_SELECT; + s_Operation = EEnvelopeEditorOp::OP_SELECT; m_SelectedQuadEnvelope = m_SelectedEnvelope; s_MouseXStart = UI()->MouseX(); @@ -6736,7 +7253,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) } else { - s_Operation = OP_CONTEXT_MENU; + s_Operation = EEnvelopeEditorOp::OP_CONTEXT_MENU; SelectTangentInPoint(i, c); UI()->SetActiveItem(pID); } @@ -6768,9 +7285,9 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) static float s_MidpointY = 0.0f; static std::vector s_vInitialPositionsX; static std::vector s_vInitialPositionsY; - if(s_Operation == OP_NONE && Input()->KeyIsPressed(KEY_S) && !Input()->ModifierIsPressed() && !m_vSelectedEnvelopePoints.empty()) + if(s_Operation == EEnvelopeEditorOp::OP_NONE && !s_NameInput.IsActive() && Input()->KeyIsPressed(KEY_S) && !Input()->ModifierIsPressed() && !m_vSelectedEnvelopePoints.empty()) { - s_Operation = OP_SCALE; + s_Operation = EEnvelopeEditorOp::OP_SCALE; s_ScaleFactorX = 1.0f; s_ScaleFactorY = 1.0f; auto [FirstPointIndex, FirstPointChannel] = m_vSelectedEnvelopePoints.front(); @@ -6800,7 +7317,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) s_MidpointY = (MaximumY - MinimumY) / 2.0f + MinimumY; } - if(s_Operation == OP_SCALE) + if(s_Operation == EEnvelopeEditorOp::OP_SCALE) { str_copy(m_aTooltip, "Press shift to scale the time. Press alt to scale along midpoint. Press ctrl to be more precise."); @@ -6868,14 +7385,15 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) else pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(s_vInitialPositionsY[k] * s_ScaleFactorY); - if(pEnvelope->GetChannels() == 4) + if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4) pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = clamp(pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel], 0, 1024); } } if(UI()->MouseButton(0)) { - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; + m_EnvOpTracker.Stop(false); } else if(UI()->MouseButton(1) || UI()->ConsumeHotkey(CUI::HOTKEY_ESCAPE)) { @@ -6890,12 +7408,12 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = std::round(s_vInitialPositionsY[k]); } RemoveTimeOffsetEnvelope(pEnvelope); - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; } } // handle box selection - if(s_Operation == OP_BOX_SELECT) + if(s_Operation == EEnvelopeEditorOp::OP_BOX_SELECT) { IGraphics::CLineItem aLines[4] = { {s_MouseXStart, s_MouseYStart, UI()->MouseX(), s_MouseYStart}, @@ -6910,7 +7428,7 @@ void CEditor::RenderEnvelopeEditor(CUIRect View) if(!UI()->MouseButton(0)) { - s_Operation = OP_NONE; + s_Operation = EEnvelopeEditorOp::OP_NONE; UI()->SetActiveItem(nullptr); float TimeStart = ScreenToEnvelopeX(View, s_MouseXStart); @@ -6957,7 +7475,7 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd static CListBox s_ListBox; s_ListBox.SetActive(m_Dialog == DIALOG_NONE && !UI()->IsPopupOpen()); - const bool GotSelection = s_ListBox.Active() && s_CommandSelectedIndex >= 0 && (size_t)s_CommandSelectedIndex < m_Map.m_vSettings.size(); + 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; @@ -6976,6 +7494,8 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd 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; @@ -6991,6 +7511,8 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd 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(); @@ -7004,12 +7526,33 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd 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; @@ -7028,12 +7571,15 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd } 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 { - str_copy(m_Map.m_vSettings[s_CommandSelectedIndex].m_aCommand, m_SettingsCommandInput.GetString()); + 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(); @@ -7062,6 +7608,7 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd { 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(); @@ -7099,6 +7646,157 @@ void CEditor::RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEd } } +void CEditor::RenderEditorHistory(CUIRect View) +{ + enum EHistoryType + { + EDITOR_HISTORY, + ENVELOPE_HISTORY, + SERVER_SETTINGS_HISTORY + }; + + static EHistoryType s_HistoryType = EDITOR_HISTORY; + static int s_ActionSelectedIndex = 0; + static CListBox s_ListBox; + 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(); + + 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); + + CUIRect TypeButtons, HistoryTypeButton; + const int HistoryTypeBtnSize = 70.0f; + ToolBar.VSplitLeft(3 * HistoryTypeBtnSize, &TypeButtons, &Label); + + // history type buttons + { + TypeButtons.VSplitLeft(HistoryTypeBtnSize, &HistoryTypeButton, &TypeButtons); + static int s_EditorHistoryButton = 0; + if(DoButton_Ex(&s_EditorHistoryButton, "Editor", s_HistoryType == EDITOR_HISTORY, &HistoryTypeButton, 0, "Show map editor history.", IGraphics::CORNER_L)) + { + s_HistoryType = EDITOR_HISTORY; + } + + TypeButtons.VSplitLeft(HistoryTypeBtnSize, &HistoryTypeButton, &TypeButtons); + static int s_EnvelopeEditorHistoryButton = 0; + if(DoButton_Ex(&s_EnvelopeEditorHistoryButton, "Envelope", s_HistoryType == ENVELOPE_HISTORY, &HistoryTypeButton, 0, "Show envelope editor history.", IGraphics::CORNER_NONE)) + { + s_HistoryType = ENVELOPE_HISTORY; + } + + TypeButtons.VSplitLeft(HistoryTypeBtnSize, &HistoryTypeButton, &TypeButtons); + static int s_ServerSettingsHistoryButton = 0; + if(DoButton_Ex(&s_ServerSettingsHistoryButton, "Settings", s_HistoryType == SERVER_SETTINGS_HISTORY, &HistoryTypeButton, 0, "Show server settings editor history.", IGraphics::CORNER_R)) + { + s_HistoryType = SERVER_SETTINGS_HISTORY; + } + } + + SLabelProperties InfoProps; + 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); + + CEditorHistory *pCurrentHistory; + if(s_HistoryType == EDITOR_HISTORY) + pCurrentHistory = &m_EditorHistory; + else if(s_HistoryType == ENVELOPE_HISTORY) + pCurrentHistory = &m_EnvelopeEditorHistory; + else if(s_HistoryType == SERVER_SETTINGS_HISTORY) + pCurrentHistory = &m_ServerSettingsHistory; + else + return; + + // 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, (!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; + } + + // actions list + int RedoSize = (int)pCurrentHistory->m_vpRedoActions.size(); + int UndoSize = (int)pCurrentHistory->m_vpUndoActions.size(); + s_ActionSelectedIndex = RedoSize; + s_ListBox.DoStart(15.0f, RedoSize + UndoSize, 1, 3, s_ActionSelectedIndex, &List); + + for(int i = 0; i < RedoSize; i++) + { + const CListboxItem Item = s_ListBox.DoNextItem(&pCurrentHistory->m_vpRedoActions[i], s_ActionSelectedIndex >= 0 && s_ActionSelectedIndex == i); + if(!Item.m_Visible) + continue; + + Item.m_Rect.VMargin(5.0f, &Label); + + SLabelProperties Props; + Props.m_MaxWidth = Label.w; + 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); + TextRender()->TextColor(TextRender()->DefaultTextColor()); + } + + for(int i = 0; i < UndoSize; i++) + { + const CListboxItem Item = s_ListBox.DoNextItem(&pCurrentHistory->m_vpUndoActions[UndoSize - i - 1], s_ActionSelectedIndex >= RedoSize && s_ActionSelectedIndex == (i + RedoSize)); + 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, pCurrentHistory->m_vpUndoActions[UndoSize - i - 1]->DisplayText(), 10.0f, TEXTALIGN_ML, Props); + } + + { // Base action "Loaded map" that cannot be undone + static int s_BaseAction; + const CListboxItem Item = s_ListBox.DoNextItem(&s_BaseAction, s_ActionSelectedIndex == RedoSize + UndoSize); + if(Item.m_Visible) + { + Item.m_Rect.VMargin(5.0f, &Label); + + UI()->DoLabel(&Label, "Loaded map", 10.0f, TEXTALIGN_ML); + } + } + + const int NewSelected = s_ListBox.DoEnd(); + if(s_ActionSelectedIndex != NewSelected) + { + // Figure out if we should undo or redo some actions + // Undo everything until the selected index + if(NewSelected > s_ActionSelectedIndex) + { + for(int i = 0; i < (NewSelected - s_ActionSelectedIndex); i++) + { + pCurrentHistory->Undo(); + } + } + else + { + for(int i = 0; i < (s_ActionSelectedIndex - NewSelected); i++) + { + pCurrentHistory->Redo(); + } + } + s_ActionSelectedIndex = NewSelected; + } +} + void CEditor::RenderExtraEditorDragBar(CUIRect View, CUIRect DragBar) { enum EDragOperation @@ -7166,7 +7864,7 @@ void CEditor::RenderMenubar(CUIRect MenuBar) if(DoButton_Menu(&s_SettingsButton, "Settings", 0, &SettingsButton, 0, nullptr)) { static SPopupMenuId s_PopupMenuEntitiesId; - UI()->DoPopupMenu(&s_PopupMenuEntitiesId, SettingsButton.x, SettingsButton.y + SettingsButton.h - 1.0f, 200.0f, 64.0f, this, PopupMenuSettings, PopupProperties); + UI()->DoPopupMenu(&s_PopupMenuEntitiesId, SettingsButton.x, SettingsButton.y + SettingsButton.h - 1.0f, 200.0f, 92.0f, this, PopupMenuSettings, PopupProperties); } CUIRect ChangedIndicator, Info, Close; @@ -7252,6 +7950,12 @@ void CEditor::Render() if(m_Dialog == DIALOG_NONE && CLineInput::GetActiveInput() == nullptr) { + // handle undo/redo hotkeys + if(Input()->KeyPress(KEY_Z) && Input()->ModifierIsPressed()) + UndoLastAction(); + if(Input()->KeyPress(KEY_Y) && Input()->ModifierIsPressed()) + RedoLastAction(); + // handle brush save/load hotkeys for(int i = KEY_1; i <= KEY_0; i++) { @@ -7418,6 +8122,10 @@ void CEditor::Render() { RenderServerSettingsEditor(ExtraEditor, s_ShowServerSettingsEditorLast); } + else if(m_ActiveExtraEditor == EXTRAEDITOR_HISTORY) + { + RenderEditorHistory(ExtraEditor); + } s_ShowServerSettingsEditorLast = m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS; } RenderStatusbar(StatusBar, &TooltipRect); @@ -7585,6 +8293,15 @@ void CEditor::Reset(bool CreateDefault) m_ResetZoomEnvelope = true; m_SettingsCommandInput.Clear(); + + m_EditorHistory.Clear(); + m_EnvelopeEditorHistory.Clear(); + m_ServerSettingsHistory.Clear(); + + m_QuadTracker.m_pEditor = this; + + m_EnvOpTracker.m_pEditor = this; + m_EnvOpTracker.Reset(); } int CEditor::GetTextureUsageFlag() @@ -7682,17 +8399,23 @@ void CEditor::PlaceBorderTiles() { std::shared_ptr pT = std::static_pointer_cast(GetSelectedLayerType(0, LAYERTYPE_TILES)); - for(int i = 0; i < pT->m_Width * 2; ++i) - pT->m_pTiles[i].m_Index = 1; - for(int i = 0; i < pT->m_Width * pT->m_Height; ++i) { - if(i % pT->m_Width < 2 || i % pT->m_Width > pT->m_Width - 3) - pT->m_pTiles[i].m_Index = 1; + if(i % pT->m_Width < 2 || i % pT->m_Width > pT->m_Width - 3 || i < pT->m_Width * 2 || i > pT->m_Width * (pT->m_Height - 2)) + { + int x = i % pT->m_Width; + int y = i / pT->m_Width; + + CTile Current = pT->m_pTiles[i]; + Current.m_Index = 1; + pT->SetTile(x, y, Current); + } } - for(int i = (pT->m_Width * (pT->m_Height - 2)); i < pT->m_Width * pT->m_Height; ++i) - pT->m_pTiles[i].m_Index = 1; + int GameGroupIndex = std::find(m_Map.m_vpGroups.begin(), m_Map.m_vpGroups.end(), m_Map.m_pGameGroup) - m_Map.m_vpGroups.begin(); + m_EditorHistory.RecordAction(std::make_shared(this, GameGroupIndex), "Tool 'Make borders'"); + + m_Map.OnModify(); } void CEditor::HandleCursorMovement() @@ -7970,15 +8693,15 @@ void CEditor::OnWindowResize() void CEditor::OnClose() { - if(m_ToolbarPreviewSound && Sound()->IsPlaying(m_ToolbarPreviewSound)) + 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) + if(m_FilePreviewSound >= 0) { Sound()->UnloadSample(m_FilePreviewSound); m_FilePreviewSound = -1; @@ -8050,7 +8773,7 @@ bool CEditor::Load(const char *pFileName, int StorageType) return Result; } -bool CEditor::Append(const char *pFileName, int StorageType) +bool CEditor::Append(const char *pFileName, int StorageType, bool IgnoreHistory) { CEditorMap NewMap; NewMap.m_pEditor = this; @@ -8062,6 +8785,12 @@ bool CEditor::Append(const char *pFileName, int StorageType) if(!NewMap.Load(pFileName, StorageType, std::move(ErrorHandler))) return false; + CEditorActionAppendMap::SPrevInfo Info{ + (int)m_Map.m_vpGroups.size(), + (int)m_Map.m_vpImages.size(), + (int)m_Map.m_vpSounds.size(), + (int)m_Map.m_vpEnvelopes.size()}; + static const auto &&s_ReplaceIndex = [](int ToReplace, int ReplaceWith) { return [ToReplace, ReplaceWith](int *pIndex) { if(*pIndex == ToReplace) @@ -8128,10 +8857,33 @@ bool CEditor::Append(const char *pFileName, int StorageType) } NewMap.m_vpGroups.clear(); - SortImages(); + auto IndexMap = SortImages(); + + if(!IgnoreHistory) + m_EditorHistory.RecordAction(std::make_shared(this, pFileName, Info, IndexMap)); // all done \o/ return true; } IEditor *CreateEditor() { return new CEditor; } + +void CEditor::UndoLastAction() +{ + if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS) + m_ServerSettingsHistory.Undo(); + else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES) + m_EnvelopeEditorHistory.Undo(); + else + m_EditorHistory.Undo(); +} + +void CEditor::RedoLastAction() +{ + if(m_ActiveExtraEditor == EXTRAEDITOR_SERVER_SETTINGS) + m_ServerSettingsHistory.Redo(); + else if(m_ActiveExtraEditor == EXTRAEDITOR_ENVELOPES) + m_EnvelopeEditorHistory.Redo(); + else + m_EditorHistory.Redo(); +} diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index 740b924a6de..9e7fb76492e 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -30,6 +30,8 @@ #include #include "auto_map.h" +#include "editor_history.h" +#include "editor_trackers.h" #include "map_view.h" #include "smooth_value.h" @@ -455,7 +457,7 @@ class CEditor : public IEditor bool Save(const char *pFilename) override; bool Load(const char *pFilename, int StorageType) override; bool HandleMapDrop(const char *pFilename, int StorageType) override; - bool Append(const char *pFilename, int StorageType); + bool Append(const char *pFilename, int StorageType, bool IgnoreHistory = false); void LoadCurrentMap(); void Render(); @@ -468,7 +470,7 @@ class CEditor : public IEditor std::shared_ptr GetSelectedLayerType(int Index, int Type) const; std::shared_ptr GetSelectedLayer(int Index) const; std::shared_ptr GetSelectedGroup() const; - CSoundSource *GetSelectedSource(); + CSoundSource *GetSelectedSource() const; void SelectLayer(int LayerIndex, int GroupIndex = -1); void AddSelectedLayer(int LayerIndex); void SelectQuad(int Index); @@ -497,7 +499,10 @@ class CEditor : public IEditor bool IsTangentInSelected() const; bool IsTangentOutSelected() const; bool IsTangentSelected() const; + 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)); CUI::SColorPickerPopupContext m_ColorPickerPopupContext; @@ -697,10 +702,11 @@ class CEditor : public IEditor EXTRAEDITOR_NONE = -1, EXTRAEDITOR_ENVELOPES, EXTRAEDITOR_SERVER_SETTINGS, + EXTRAEDITOR_HISTORY, NUM_EXTRAEDITORS, }; EExtraEditor m_ActiveExtraEditor = EXTRAEDITOR_NONE; - float m_aExtraEditorSplits[NUM_EXTRAEDITORS] = {250.0f, 250.0f}; + float m_aExtraEditorSplits[NUM_EXTRAEDITORS] = {250.0f, 250.0f, 250.0f}; enum EShowEnvelope { @@ -755,7 +761,7 @@ class CEditor : public IEditor CImageInfo m_TileartImageInfo; char m_aTileartFilename[IO_MAX_PATH_LENGTH]; - void AddTileart(); + void AddTileart(bool IgnoreHistory = false); void TileartCheckColors(); void PlaceBorderTiles(); @@ -782,7 +788,7 @@ class CEditor : public IEditor void RenderBackground(CUIRect View, IGraphics::CTextureHandle Texture, float Size, float Brightness); - int 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, 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); @@ -792,6 +798,7 @@ class CEditor : public IEditor { CEditor *m_pEditor; std::vector> m_vpLayers; + std::vector m_vLayerIndices; CLayerTiles::SCommonPropState m_CommonPropState; }; static CUI::EPopupMenuFunctionResult PopupLayer(void *pContext, CUIRect View, bool Active); @@ -840,7 +847,7 @@ 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(CQuad *pQuad, int QuadIndex, int v); + void DoQuadPoint(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); @@ -849,12 +856,62 @@ class CEditor : public IEditor void DoSoundSource(CSoundSource *pSource, int Index); + enum class EAxis + { + AXIS_NONE = 0, + AXIS_X, + AXIS_Y + }; + struct SAxisAlignedBoundingBox + { + enum + { + POINT_TL = 0, + POINT_TR, + POINT_BL, + POINT_BR, + POINT_CENTER, + NUM_POINTS + }; + CPoint m_aPoints[NUM_POINTS]; + }; void DoMapEditor(CUIRect View); void DoToolbarLayers(CUIRect Toolbar); void DoToolbarSounds(CUIRect Toolbar); - void DoQuad(CQuad *pQuad, int Index); + void DoQuad(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); + // Alignment methods + // These methods take `OffsetX` and `OffsetY` because the calculations are made with the original positions + // of the quad(s), before we started dragging. This allows us to edit `OffsetX` and `OffsetY` based on the previously + // calculated alignments. + struct SAlignmentInfo + { + CPoint m_AlignedPoint; // The "aligned" point, which we want to align/snap to + union + { + // The current changing value when aligned to this point. When aligning to a point on the X axis, then the X value is changing because + // we aligned the Y values (X axis aligned => Y values are the same, Y axis aligned => X values are the same). + int m_X; + int m_Y; + }; + EAxis m_Axis; // The axis we are aligning on + int m_PointIndex; // The point index we are aligning + int m_Diff; // Store the difference + }; + 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 QuadSelectionAABB(const std::shared_ptr &pLayer, SAxisAlignedBoundingBox &OutAABB); + void ApplyAlignments(const std::vector &vAlignments, int &OffsetX, int &OffsetY); + void ApplyAxisAlignment(int &OffsetX, int &OffsetY); + bool ReplaceImage(const char *pFilename, int StorageType, bool CheckDuplicate); static bool ReplaceImageCallback(const char *pFilename, int StorageType, void *pUser); bool ReplaceSound(const char *pFileName, int StorageType, bool CheckDuplicate); @@ -877,6 +934,7 @@ class CEditor : public IEditor void RenderEnvelopeEditor(CUIRect View); void RenderServerSettingsEditor(CUIRect View, bool ShowServerSettingsEditorLast); + void RenderEditorHistory(CUIRect View); void RenderExtraEditorDragBar(CUIRect View, CUIRect DragBar); @@ -886,7 +944,7 @@ class CEditor : public IEditor void RenderFileDialog(); void SelectGameLayer(); - void SortImages(); + std::vector SortImages(); bool SelectLayerByTile(); void DoAudioPreview(CUIRect View, const void *pPlayPauseButtonID, const void *pStopButtonID, const void *pSeekBarID, const int SampleID); @@ -1005,6 +1063,21 @@ class CEditor : public IEditor unsigned char m_SwitchNum; unsigned char m_SwitchDelay; + +public: + // Undo/Redo + CEditorHistory m_EditorHistory; + CEditorHistory m_ServerSettingsHistory; + CEditorHistory m_EnvelopeEditorHistory; + CQuadEditTracker m_QuadTracker; + CEnvelopeEditorOperationTracker m_EnvOpTracker; + +private: + void UndoLastAction(); + void RedoLastAction(); + +private: + std::map m_QuadDragOriginalPoints; }; // make sure to inline this function diff --git a/src/game/editor/editor_action.h b/src/game/editor/editor_action.h new file mode 100644 index 00000000000..d4fb124df45 --- /dev/null +++ b/src/game/editor/editor_action.h @@ -0,0 +1,30 @@ +#ifndef GAME_EDITOR_EDITOR_ACTION_H +#define GAME_EDITOR_EDITOR_ACTION_H + +#include + +class CEditor; + +class IEditorAction +{ +public: + IEditorAction(CEditor *pEditor) : + m_pEditor(pEditor) {} + + IEditorAction() = default; + + virtual ~IEditorAction() = default; + + virtual void Undo() = 0; + virtual void Redo() = 0; + + virtual bool IsEmpty() { return false; } + + const char *DisplayText() const { return m_aDisplayText; } + +protected: + CEditor *m_pEditor; + char m_aDisplayText[256]; +}; + +#endif diff --git a/src/game/editor/editor_actions.cpp b/src/game/editor/editor_actions.cpp new file mode 100644 index 00000000000..88ce5d55907 --- /dev/null +++ b/src/game/editor/editor_actions.cpp @@ -0,0 +1,2067 @@ +#include "editor_actions.h" +#include + +CEditorBrushDrawAction::CEditorBrushDrawAction(CEditor *pEditor, int Group) : + IEditorAction(pEditor), m_Group(Group) +{ + auto &Map = pEditor->m_Map; + for(size_t k = 0; k < Map.m_vpGroups[Group]->m_vpLayers.size(); k++) + { + auto pLayer = Map.m_vpGroups[Group]->m_vpLayers[k]; + + if(pLayer->m_Type == LAYERTYPE_TILES) + { + auto pLayerTiles = std::static_pointer_cast(pLayer); + + if(pLayer == Map.m_pTeleLayer) + { + if(!Map.m_pTeleLayer->m_History.empty()) + { + m_TeleTileChanges = std::map(Map.m_pTeleLayer->m_History); + Map.m_pTeleLayer->ClearHistory(); + } + } + else if(pLayer == Map.m_pTuneLayer) + { + if(!Map.m_pTuneLayer->m_History.empty()) + { + m_TuneTileChanges = std::map(Map.m_pTuneLayer->m_History); + Map.m_pTuneLayer->ClearHistory(); + } + } + else if(pLayer == Map.m_pSwitchLayer) + { + if(!Map.m_pSwitchLayer->m_History.empty()) + { + m_SwitchTileChanges = std::map(Map.m_pSwitchLayer->m_History); + Map.m_pSwitchLayer->ClearHistory(); + } + } + else if(pLayer == Map.m_pSpeedupLayer) + { + if(!Map.m_pSpeedupLayer->m_History.empty()) + { + m_SpeedupTileChanges = std::map(Map.m_pSpeedupLayer->m_History); + Map.m_pSpeedupLayer->ClearHistory(); + } + } + + if(!pLayerTiles->m_TilesHistory.empty()) + { + m_vTileChanges.emplace_back(k, std::map(pLayerTiles->m_TilesHistory)); + pLayerTiles->ClearHistory(); + } + } + } + + SetInfos(); + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Brush draw (x%d) on %d layers", m_TotalTilesDrawn, m_TotalLayers); +} + +void CEditorBrushDrawAction::SetInfos() +{ + m_TotalTilesDrawn = 0; + m_TotalLayers = 0; + + // Process normal tiles + for(auto const &Pair : m_vTileChanges) + { + int Layer = Pair.first; + std::shared_ptr pLayer = m_pEditor->m_Map.m_vpGroups[m_Group]->m_vpLayers[Layer]; + m_TotalLayers++; + + if(pLayer->m_Type == LAYERTYPE_TILES) + { + std::shared_ptr pLayerTiles = std::static_pointer_cast(pLayer); + auto Changes = Pair.second; + for(auto &Change : Changes) + { + m_TotalTilesDrawn += Change.second.size(); + } + } + } + + // Process speedup tiles + for(auto const &SpeedupChange : m_SpeedupTileChanges) + { + m_TotalTilesDrawn += SpeedupChange.second.size(); + } + + // Process tele tiles + for(auto const &TeleChange : m_TeleTileChanges) + { + m_TotalTilesDrawn += TeleChange.second.size(); + } + + // Process switch tiles + for(auto const &SwitchChange : m_SwitchTileChanges) + { + m_TotalTilesDrawn += SwitchChange.second.size(); + } + + // Process tune tiles + for(auto const &TuneChange : m_TuneTileChanges) + { + m_TotalTilesDrawn += TuneChange.second.size(); + } + + m_TotalLayers += !m_SpeedupTileChanges.empty(); + m_TotalLayers += !m_SwitchTileChanges.empty(); + m_TotalLayers += !m_TeleTileChanges.empty(); + m_TotalLayers += !m_TuneTileChanges.empty(); +} + +bool CEditorBrushDrawAction::IsEmpty() +{ + return m_vTileChanges.empty() && m_SpeedupTileChanges.empty() && m_SwitchTileChanges.empty() && m_TeleTileChanges.empty() && m_TuneTileChanges.empty(); +} + +void CEditorBrushDrawAction::Undo() +{ + Apply(true); +} + +void CEditorBrushDrawAction::Redo() +{ + Apply(false); +} + +void CEditorBrushDrawAction::Apply(bool Undo) +{ + auto &Map = m_pEditor->m_Map; + + // Process normal tiles + for(auto const &Pair : m_vTileChanges) + { + int Layer = Pair.first; + std::shared_ptr pLayer = Map.m_vpGroups[m_Group]->m_vpLayers[Layer]; + + if(pLayer->m_Type == LAYERTYPE_TILES) + { + std::shared_ptr pLayerTiles = std::static_pointer_cast(pLayer); + auto Changes = Pair.second; + for(auto &Change : Changes) + { + int y = Change.first; + auto Line = Change.second; + for(auto &Tile : Line) + { + int x = Tile.first; + STileStateChange State = Tile.second; + pLayerTiles->SetTileIgnoreHistory(x, y, Undo ? State.m_Previous : State.m_Current); + } + } + } + } + + // Process speedup tiles + for(auto const &SpeedupChange : m_SpeedupTileChanges) + { + int y = SpeedupChange.first; + auto Line = SpeedupChange.second; + for(auto &Tile : Line) + { + int x = Tile.first; + int Index = y * Map.m_pSpeedupLayer->m_Width + x; + SSpeedupTileStateChange State = Tile.second; + SSpeedupTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current; + + Map.m_pSpeedupLayer->m_pSpeedupTile[Index].m_Force = Data.m_Force; + Map.m_pSpeedupLayer->m_pSpeedupTile[Index].m_MaxSpeed = Data.m_MaxSpeed; + Map.m_pSpeedupLayer->m_pSpeedupTile[Index].m_Angle = Data.m_Angle; + Map.m_pSpeedupLayer->m_pSpeedupTile[Index].m_Type = Data.m_Type; + Map.m_pSpeedupLayer->m_pTiles[Index].m_Index = Data.m_Index; + } + } + + // Process tele tiles + for(auto const &TeleChange : m_TeleTileChanges) + { + int y = TeleChange.first; + auto Line = TeleChange.second; + for(auto &Tile : Line) + { + int x = Tile.first; + int Index = y * Map.m_pTeleLayer->m_Width + x; + STeleTileStateChange State = Tile.second; + STeleTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current; + + Map.m_pTeleLayer->m_pTeleTile[Index].m_Number = Data.m_Number; + Map.m_pTeleLayer->m_pTeleTile[Index].m_Type = Data.m_Type; + Map.m_pTeleLayer->m_pTiles[Index].m_Index = Data.m_Index; + } + } + + // Process switch tiles + for(auto const &SwitchChange : m_SwitchTileChanges) + { + int y = SwitchChange.first; + auto Line = SwitchChange.second; + for(auto &Tile : Line) + { + int x = Tile.first; + int Index = y * Map.m_pSwitchLayer->m_Width + x; + SSwitchTileStateChange State = Tile.second; + SSwitchTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current; + + Map.m_pSwitchLayer->m_pSwitchTile[Index].m_Number = Data.m_Number; + Map.m_pSwitchLayer->m_pSwitchTile[Index].m_Type = Data.m_Type; + Map.m_pSwitchLayer->m_pSwitchTile[Index].m_Flags = Data.m_Flags; + Map.m_pSwitchLayer->m_pSwitchTile[Index].m_Delay = Data.m_Delay; + Map.m_pSwitchLayer->m_pTiles[Index].m_Index = Data.m_Index; + } + } + + // Process tune tiles + for(auto const &TuneChange : m_TuneTileChanges) + { + int y = TuneChange.first; + auto Line = TuneChange.second; + for(auto &Tile : Line) + { + int x = Tile.first; + int Index = y * Map.m_pTuneLayer->m_Width + x; + STuneTileStateChange State = Tile.second; + STuneTileStateChange::SData Data = Undo ? State.m_Previous : State.m_Current; + + Map.m_pTuneLayer->m_pTuneTile[Index].m_Number = Data.m_Number; + Map.m_pTuneLayer->m_pTuneTile[Index].m_Type = Data.m_Type; + Map.m_pTuneLayer->m_pTiles[Index].m_Index = Data.m_Index; + } + } +} + +// ------------------------------------------- + +CEditorActionQuadPlace::CEditorActionQuadPlace(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector &vBrush) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_vBrush(vBrush) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Quad place (x%d)", (int)m_vBrush.size()); +} + +void CEditorActionQuadPlace::Undo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + for(size_t k = 0; k < m_vBrush.size(); k++) + pLayerQuads->m_vQuads.pop_back(); + + m_pEditor->m_Map.OnModify(); +} +void CEditorActionQuadPlace::Redo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + for(auto &Brush : m_vBrush) + pLayerQuads->m_vQuads.push_back(Brush); + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionSoundPlace::CEditorActionSoundPlace(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector &vBrush) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_vBrush(vBrush) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Sound place (x%d)", (int)m_vBrush.size()); +} + +void CEditorActionSoundPlace::Undo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + for(size_t k = 0; k < m_vBrush.size(); k++) + pLayerSounds->m_vSources.pop_back(); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionSoundPlace::Redo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + for(auto &Brush : m_vBrush) + pLayerSounds->m_vSources.push_back(Brush); + + m_pEditor->m_Map.OnModify(); +} + +// --------------------------------------------------------------------------------------- + +CEditorActionDeleteQuad::CEditorActionDeleteQuad(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector const &vQuadsIndices, std::vector const &vDeletedQuads) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_vQuadsIndices(vQuadsIndices), m_vDeletedQuads(vDeletedQuads) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete quad (x%d)", (int)m_vDeletedQuads.size()); +} + +void CEditorActionDeleteQuad::Undo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + for(size_t k = 0; k < m_vQuadsIndices.size(); k++) + { + pLayerQuads->m_vQuads.insert(pLayerQuads->m_vQuads.begin() + m_vQuadsIndices[k], m_vDeletedQuads[k]); + } +} + +void CEditorActionDeleteQuad::Redo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + std::vector vQuads(m_vQuadsIndices); + + for(int i = 0; i < (int)vQuads.size(); ++i) + { + pLayerQuads->m_vQuads.erase(pLayerQuads->m_vQuads.begin() + vQuads[i]); + for(int j = i + 1; j < (int)vQuads.size(); ++j) + if(vQuads[j] > vQuads[i]) + vQuads[j]--; + + vQuads.erase(vQuads.begin() + i); + + i--; + } +} + +// --------------------------------------------------------------------------------------- + +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"); +} + +void CEditorActionEditQuadPoint::Undo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex]; + for(int k = 0; k < 5; k++) + Quad.m_aPoints[k] = m_vPreviousPoints[k]; +} + +void CEditorActionEditQuadPoint::Redo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex]; + for(int k = 0; k < 5; k++) + Quad.m_aPoints[k] = m_vCurrentPoints[k]; +} + +CEditorActionEditQuadProp::CEditorActionEditQuadProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, EQuadProp Prop, int Previous, int Current) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_QuadIndex(QuadIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current) +{ + static const char *s_apNames[] = { + "order", + "pos X", + "pos Y", + "pos env", + "pos env offset", + "color env", + "color env offset"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quad %s property in layer %d of group %d", s_apNames[(int)m_Prop], m_LayerIndex, m_GroupIndex); +} + +void CEditorActionEditQuadProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditQuadProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditQuadProp::Apply(int Value) +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex]; + if(m_Prop == EQuadProp::PROP_POS_ENV) + Quad.m_PosEnv = Value; + else if(m_Prop == EQuadProp::PROP_POS_ENV_OFFSET) + Quad.m_PosEnvOffset = Value; + else if(m_Prop == EQuadProp::PROP_COLOR_ENV) + Quad.m_ColorEnv = Value; + else if(m_Prop == EQuadProp::PROP_COLOR_ENV_OFFSET) + Quad.m_ColorEnvOffset = Value; +} + +CEditorActionEditQuadPointProp::CEditorActionEditQuadPointProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, int PointIndex, EQuadPointProp Prop, int Previous, int Current) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_QuadIndex(QuadIndex), m_PointIndex(PointIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current) +{ + static const char *s_apNames[] = { + "pos X", + "pos Y", + "color", + "tex U", + "tex V"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quad point %s property in layer %d of group %d", s_apNames[(int)m_Prop], m_LayerIndex, m_GroupIndex); +} + +void CEditorActionEditQuadPointProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditQuadPointProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditQuadPointProp::Apply(int Value) +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + CQuad &Quad = pLayerQuads->m_vQuads[m_QuadIndex]; + + if(m_Prop == EQuadPointProp::PROP_COLOR) + { + const ColorRGBA ColorPick = ColorRGBA::UnpackAlphaLast(Value); + + Quad.m_aColors[m_PointIndex].r = ColorPick.r * 255.0f; + Quad.m_aColors[m_PointIndex].g = ColorPick.g * 255.0f; + Quad.m_aColors[m_PointIndex].b = ColorPick.b * 255.0f; + Quad.m_aColors[m_PointIndex].a = ColorPick.a * 255.0f; + + m_pEditor->m_ColorPickerPopupContext.m_RgbaColor = ColorPick; + m_pEditor->m_ColorPickerPopupContext.m_HslaColor = color_cast(ColorPick); + m_pEditor->m_ColorPickerPopupContext.m_HsvaColor = color_cast(m_pEditor->m_ColorPickerPopupContext.m_HslaColor); + } + else if(m_Prop == EQuadPointProp::PROP_TEX_U) + { + Quad.m_aTexcoords[m_PointIndex].x = Value; + } + else if(m_Prop == EQuadPointProp::PROP_TEX_V) + { + Quad.m_aTexcoords[m_PointIndex].y = Value; + } +} + +// --------------------------------------------------------------------------------------- + +CEditorActionBulk::CEditorActionBulk(CEditor *pEditor, const std::vector> &vpActions, const char *pDisplay, bool Reverse) : + IEditorAction(pEditor), m_vpActions(vpActions), m_Reverse(Reverse) +{ + // Assuming we only use bulk for actions of same type, if no display was provided + if(!pDisplay) + { + const char *pBaseDisplay = m_vpActions[0]->DisplayText(); + if(m_vpActions.size() == 1) + str_copy(m_aDisplayText, pBaseDisplay); + else + str_format(m_aDisplayText, sizeof(m_aDisplayText), "%s (x%d)", pBaseDisplay, (int)m_vpActions.size()); + } + else + { + str_copy(m_aDisplayText, pDisplay); + } +} + +void CEditorActionBulk::Undo() +{ + if(m_Reverse) + { + for(auto pIt = m_vpActions.rbegin(); pIt != m_vpActions.rend(); pIt++) + { + auto &pAction = *pIt; + pAction->Undo(); + } + } + else + { + for(auto &pAction : m_vpActions) + { + pAction->Undo(); + } + } +} + +void CEditorActionBulk::Redo() +{ + for(auto &pAction : m_vpActions) + { + pAction->Redo(); + } +} + +// --------- + +CEditorActionAutoMap::CEditorActionAutoMap(CEditor *pEditor, int GroupIndex, int LayerIndex, const EditorTileStateChangeHistory &Changes) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_Changes(Changes) +{ + ComputeInfos(); + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Auto map (x%d)", m_TotalChanges); +} + +void CEditorActionAutoMap::Undo() +{ + Apply(true); +} + +void CEditorActionAutoMap::Redo() +{ + Apply(false); +} + +void CEditorActionAutoMap::Apply(bool Undo) +{ + auto &Map = m_pEditor->m_Map; + std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); + for(auto &Change : m_Changes) + { + int y = Change.first; + auto Line = Change.second; + for(auto &Tile : Line) + { + int x = Tile.first; + STileStateChange State = Tile.second; + pLayerTiles->SetTileIgnoreHistory(x, y, Undo ? State.m_Previous : State.m_Current); + } + } + + Map.OnModify(); +} + +void CEditorActionAutoMap::ComputeInfos() +{ + m_TotalChanges = 0; + for(auto &Line : m_Changes) + m_TotalChanges += Line.second.size(); +} + +// --------- + +CEditorActionLayerBase::CEditorActionLayerBase(CEditor *pEditor, int GroupIndex, int LayerIndex) : + IEditorAction(pEditor), m_GroupIndex(GroupIndex), m_LayerIndex(LayerIndex) +{ + m_pLayer = pEditor->m_Map.m_vpGroups[GroupIndex]->m_vpLayers[LayerIndex]; +} + +// ---------- + +CEditorActionAddLayer::CEditorActionAddLayer(CEditor *pEditor, int GroupIndex, int LayerIndex, bool Duplicate) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_Duplicate(Duplicate) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "%s %s layer in group %d", m_Duplicate ? "Duplicate" : "New", m_pLayer->TypeName(), m_GroupIndex); +} + +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; + vLayers.erase(vLayers.begin() + m_LayerIndex); + + m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_Collapse = false; + if(m_LayerIndex >= (int)vLayers.size()) + m_pEditor->SelectLayer(vLayers.size() - 1, m_GroupIndex); + + m_pEditor->m_Map.OnModify(); +} + +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; + vLayers.insert(vLayers.begin() + m_LayerIndex, m_pLayer); + + m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_Collapse = false; + m_pEditor->SelectLayer(m_LayerIndex, m_GroupIndex); + m_pEditor->m_Map.OnModify(); +} + +CEditorActionDeleteLayer::CEditorActionDeleteLayer(CEditor *pEditor, int GroupIndex, int LayerIndex) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete %s layer of group %d", m_pLayer->TypeName(), m_GroupIndex); +} + +void CEditorActionDeleteLayer::Redo() +{ + // Redo: 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; + } + + m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->DeleteLayer(m_LayerIndex); + + m_pEditor->m_Map.m_vpGroups[m_GroupIndex]->m_Collapse = false; + if(m_LayerIndex >= (int)vLayers.size()) + m_pEditor->SelectLayer(vLayers.size() - 1, m_GroupIndex); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionDeleteLayer::Undo() +{ + // Undo: 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; + m_pEditor->SelectLayer(m_LayerIndex, m_GroupIndex); + m_pEditor->m_Map.OnModify(); +} + +CEditorActionGroup::CEditorActionGroup(CEditor *pEditor, int GroupIndex, bool Delete) : + IEditorAction(pEditor), m_GroupIndex(GroupIndex), m_Delete(Delete) +{ + m_pGroup = pEditor->m_Map.m_vpGroups[GroupIndex]; + 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"); +} + +void CEditorActionGroup::Undo() +{ + if(m_Delete) + { + // Undo: add back the group + m_pEditor->m_Map.m_vpGroups.insert(m_pEditor->m_Map.m_vpGroups.begin() + m_GroupIndex, m_pGroup); + m_pEditor->m_SelectedGroup = m_GroupIndex; + m_pEditor->m_Map.OnModify(); + } + else + { + // Undo: delete the group + m_pEditor->m_Map.DeleteGroup(m_GroupIndex); + m_pEditor->m_SelectedGroup = maximum(0, m_GroupIndex - 1); + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionGroup::Redo() +{ + if(!m_Delete) + { + // Redo: add back the group + m_pEditor->m_Map.m_vpGroups.insert(m_pEditor->m_Map.m_vpGroups.begin() + m_GroupIndex, m_pGroup); + m_pEditor->m_SelectedGroup = m_GroupIndex; + } + else + { + // Redo: delete the group + m_pEditor->m_Map.DeleteGroup(m_GroupIndex); + m_pEditor->m_SelectedGroup = maximum(0, m_GroupIndex - 1); + } + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionEditGroupProp::CEditorActionEditGroupProp(CEditor *pEditor, int GroupIndex, EGroupProp Prop, int Previous, int Current) : + IEditorAction(pEditor), m_GroupIndex(GroupIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current) +{ + static const char *s_apNames[] = { + "order", + "pos X", + "pos Y", + "para X", + "para Y", + "use clipping", + "clip X", + "clip Y", + "clip W", + "clip H"}; + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit group %d %s property", m_GroupIndex, s_apNames[(int)Prop]); +} + +void CEditorActionEditGroupProp::Undo() +{ + auto pGroup = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]; + + if(m_Prop == EGroupProp::PROP_ORDER) + { + int CurrentOrder = m_Current; + bool Dir = m_Current > m_Previous; + while(CurrentOrder != m_Previous) + { + CurrentOrder = m_pEditor->m_Map.SwapGroups(CurrentOrder, Dir ? CurrentOrder - 1 : CurrentOrder + 1); + } + m_pEditor->m_SelectedGroup = m_Previous; + } + else + Apply(m_Previous); +} + +void CEditorActionEditGroupProp::Redo() +{ + auto pGroup = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]; + + if(m_Prop == EGroupProp::PROP_ORDER) + { + int CurrentOrder = m_Previous; + bool Dir = m_Previous > m_Current; + while(CurrentOrder != m_Current) + { + CurrentOrder = m_pEditor->m_Map.SwapGroups(CurrentOrder, Dir ? CurrentOrder - 1 : CurrentOrder + 1); + } + m_pEditor->m_SelectedGroup = m_Current; + } + else + Apply(m_Current); +} + +void CEditorActionEditGroupProp::Apply(int Value) +{ + auto pGroup = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]; + + if(m_Prop == EGroupProp::PROP_POS_X) + pGroup->m_OffsetX = Value; + if(m_Prop == EGroupProp::PROP_POS_Y) + pGroup->m_OffsetY = Value; + if(m_Prop == EGroupProp::PROP_PARA_X) + pGroup->m_ParallaxX = Value; + if(m_Prop == EGroupProp::PROP_PARA_Y) + pGroup->m_ParallaxY = Value; + if(m_Prop == EGroupProp::PROP_USE_CLIPPING) + pGroup->m_UseClipping = Value; + if(m_Prop == EGroupProp::PROP_CLIP_X) + pGroup->m_ClipX = Value; + if(m_Prop == EGroupProp::PROP_CLIP_Y) + pGroup->m_ClipY = Value; + if(m_Prop == EGroupProp::PROP_CLIP_W) + pGroup->m_ClipW = Value; + if(m_Prop == EGroupProp::PROP_CLIP_H) + pGroup->m_ClipH = Value; + + m_pEditor->m_Map.OnModify(); +} + +template +CEditorActionEditLayerPropBase::CEditorActionEditLayerPropBase(CEditor *pEditor, int GroupIndex, int LayerIndex, E Prop, int Previous, int Current) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_Prop(Prop), m_Previous(Previous), m_Current(Current) +{ +} + +CEditorActionEditLayerProp::CEditorActionEditLayerProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current) +{ + static const char *s_apNames[] = { + "group", + "order", + "HQ"}; + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)m_Prop]); +} + +void CEditorActionEditLayerProp::Undo() +{ + auto pCurrentGroup = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]; + + if(m_Prop == ELayerProp::PROP_ORDER) + { + m_pEditor->SelectLayer(pCurrentGroup->SwapLayers(m_Current, m_Previous)); + } + else + Apply(m_Previous); +} + +void CEditorActionEditLayerProp::Redo() +{ + auto pCurrentGroup = m_pEditor->m_Map.m_vpGroups[m_GroupIndex]; + + if(m_Prop == ELayerProp::PROP_ORDER) + { + m_pEditor->SelectLayer(pCurrentGroup->SwapLayers(m_Previous, m_Current)); + } + else + Apply(m_Current); +} + +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); + m_pEditor->m_SelectedGroup = Value; + m_pEditor->SelectLayer(m_pEditor->m_Map.m_vpGroups[Value]->m_vpLayers.size() - 1); + } + else if(m_Prop == ELayerProp::PROP_HQ) + { + m_pLayer->m_Flags = Value; + } + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionEditLayerTilesProp::CEditorActionEditLayerTilesProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ETilesProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current) +{ + static const char *s_apNames[] = { + "width", + "height", + "shift", + "shift by", + "image", + "color", + "color env", + "color env offset", + "automapper", + "seed"}; + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit tiles layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)Prop]); +} + +void CEditorActionEditLayerTilesProp::SetSavedLayers(const std::map> &SavedLayers) +{ + m_SavedLayers = std::map(SavedLayers); +} + +void CEditorActionEditLayerTilesProp::Undo() +{ + std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); + std::shared_ptr pSavedLayerTiles = nullptr; + + if(m_Prop == ETilesProp::PROP_WIDTH || m_Prop == ETilesProp::PROP_HEIGHT) + { + if(m_Prop == ETilesProp::PROP_HEIGHT) + pLayerTiles->Resize(pLayerTiles->m_Width, m_Previous); + else if(m_Prop == ETilesProp::PROP_WIDTH) + pLayerTiles->Resize(m_Previous, pLayerTiles->m_Height); + + RestoreLayer(LAYERTYPE_TILES, pLayerTiles); + if(pLayerTiles->m_Game || pLayerTiles->m_Front || pLayerTiles->m_Switch || pLayerTiles->m_Speedup || pLayerTiles->m_Tune) + { + if(m_pEditor->m_Map.m_pFrontLayer && !pLayerTiles->m_Front) + RestoreLayer(LAYERTYPE_FRONT, m_pEditor->m_Map.m_pFrontLayer); + if(m_pEditor->m_Map.m_pTeleLayer && !pLayerTiles->m_Tele) + RestoreLayer(LAYERTYPE_TELE, m_pEditor->m_Map.m_pTeleLayer); + if(m_pEditor->m_Map.m_pSwitchLayer && !pLayerTiles->m_Switch) + RestoreLayer(LAYERTYPE_SWITCH, m_pEditor->m_Map.m_pSwitchLayer); + if(m_pEditor->m_Map.m_pSpeedupLayer && !pLayerTiles->m_Speedup) + RestoreLayer(LAYERTYPE_SPEEDUP, m_pEditor->m_Map.m_pSpeedupLayer); + if(m_pEditor->m_Map.m_pTuneLayer && !pLayerTiles->m_Tune) + RestoreLayer(LAYERTYPE_TUNE, m_pEditor->m_Map.m_pTuneLayer); + if(!pLayerTiles->m_Game) + RestoreLayer(LAYERTYPE_GAME, m_pEditor->m_Map.m_pGameLayer); + } + } + else if(m_Prop == ETilesProp::PROP_SHIFT) + { + RestoreLayer(LAYERTYPE_TILES, pLayerTiles); + } + else if(m_Prop == ETilesProp::PROP_SHIFT_BY) + { + m_pEditor->m_ShiftBy = m_Previous; + } + else if(m_Prop == ETilesProp::PROP_IMAGE) + { + if(m_Previous == -1) + { + pLayerTiles->m_Image = -1; + } + else + { + pLayerTiles->m_Image = m_Previous % m_pEditor->m_Map.m_vpImages.size(); + pLayerTiles->m_AutoMapperConfig = -1; + } + } + else if(m_Prop == ETilesProp::PROP_COLOR) + { + const ColorRGBA ColorPick = ColorRGBA::UnpackAlphaLast(m_Previous); + + pLayerTiles->m_Color.r = ColorPick.r * 255.0f; + pLayerTiles->m_Color.g = ColorPick.g * 255.0f; + pLayerTiles->m_Color.b = ColorPick.b * 255.0f; + pLayerTiles->m_Color.a = ColorPick.a * 255.0f; + + m_pEditor->m_ColorPickerPopupContext.m_RgbaColor = ColorPick; + m_pEditor->m_ColorPickerPopupContext.m_HslaColor = color_cast(ColorPick); + m_pEditor->m_ColorPickerPopupContext.m_HsvaColor = color_cast(m_pEditor->m_ColorPickerPopupContext.m_HslaColor); + } + else if(m_Prop == ETilesProp::PROP_COLOR_ENV) + { + pLayerTiles->m_ColorEnv = m_Previous; + } + else if(m_Prop == ETilesProp::PROP_COLOR_ENV_OFFSET) + { + pLayerTiles->m_ColorEnvOffset = m_Previous; + } + else if(m_Prop == ETilesProp::PROP_AUTOMAPPER) + { + pLayerTiles->m_AutoMapperConfig = m_Previous; + } + else if(m_Prop == ETilesProp::PROP_SEED) + { + pLayerTiles->m_Seed = m_Previous; + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionEditLayerTilesProp::Redo() +{ + std::shared_ptr pLayerTiles = std::static_pointer_cast(m_pLayer); + + if(m_Prop == ETilesProp::PROP_WIDTH || m_Prop == ETilesProp::PROP_HEIGHT) + { + if(m_Prop == ETilesProp::PROP_HEIGHT) + pLayerTiles->Resize(pLayerTiles->m_Width, m_Current); + else if(m_Prop == ETilesProp::PROP_WIDTH) + pLayerTiles->Resize(m_Current, pLayerTiles->m_Height); + + if(pLayerTiles->m_Game || pLayerTiles->m_Front || pLayerTiles->m_Switch || pLayerTiles->m_Speedup || pLayerTiles->m_Tune) + { + if(m_pEditor->m_Map.m_pFrontLayer && !pLayerTiles->m_Front) + m_pEditor->m_Map.m_pFrontLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + if(m_pEditor->m_Map.m_pTeleLayer && !pLayerTiles->m_Tele) + m_pEditor->m_Map.m_pTeleLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + if(m_pEditor->m_Map.m_pSwitchLayer && !pLayerTiles->m_Switch) + m_pEditor->m_Map.m_pSwitchLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + if(m_pEditor->m_Map.m_pSpeedupLayer && !pLayerTiles->m_Speedup) + m_pEditor->m_Map.m_pSpeedupLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + if(m_pEditor->m_Map.m_pTuneLayer && !pLayerTiles->m_Tune) + m_pEditor->m_Map.m_pTuneLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + if(!pLayerTiles->m_Game) + m_pEditor->m_Map.m_pGameLayer->Resize(pLayerTiles->m_Width, pLayerTiles->m_Height); + } + } + else if(m_Prop == ETilesProp::PROP_SHIFT) + { + pLayerTiles->Shift(m_Current); + } + else if(m_Prop == ETilesProp::PROP_SHIFT_BY) + { + m_pEditor->m_ShiftBy = m_Current; + } + else if(m_Prop == ETilesProp::PROP_IMAGE) + { + if(m_Current == -1) + { + pLayerTiles->m_Image = -1; + } + else + { + pLayerTiles->m_Image = m_Current % m_pEditor->m_Map.m_vpImages.size(); + pLayerTiles->m_AutoMapperConfig = -1; + } + } + else if(m_Prop == ETilesProp::PROP_COLOR) + { + const ColorRGBA ColorPick = ColorRGBA::UnpackAlphaLast(m_Current); + + pLayerTiles->m_Color.r = ColorPick.r * 255.0f; + pLayerTiles->m_Color.g = ColorPick.g * 255.0f; + pLayerTiles->m_Color.b = ColorPick.b * 255.0f; + pLayerTiles->m_Color.a = ColorPick.a * 255.0f; + + m_pEditor->m_ColorPickerPopupContext.m_RgbaColor = ColorPick; + m_pEditor->m_ColorPickerPopupContext.m_HslaColor = color_cast(ColorPick); + m_pEditor->m_ColorPickerPopupContext.m_HsvaColor = color_cast(m_pEditor->m_ColorPickerPopupContext.m_HslaColor); + } + else if(m_Prop == ETilesProp::PROP_COLOR_ENV) + { + pLayerTiles->m_ColorEnv = m_Current; + } + else if(m_Prop == ETilesProp::PROP_COLOR_ENV_OFFSET) + { + pLayerTiles->m_ColorEnvOffset = m_Current; + } + else if(m_Prop == ETilesProp::PROP_AUTOMAPPER) + { + pLayerTiles->m_AutoMapperConfig = m_Current; + } + else if(m_Prop == ETilesProp::PROP_SEED) + { + pLayerTiles->m_Seed = m_Current; + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionEditLayerTilesProp::RestoreLayer(int Layer, const std::shared_ptr &pLayerTiles) +{ + if(m_SavedLayers[Layer] != nullptr) + { + std::shared_ptr pSavedLayerTiles = std::static_pointer_cast(m_SavedLayers[Layer]); + mem_copy(pLayerTiles->m_pTiles, pSavedLayerTiles->m_pTiles, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTile)); + + if(pLayerTiles->m_Tele) + { + std::shared_ptr pLayerTele = std::static_pointer_cast(pLayerTiles); + std::shared_ptr pSavedLayerTele = std::static_pointer_cast(pSavedLayerTiles); + mem_copy(pLayerTele->m_pTeleTile, pSavedLayerTele->m_pTeleTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTeleTile)); + } + else if(pLayerTiles->m_Speedup) + { + std::shared_ptr pLayerSpeedup = std::static_pointer_cast(pLayerTiles); + std::shared_ptr pSavedLayerSpeedup = std::static_pointer_cast(pSavedLayerTiles); + mem_copy(pLayerSpeedup->m_pSpeedupTile, pSavedLayerSpeedup->m_pSpeedupTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CSpeedupTile)); + } + else if(pLayerTiles->m_Switch) + { + std::shared_ptr pLayerSwitch = std::static_pointer_cast(pLayerTiles); + std::shared_ptr pSavedLayerSwitch = std::static_pointer_cast(pSavedLayerTiles); + mem_copy(pLayerSwitch->m_pSwitchTile, pSavedLayerSwitch->m_pSwitchTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CSwitchTile)); + } + else if(pLayerTiles->m_Tune) + { + std::shared_ptr pLayerTune = std::static_pointer_cast(pLayerTiles); + std::shared_ptr pSavedLayerTune = std::static_pointer_cast(pSavedLayerTiles); + mem_copy(pLayerTune->m_pTuneTile, pSavedLayerTune->m_pTuneTile, (size_t)pLayerTiles->m_Width * pLayerTiles->m_Height * sizeof(CTuneTile)); + } + } +} + +CEditorActionEditLayerQuadsProp::CEditorActionEditLayerQuadsProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerQuadsProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current) +{ + static const char *s_apNames[] = { + "image"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit quads layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)m_Prop]); +} + +void CEditorActionEditLayerQuadsProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditLayerQuadsProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditLayerQuadsProp::Apply(int Value) +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + if(m_Prop == ELayerQuadsProp::PROP_IMAGE) + { + if(Value >= 0) + pLayerQuads->m_Image = Value % m_pEditor->m_Map.m_vpImages.size(); + else + pLayerQuads->m_Image = -1; + } + + m_pEditor->m_Map.OnModify(); +} + +// -------------------------------------------------------------- + +CEditorActionEditLayersGroupAndOrder::CEditorActionEditLayersGroupAndOrder(CEditor *pEditor, int GroupIndex, const std::vector &LayerIndices, int NewGroupIndex, const std::vector &NewLayerIndices) : + IEditorAction(pEditor), m_GroupIndex(GroupIndex), m_LayerIndices(LayerIndices), m_NewGroupIndex(NewGroupIndex), m_NewLayerIndices(NewLayerIndices) +{ + std::sort(m_LayerIndices.begin(), m_LayerIndices.end()); + std::sort(m_NewLayerIndices.begin(), m_NewLayerIndices.end()); + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit layers group and order (x%d)", (int)m_LayerIndices.size()); +} + +void CEditorActionEditLayersGroupAndOrder::Undo() +{ + // Undo : restore group and order + auto &Map = m_pEditor->m_Map; + auto &pCurrentGroup = Map.m_vpGroups[m_NewGroupIndex]; + auto &pPreviousGroup = Map.m_vpGroups[m_GroupIndex]; + std::vector> vpLayers; + vpLayers.reserve(m_NewLayerIndices.size()); + for(auto &LayerIndex : m_NewLayerIndices) + vpLayers.push_back(pCurrentGroup->m_vpLayers[LayerIndex]); + + int k = 0; + for(auto &pLayer : vpLayers) + { + pCurrentGroup->m_vpLayers.erase(std::find(pCurrentGroup->m_vpLayers.begin(), pCurrentGroup->m_vpLayers.end(), pLayer)); + pPreviousGroup->m_vpLayers.insert(pPreviousGroup->m_vpLayers.begin() + m_LayerIndices[k++], pLayer); + } + + m_pEditor->m_vSelectedLayers = m_LayerIndices; + m_pEditor->m_SelectedGroup = m_GroupIndex; +} + +void CEditorActionEditLayersGroupAndOrder::Redo() +{ + // Redo : move layers + auto &Map = m_pEditor->m_Map; + auto &pCurrentGroup = Map.m_vpGroups[m_GroupIndex]; + auto &pPreviousGroup = Map.m_vpGroups[m_NewGroupIndex]; + std::vector> vpLayers; + vpLayers.reserve(m_LayerIndices.size()); + for(auto &LayerIndex : m_LayerIndices) + vpLayers.push_back(pCurrentGroup->m_vpLayers[LayerIndex]); + + int k = 0; + for(auto &pLayer : vpLayers) + { + pCurrentGroup->m_vpLayers.erase(std::find(pCurrentGroup->m_vpLayers.begin(), pCurrentGroup->m_vpLayers.end(), pLayer)); + pPreviousGroup->m_vpLayers.insert(pPreviousGroup->m_vpLayers.begin() + m_NewLayerIndices[k++], pLayer); + } + + m_pEditor->m_vSelectedLayers = m_NewLayerIndices; + m_pEditor->m_SelectedGroup = m_NewGroupIndex; +} + +// ----------------------------------- + +CEditorActionAppendMap::CEditorActionAppendMap(CEditor *pEditor, const char *pMapName, const SPrevInfo &PrevInfo, std::vector &vImageIndexMap) : + IEditorAction(pEditor), m_PrevInfo(PrevInfo), m_vImageIndexMap(vImageIndexMap) +{ + str_copy(m_aMapName, pMapName); + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Append %s", m_aMapName); +} + +void CEditorActionAppendMap::Undo() +{ + auto &Map = m_pEditor->m_Map; + // Undo append: + // - delete added groups + // - delete added envelopes + // - delete added images + // - delete added sounds + + // Delete added groups + while((int)Map.m_vpGroups.size() != m_PrevInfo.m_Groups) + { + Map.m_vpGroups.pop_back(); + } + + // Delete added envelopes + while((int)Map.m_vpEnvelopes.size() != m_PrevInfo.m_Envelopes) + { + Map.m_vpEnvelopes.pop_back(); + } + + // Delete added sounds + while((int)Map.m_vpSounds.size() != m_PrevInfo.m_Sounds) + { + Map.m_vpSounds.pop_back(); + } + + // Delete added images + // Images are sorted when appending, so we need to revert sorting before deleting the images + if(!m_vImageIndexMap.empty()) + { + std::vector vReverseIndexMap; + vReverseIndexMap.resize(m_vImageIndexMap.size()); + + for(int k = 0; k < (int)m_vImageIndexMap.size(); k++) + vReverseIndexMap[m_vImageIndexMap[k]] = k; + + std::vector> vpRevertedImages; + vpRevertedImages.resize(Map.m_vpImages.size()); + + for(int k = 0; k < (int)vReverseIndexMap.size(); k++) + { + vpRevertedImages[vReverseIndexMap[k]] = Map.m_vpImages[k]; + } + Map.m_vpImages = vpRevertedImages; + + Map.ModifyImageIndex([vReverseIndexMap](int *pIndex) { + if(*pIndex >= 0) + { + *pIndex = vReverseIndexMap[*pIndex]; + } + }); + } + + while((int)Map.m_vpImages.size() != m_PrevInfo.m_Images) + { + Map.m_vpImages.pop_back(); + } +} + +void CEditorActionAppendMap::Redo() +{ + // Redo is just re-appending the same map + m_pEditor->Append(m_aMapName, IStorage::TYPE_ALL, true); +} + +// --------------------------- + +CEditorActionTileArt::CEditorActionTileArt(CEditor *pEditor, int PreviousImageCount, const char *pTileArtFile, std::vector &vImageIndexMap) : + IEditorAction(pEditor), m_PreviousImageCount(PreviousImageCount), m_vImageIndexMap(vImageIndexMap) +{ + str_copy(m_aTileArtFile, pTileArtFile); + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Tile art"); +} + +void CEditorActionTileArt::Undo() +{ + auto &Map = m_pEditor->m_Map; + + // Delete added group + Map.m_vpGroups.pop_back(); + + // Delete added images + // Images are sorted when appending, so we need to revert sorting before deleting the images + if(!m_vImageIndexMap.empty()) + { + std::vector vReverseIndexMap; + vReverseIndexMap.resize(m_vImageIndexMap.size()); + + for(int k = 0; k < (int)m_vImageIndexMap.size(); k++) + vReverseIndexMap[m_vImageIndexMap[k]] = k; + + std::vector> vpRevertedImages; + vpRevertedImages.resize(Map.m_vpImages.size()); + + for(int k = 0; k < (int)vReverseIndexMap.size(); k++) + { + vpRevertedImages[vReverseIndexMap[k]] = Map.m_vpImages[k]; + } + Map.m_vpImages = vpRevertedImages; + + Map.ModifyImageIndex([vReverseIndexMap](int *pIndex) { + if(*pIndex >= 0) + { + *pIndex = vReverseIndexMap[*pIndex]; + } + }); + } + + while((int)Map.m_vpImages.size() != m_PreviousImageCount) + { + Map.m_vpImages.pop_back(); + } +} + +void CEditorActionTileArt::Redo() +{ + 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; + } + + IStorage::StripPathAndExtension(m_aTileArtFile, m_pEditor->m_aTileartFilename, sizeof(m_pEditor->m_aTileartFilename)); + m_pEditor->AddTileart(true); +} + +// --------------------------------- + +CEditorCommandAction::CEditorCommandAction(CEditor *pEditor, EType Type, int *pSelectedCommandIndex, int CommandIndex, const char *pPreviousCommand, const char *pCurrentCommand) : + IEditorAction(pEditor), m_Type(Type), m_pSelectedCommandIndex(pSelectedCommandIndex), m_CommandIndex(CommandIndex) +{ + if(pPreviousCommand != nullptr) + m_PreviousCommand = std::string(pPreviousCommand); + if(pCurrentCommand != nullptr) + m_CurrentCommand = std::string(pCurrentCommand); + + switch(m_Type) + { + case EType::ADD: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Add command"); + break; + case EType::EDIT: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit command %d", m_CommandIndex); + break; + case EType::DELETE: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete command %d", m_CommandIndex); + break; + case EType::MOVE_UP: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Move command %d up", m_CommandIndex); + break; + case EType::MOVE_DOWN: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Move command %d down", m_CommandIndex); + break; + default: + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit command %d", m_CommandIndex); + break; + } +} + +void CEditorCommandAction::Undo() +{ + auto &Map = m_pEditor->m_Map; + switch(m_Type) + { + case EType::DELETE: + { + Map.m_vSettings.insert(Map.m_vSettings.begin() + m_CommandIndex, m_PreviousCommand.c_str()); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::ADD: + { + Map.m_vSettings.erase(Map.m_vSettings.begin() + m_CommandIndex); + *m_pSelectedCommandIndex = Map.m_vSettings.size() - 1; + break; + } + case EType::EDIT: + { + str_copy(Map.m_vSettings[m_CommandIndex].m_aCommand, m_PreviousCommand.c_str()); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::MOVE_DOWN: + { + std::swap(Map.m_vSettings[m_CommandIndex], Map.m_vSettings[m_CommandIndex + 1]); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::MOVE_UP: + { + std::swap(Map.m_vSettings[m_CommandIndex], Map.m_vSettings[m_CommandIndex - 1]); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + } +} + +void CEditorCommandAction::Redo() +{ + auto &Map = m_pEditor->m_Map; + switch(m_Type) + { + case EType::DELETE: + { + Map.m_vSettings.erase(Map.m_vSettings.begin() + m_CommandIndex); + *m_pSelectedCommandIndex = Map.m_vSettings.size() - 1; + break; + } + case EType::ADD: + { + Map.m_vSettings.insert(Map.m_vSettings.begin() + m_CommandIndex, m_PreviousCommand.c_str()); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::EDIT: + { + str_copy(Map.m_vSettings[m_CommandIndex].m_aCommand, m_CurrentCommand.c_str()); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::MOVE_DOWN: + { + std::swap(Map.m_vSettings[m_CommandIndex], Map.m_vSettings[m_CommandIndex + 1]); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + case EType::MOVE_UP: + { + std::swap(Map.m_vSettings[m_CommandIndex], Map.m_vSettings[m_CommandIndex - 1]); + *m_pSelectedCommandIndex = m_CommandIndex; + break; + } + } +} + +// ------------------------------------------------ + +CEditorActionEnvelopeAdd::CEditorActionEnvelopeAdd(CEditor *pEditor, const std::shared_ptr &pEnv) : + IEditorAction(pEditor), m_pEnv(pEnv) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Add new %s envelope", pEnv->Type() == CEnvelope::EType::COLOR ? "color" : (pEnv->Type() == CEnvelope::EType::POSITION ? "position" : "sound")); +} + +void CEditorActionEnvelopeAdd::Undo() +{ + // Undo is removing the envelope, which was added at the back of the list + m_pEditor->m_Map.m_vpEnvelopes.pop_back(); + m_pEditor->m_SelectedEnvelope = m_pEditor->m_Map.m_vpEnvelopes.size() - 1; +} + +void CEditorActionEnvelopeAdd::Redo() +{ + // Redo is adding back at the back the saved envelope + m_pEditor->m_Map.m_vpEnvelopes.push_back(m_pEnv); + m_pEditor->m_SelectedEnvelope = m_pEditor->m_Map.m_vpEnvelopes.size() - 1; +} + +CEditorActionEveloppeDelete::CEditorActionEveloppeDelete(CEditor *pEditor, int EnvelopeIndex) : + IEditorAction(pEditor), m_EnvelopeIndex(EnvelopeIndex), m_pEnv(pEditor->m_Map.m_vpEnvelopes[EnvelopeIndex]) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete envelope %d", m_EnvelopeIndex); +} + +void CEditorActionEveloppeDelete::Undo() +{ + // Undo is adding back the envelope + m_pEditor->m_Map.m_vpEnvelopes.insert(m_pEditor->m_Map.m_vpEnvelopes.begin() + m_EnvelopeIndex, m_pEnv); + m_pEditor->m_SelectedEnvelope = m_EnvelopeIndex; +} + +void CEditorActionEveloppeDelete::Redo() +{ + // Redo is erasing the same envelope index + m_pEditor->m_Map.m_vpEnvelopes.erase(m_pEditor->m_Map.m_vpEnvelopes.begin() + m_EnvelopeIndex); + if(m_pEditor->m_SelectedEnvelope >= (int)m_pEditor->m_Map.m_vpEnvelopes.size()) + m_pEditor->m_SelectedEnvelope = m_pEditor->m_Map.m_vpEnvelopes.size() - 1; +} + +CEditorActionEnvelopeEdit::CEditorActionEnvelopeEdit(CEditor *pEditor, int EnvelopeIndex, EEditType EditType, int Previous, int Current) : + IEditorAction(pEditor), m_EnvelopeIndex(EnvelopeIndex), m_EditType(EditType), m_Previous(Previous), m_Current(Current), m_pEnv(pEditor->m_Map.m_vpEnvelopes[EnvelopeIndex]) +{ + static const char *s_apNames[] = { + "sync", + "order"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit envelope %d %s", m_EnvelopeIndex, s_apNames[(int)m_EditType]); +} + +void CEditorActionEnvelopeEdit::Undo() +{ + switch(m_EditType) + { + case EEditType::ORDER: + { + m_pEditor->m_Map.SwapEnvelopes(m_Current, m_Previous); + break; + } + case EEditType::SYNC: + { + m_pEnv->m_Synchronized = m_Previous; + break; + } + } + m_pEditor->m_Map.OnModify(); + m_pEditor->m_SelectedEnvelope = m_EnvelopeIndex; +} + +void CEditorActionEnvelopeEdit::Redo() +{ + switch(m_EditType) + { + case EEditType::ORDER: + { + m_pEditor->m_Map.SwapEnvelopes(m_Previous, m_Current); + break; + } + case EEditType::SYNC: + { + m_pEnv->m_Synchronized = m_Current; + break; + } + } + m_pEditor->m_Map.OnModify(); + m_pEditor->m_SelectedEnvelope = m_EnvelopeIndex; +} + +CEditorActionEnvelopeEditPoint::CEditorActionEnvelopeEditPoint(CEditor *pEditor, int EnvelopeIndex, int PointIndex, int Channel, EEditType EditType, int Previous, int Current) : + IEditorAction(pEditor), m_EnvelopeIndex(EnvelopeIndex), m_PointIndex(PointIndex), m_Channel(Channel), m_EditType(EditType), m_Previous(Previous), m_Current(Current), m_pEnv(pEditor->m_Map.m_vpEnvelopes[EnvelopeIndex]) +{ + static const char *s_apNames[] = { + "time", + "value", + "curve type"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit %s of point %d (channel %d) of env %d", s_apNames[(int)m_EditType], m_PointIndex, m_Channel, m_EnvelopeIndex); +} + +void CEditorActionEnvelopeEditPoint::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEnvelopeEditPoint::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEnvelopeEditPoint::Apply(int Value) +{ + if(m_EditType == EEditType::TIME) + { + m_pEnv->m_vPoints[m_PointIndex].m_Time = Value; + } + else if(m_EditType == EEditType::VALUE) + { + m_pEnv->m_vPoints[m_PointIndex].m_aValues[m_Channel] = Value; + + if(m_pEnv->GetChannels() == 4) + { + auto *pValues = m_pEnv->m_vPoints[m_PointIndex].m_aValues; + const ColorRGBA Color = ColorRGBA(fx2f(pValues[0]), fx2f(pValues[1]), fx2f(pValues[2]), fx2f(pValues[3])); + + m_pEditor->m_ColorPickerPopupContext.m_RgbaColor = Color; + m_pEditor->m_ColorPickerPopupContext.m_HslaColor = color_cast(Color); + m_pEditor->m_ColorPickerPopupContext.m_HsvaColor = color_cast(m_pEditor->m_ColorPickerPopupContext.m_HslaColor); + } + } + else if(m_EditType == EEditType::CURVE_TYPE) + { + m_pEnv->m_vPoints[m_PointIndex].m_Curvetype = Value; + } + + m_pEditor->m_Map.OnModify(); +} + +// ---- + +CEditorActionEditEnvelopePointValue::CEditorActionEditEnvelopePointValue(CEditor *pEditor, int EnvIndex, int PointIndex, int Channel, EType Type, int OldTime, int OldValue, int NewTime, int NewValue) : + IEditorAction(pEditor), m_EnvIndex(EnvIndex), m_PtIndex(PointIndex), m_Channel(Channel), m_Type(Type), m_OldTime(OldTime), m_OldValue(OldValue), m_NewTime(NewTime), m_NewValue(NewValue) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit point %d%s value (envelope %d, channel %d)", PointIndex, m_Type == EType::TANGENT_IN ? "tangent in" : (m_Type == EType::TANGENT_OUT ? "tangent out" : ""), m_EnvIndex, m_Channel); +} + +void CEditorActionEditEnvelopePointValue::Undo() +{ + Apply(true); +} + +void CEditorActionEditEnvelopePointValue::Redo() +{ + Apply(false); +} + +void CEditorActionEditEnvelopePointValue::Apply(bool Undo) +{ + float CurrentValue = fx2f(Undo ? m_OldValue : m_NewValue); + float CurrentTime = (Undo ? m_OldTime : m_NewTime) / 1000.0f; + + std::shared_ptr pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + if(m_Type == EType::TANGENT_IN) + { + pEnvelope->m_vPoints[m_PtIndex].m_Bezier.m_aInTangentDeltaX[m_Channel] = minimum(CurrentTime * 1000.0f - pEnvelope->m_vPoints[m_PtIndex].m_Time, 0); + pEnvelope->m_vPoints[m_PtIndex].m_Bezier.m_aInTangentDeltaY[m_Channel] = f2fx(CurrentValue) - pEnvelope->m_vPoints[m_PtIndex].m_aValues[m_Channel]; + } + else if(m_Type == EType::TANGENT_OUT) + { + pEnvelope->m_vPoints[m_PtIndex].m_Bezier.m_aOutTangentDeltaX[m_Channel] = maximum(CurrentTime * 1000.0f - pEnvelope->m_vPoints[m_PtIndex].m_Time, 0); + pEnvelope->m_vPoints[m_PtIndex].m_Bezier.m_aOutTangentDeltaY[m_Channel] = f2fx(CurrentValue) - pEnvelope->m_vPoints[m_PtIndex].m_aValues[m_Channel]; + } + else + { + if(pEnvelope->GetChannels() == 1 || pEnvelope->GetChannels() == 4) + CurrentValue = clamp(CurrentValue, 0.0f, 1.0f); + pEnvelope->m_vPoints[m_PtIndex].m_aValues[m_Channel] = f2fx(CurrentValue); + + if(m_PtIndex != 0) + { + pEnvelope->m_vPoints[m_PtIndex].m_Time = CurrentTime * 1000.0f; + + if(pEnvelope->m_vPoints[m_PtIndex].m_Time < pEnvelope->m_vPoints[m_PtIndex - 1].m_Time) + pEnvelope->m_vPoints[m_PtIndex].m_Time = pEnvelope->m_vPoints[m_PtIndex - 1].m_Time + 1; + if(static_cast(m_PtIndex) + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[m_PtIndex].m_Time > pEnvelope->m_vPoints[m_PtIndex + 1].m_Time) + pEnvelope->m_vPoints[m_PtIndex].m_Time = pEnvelope->m_vPoints[m_PtIndex + 1].m_Time - 1; + } + else + { + pEnvelope->m_vPoints[m_PtIndex].m_Time = 0.0f; + } + } + + m_pEditor->m_Map.OnModify(); + m_pEditor->m_UpdateEnvPointInfo = true; +} + +// --------------------- + +CEditorActionResetEnvelopePointTangent::CEditorActionResetEnvelopePointTangent(CEditor *pEditor, int EnvIndex, int PointIndex, int Channel, bool In) : + IEditorAction(pEditor), m_EnvIndex(EnvIndex), m_PointIndex(PointIndex), m_Channel(Channel), m_In(In) +{ + std::shared_ptr pEnvelope = pEditor->m_Map.m_vpEnvelopes[EnvIndex]; + if(In) + { + m_Previous[0] = pEnvelope->m_vPoints[PointIndex].m_Bezier.m_aInTangentDeltaX[Channel]; + m_Previous[1] = pEnvelope->m_vPoints[PointIndex].m_Bezier.m_aInTangentDeltaY[Channel]; + } + else + { + m_Previous[0] = pEnvelope->m_vPoints[PointIndex].m_Bezier.m_aOutTangentDeltaX[Channel]; + m_Previous[1] = pEnvelope->m_vPoints[PointIndex].m_Bezier.m_aOutTangentDeltaY[Channel]; + } + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Reset point %d of env %d tangent %s", m_PointIndex, m_EnvIndex, m_In ? "in" : "out"); +} + +void CEditorActionResetEnvelopePointTangent::Undo() +{ + std::shared_ptr pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + if(m_In) + { + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aInTangentDeltaX[m_Channel] = m_Previous[0]; + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aInTangentDeltaY[m_Channel] = m_Previous[1]; + } + else + { + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aOutTangentDeltaX[m_Channel] = m_Previous[0]; + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aOutTangentDeltaY[m_Channel] = m_Previous[1]; + } + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionResetEnvelopePointTangent::Redo() +{ + std::shared_ptr pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + if(m_In) + { + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aInTangentDeltaX[m_Channel] = 0.0f; + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aInTangentDeltaY[m_Channel] = 0.0f; + } + else + { + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aOutTangentDeltaX[m_Channel] = 0.0f; + pEnvelope->m_vPoints[m_PointIndex].m_Bezier.m_aOutTangentDeltaY[m_Channel] = 0.0f; + } + m_pEditor->m_Map.OnModify(); +} + +// ------------------ + +CEditorActionAddEnvelopePoint::CEditorActionAddEnvelopePoint(CEditor *pEditor, int EnvIndex, int Time, ColorRGBA Channels) : + IEditorAction(pEditor), m_EnvIndex(EnvIndex), m_Time(Time), m_Channels(Channels) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Add new point in envelope %d at time %f", m_EnvIndex, Time / 1000.0); +} + +void CEditorActionAddEnvelopePoint::Undo() +{ + // Delete added point + auto pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + auto pIt = std::find_if(pEnvelope->m_vPoints.begin(), pEnvelope->m_vPoints.end(), [this](const CEnvPoint_runtime &Point) { + return Point.m_Time == m_Time; + }); + if(pIt != pEnvelope->m_vPoints.end()) + { + pEnvelope->m_vPoints.erase(pIt); + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionAddEnvelopePoint::Redo() +{ + auto pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + pEnvelope->AddPoint(m_Time, + f2fx(m_Channels.r), f2fx(m_Channels.g), + f2fx(m_Channels.b), f2fx(m_Channels.a)); + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionDeleteEnvelopePoint::CEditorActionDeleteEnvelopePoint(CEditor *pEditor, int EnvIndex, int PointIndex) : + IEditorAction(pEditor), m_EnvIndex(EnvIndex), m_PointIndex(PointIndex), m_Point(pEditor->m_Map.m_vpEnvelopes[EnvIndex]->m_vPoints[PointIndex]) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete point %d of envelope %d", m_PointIndex, m_EnvIndex); +} + +void CEditorActionDeleteEnvelopePoint::Undo() +{ + std::shared_ptr pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + pEnvelope->m_vPoints.insert(pEnvelope->m_vPoints.begin() + m_PointIndex, m_Point); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionDeleteEnvelopePoint::Redo() +{ + std::shared_ptr pEnvelope = m_pEditor->m_Map.m_vpEnvelopes[m_EnvIndex]; + pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + m_PointIndex); + + auto pSelectedPointIt = std::find_if(m_pEditor->m_vSelectedEnvelopePoints.begin(), m_pEditor->m_vSelectedEnvelopePoints.end(), [this](const std::pair Pair) { + return Pair.first == m_PointIndex; + }); + + if(pSelectedPointIt != m_pEditor->m_vSelectedEnvelopePoints.end()) + m_pEditor->m_vSelectedEnvelopePoints.erase(pSelectedPointIt); + + m_pEditor->m_Map.OnModify(); +} + +// ------------------------------- + +CEditorActionEditLayerSoundsProp::CEditorActionEditLayerSoundsProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerSoundsProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current) +{ + static const char *s_apNames[] = { + "sound"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sounds layer %d in group %d %s property", m_LayerIndex, m_GroupIndex, s_apNames[(int)m_Prop]); +} + +void CEditorActionEditLayerSoundsProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditLayerSoundsProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditLayerSoundsProp::Apply(int Value) +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + if(m_Prop == ELayerSoundsProp::PROP_SOUND) + { + if(Value >= 0) + pLayerSounds->m_Sound = Value % m_pEditor->m_Map.m_vpSounds.size(); + else + pLayerSounds->m_Sound = -1; + } + + m_pEditor->m_Map.OnModify(); +} + +// --- + +CEditorActionDeleteSoundSource::CEditorActionDeleteSoundSource(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_SourceIndex(SourceIndex) +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + m_Source = pLayerSounds->m_vSources[SourceIndex]; + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Delete sound source %d in layer %d of group %d", SourceIndex, LayerIndex, GroupIndex); +} + +void CEditorActionDeleteSoundSource::Undo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + pLayerSounds->m_vSources.insert(pLayerSounds->m_vSources.begin() + m_SourceIndex, m_Source); + m_pEditor->m_SelectedSource = m_SourceIndex; + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionDeleteSoundSource::Redo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + pLayerSounds->m_vSources.erase(pLayerSounds->m_vSources.begin() + m_SourceIndex); + m_pEditor->m_SelectedSource--; + m_pEditor->m_Map.OnModify(); +} + +// --------------- + +CEditorActionEditSoundSource::CEditorActionEditSoundSource(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, EEditType Type, int Value) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_SourceIndex(SourceIndex), m_EditType(Type), m_CurrentValue(Value), m_pSavedObject(nullptr) +{ + Save(); + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d", SourceIndex, LayerIndex, GroupIndex); +} + +void CEditorActionEditSoundSource::Undo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_EditType == EEditType::SHAPE) + { + CSoundShape *pSavedShape = (CSoundShape *)m_pSavedObject; + pSource->m_Shape.m_Type = pSavedShape->m_Type; + + // set default values + switch(pSource->m_Shape.m_Type) + { + case CSoundShape::SHAPE_CIRCLE: + { + pSource->m_Shape.m_Circle.m_Radius = pSavedShape->m_Circle.m_Radius; + break; + } + case CSoundShape::SHAPE_RECTANGLE: + { + pSource->m_Shape.m_Rectangle.m_Width = pSavedShape->m_Rectangle.m_Width; + pSource->m_Shape.m_Rectangle.m_Height = pSavedShape->m_Rectangle.m_Height; + break; + } + } + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionEditSoundSource::Redo() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_EditType == EEditType::SHAPE) + { + pSource->m_Shape.m_Type = m_CurrentValue; + + // set default values + switch(pSource->m_Shape.m_Type) + { + case CSoundShape::SHAPE_CIRCLE: + { + pSource->m_Shape.m_Circle.m_Radius = 1000.0f; + break; + } + case CSoundShape::SHAPE_RECTANGLE: + { + pSource->m_Shape.m_Rectangle.m_Width = f2fx(1000.0f); + pSource->m_Shape.m_Rectangle.m_Height = f2fx(800.0f); + break; + } + } + } + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionEditSoundSource::Save() +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_EditType == EEditType::SHAPE) + { + CSoundShape *pShapeInfo = new CSoundShape; + pShapeInfo->m_Type = pSource->m_Shape.m_Type; + + switch(pSource->m_Shape.m_Type) + { + case CSoundShape::SHAPE_CIRCLE: + { + pShapeInfo->m_Circle.m_Radius = pSource->m_Shape.m_Circle.m_Radius; + break; + } + case CSoundShape::SHAPE_RECTANGLE: + { + pShapeInfo->m_Rectangle.m_Width = pSource->m_Shape.m_Rectangle.m_Width; + pShapeInfo->m_Rectangle.m_Height = pSource->m_Shape.m_Rectangle.m_Height; + break; + } + } + + m_pSavedObject = pShapeInfo; + } +} + +CEditorActionEditSoundSource::~CEditorActionEditSoundSource() +{ + if(m_EditType == EEditType::SHAPE) + { + CSoundShape *pSavedShape = (CSoundShape *)m_pSavedObject; + delete pSavedShape; + } +} + +// ----- + +CEditorActionEditSoundSourceProp::CEditorActionEditSoundSourceProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ESoundProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current), m_SourceIndex(SourceIndex) +{ + static const char *s_apNames[] = { + "pos X", + "pos Y", + "loop", + "pan", + "time delay", + "falloff", + "pos env", + "pos env offset", + "sound env", + "sound env offset"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d %s property", SourceIndex, LayerIndex, GroupIndex, s_apNames[(int)Prop]); +} + +void CEditorActionEditSoundSourceProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditSoundSourceProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditSoundSourceProp::Apply(int Value) +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_Prop == ESoundProp::PROP_POS_X) + { + pSource->m_Position.x = Value; + } + else if(m_Prop == ESoundProp::PROP_POS_Y) + { + pSource->m_Position.y = Value; + } + else if(m_Prop == ESoundProp::PROP_LOOP) + { + pSource->m_Loop = Value; + } + else if(m_Prop == ESoundProp::PROP_PAN) + { + pSource->m_Pan = Value; + } + else if(m_Prop == ESoundProp::PROP_TIME_DELAY) + { + pSource->m_TimeDelay = Value; + } + else if(m_Prop == ESoundProp::PROP_FALLOFF) + { + pSource->m_Falloff = Value; + } + else if(m_Prop == ESoundProp::PROP_POS_ENV) + { + pSource->m_PosEnv = Value; + } + else if(m_Prop == ESoundProp::PROP_POS_ENV_OFFSET) + { + pSource->m_PosEnvOffset = Value; + } + else if(m_Prop == ESoundProp::PROP_SOUND_ENV) + { + pSource->m_SoundEnv = Value; + } + else if(m_Prop == ESoundProp::PROP_SOUND_ENV_OFFSET) + { + pSource->m_SoundEnvOffset = Value; + } + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionEditRectSoundSourceShapeProp::CEditorActionEditRectSoundSourceShapeProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ERectangleShapeProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current), m_SourceIndex(SourceIndex) +{ + static const char *s_apNames[] = { + "width", + "height"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d sound shape %s property", m_SourceIndex, m_LayerIndex, m_GroupIndex, s_apNames[(int)Prop]); +} + +void CEditorActionEditRectSoundSourceShapeProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditRectSoundSourceShapeProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditRectSoundSourceShapeProp::Apply(int Value) +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_Prop == ERectangleShapeProp::PROP_RECTANGLE_WIDTH) + { + pSource->m_Shape.m_Rectangle.m_Width = Value; + } + else if(m_Prop == ERectangleShapeProp::PROP_RECTANGLE_HEIGHT) + { + pSource->m_Shape.m_Rectangle.m_Height = Value; + } + + m_pEditor->m_Map.OnModify(); +} + +CEditorActionEditCircleSoundSourceShapeProp::CEditorActionEditCircleSoundSourceShapeProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ECircleShapeProp Prop, int Previous, int Current) : + CEditorActionEditLayerPropBase(pEditor, GroupIndex, LayerIndex, Prop, Previous, Current), m_SourceIndex(SourceIndex) +{ + static const char *s_apNames[] = { + "radius"}; + str_format(m_aDisplayText, sizeof(m_aDisplayText), "Edit sound source %d in layer %d of group %d sound shape %s property", m_SourceIndex, m_LayerIndex, m_GroupIndex, s_apNames[(int)Prop]); +} + +void CEditorActionEditCircleSoundSourceShapeProp::Undo() +{ + Apply(m_Previous); +} + +void CEditorActionEditCircleSoundSourceShapeProp::Redo() +{ + Apply(m_Current); +} + +void CEditorActionEditCircleSoundSourceShapeProp::Apply(int Value) +{ + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + CSoundSource *pSource = &pLayerSounds->m_vSources[m_SourceIndex]; + + if(m_Prop == ECircleShapeProp::PROP_CIRCLE_RADIUS) + { + pSource->m_Shape.m_Circle.m_Radius = Value; + } + + m_pEditor->m_Map.OnModify(); +} + +// -------------------------- + +CEditorActionNewEmptySound::CEditorActionNewEmptySound(CEditor *pEditor, int GroupIndex, int LayerIndex, int x, int y) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_X(x), m_Y(y) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "New sound in layer %d of group %d", LayerIndex, GroupIndex); +} + +void CEditorActionNewEmptySound::Undo() +{ + // Undo is simply deleting the added source + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + pLayerSounds->m_vSources.pop_back(); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionNewEmptySound::Redo() +{ + auto &Map = m_pEditor->m_Map; + std::shared_ptr pLayerSounds = std::static_pointer_cast(m_pLayer); + pLayerSounds->NewSource(m_X, m_Y); + + Map.OnModify(); +} + +CEditorActionNewEmptyQuad::CEditorActionNewEmptyQuad(CEditor *pEditor, int GroupIndex, int LayerIndex, int x, int y) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex), m_X(x), m_Y(y) +{ + str_format(m_aDisplayText, sizeof(m_aDisplayText), "New quad in layer %d of group %d", LayerIndex, GroupIndex); +} + +void CEditorActionNewEmptyQuad::Undo() +{ + // Undo is simply deleting the added quad + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + pLayerQuads->m_vQuads.pop_back(); + + m_pEditor->m_Map.OnModify(); +} + +void CEditorActionNewEmptyQuad::Redo() +{ + auto &Map = m_pEditor->m_Map; + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + + int Width = 64; + int Height = 64; + if(pLayerQuads->m_Image >= 0) + { + Width = Map.m_vpImages[pLayerQuads->m_Image]->m_Width; + Height = Map.m_vpImages[pLayerQuads->m_Image]->m_Height; + } + + pLayerQuads->NewQuad(m_X, m_Y, Width, Height); + + Map.OnModify(); +} + +// ------------- + +CEditorActionNewQuad::CEditorActionNewQuad(CEditor *pEditor, int GroupIndex, int LayerIndex) : + CEditorActionLayerBase(pEditor, GroupIndex, LayerIndex) +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + m_Quad = pLayerQuads->m_vQuads[pLayerQuads->m_vQuads.size() - 1]; + + str_format(m_aDisplayText, sizeof(m_aDisplayText), "New quad in layer %d of group %d", LayerIndex, GroupIndex); +} + +void CEditorActionNewQuad::Undo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + pLayerQuads->m_vQuads.pop_back(); +} + +void CEditorActionNewQuad::Redo() +{ + std::shared_ptr pLayerQuads = std::static_pointer_cast(m_pLayer); + pLayerQuads->m_vQuads.emplace_back(m_Quad); +} diff --git a/src/game/editor/editor_actions.h b/src/game/editor/editor_actions.h new file mode 100644 index 00000000000..198e02a6f3a --- /dev/null +++ b/src/game/editor/editor_actions.h @@ -0,0 +1,648 @@ +#ifndef GAME_EDITOR_EDITOR_ACTIONS_H +#define GAME_EDITOR_EDITOR_ACTIONS_H + +#include "editor.h" +#include "editor_action.h" + +class CEditorActionLayerBase : public IEditorAction +{ +public: + CEditorActionLayerBase(CEditor *pEditor, int GroupIndex, int LayerIndex); + + virtual void Undo() override {} + virtual void Redo() override {} + +protected: + int m_GroupIndex; + int m_LayerIndex; + std::shared_ptr m_pLayer; +}; + +class CEditorBrushDrawAction : public IEditorAction +{ +public: + CEditorBrushDrawAction(CEditor *pEditor, int Group); + + void Undo() override; + void Redo() override; + bool IsEmpty() override; + +private: + int m_Group; + // m_vTileChanges is a list of changes for each layer that was modified. + // The std::pair is used to pair one layer (index) with its history (2D map). + // EditorTileStateChangeHistory is a 2D map, storing a change item at a specific y,x position. + std::vector>> m_vTileChanges; + EditorTileStateChangeHistory m_TeleTileChanges; + EditorTileStateChangeHistory m_SpeedupTileChanges; + EditorTileStateChangeHistory m_SwitchTileChanges; + EditorTileStateChangeHistory m_TuneTileChanges; + + int m_TotalTilesDrawn; + int m_TotalLayers; + + void Apply(bool Undo); + void SetInfos(); +}; + +// --------------------------------------------------------- + +class CEditorActionQuadPlace : public CEditorActionLayerBase +{ +public: + CEditorActionQuadPlace(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector &vBrush); + + void Undo() override; + void Redo() override; + +private: + std::vector m_vBrush; +}; + +class CEditorActionSoundPlace : public CEditorActionLayerBase +{ +public: + CEditorActionSoundPlace(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector &vBrush); + + void Undo() override; + void Redo() override; + +private: + std::vector m_vBrush; +}; + +// ------------------------------------------------------------- + +class CEditorActionDeleteQuad : public CEditorActionLayerBase +{ +public: + CEditorActionDeleteQuad(CEditor *pEditor, int GroupIndex, int LayerIndex, std::vector const &vQuadsIndices, std::vector const &vDeletedQuads); + + void Undo() override; + void Redo() override; + +private: + std::vector m_vQuadsIndices; + std::vector m_vDeletedQuads; +}; + +// ------------------------------------------------------------- + +class CEditorActionEditQuadPoint : public CEditorActionLayerBase +{ +public: + CEditorActionEditQuadPoint(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, std::vector const &vPreviousPoints, std::vector const &vCurrentPoints); + + void Undo() override; + void Redo() override; + +private: + int m_QuadIndex; + std::vector m_vPreviousPoints; + std::vector m_vCurrentPoints; +}; + +class CEditorActionEditQuadProp : public CEditorActionLayerBase +{ +public: + CEditorActionEditQuadProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, EQuadProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_QuadIndex; + EQuadProp m_Prop; + int m_Previous; + int m_Current; + + void Apply(int Value); +}; + +class CEditorActionEditQuadPointProp : public CEditorActionLayerBase +{ +public: + CEditorActionEditQuadPointProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int QuadIndex, int PointIndex, EQuadPointProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_QuadIndex; + int m_PointIndex; + EQuadPointProp m_Prop; + int m_Previous; + int m_Current; + + void Apply(int Value); +}; + +// ------------------------------------------------------------- + +class CEditorActionBulk : public IEditorAction +{ +public: + CEditorActionBulk(CEditor *pEditor, const std::vector> &vpActions, const char *pDisplay = nullptr, bool Reverse = false); + + void Undo() override; + void Redo() override; + +private: + std::vector> m_vpActions; + std::string m_Display; + bool m_Reverse; +}; + +// + +class CEditorActionAutoMap : public CEditorActionLayerBase +{ +public: + CEditorActionAutoMap(CEditor *pEditor, int GroupIndex, int LayerIndex, const EditorTileStateChangeHistory &Changes); + + void Undo() override; + void Redo() override; + +private: + EditorTileStateChangeHistory m_Changes; + int m_TotalChanges; + + void ComputeInfos(); + void Apply(bool Undo); +}; + +// ------------------------------------------------------------- + +class CEditorActionAddLayer : public CEditorActionLayerBase +{ +public: + CEditorActionAddLayer(CEditor *pEditor, int GroupIndex, int LayerIndex, bool Duplicate = false); + + void Undo() override; + void Redo() override; + +private: + bool m_Duplicate; +}; + +class CEditorActionDeleteLayer : public CEditorActionLayerBase +{ +public: + CEditorActionDeleteLayer(CEditor *pEditor, int GroupIndex, int LayerIndex); + + void Undo() override; + void Redo() override; +}; + +class CEditorActionGroup : public IEditorAction +{ +public: + CEditorActionGroup(CEditor *pEditor, int GroupIndex, bool Delete); + + void Undo() override; + void Redo() override; + +private: + int m_GroupIndex; + bool m_Delete; + std::shared_ptr m_pGroup; +}; + +class CEditorActionEditGroupProp : public IEditorAction +{ +public: + CEditorActionEditGroupProp(CEditor *pEditor, int GroupIndex, EGroupProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_GroupIndex; + EGroupProp m_Prop; + int m_Previous; + int m_Current; + + void Apply(int Value); +}; + +template +class CEditorActionEditLayerPropBase : public CEditorActionLayerBase +{ +public: + CEditorActionEditLayerPropBase(CEditor *pEditor, int GroupIndex, int LayerIndex, E Prop, int Previous, int Current); + + virtual void Undo() override {} + virtual void Redo() override {} + +protected: + E m_Prop; + int m_Previous; + int m_Current; +}; + +class CEditorActionEditLayerProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditLayerProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + void Apply(int Value); +}; + +class CEditorActionEditLayerTilesProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditLayerTilesProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ETilesProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + + void SetSavedLayers(const std::map> &SavedLayers); + +private: + std::map> m_SavedLayers; + + void RestoreLayer(int Layer, const std::shared_ptr &pLayerTiles); +}; + +class CEditorActionEditLayerQuadsProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditLayerQuadsProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerQuadsProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + void Apply(int Value); +}; + +class CEditorActionEditLayersGroupAndOrder : public IEditorAction +{ +public: + CEditorActionEditLayersGroupAndOrder(CEditor *pEditor, int GroupIndex, const std::vector &LayerIndices, int NewGroupIndex, const std::vector &NewLayerIndices); + + void Undo() override; + void Redo() override; + +private: + int m_GroupIndex; + std::vector m_LayerIndices; + int m_NewGroupIndex; + std::vector m_NewLayerIndices; +}; + +// -------------- + +class CEditorActionAppendMap : public IEditorAction +{ +public: + struct SPrevInfo + { + int m_Groups; + int m_Images; + int m_Sounds; + int m_Envelopes; + }; + +public: + CEditorActionAppendMap(CEditor *pEditor, const char *pMapName, const SPrevInfo &PrevInfo, std::vector &vImageIndexMap); + + void Undo() override; + void Redo() override; + +private: + char m_aMapName[IO_MAX_PATH_LENGTH]; + SPrevInfo m_PrevInfo; + std::vector m_vImageIndexMap; +}; + +// -------------- + +class CEditorActionTileArt : public IEditorAction +{ +public: + CEditorActionTileArt(CEditor *pEditor, int PreviousImageCount, const char *pTileArtFile, std::vector &vImageIndexMap); + + void Undo() override; + void Redo() override; + +private: + int m_PreviousImageCount; + char m_aTileArtFile[IO_MAX_PATH_LENGTH]; + std::vector m_vImageIndexMap; +}; + +// ---------------------- + +class CEditorCommandAction : public IEditorAction +{ +public: + enum class EType + { + DELETE, + ADD, + EDIT, + MOVE_UP, + MOVE_DOWN + }; + + CEditorCommandAction(CEditor *pEditor, EType Type, int *pSelectedCommandIndex, int CommandIndex, const char *pPreviousCommand = nullptr, const char *pCurrentCommand = nullptr); + + void Undo() override; + void Redo() override; + +private: + EType m_Type; + int *m_pSelectedCommandIndex; + int m_CommandIndex; + std::string m_PreviousCommand; + std::string m_CurrentCommand; +}; + +// ------------------------------ + +class CEditorActionEnvelopeAdd : public IEditorAction +{ +public: + CEditorActionEnvelopeAdd(CEditor *pEditor, const std::shared_ptr &pEnv); + + void Undo() override; + void Redo() override; + +private: + std::shared_ptr m_pEnv; +}; + +class CEditorActionEveloppeDelete : public IEditorAction +{ +public: + CEditorActionEveloppeDelete(CEditor *pEditor, int EnvelopeIndex); + + void Undo() override; + void Redo() override; + +private: + int m_EnvelopeIndex; + std::shared_ptr m_pEnv; +}; + +class CEditorActionEnvelopeEdit : public IEditorAction +{ +public: + enum class EEditType + { + SYNC, + ORDER + }; + + CEditorActionEnvelopeEdit(CEditor *pEditor, int EnvelopeIndex, EEditType EditType, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_EnvelopeIndex; + EEditType m_EditType; + int m_Previous; + int m_Current; + std::shared_ptr m_pEnv; +}; + +class CEditorActionEnvelopeEditPoint : public IEditorAction +{ +public: + enum class EEditType + { + TIME, + VALUE, + CURVE_TYPE, + HANDLE + }; + + CEditorActionEnvelopeEditPoint(CEditor *pEditor, int EnvelopeIndex, int PointIndex, int Channel, EEditType EditType, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_EnvelopeIndex; + int m_PointIndex; + int m_Channel; + EEditType m_EditType; + int m_Previous; + int m_Current; + std::shared_ptr m_pEnv; + + void Apply(int Value); +}; + +class CEditorActionAddEnvelopePoint : public IEditorAction +{ +public: + CEditorActionAddEnvelopePoint(CEditor *pEditor, int EnvIndex, int Time, ColorRGBA Channels); + + void Undo() override; + void Redo() override; + +private: + int m_EnvIndex; + int m_Time; + ColorRGBA m_Channels; +}; + +class CEditorActionDeleteEnvelopePoint : public IEditorAction +{ +public: + CEditorActionDeleteEnvelopePoint(CEditor *pEditor, int EnvIndex, int PointIndex); + + void Undo() override; + void Redo() override; + +private: + int m_EnvIndex; + int m_PointIndex; + CEnvPoint_runtime m_Point; +}; + +class CEditorActionEditEnvelopePointValue : public IEditorAction +{ +public: + enum class EType + { + TANGENT_IN, + TANGENT_OUT, + POINT + }; + + CEditorActionEditEnvelopePointValue(CEditor *pEditor, int EnvIndex, int PointIndex, int Channel, EType Type, int OldTime, int OldValue, int NewTime, int NewValue); + + void Undo() override; + void Redo() override; + +private: + int m_EnvIndex; + int m_PtIndex; + int m_Channel; + EType m_Type; + int m_OldTime; + int m_OldValue; + int m_NewTime; + int m_NewValue; + + void Apply(bool Undo); +}; + +class CEditorActionResetEnvelopePointTangent : public IEditorAction +{ +public: + CEditorActionResetEnvelopePointTangent(CEditor *pEditor, int EnvIndex, int PointIndex, int Channel, bool In); + + void Undo() override; + void Redo() override; + +private: + int m_EnvIndex; + int m_PointIndex; + int m_Channel; + bool m_In; + int m_Previous[2]; +}; + +class CEditorActionEditLayerSoundsProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditLayerSoundsProp(CEditor *pEditor, int GroupIndex, int LayerIndex, ELayerSoundsProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + void Apply(int Value); +}; + +class CEditorActionDeleteSoundSource : public CEditorActionLayerBase +{ +public: + CEditorActionDeleteSoundSource(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex); + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + CSoundSource m_Source; +}; + +class CEditorActionEditSoundSource : public CEditorActionLayerBase +{ +public: + enum class EEditType + { + SHAPE + }; + + CEditorActionEditSoundSource(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, EEditType Type, int Value); + ~CEditorActionEditSoundSource() override; + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + EEditType m_EditType; + int m_CurrentValue; + + std::vector m_vOriginalValues; + void *m_pSavedObject; + + void Save(); +}; + +class CEditorActionEditSoundSourceProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditSoundSourceProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ESoundProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + +private: + void Apply(int Value); +}; + +class CEditorActionEditRectSoundSourceShapeProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditRectSoundSourceShapeProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ERectangleShapeProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + +private: + void Apply(int Value); +}; + +class CEditorActionEditCircleSoundSourceShapeProp : public CEditorActionEditLayerPropBase +{ +public: + CEditorActionEditCircleSoundSourceShapeProp(CEditor *pEditor, int GroupIndex, int LayerIndex, int SourceIndex, ECircleShapeProp Prop, int Previous, int Current); + + void Undo() override; + void Redo() override; + +private: + int m_SourceIndex; + +private: + void Apply(int Value); +}; + +class CEditorActionNewEmptySound : public CEditorActionLayerBase +{ +public: + CEditorActionNewEmptySound(CEditor *pEditor, int GroupIndex, int LayerIndex, int x, int y); + + void Undo() override; + void Redo() override; + +private: + int m_X; + int m_Y; +}; + +class CEditorActionNewEmptyQuad : public CEditorActionLayerBase +{ +public: + CEditorActionNewEmptyQuad(CEditor *pEditor, int GroupIndex, int LayerIndex, int x, int y); + + void Undo() override; + void Redo() override; + +private: + int m_X; + int m_Y; +}; + +class CEditorActionNewQuad : public CEditorActionLayerBase +{ +public: + CEditorActionNewQuad(CEditor *pEditor, int GroupIndex, int LayerIndex); + + void Undo() override; + void Redo() override; + +private: + CQuad m_Quad; +}; + +#endif diff --git a/src/game/editor/editor_history.cpp b/src/game/editor/editor_history.cpp new file mode 100644 index 00000000000..46110de2d55 --- /dev/null +++ b/src/game/editor/editor_history.cpp @@ -0,0 +1,65 @@ +#include + +#include "editor.h" +#include "editor_actions.h" +#include "editor_history.h" + +void CEditorHistory::RecordAction(const std::shared_ptr &pAction) +{ + RecordAction(pAction, nullptr); +} + +void CEditorHistory::Execute(const std::shared_ptr &pAction, const char *pDisplay) +{ + pAction->Redo(); + RecordAction(pAction, pDisplay); +} + +void CEditorHistory::RecordAction(const std::shared_ptr &pAction, const char *pDisplay) +{ + m_vpRedoActions.clear(); + + if((int)m_vpUndoActions.size() >= g_Config.m_ClEditorMaxHistory) + { + m_vpUndoActions.pop_front(); + } + + if(pDisplay == nullptr) + m_vpUndoActions.emplace_back(pAction); + else + m_vpUndoActions.emplace_back(std::make_shared(m_pEditor, std::vector>{pAction}, pDisplay)); +} + +bool CEditorHistory::Undo() +{ + if(m_vpUndoActions.empty()) + return false; + + auto pLastAction = m_vpUndoActions.back(); + m_vpUndoActions.pop_back(); + + pLastAction->Undo(); + + m_vpRedoActions.emplace_back(pLastAction); + return true; +} + +bool CEditorHistory::Redo() +{ + if(m_vpRedoActions.empty()) + return false; + + auto pLastAction = m_vpRedoActions.back(); + m_vpRedoActions.pop_back(); + + pLastAction->Redo(); + + m_vpUndoActions.emplace_back(pLastAction); + return true; +} + +void CEditorHistory::Clear() +{ + m_vpUndoActions.clear(); + m_vpRedoActions.clear(); +} diff --git a/src/game/editor/editor_history.h b/src/game/editor/editor_history.h new file mode 100644 index 00000000000..144147140f6 --- /dev/null +++ b/src/game/editor/editor_history.h @@ -0,0 +1,37 @@ +#ifndef GAME_EDITOR_EDITOR_HISTORY_H +#define GAME_EDITOR_EDITOR_HISTORY_H + +#include "editor_action.h" + +#include + +class CEditorHistory +{ +public: + CEditorHistory() + { + m_pEditor = nullptr; + } + + ~CEditorHistory() + { + Clear(); + } + + void RecordAction(const std::shared_ptr &pAction); + void RecordAction(const std::shared_ptr &pAction, const char *pDisplay); + void Execute(const std::shared_ptr &pAction, const char *pDisplay = nullptr); + + bool Undo(); + bool Redo(); + + void Clear(); + bool CanUndo() const { return !m_vpUndoActions.empty(); } + bool CanRedo() const { return !m_vpRedoActions.empty(); } + + CEditor *m_pEditor; + std::deque> m_vpUndoActions; + std::deque> m_vpRedoActions; +}; + +#endif diff --git a/src/game/editor/editor_props.cpp b/src/game/editor/editor_props.cpp new file mode 100644 index 00000000000..7d57b9c8f2f --- /dev/null +++ b/src/game/editor/editor_props.cpp @@ -0,0 +1,282 @@ +#include "editor.h" + +#include +#include + +int CEditor::DoProperties(CUIRect *pToolbox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color) +{ + auto Res = DoPropertiesWithState(pToolbox, pProps, pIDs, pNewVal, Color); + return Res.m_Value; +} + +template +SEditResult CEditor::DoPropertiesWithState(CUIRect *pToolBox, CProperty *pProps, int *pIDs, int *pNewVal, ColorRGBA Color) +{ + int Change = -1; + EEditState State = EEditState::EDITING; + + for(int i = 0; pProps[i].m_pName; 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); + + if(pProps[i].m_Type == PROPTYPE_INT_STEP) + { + 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); + int NewValue = NewValueRes.m_Value; + if(NewValue != pProps[i].m_Value || 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")) + { + *pNewVal = pProps[i].m_Value - 1; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, nullptr, 0, &Inc, 0, "Increase")) + { + *pNewVal = pProps[i].m_Value + 1; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_BOOL) + { + CUIRect No, Yes; + Shifter.VSplitMid(&No, &Yes); + if(DoButton_ButtonDec(&pIDs[i], "No", !pProps[i].m_Value, &No, 0, "")) + { + *pNewVal = 0; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonInc(((char *)&pIDs[i]) + 1, "Yes", pProps[i].m_Value, &Yes, 0, "")) + { + *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; + Shifter.VSplitRight(10.0f, &Shifter, &Inc); + Shifter.VSplitLeft(10.0f, &Dec, &Shifter); + const bool Shift = Input()->ShiftIsPressed(); + 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); + int NewValue = NewValueRes.m_Value; + if(DoButton_ButtonDec(&pIDs[i] + 1, nullptr, 0, &Dec, 0, "Decrease")) + { + 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")) + { + NewValue = (pProps[i].m_Value + Step) / Step * Step; + State = EEditState::ONE_GO; + } + + if(NewValue != pProps[i].m_Value || NewValueRes.m_State != EEditState::EDITING) + { + *pNewVal = NewValue % 360; + Change = i; + State = NewValueRes.m_State; + } + } + else if(pProps[i].m_Type == PROPTYPE_COLOR) + { + const auto &&SetColor = [&](ColorRGBA NewColor) { + const int NewValue = NewColor.PackAlphaLast(); + if(NewValue != pProps[i].m_Value || m_ColorPickerPopupContext.m_State != EEditState::EDITING) + { + *pNewVal = NewValue; + Change = i; + State = m_ColorPickerPopupContext.m_State; + } + }; + DoColorPickerButton(&pIDs[i], &Shifter, ColorRGBA::UnpackAlphaLast(pProps[i].m_Value), SetColor); + } + else if(pProps[i].m_Type == PROPTYPE_IMAGE) + { + const char *pName; + if(pProps[i].m_Value < 0) + pName = "None"; + 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()); + + int r = PopupSelectImageResult(); + if(r >= -1) + { + *pNewVal = r; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_SHIFT) + { + CUIRect Left, Right, Up, Down; + 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); + 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")) + { + *pNewVal = DIRECTION_LEFT; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonInc(((char *)&pIDs[i]) + 3, "+", 0, &Right, 0, "Right")) + { + *pNewVal = DIRECTION_RIGHT; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonDec(((char *)&pIDs[i]) + 1, "-", 0, &Up, 0, "Up")) + { + *pNewVal = DIRECTION_UP; + Change = i; + State = EEditState::ONE_GO; + } + if(DoButton_ButtonInc(((char *)&pIDs[i]) + 2, "+", 0, &Down, 0, "Down")) + { + *pNewVal = DIRECTION_DOWN; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_SOUND) + { + const char *pName; + if(pProps[i].m_Value < 0) + pName = "None"; + 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()); + + int r = PopupSelectSoundResult(); + if(r >= -1) + { + *pNewVal = r; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_AUTOMAPPER) + { + const char *pName; + if(pProps[i].m_Value < 0 || pProps[i].m_Min < 0 || pProps[i].m_Min >= (int)m_Map.m_vpImages.size()) + pName = "None"; + 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()); + + int r = PopupSelectConfigAutoMapResult(); + if(r >= -1) + { + *pNewVal = r; + Change = i; + State = EEditState::ONE_GO; + } + } + else if(pProps[i].m_Type == PROPTYPE_ENVELOPE) + { + CUIRect Inc, Dec; + char aBuf[8]; + int CurValue = pProps[i].m_Value; + + Shifter.VSplitRight(10.0f, &Shifter, &Inc); + Shifter.VSplitLeft(10.0f, &Dec, &Shifter); + + if(CurValue <= 0) + str_copy(aBuf, "None:"); + else if(m_Map.m_vpEnvelopes[CurValue - 1]->m_aName[0]) + { + str_format(aBuf, sizeof(aBuf), "%s:", m_Map.m_vpEnvelopes[CurValue - 1]->m_aName); + if(!str_endswith(aBuf, ":")) + { + aBuf[sizeof(aBuf) - 2] = ':'; + aBuf[sizeof(aBuf) - 1] = '\0'; + } + } + 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); + int NewVal = NewValueRes.m_Value; + if(NewVal != CurValue || 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")) + { + *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")) + { + *pNewVal = pProps[i].m_Value + 1; + Change = i; + State = EEditState::ONE_GO; + } + } + } + + 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); diff --git a/src/game/editor/editor_trackers.cpp b/src/game/editor/editor_trackers.cpp new file mode 100644 index 00000000000..8f3660422fc --- /dev/null +++ b/src/game/editor/editor_trackers.cpp @@ -0,0 +1,632 @@ +#include "editor_trackers.h" + +#include +#include + +#include "editor.h" +#include "editor_actions.h" + +CQuadEditTracker::CQuadEditTracker() : + m_pEditor(nullptr), m_TrackedProp(EQuadProp::PROP_NONE) {} + +CQuadEditTracker::~CQuadEditTracker() +{ + m_InitalPoints.clear(); + m_vSelectedQuads.clear(); +} + +void CQuadEditTracker::BeginQuadTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads) +{ + if(m_Tracking) + return; + m_Tracking = true; + m_vSelectedQuads.clear(); + m_pLayer = pLayer; + // Init all points + for(auto QuadIndex : vSelectedQuads) + { + auto &pQuad = pLayer->m_vQuads[QuadIndex]; + m_InitalPoints[QuadIndex] = std::vector(pQuad.m_aPoints, pQuad.m_aPoints + 5); + m_vSelectedQuads.push_back(QuadIndex); + } +} + +void CQuadEditTracker::EndQuadTrack() +{ + if(!m_Tracking) + 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)); + } + 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) +{ + if(m_TrackedProp != EQuadProp::PROP_NONE) + return; + m_TrackedProp = Prop; + m_pLayer = pLayer; + m_vSelectedQuads = vSelectedQuads; + m_PreviousValues.clear(); + + for(auto QuadIndex : vSelectedQuads) + { + auto &Quad = pLayer->m_vQuads[QuadIndex]; + if(Prop == EQuadProp::PROP_POS_X || Prop == EQuadProp::PROP_POS_Y) + m_InitalPoints[QuadIndex] = std::vector(Quad.m_aPoints, Quad.m_aPoints + 5); + else if(Prop == EQuadProp::PROP_POS_ENV) + m_PreviousValues[QuadIndex] = Quad.m_PosEnv; + else if(Prop == EQuadProp::PROP_POS_ENV_OFFSET) + m_PreviousValues[QuadIndex] = Quad.m_PosEnvOffset; + else if(Prop == EQuadProp::PROP_COLOR_ENV) + m_PreviousValues[QuadIndex] = Quad.m_ColorEnv; + else if(Prop == EQuadProp::PROP_COLOR_ENV_OFFSET) + m_PreviousValues[QuadIndex] = Quad.m_ColorEnvOffset; + } +} +void CQuadEditTracker::EndQuadPropTrack(EQuadProp Prop) +{ + if(m_TrackedProp != Prop) + return; + m_TrackedProp = EQuadProp::PROP_NONE; + + 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)); + } + else + { + int Value = 0; + if(Prop == EQuadProp::PROP_POS_ENV) + Value = Quad.m_PosEnv; + else if(Prop == EQuadProp::PROP_POS_ENV_OFFSET) + Value = Quad.m_PosEnvOffset; + else if(Prop == EQuadProp::PROP_COLOR_ENV) + Value = Quad.m_ColorEnv; + else if(Prop == EQuadProp::PROP_COLOR_ENV_OFFSET) + 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)); + } + } + + if(!vpActions.empty()) + 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) +{ + if(!m_vTrackedProps.empty()) + return; + + m_pLayer = pLayer; + m_SelectedQuadPoints = SelectedQuadPoints; + m_vSelectedQuads = vSelectedQuads; + m_PreviousValuesPoint.clear(); + + for(auto QuadIndex : vSelectedQuads) + { + m_PreviousValuesPoint[QuadIndex] = std::vector>(4); + } +} + +void CQuadEditTracker::AddQuadPointPropTrack(EQuadPointProp Prop) +{ + if(std::find(m_vTrackedProps.begin(), m_vTrackedProps.end(), Prop) != m_vTrackedProps.end()) + return; + + m_vTrackedProps.push_back(Prop); + + for(auto QuadIndex : m_vSelectedQuads) + { + auto &Quad = m_pLayer->m_vQuads[QuadIndex]; + if(Prop == EQuadPointProp::PROP_POS_X || Prop == EQuadPointProp::PROP_POS_Y) + m_InitalPoints[QuadIndex] = std::vector(Quad.m_aPoints, Quad.m_aPoints + 5); + else if(Prop == EQuadPointProp::PROP_COLOR) + { + for(int v = 0; v < 4; v++) + { + if(m_SelectedQuadPoints & (1 << v)) + { + int Color = PackColor(Quad.m_aColors[v]); + m_PreviousValuesPoint[QuadIndex][v][Prop] = Color; + } + } + } + else if(Prop == EQuadPointProp::PROP_TEX_U) + { + for(int v = 0; v < 4; v++) + { + if(m_SelectedQuadPoints & (1 << v)) + { + m_PreviousValuesPoint[QuadIndex][v][Prop] = Quad.m_aTexcoords[v].x; + } + } + } + else if(Prop == EQuadPointProp::PROP_TEX_V) + { + for(int v = 0; v < 4; v++) + { + if(m_SelectedQuadPoints & (1 << v)) + { + m_PreviousValuesPoint[QuadIndex][v][Prop] = Quad.m_aTexcoords[v].y; + } + } + } + } +} + +void CQuadEditTracker::EndQuadPointPropTrack(EQuadPointProp Prop) +{ + auto It = std::find(m_vTrackedProps.begin(), m_vTrackedProps.end(), Prop); + if(It == m_vTrackedProps.end()) + return; + + m_vTrackedProps.erase(It); + + 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)); + } + else + { + int Value = 0; + for(int v = 0; v < 4; v++) + { + if(m_SelectedQuadPoints & (1 << v)) + { + if(Prop == EQuadPointProp::PROP_COLOR) + { + Value = PackColor(Quad.m_aColors[v]); + } + else if(Prop == EQuadPointProp::PROP_TEX_U) + { + Value = Quad.m_aTexcoords[v].x; + } + else if(Prop == EQuadPointProp::PROP_TEX_V) + { + Value = Quad.m_aTexcoords[v].y; + } + + 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)); + } + } + } + } + + if(!vpActions.empty()) + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions)); +} + +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)); + } + else + { + int Value = 0; + for(int v = 0; v < 4; v++) + { + if(m_SelectedQuadPoints & (1 << v)) + { + if(Prop == EQuadPointProp::PROP_COLOR) + { + Value = PackColor(Quad.m_aColors[v]); + } + else if(Prop == EQuadPointProp::PROP_TEX_U) + { + Value = Quad.m_aTexcoords[v].x; + } + else if(Prop == EQuadPointProp::PROP_TEX_V) + { + Value = Quad.m_aTexcoords[v].y; + } + + 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)); + } + } + } + } + } + + if(!vpActions.empty()) + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions)); + + 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) +{ + if(m_TrackedOp == Operation) + return; + + if(m_TrackedOp != EEnvelopeEditorOp::OP_NONE) + { + Stop(true); + } + + m_TrackedOp = Operation; + + if(Operation == EEnvelopeEditorOp::OP_DRAG_POINT || Operation == EEnvelopeEditorOp::OP_DRAG_POINT_X || Operation == EEnvelopeEditorOp::OP_DRAG_POINT_Y || Operation == EEnvelopeEditorOp::OP_SCALE) + { + HandlePointDragStart(); + } +} + +void CEnvelopeEditorOperationTracker::Stop(bool Switch) +{ + if(m_TrackedOp == EEnvelopeEditorOp::OP_NONE) + return; + + if(m_TrackedOp == EEnvelopeEditorOp::OP_DRAG_POINT || m_TrackedOp == EEnvelopeEditorOp::OP_DRAG_POINT_X || m_TrackedOp == EEnvelopeEditorOp::OP_DRAG_POINT_Y || m_TrackedOp == EEnvelopeEditorOp::OP_SCALE) + { + HandlePointDragEnd(Switch); + } + + m_TrackedOp = EEnvelopeEditorOp::OP_NONE; +} + +void CEnvelopeEditorOperationTracker::HandlePointDragStart() +{ + // Figure out which points are selected and which channels + // Save their X and Y position (time and value) + auto pEnv = m_pEditor->m_Map.m_vpEnvelopes[m_pEditor->m_SelectedEnvelope]; + + for(auto [PointIndex, Channel] : m_pEditor->m_vSelectedEnvelopePoints) + { + auto &Point = pEnv->m_vPoints[PointIndex]; + auto &Data = m_SavedValues[PointIndex]; + Data.m_Values[Channel] = Point.m_aValues[Channel]; + if(Data.m_Used) + continue; + Data.m_Time = Point.m_Time; + Data.m_Used = true; + } +} + +void CEnvelopeEditorOperationTracker::HandlePointDragEnd(bool Switch) +{ + if(Switch && m_TrackedOp != EEnvelopeEditorOp::OP_SCALE) + return; + + int EnvIndex = m_pEditor->m_SelectedEnvelope; + auto pEnv = m_pEditor->m_Map.m_vpEnvelopes[EnvIndex]; + std::vector> vpActions; + + for(auto const &Entry : m_SavedValues) + { + int PointIndex = Entry.first; + auto &Point = pEnv->m_vPoints[PointIndex]; + const auto &Data = Entry.second; + + if(Data.m_Time != Point.m_Time) + { // Save time + vpActions.push_back(std::make_shared(m_pEditor, EnvIndex, PointIndex, 0, CEditorActionEnvelopeEditPoint::EEditType::TIME, Data.m_Time, Point.m_Time)); + } + + for(auto Value : Data.m_Values) + { + // Value.second is the saved value, Value.first is the channel + int Channel = Value.first; + if(Value.second != Point.m_aValues[Channel]) + { // Save value + vpActions.push_back(std::make_shared(m_pEditor, EnvIndex, PointIndex, Channel, CEditorActionEnvelopeEditPoint::EEditType::VALUE, Value.second, Point.m_aValues[Channel])); + } + } + } + + if(!vpActions.empty()) + { + m_pEditor->m_EnvelopeEditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, "Envelope point drag")); + } + + m_SavedValues.clear(); +} + +// ----------------------------------------------------------------------- + +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)); +} + +int CLayerPropTracker::PropToValue(ELayerProp Prop) +{ + switch(Prop) + { + case ELayerProp::PROP_GROUP: return m_pEditor->m_SelectedGroup; + case ELayerProp::PROP_HQ: return m_pObject->m_Flags; + case ELayerProp::PROP_ORDER: return m_pEditor->m_vSelectedLayers[0]; + default: return 0; + } +} + +// ----------------------------------------------------------------------- + +bool CLayerTilesPropTracker::EndChecker(ETilesProp Prop, EEditState State, int Value) +{ + return (State == EEditState::END || State == EEditState::ONE_GO) && (Value != m_OriginalValue || Prop == ETilesProp::PROP_SHIFT); +} + +void CLayerTilesPropTracker::OnStart(ETilesProp Prop) +{ + if(Prop == ETilesProp::PROP_WIDTH || Prop == ETilesProp::PROP_HEIGHT) + { + m_SavedLayers[LAYERTYPE_TILES] = m_pObject->Duplicate(); + if(m_pObject->m_Game || m_pObject->m_Front || m_pObject->m_Switch || m_pObject->m_Speedup || m_pObject->m_Tune || m_pObject->m_Tele) + { // Need to save all entities layers when any entity layer + if(m_pEditor->m_Map.m_pFrontLayer && !m_pObject->m_Front) + m_SavedLayers[LAYERTYPE_FRONT] = m_pEditor->m_Map.m_pFrontLayer->Duplicate(); + if(m_pEditor->m_Map.m_pTeleLayer && !m_pObject->m_Tele) + m_SavedLayers[LAYERTYPE_TELE] = m_pEditor->m_Map.m_pTeleLayer->Duplicate(); + if(m_pEditor->m_Map.m_pSwitchLayer && !m_pObject->m_Switch) + m_SavedLayers[LAYERTYPE_SWITCH] = m_pEditor->m_Map.m_pSwitchLayer->Duplicate(); + if(m_pEditor->m_Map.m_pSpeedupLayer && !m_pObject->m_Speedup) + m_SavedLayers[LAYERTYPE_SPEEDUP] = m_pEditor->m_Map.m_pSpeedupLayer->Duplicate(); + if(m_pEditor->m_Map.m_pTuneLayer && !m_pObject->m_Tune) + m_SavedLayers[LAYERTYPE_TUNE] = m_pEditor->m_Map.m_pTuneLayer->Duplicate(); + if(!m_pObject->m_Game) + m_SavedLayers[LAYERTYPE_GAME] = m_pEditor->m_Map.m_pGameLayer->Duplicate(); + } + } + else if(Prop == ETilesProp::PROP_SHIFT) + { + m_SavedLayers[LAYERTYPE_TILES] = m_pObject->Duplicate(); + } +} + +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); + + pAction->SetSavedLayers(m_SavedLayers); + m_SavedLayers.clear(); + + m_pEditor->m_EditorHistory.RecordAction(pAction); +} + +int CLayerTilesPropTracker::PropToValue(ETilesProp Prop) +{ + switch(Prop) + { + case ETilesProp::PROP_AUTOMAPPER: return m_pObject->m_AutoMapperConfig; + case ETilesProp::PROP_COLOR: return PackColor(m_pObject->m_Color); + case ETilesProp::PROP_COLOR_ENV: return m_pObject->m_ColorEnv; + case ETilesProp::PROP_COLOR_ENV_OFFSET: return m_pObject->m_ColorEnvOffset; + case ETilesProp::PROP_HEIGHT: return m_pObject->m_Height; + case ETilesProp::PROP_WIDTH: return m_pObject->m_Width; + case ETilesProp::PROP_IMAGE: return m_pObject->m_Image; + case ETilesProp::PROP_SEED: return m_pObject->m_Seed; + case ETilesProp::PROP_SHIFT_BY: return m_pEditor->m_ShiftBy; + default: return 0; + } +} + +// ------------------------------ + +void CLayerTilesCommonPropTracker::OnStart(ETilesCommonProp Prop) +{ + for(auto &pLayer : m_vpLayers) + { + if(Prop == ETilesCommonProp::PROP_SHIFT) + { + m_SavedLayers[pLayer][LAYERTYPE_TILES] = pLayer->Duplicate(); + } + } +} + +void CLayerTilesCommonPropTracker::OnEnd(ETilesCommonProp Prop, int Value) +{ + std::vector> vpActions; + + static std::map s_PropMap{ + {ETilesCommonProp::PROP_COLOR, ETilesProp::PROP_COLOR}, + {ETilesCommonProp::PROP_SHIFT, ETilesProp::PROP_SHIFT}, + {ETilesCommonProp::PROP_SHIFT_BY, ETilesProp::PROP_SHIFT_BY}}; + + int j = 0; + 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); + pAction->SetSavedLayers(m_SavedLayers[pLayer]); + vpActions.push_back(pAction); + } + + char aDisplay[256]; + static const char *s_apNames[] = { + "width", + "height", + "shift", + "shift by", + "color", + }; + + str_format(aDisplay, sizeof(aDisplay), "Edit %d layers common property: %s", (int)m_vpLayers.size(), s_apNames[(int)Prop]); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, aDisplay)); +} + +bool CLayerTilesCommonPropTracker::EndChecker(ETilesCommonProp Prop, EEditState State, int Value) +{ + return (State == EEditState::END || State == EEditState::ONE_GO) && (Value != m_OriginalValue || Prop == ETilesCommonProp::PROP_SHIFT); +} + +int CLayerTilesCommonPropTracker::PropToValue(ETilesCommonProp Prop) +{ + if(Prop == ETilesCommonProp::PROP_SHIFT_BY) + return m_pEditor->m_ShiftBy; + return 0; +} + +// ------------------------------ + +void CLayerGroupPropTracker::OnEnd(EGroupProp Prop, int Value) +{ + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, Prop, m_OriginalValue, Value)); +} + +int CLayerGroupPropTracker::PropToValue(EGroupProp Prop) +{ + switch(Prop) + { + case EGroupProp::PROP_ORDER: return m_pEditor->m_SelectedGroup; + case EGroupProp::PROP_POS_X: return m_pObject->m_OffsetX; + case EGroupProp::PROP_POS_Y: return m_pObject->m_OffsetY; + case EGroupProp::PROP_PARA_X: return m_pObject->m_ParallaxX; + case EGroupProp::PROP_PARA_Y: return m_pObject->m_ParallaxY; + case EGroupProp::PROP_USE_CLIPPING: return m_pObject->m_UseClipping; + case EGroupProp::PROP_CLIP_X: return m_pObject->m_ClipX; + case EGroupProp::PROP_CLIP_Y: return m_pObject->m_ClipY; + case EGroupProp::PROP_CLIP_W: return m_pObject->m_ClipW; + case EGroupProp::PROP_CLIP_H: return m_pObject->m_ClipH; + default: return 0; + } +} + +// ------------------------------------------------------------------ + +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); + m_pEditor->m_EditorHistory.RecordAction(pAction); +} + +int CLayerQuadsPropTracker::PropToValue(ELayerQuadsProp Prop) +{ + if(Prop == ELayerQuadsProp::PROP_IMAGE) + return m_pObject->m_Image; + return 0; +} + +// ------------------------------------------------------------------- + +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); + m_pEditor->m_EditorHistory.RecordAction(pAction); +} + +int CLayerSoundsPropTracker::PropToValue(ELayerSoundsProp Prop) +{ + if(Prop == ELayerSoundsProp::PROP_SOUND) + return m_pObject->m_Sound; + return 0; +} + +// ---- + +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)); +} + +int CSoundSourcePropTracker::PropToValue(ESoundProp Prop) +{ + switch(Prop) + { + case ESoundProp::PROP_POS_X: return m_pObject->m_Position.x; + case ESoundProp::PROP_POS_Y: return m_pObject->m_Position.y; + case ESoundProp::PROP_LOOP: return m_pObject->m_Loop; + case ESoundProp::PROP_PAN: return m_pObject->m_Pan; + case ESoundProp::PROP_TIME_DELAY: return m_pObject->m_TimeDelay; + case ESoundProp::PROP_FALLOFF: return m_pObject->m_Falloff; + case ESoundProp::PROP_POS_ENV: return m_pObject->m_PosEnv; + case ESoundProp::PROP_POS_ENV_OFFSET: return m_pObject->m_PosEnvOffset; + case ESoundProp::PROP_SOUND_ENV: return m_pObject->m_SoundEnv; + case ESoundProp::PROP_SOUND_ENV_OFFSET: return m_pObject->m_SoundEnvOffset; + default: return 0; + } +} + +// ---- + +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)); +} + +int CSoundSourceRectShapePropTracker::PropToValue(ERectangleShapeProp Prop) +{ + switch(Prop) + { + case ERectangleShapeProp::PROP_RECTANGLE_WIDTH: return m_pObject->m_Shape.m_Rectangle.m_Width; + case ERectangleShapeProp::PROP_RECTANGLE_HEIGHT: return m_pObject->m_Shape.m_Rectangle.m_Height; + default: return 0; + } +} + +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)); +} + +int CSoundSourceCircleShapePropTracker::PropToValue(ECircleShapeProp Prop) +{ + switch(Prop) + { + case ECircleShapeProp::PROP_CIRCLE_RADIUS: return m_pObject->m_Shape.m_Circle.m_Radius; + default: return 0; + } +} diff --git a/src/game/editor/editor_trackers.h b/src/game/editor/editor_trackers.h new file mode 100644 index 00000000000..156096b6af8 --- /dev/null +++ b/src/game/editor/editor_trackers.h @@ -0,0 +1,271 @@ +#ifndef GAME_EDITOR_EDITOR_TRACKERS_H +#define GAME_EDITOR_EDITOR_TRACKERS_H + +#include +#include +#include + +#include +#include +#include + +class CEditor; +class CLayerTiles; +class CLayerGroup; +class CLayerSounds; +struct CSoundSource; + +class CQuadEditTracker +{ +public: + CQuadEditTracker(); + ~CQuadEditTracker(); + + void BeginQuadTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads); + void EndQuadTrack(); + + void BeginQuadPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, EQuadProp Prop); + void EndQuadPropTrack(EQuadProp Prop); + + void BeginQuadPointPropTrack(const std::shared_ptr &pLayer, const std::vector &vSelectedQuads, int SelectedQuadPoints); + void AddQuadPointPropTrack(EQuadPointProp Prop); + void EndQuadPointPropTrack(EQuadPointProp Prop); + void EndQuadPointPropTrackAll(); + + CEditor *m_pEditor; + +private: + std::vector m_vSelectedQuads; + int m_SelectedQuadPoints; + std::map> m_InitalPoints; + + bool m_Tracking = false; + std::shared_ptr m_pLayer; + + EQuadProp m_TrackedProp; + 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; +}; + +enum class EEnvelopeEditorOp +{ + OP_NONE = 0, + OP_SELECT, + OP_DRAG_POINT, + OP_DRAG_POINT_X, + OP_DRAG_POINT_Y, + OP_CONTEXT_MENU, + OP_BOX_SELECT, + OP_SCALE +}; + +class CEnvelopeEditorOperationTracker +{ +public: + CEnvelopeEditorOperationTracker() = default; + + void Begin(EEnvelopeEditorOp Operation); + void Stop(bool Switch = true); + inline void Reset() { m_TrackedOp = EEnvelopeEditorOp::OP_NONE; } + + CEditor *m_pEditor; + +private: + EEnvelopeEditorOp m_TrackedOp = EEnvelopeEditorOp::OP_NONE; + + struct SPointData + { + bool m_Used; + int m_Time; + std::map m_Values; + }; + + std::map m_SavedValues; + + void HandlePointDragStart(); + void HandlePointDragEnd(bool Switch); +}; + +template +class CPropTracker +{ +public: + CPropTracker(CEditor *pEditor) : + m_pEditor(pEditor), m_OriginalValue(0) {} + CEditor *m_pEditor; + + void Begin(T *pObject, E Prop, EEditState State) + { + if(Prop == static_cast(-1)) + return; + m_pObject = pObject; + int Value = PropToValue(Prop); + if(StartChecker(Prop, State, Value)) + { + m_OriginalValue = Value; + OnStart(Prop); + } + } + + void End(E Prop, EEditState State) + { + if(Prop == static_cast(-1)) + return; + int Value = PropToValue(Prop); + if(EndChecker(Prop, State, Value)) + { + OnEnd(Prop, Value); + } + } + +protected: + virtual void OnStart(E Prop) {} + virtual void OnEnd(E Prop, int Value) {} + virtual int PropToValue(E Prop) { return 0; } + virtual bool StartChecker(E Prop, EEditState State, int Value) + { + return State == EEditState::START || State == EEditState::ONE_GO; + } + virtual bool EndChecker(E Prop, EEditState State, int Value) + { + return (State == EEditState::END || State == EEditState::ONE_GO) && (Value != m_OriginalValue); + } + + int m_OriginalValue; + T *m_pObject; +}; + +class CLayerPropTracker : public CPropTracker +{ +public: + CLayerPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnEnd(ELayerProp Prop, int Value) override; + int PropToValue(ELayerProp Prop) override; +}; + +class CLayerTilesPropTracker : public CPropTracker +{ +public: + CLayerTilesPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnStart(ETilesProp Prop) override; + void OnEnd(ETilesProp Prop, int Value) override; + bool EndChecker(ETilesProp Prop, EEditState State, int Value) override; + + int PropToValue(ETilesProp Prop) override; + +private: + std::map> m_SavedLayers; +}; + +class CLayerTilesCommonPropTracker : public CPropTracker +{ +public: + CLayerTilesCommonPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnStart(ETilesCommonProp Prop) override; + void OnEnd(ETilesCommonProp Prop, int Value) override; + bool EndChecker(ETilesCommonProp Prop, EEditState State, int Value) override; + + int PropToValue(ETilesCommonProp Prop) override; + +private: + std::map, std::map>> m_SavedLayers; + +public: + std::vector> m_vpLayers; + std::vector m_vLayerIndices; +}; + +class CLayerGroupPropTracker : public CPropTracker +{ +public: + CLayerGroupPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnEnd(EGroupProp Prop, int Value) override; + int PropToValue(EGroupProp Prop) override; +}; + +class CLayerQuadsPropTracker : public CPropTracker +{ +public: + CLayerQuadsPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnEnd(ELayerQuadsProp Prop, int Value) override; + int PropToValue(ELayerQuadsProp Prop) override; +}; + +class CLayerSoundsPropTracker : public CPropTracker +{ +public: + CLayerSoundsPropTracker(CEditor *pEditor) : + CPropTracker(pEditor){}; + +protected: + void OnEnd(ELayerSoundsProp Prop, int Value) override; + int PropToValue(ELayerSoundsProp Prop) override; +}; + +class CSoundSourcePropTracker : public CPropTracker +{ +public: + CSoundSourcePropTracker(CEditor *pEditor) : + CPropTracker(pEditor) {} + +protected: + void OnEnd(ESoundProp Prop, int Value) override; + int PropToValue(ESoundProp Prop) override; +}; + +class CSoundSourceRectShapePropTracker : public CPropTracker +{ +public: + CSoundSourceRectShapePropTracker(CEditor *pEditor) : + CPropTracker(pEditor) {} + +protected: + void OnEnd(ERectangleShapeProp Prop, int Value) override; + int PropToValue(ERectangleShapeProp Prop) override; +}; + +class CSoundSourceCircleShapePropTracker : public CPropTracker +{ +public: + CSoundSourceCircleShapePropTracker(CEditor *pEditor) : + CPropTracker(pEditor) {} + +protected: + void OnEnd(ECircleShapeProp Prop, int Value) override; + int PropToValue(ECircleShapeProp Prop) override; +}; + +#endif diff --git a/src/game/editor/map_grid.cpp b/src/game/editor/map_grid.cpp index 43e413bad93..e241642aa6d 100644 --- a/src/game/editor/map_grid.cpp +++ b/src/game/editor/map_grid.cpp @@ -74,7 +74,7 @@ int CMapGrid::GridLineDistance() const return 512; } -void CMapGrid::SnapToGrid(float &x, float &y) +void CMapGrid::SnapToGrid(float &x, float &y) const { const int GridDistance = GridLineDistance() * m_GridFactor; x = (int)((x + (x >= 0 ? 1.0f : -1.0f) * GridDistance / 2) / GridDistance) * GridDistance; diff --git a/src/game/editor/map_grid.h b/src/game/editor/map_grid.h index dc589018557..3a6e26a3be1 100644 --- a/src/game/editor/map_grid.h +++ b/src/game/editor/map_grid.h @@ -9,7 +9,7 @@ class CMapGrid : public CEditorComponent void OnReset() override; void OnRender(CUIRect View) override; - void SnapToGrid(float &x, float &y); + void SnapToGrid(float &x, float &y) const; int GridLineDistance() const; /** diff --git a/src/game/editor/map_view.cpp b/src/game/editor/map_view.cpp index 35bdd8c03c3..99337b8f76b 100644 --- a/src/game/editor/map_view.cpp +++ b/src/game/editor/map_view.cpp @@ -135,7 +135,7 @@ void CMapView::ResetZoom() m_Zoom.SetValue(100.0f); } -float CMapView::ScaleLength(float Value) +float CMapView::ScaleLength(float Value) const { return m_WorldZoom * Value; } diff --git a/src/game/editor/map_view.h b/src/game/editor/map_view.h index 5da4db23609..5aa86254d64 100644 --- a/src/game/editor/map_view.h +++ b/src/game/editor/map_view.h @@ -34,7 +34,7 @@ class CMapView : public CEditorComponent /** * Scale length according to zoom value. */ - float ScaleLength(float Value); + float ScaleLength(float Value) const; bool m_ShowPicker; // TODO: make private diff --git a/src/game/editor/mapitems.h b/src/game/editor/mapitems.h new file mode 100644 index 00000000000..7873ee7e736 --- /dev/null +++ b/src/game/editor/mapitems.h @@ -0,0 +1,125 @@ +#ifndef GAME_EDITOR_MAPITEMS_H +#define GAME_EDITOR_MAPITEMS_H + +enum class EQuadProp +{ + PROP_NONE = -1, + PROP_ORDER, + PROP_POS_X, + PROP_POS_Y, + PROP_POS_ENV, + PROP_POS_ENV_OFFSET, + PROP_COLOR_ENV, + PROP_COLOR_ENV_OFFSET, + NUM_PROPS, +}; + +enum class EQuadPointProp +{ + PROP_NONE = -1, + PROP_POS_X, + PROP_POS_Y, + PROP_COLOR, + PROP_TEX_U, + PROP_TEX_V, + NUM_PROPS, +}; + +enum class ESoundProp +{ + PROP_NONE = -1, + PROP_POS_X, + PROP_POS_Y, + PROP_LOOP, + PROP_PAN, + PROP_TIME_DELAY, + PROP_FALLOFF, + PROP_POS_ENV, + PROP_POS_ENV_OFFSET, + PROP_SOUND_ENV, + PROP_SOUND_ENV_OFFSET, + NUM_PROPS, +}; + +enum class ERectangleShapeProp +{ + PROP_NONE = -1, + PROP_RECTANGLE_WIDTH, + PROP_RECTANGLE_HEIGHT, + NUM_RECTANGLE_PROPS, +}; + +enum class ECircleShapeProp +{ + PROP_NONE = -1, + PROP_CIRCLE_RADIUS, + NUM_CIRCLE_PROPS, +}; + +enum class ELayerProp +{ + PROP_NONE = -1, + PROP_GROUP, + PROP_ORDER, + PROP_HQ, + NUM_PROPS, +}; + +enum class ETilesProp +{ + PROP_NONE = -1, + PROP_WIDTH, + PROP_HEIGHT, + PROP_SHIFT, + PROP_SHIFT_BY, + PROP_IMAGE, + PROP_COLOR, + PROP_COLOR_ENV, + PROP_COLOR_ENV_OFFSET, + PROP_AUTOMAPPER, + PROP_SEED, + NUM_PROPS +}; + +enum class ETilesCommonProp +{ + PROP_NONE = -1, + PROP_WIDTH, + PROP_HEIGHT, + PROP_SHIFT, + PROP_SHIFT_BY, + PROP_COLOR, + NUM_PROPS, +}; + +enum class EGroupProp +{ + PROP_NONE = -1, + PROP_ORDER, + PROP_POS_X, + PROP_POS_Y, + PROP_PARA_X, + PROP_PARA_Y, + PROP_USE_CLIPPING, + PROP_CLIP_X, + PROP_CLIP_Y, + PROP_CLIP_W, + PROP_CLIP_H, + NUM_PROPS, +}; + +enum class ELayerQuadsProp +{ + PROP_NONE = -1, + PROP_IMAGE, + NUM_PROPS, +}; + +enum class ELayerSoundsProp +{ + PROP_NONE = -1, + PROP_SOUND, + NUM_PROPS, +}; + +#endif diff --git a/src/game/editor/mapitems/envelope.h b/src/game/editor/mapitems/envelope.h index 0d167e459c1..2b25c9f3620 100644 --- a/src/game/editor/mapitems/envelope.h +++ b/src/game/editor/mapitems/envelope.h @@ -25,6 +25,7 @@ class CEnvelope void AddPoint(int Time, int v0, int v1 = 0, int v2 = 0, int v3 = 0); float EndTime() const; int GetChannels() const; + EType Type() const { return m_Type; } private: void Resort(); diff --git a/src/game/editor/mapitems/layer.h b/src/game/editor/mapitems/layer.h index 298337d0591..238c9e4ccdf 100644 --- a/src/game/editor/mapitems/layer.h +++ b/src/game/editor/mapitems/layer.h @@ -62,6 +62,7 @@ class CLayer virtual void ModifySoundIndex(FIndexModifyFunction pfnFunc) {} virtual std::shared_ptr Duplicate() const = 0; + virtual const char *TypeName() const = 0; virtual void GetSize(float *pWidth, float *pHeight) { diff --git a/src/game/editor/mapitems/layer_front.cpp b/src/game/editor/mapitems/layer_front.cpp index 47d26058673..f046299c4b8 100644 --- a/src/game/editor/mapitems/layer_front.cpp +++ b/src/game/editor/mapitems/layer_front.cpp @@ -45,3 +45,8 @@ void CLayerFront::Resize(int NewW, int NewH) if(m_pEditor->m_Map.m_pGameLayer->m_Width != NewW || m_pEditor->m_Map.m_pGameLayer->m_Height != NewH) m_pEditor->m_Map.m_pGameLayer->Resize(NewW, NewH); } + +const char *CLayerFront::TypeName() const +{ + return "front"; +} diff --git a/src/game/editor/mapitems/layer_front.h b/src/game/editor/mapitems/layer_front.h index f5aeb3a5805..fbddfac1fa4 100644 --- a/src/game/editor/mapitems/layer_front.h +++ b/src/game/editor/mapitems/layer_front.h @@ -10,6 +10,7 @@ class CLayerFront : public CLayerTiles void Resize(int NewW, int NewH) override; void SetTile(int x, int y, CTile Tile) override; + const char *TypeName() const override; }; #endif diff --git a/src/game/editor/mapitems/layer_game.cpp b/src/game/editor/mapitems/layer_game.cpp index 48d0618110f..eb0ee541fcd 100644 --- a/src/game/editor/mapitems/layer_game.cpp +++ b/src/game/editor/mapitems/layer_game.cpp @@ -72,3 +72,8 @@ CUI::EPopupMenuFunctionResult CLayerGame::RenderProperties(CUIRect *pToolbox) m_Image = -1; return Result; } + +const char *CLayerGame::TypeName() const +{ + return "game"; +} diff --git a/src/game/editor/mapitems/layer_game.h b/src/game/editor/mapitems/layer_game.h index 4c00c7e7fcf..ec6f1d78fa0 100644 --- a/src/game/editor/mapitems/layer_game.h +++ b/src/game/editor/mapitems/layer_game.h @@ -11,6 +11,7 @@ class CLayerGame : public CLayerTiles CTile GetTile(int x, int y) override; void SetTile(int x, int y, CTile Tile) override; + const char *TypeName() const override; CUI::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; }; diff --git a/src/game/editor/mapitems/layer_group.cpp b/src/game/editor/mapitems/layer_group.cpp index 6529e0584c0..2607fc9a3fc 100644 --- a/src/game/editor/mapitems/layer_group.cpp +++ b/src/game/editor/mapitems/layer_group.cpp @@ -27,13 +27,13 @@ CLayerGroup::~CLayerGroup() m_vpLayers.clear(); } -void CLayerGroup::Convert(CUIRect *pRect) +void CLayerGroup::Convert(CUIRect *pRect) const { pRect->x += m_OffsetX; pRect->y += m_OffsetY; } -void CLayerGroup::Mapping(float *pPoints) +void CLayerGroup::Mapping(float *pPoints) const { float NormalParallaxZoom = clamp((double)(maximum(m_ParallaxX, m_ParallaxY)), 0., 100.); float ParallaxZoom = m_pMap->m_pEditor->m_PreviewZoom ? NormalParallaxZoom : 100.0f; @@ -49,7 +49,7 @@ void CLayerGroup::Mapping(float *pPoints) pPoints[3] += m_pMap->m_pEditor->MapView()->GetEditorOffset().y; } -void CLayerGroup::MapScreen() +void CLayerGroup::MapScreen() const { float aPoints[4]; Mapping(aPoints); diff --git a/src/game/editor/mapitems/layer_group.h b/src/game/editor/mapitems/layer_group.h index 8325069a41c..78e1488e0b3 100644 --- a/src/game/editor/mapitems/layer_group.h +++ b/src/game/editor/mapitems/layer_group.h @@ -33,10 +33,10 @@ class CLayerGroup CLayerGroup(); ~CLayerGroup(); - void Convert(CUIRect *pRect); + void Convert(CUIRect *pRect) const; void Render(); - void MapScreen(); - void Mapping(float *pPoints); + void MapScreen() const; + void Mapping(float *pPoints) const; void GetSize(float *pWidth, float *pHeight) const; diff --git a/src/game/editor/mapitems/layer_quads.cpp b/src/game/editor/mapitems/layer_quads.cpp index e1e58f3c918..3f69708fd58 100644 --- a/src/game/editor/mapitems/layer_quads.cpp +++ b/src/game/editor/mapitems/layer_quads.cpp @@ -3,6 +3,7 @@ #include "layer_quads.h" #include +#include #include "image.h" @@ -140,6 +141,7 @@ int CLayerQuads::BrushGrab(std::shared_ptr pBrush, CUIRect Rect) void CLayerQuads::BrushPlace(std::shared_ptr pBrush, float wx, float wy) { std::shared_ptr pQuadLayer = std::static_pointer_cast(pBrush); + std::vector vAddedQuads; for(const auto &Quad : pQuadLayer->m_vQuads) { CQuad n = Quad; @@ -151,7 +153,9 @@ void CLayerQuads::BrushPlace(std::shared_ptr pBrush, float wx, float wy) } m_vQuads.push_back(n); + vAddedQuads.push_back(n); } + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], vAddedQuads)); m_pEditor->m_Map.OnModify(); } @@ -219,26 +223,23 @@ void CLayerQuads::GetSize(float *pWidth, float *pHeight) CUI::EPopupMenuFunctionResult CLayerQuads::RenderProperties(CUIRect *pToolBox) { - enum - { - PROP_IMAGE = 0, - NUM_PROPS, - }; - CProperty aProps[] = { {"Image", m_Image, PROPTYPE_IMAGE, -1, 0}, {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ELayerQuadsProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = m_pEditor->DoProperties(pToolBox, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto [State, Prop] = m_pEditor->DoPropertiesWithState(pToolBox, aProps, s_aIds, &NewVal); + if(Prop != ELayerQuadsProp::PROP_NONE) { m_pEditor->m_Map.OnModify(); } - if(Prop == PROP_IMAGE) + static CLayerQuadsPropTracker s_Tracker(m_pEditor); + s_Tracker.Begin(this, Prop, State); + + if(Prop == ELayerQuadsProp::PROP_IMAGE) { if(NewVal >= 0) m_Image = NewVal % m_pEditor->m_Map.m_vpImages.size(); @@ -246,6 +247,8 @@ CUI::EPopupMenuFunctionResult CLayerQuads::RenderProperties(CUIRect *pToolBox) m_Image = -1; } + s_Tracker.End(Prop, State); + return CUI::POPUP_KEEP_OPEN; } @@ -280,3 +283,8 @@ int CLayerQuads::SwapQuads(int Index0, int Index1) std::swap(m_vQuads[Index0], m_vQuads[Index1]); return Index1; } + +const char *CLayerQuads::TypeName() const +{ + return "quads"; +} diff --git a/src/game/editor/mapitems/layer_quads.h b/src/game/editor/mapitems/layer_quads.h index 7ba890e6894..dd826b385eb 100644 --- a/src/game/editor/mapitems/layer_quads.h +++ b/src/game/editor/mapitems/layer_quads.h @@ -28,6 +28,7 @@ class CLayerQuads : public CLayer void GetSize(float *pWidth, float *pHeight) override; std::shared_ptr Duplicate() const override; + const char *TypeName() const override; int m_Image; std::vector m_vQuads; diff --git a/src/game/editor/mapitems/layer_sounds.cpp b/src/game/editor/mapitems/layer_sounds.cpp index 5742368c08e..ff8250617a7 100644 --- a/src/game/editor/mapitems/layer_sounds.cpp +++ b/src/game/editor/mapitems/layer_sounds.cpp @@ -1,7 +1,7 @@ #include "layer_sounds.h" #include - +#include #include static const float s_SourceVisualSize = 32.0f; @@ -171,6 +171,7 @@ int CLayerSounds::BrushGrab(std::shared_ptr pBrush, CUIRect Rect) void CLayerSounds::BrushPlace(std::shared_ptr pBrush, float wx, float wy) { std::shared_ptr pSoundLayer = std::static_pointer_cast(pBrush); + std::vector vAddedSources; for(const auto &Source : pSoundLayer->m_vSources) { CSoundSource n = Source; @@ -179,32 +180,31 @@ void CLayerSounds::BrushPlace(std::shared_ptr pBrush, float wx, float wy n.m_Position.y += f2fx(wy); m_vSources.push_back(n); + vAddedSources.push_back(n); } + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, m_pEditor->m_SelectedGroup, m_pEditor->m_vSelectedLayers[0], vAddedSources)); m_pEditor->m_Map.OnModify(); } CUI::EPopupMenuFunctionResult CLayerSounds::RenderProperties(CUIRect *pToolBox) { - enum - { - PROP_SOUND = 0, - NUM_PROPS, - }; - CProperty aProps[] = { {"Sound", m_Sound, PROPTYPE_SOUND, -1, 0}, {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ELayerSoundsProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = m_pEditor->DoProperties(pToolBox, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto [State, Prop] = m_pEditor->DoPropertiesWithState(pToolBox, aProps, s_aIds, &NewVal); + if(Prop != ELayerSoundsProp::PROP_NONE) { m_pEditor->m_Map.OnModify(); } - if(Prop == PROP_SOUND) + static CLayerSoundsPropTracker s_Tracker(m_pEditor); + s_Tracker.Begin(this, Prop, State); + + if(Prop == ELayerSoundsProp::PROP_SOUND) { if(NewVal >= 0) m_Sound = NewVal % m_pEditor->m_Map.m_vpSounds.size(); @@ -212,6 +212,8 @@ CUI::EPopupMenuFunctionResult CLayerSounds::RenderProperties(CUIRect *pToolBox) m_Sound = -1; } + s_Tracker.End(Prop, State); + return CUI::POPUP_KEEP_OPEN; } @@ -233,3 +235,8 @@ std::shared_ptr CLayerSounds::Duplicate() const { return std::make_shared(*this); } + +const char *CLayerSounds::TypeName() const +{ + return "sounds"; +} diff --git a/src/game/editor/mapitems/layer_sounds.h b/src/game/editor/mapitems/layer_sounds.h index 0d89329f650..bc71b13642f 100644 --- a/src/game/editor/mapitems/layer_sounds.h +++ b/src/game/editor/mapitems/layer_sounds.h @@ -23,6 +23,7 @@ class CLayerSounds : public CLayer void ModifySoundIndex(FIndexModifyFunction pfnFunc) override; std::shared_ptr Duplicate() const override; + const char *TypeName() const override; int m_Sound; std::vector m_vSources; diff --git a/src/game/editor/mapitems/layer_speedup.cpp b/src/game/editor/mapitems/layer_speedup.cpp index bcdd43d064d..02795a43169 100644 --- a/src/game/editor/mapitems/layer_speedup.cpp +++ b/src/game/editor/mapitems/layer_speedup.cpp @@ -12,6 +12,16 @@ CLayerSpeedup::CLayerSpeedup(CEditor *pEditor, int w, int h) : mem_zero(m_pSpeedupTile, (size_t)w * h * sizeof(CSpeedupTile)); } +CLayerSpeedup::CLayerSpeedup(const CLayerSpeedup &Other) : + CLayerTiles(Other) +{ + str_copy(m_aName, "Speedup copy"); + m_Speedup = 1; + + m_pSpeedupTile = new CSpeedupTile[m_Width * m_Height]; + mem_copy(m_pSpeedupTile, Other.m_pSpeedupTile, (size_t)m_Width * m_Height * sizeof(CSpeedupTile)); +} + CLayerSpeedup::~CLayerSpeedup() { delete[] m_pSpeedupTile; @@ -84,53 +94,78 @@ void CLayerSpeedup::BrushDraw(std::shared_ptr pBrush, float wx, float wy if(!Destructive && GetTile(fx, fy).m_Index) continue; + int Index = fy * m_Width + fx; + SSpeedupTileStateChange::SData Previous{ + m_pSpeedupTile[Index].m_Force, + m_pSpeedupTile[Index].m_Angle, + m_pSpeedupTile[Index].m_MaxSpeed, + m_pSpeedupTile[Index].m_Type, + m_pTiles[Index].m_Index}; + if((m_pEditor->m_AllowPlaceUnusedTiles || IsValidSpeedupTile(pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index)) && pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index != TILE_AIR) { if(m_pEditor->m_SpeedupAngle != pSpeedupLayer->m_SpeedupAngle || m_pEditor->m_SpeedupForce != pSpeedupLayer->m_SpeedupForce || m_pEditor->m_SpeedupMaxSpeed != pSpeedupLayer->m_SpeedupMaxSpeed) { - m_pSpeedupTile[fy * m_Width + fx].m_Force = m_pEditor->m_SpeedupForce; - m_pSpeedupTile[fy * m_Width + fx].m_MaxSpeed = m_pEditor->m_SpeedupMaxSpeed; - m_pSpeedupTile[fy * m_Width + fx].m_Angle = m_pEditor->m_SpeedupAngle; - m_pSpeedupTile[fy * m_Width + fx].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pSpeedupTile[Index].m_Force = m_pEditor->m_SpeedupForce; + m_pSpeedupTile[Index].m_MaxSpeed = m_pEditor->m_SpeedupMaxSpeed; + m_pSpeedupTile[Index].m_Angle = m_pEditor->m_SpeedupAngle; + m_pSpeedupTile[Index].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; } else if(pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_Force) { - m_pSpeedupTile[fy * m_Width + fx].m_Force = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_Force; - m_pSpeedupTile[fy * m_Width + fx].m_Angle = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_Angle; - m_pSpeedupTile[fy * m_Width + fx].m_MaxSpeed = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_MaxSpeed; - m_pSpeedupTile[fy * m_Width + fx].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pSpeedupTile[Index].m_Force = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_Force; + m_pSpeedupTile[Index].m_Angle = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_Angle; + m_pSpeedupTile[Index].m_MaxSpeed = pSpeedupLayer->m_pSpeedupTile[y * pSpeedupLayer->m_Width + x].m_MaxSpeed; + m_pSpeedupTile[Index].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; } else if(m_pEditor->m_SpeedupForce) { - m_pSpeedupTile[fy * m_Width + fx].m_Force = m_pEditor->m_SpeedupForce; - m_pSpeedupTile[fy * m_Width + fx].m_MaxSpeed = m_pEditor->m_SpeedupMaxSpeed; - m_pSpeedupTile[fy * m_Width + fx].m_Angle = m_pEditor->m_SpeedupAngle; - m_pSpeedupTile[fy * m_Width + fx].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pSpeedupTile[Index].m_Force = m_pEditor->m_SpeedupForce; + m_pSpeedupTile[Index].m_MaxSpeed = m_pEditor->m_SpeedupMaxSpeed; + m_pSpeedupTile[Index].m_Angle = m_pEditor->m_SpeedupAngle; + m_pSpeedupTile[Index].m_Type = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Index = pSpeedupLayer->m_pTiles[y * pSpeedupLayer->m_Width + x].m_Index; } else { - m_pSpeedupTile[fy * m_Width + fx].m_Force = 0; - m_pSpeedupTile[fy * m_Width + fx].m_MaxSpeed = 0; - m_pSpeedupTile[fy * m_Width + fx].m_Angle = 0; - m_pSpeedupTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pSpeedupTile[Index].m_Force = 0; + m_pSpeedupTile[Index].m_MaxSpeed = 0; + m_pSpeedupTile[Index].m_Angle = 0; + m_pSpeedupTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; } } else { - m_pSpeedupTile[fy * m_Width + fx].m_Force = 0; - m_pSpeedupTile[fy * m_Width + fx].m_MaxSpeed = 0; - m_pSpeedupTile[fy * m_Width + fx].m_Angle = 0; - m_pSpeedupTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pSpeedupTile[Index].m_Force = 0; + m_pSpeedupTile[Index].m_MaxSpeed = 0; + m_pSpeedupTile[Index].m_Angle = 0; + m_pSpeedupTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; } + + SSpeedupTileStateChange::SData Current{ + m_pSpeedupTile[Index].m_Force, + m_pSpeedupTile[Index].m_Angle, + m_pSpeedupTile[Index].m_MaxSpeed, + m_pSpeedupTile[Index].m_Type, + m_pTiles[Index].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } FlagModified(sx, sy, pSpeedupLayer->m_Width, pSpeedupLayer->m_Height); } +void CLayerSpeedup::RecordStateChange(int x, int y, SSpeedupTileStateChange::SData Previous, SSpeedupTileStateChange::SData Current) +{ + if(!m_History[y][x].m_Changed) + m_History[y][x] = SSpeedupTileStateChange{true, Previous, Current}; + else + m_History[y][x].m_Current = Current; +} + void CLayerSpeedup::BrushFlipX() { CLayerTiles::BrushFlipX(); @@ -211,6 +246,13 @@ void CLayerSpeedup::FillSelection(bool Empty, std::shared_ptr pBrush, CU const int SrcIndex = Empty ? 0 : (y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height); const int TgtIndex = fy * m_Width + fx; + SSpeedupTileStateChange::SData Previous{ + m_pSpeedupTile[TgtIndex].m_Force, + m_pSpeedupTile[TgtIndex].m_Angle, + m_pSpeedupTile[TgtIndex].m_MaxSpeed, + m_pSpeedupTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + if(Empty || (!m_pEditor->m_AllowPlaceUnusedTiles && !IsValidSpeedupTile((pLt->m_pTiles[SrcIndex]).m_Index))) // no speed up tile chosen: reset { m_pTiles[TgtIndex].m_Index = 0; @@ -240,7 +282,26 @@ void CLayerSpeedup::FillSelection(bool Empty, std::shared_ptr pBrush, CU m_pSpeedupTile[TgtIndex].m_MaxSpeed = pLt->m_pSpeedupTile[SrcIndex].m_MaxSpeed; } } + + SSpeedupTileStateChange::SData Current{ + m_pSpeedupTile[TgtIndex].m_Force, + m_pSpeedupTile[TgtIndex].m_Angle, + m_pSpeedupTile[TgtIndex].m_MaxSpeed, + m_pSpeedupTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } } FlagModified(sx, sy, w, h); } + +std::shared_ptr CLayerSpeedup::Duplicate() const +{ + return std::make_shared(*this); +} + +const char *CLayerSpeedup::TypeName() const +{ + return "speedup"; +} diff --git a/src/game/editor/mapitems/layer_speedup.h b/src/game/editor/mapitems/layer_speedup.h index 74d0a14c55e..990f488e7d3 100644 --- a/src/game/editor/mapitems/layer_speedup.h +++ b/src/game/editor/mapitems/layer_speedup.h @@ -3,10 +3,24 @@ #include "layer_tiles.h" +struct SSpeedupTileStateChange +{ + bool m_Changed; + struct SData + { + int m_Force; + int m_Angle; + int m_MaxSpeed; + int m_Type; + int m_Index; + } m_Previous, m_Current; +}; + class CLayerSpeedup : public CLayerTiles { public: CLayerSpeedup(CEditor *pEditor, int w, int h); + CLayerSpeedup(const CLayerSpeedup &Other); ~CLayerSpeedup(); CSpeedupTile *m_pSpeedupTile; @@ -22,6 +36,19 @@ class CLayerSpeedup : public CLayerTiles void BrushFlipY() override; void BrushRotate(float Amount) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; + + EditorTileStateChangeHistory m_History; + void ClearHistory() override + { + CLayerTiles::ClearHistory(); + m_History.clear(); + } + + std::shared_ptr Duplicate() const override; + const char *TypeName() const override; + +private: + void RecordStateChange(int x, int y, SSpeedupTileStateChange::SData Previous, SSpeedupTileStateChange::SData Current); }; #endif diff --git a/src/game/editor/mapitems/layer_switch.cpp b/src/game/editor/mapitems/layer_switch.cpp index ef46a33f1d1..6d59903b6a4 100644 --- a/src/game/editor/mapitems/layer_switch.cpp +++ b/src/game/editor/mapitems/layer_switch.cpp @@ -12,6 +12,16 @@ CLayerSwitch::CLayerSwitch(CEditor *pEditor, int w, int h) : mem_zero(m_pSwitchTile, (size_t)w * h * sizeof(CSwitchTile)); } +CLayerSwitch::CLayerSwitch(const CLayerSwitch &Other) : + CLayerTiles(Other) +{ + str_copy(m_aName, "Switch copy"); + m_Switch = 1; + + m_pSwitchTile = new CSwitchTile[m_Width * m_Height]; + mem_copy(m_pSwitchTile, Other.m_pSwitchTile, (size_t)m_Width * m_Height * sizeof(CSwitchTile)); +} + CLayerSwitch::~CLayerSwitch() { delete[] m_pSwitchTile; @@ -83,54 +93,79 @@ void CLayerSwitch::BrushDraw(std::shared_ptr pBrush, float wx, float wy) if(!Destructive && GetTile(fx, fy).m_Index) continue; + int Index = fy * m_Width + fx; + SSwitchTileStateChange::SData Previous{ + m_pSwitchTile[Index].m_Number, + m_pSwitchTile[Index].m_Type, + m_pSwitchTile[Index].m_Flags, + m_pSwitchTile[Index].m_Delay, + m_pTiles[Index].m_Index}; + if((m_pEditor->m_AllowPlaceUnusedTiles || IsValidSwitchTile(pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index)) && pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index != TILE_AIR) { if(m_pEditor->m_SwitchNum != pSwitchLayer->m_SwitchNumber || m_pEditor->m_SwitchDelay != pSwitchLayer->m_SwitchDelay) { - m_pSwitchTile[fy * m_Width + fx].m_Number = m_pEditor->m_SwitchNum; - m_pSwitchTile[fy * m_Width + fx].m_Delay = m_pEditor->m_SwitchDelay; + m_pSwitchTile[Index].m_Number = m_pEditor->m_SwitchNum; + m_pSwitchTile[Index].m_Delay = m_pEditor->m_SwitchDelay; } else if(pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Number) { - m_pSwitchTile[fy * m_Width + fx].m_Number = pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Number; - m_pSwitchTile[fy * m_Width + fx].m_Delay = pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Delay; + m_pSwitchTile[Index].m_Number = pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Number; + m_pSwitchTile[Index].m_Delay = pSwitchLayer->m_pSwitchTile[y * pSwitchLayer->m_Width + x].m_Delay; } else { - m_pSwitchTile[fy * m_Width + fx].m_Number = m_pEditor->m_SwitchNum; - m_pSwitchTile[fy * m_Width + fx].m_Delay = m_pEditor->m_SwitchDelay; + m_pSwitchTile[Index].m_Number = m_pEditor->m_SwitchNum; + m_pSwitchTile[Index].m_Delay = m_pEditor->m_SwitchDelay; } - m_pSwitchTile[fy * m_Width + fx].m_Type = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index; - m_pSwitchTile[fy * m_Width + fx].m_Flags = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Flags; - m_pTiles[fy * m_Width + fx].m_Index = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Flags = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Flags; + m_pSwitchTile[Index].m_Type = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index; + m_pSwitchTile[Index].m_Flags = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Flags; + m_pTiles[Index].m_Index = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Flags = pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Flags; if(!IsSwitchTileFlagsUsed(pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index)) { - m_pSwitchTile[fy * m_Width + fx].m_Flags = 0; + m_pSwitchTile[Index].m_Flags = 0; } if(!IsSwitchTileNumberUsed(pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index)) { - m_pSwitchTile[fy * m_Width + fx].m_Number = 0; + m_pSwitchTile[Index].m_Number = 0; } if(!IsSwitchTileDelayUsed(pSwitchLayer->m_pTiles[y * pSwitchLayer->m_Width + x].m_Index)) { - m_pSwitchTile[fy * m_Width + fx].m_Delay = 0; + m_pSwitchTile[Index].m_Delay = 0; } } else { - m_pSwitchTile[fy * m_Width + fx].m_Number = 0; - m_pSwitchTile[fy * m_Width + fx].m_Type = 0; - m_pSwitchTile[fy * m_Width + fx].m_Flags = 0; - m_pSwitchTile[fy * m_Width + fx].m_Delay = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pSwitchTile[Index].m_Number = 0; + m_pSwitchTile[Index].m_Type = 0; + m_pSwitchTile[Index].m_Flags = 0; + m_pSwitchTile[Index].m_Delay = 0; + m_pTiles[Index].m_Index = 0; } + + SSwitchTileStateChange::SData Current{ + m_pSwitchTile[Index].m_Number, + m_pSwitchTile[Index].m_Type, + m_pSwitchTile[Index].m_Flags, + m_pSwitchTile[Index].m_Delay, + m_pTiles[Index].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } FlagModified(sx, sy, pSwitchLayer->m_Width, pSwitchLayer->m_Height); } +void CLayerSwitch::RecordStateChange(int x, int y, SSwitchTileStateChange::SData Previous, SSwitchTileStateChange::SData Current) +{ + if(!m_History[y][x].m_Changed) + m_History[y][x] = SSwitchTileStateChange{true, Previous, Current}; + else + m_History[y][x].m_Current = Current; +} + void CLayerSwitch::BrushFlipX() { CLayerTiles::BrushFlipX(); @@ -217,6 +252,13 @@ void CLayerSwitch::FillSelection(bool Empty, std::shared_ptr pBrush, CUI const int SrcIndex = Empty ? 0 : (y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height); const int TgtIndex = fy * m_Width + fx; + SSwitchTileStateChange::SData Previous{ + m_pSwitchTile[TgtIndex].m_Number, + m_pSwitchTile[TgtIndex].m_Type, + m_pSwitchTile[TgtIndex].m_Flags, + m_pSwitchTile[TgtIndex].m_Delay, + m_pTiles[TgtIndex].m_Index}; + if(Empty || (!m_pEditor->m_AllowPlaceUnusedTiles && !IsValidSwitchTile((pLt->m_pTiles[SrcIndex]).m_Index))) { m_pTiles[TgtIndex].m_Index = 0; @@ -250,6 +292,15 @@ void CLayerSwitch::FillSelection(bool Empty, std::shared_ptr pBrush, CUI m_pSwitchTile[TgtIndex].m_Flags = pLt->m_pSwitchTile[SrcIndex].m_Flags; } } + + SSwitchTileStateChange::SData Current{ + m_pSwitchTile[TgtIndex].m_Number, + m_pSwitchTile[TgtIndex].m_Type, + m_pSwitchTile[TgtIndex].m_Flags, + m_pSwitchTile[TgtIndex].m_Delay, + m_pTiles[TgtIndex].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } } FlagModified(sx, sy, w, h); @@ -270,3 +321,13 @@ bool CLayerSwitch::ContainsElementWithId(int Id) return false; } + +std::shared_ptr CLayerSwitch::Duplicate() const +{ + return std::make_shared(*this); +} + +const char *CLayerSwitch::TypeName() const +{ + return "switch"; +} diff --git a/src/game/editor/mapitems/layer_switch.h b/src/game/editor/mapitems/layer_switch.h index 4011105cc40..7a8aae79b2d 100644 --- a/src/game/editor/mapitems/layer_switch.h +++ b/src/game/editor/mapitems/layer_switch.h @@ -3,10 +3,24 @@ #include "layer_tiles.h" +struct SSwitchTileStateChange +{ + bool m_Changed; + struct SData + { + int m_Number; + int m_Type; + int m_Flags; + int m_Delay; + int m_Index; + } m_Previous, m_Current; +}; + class CLayerSwitch : public CLayerTiles { public: CLayerSwitch(CEditor *pEditor, int w, int h); + CLayerSwitch(const CLayerSwitch &Other); ~CLayerSwitch(); CSwitchTile *m_pSwitchTile; @@ -22,6 +36,19 @@ class CLayerSwitch : public CLayerTiles void BrushRotate(float Amount) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; virtual bool ContainsElementWithId(int Id); + + EditorTileStateChangeHistory m_History; + inline void ClearHistory() override + { + CLayerTiles::ClearHistory(); + m_History.clear(); + } + + std::shared_ptr Duplicate() const override; + const char *TypeName() const override; + +private: + void RecordStateChange(int x, int y, SSwitchTileStateChange::SData Previous, SSwitchTileStateChange::SData Current); }; #endif diff --git a/src/game/editor/mapitems/layer_tele.cpp b/src/game/editor/mapitems/layer_tele.cpp index 6b397b4db66..0006a74c908 100644 --- a/src/game/editor/mapitems/layer_tele.cpp +++ b/src/game/editor/mapitems/layer_tele.cpp @@ -12,6 +12,16 @@ CLayerTele::CLayerTele(CEditor *pEditor, int w, int h) : mem_zero(m_pTeleTile, (size_t)w * h * sizeof(CTeleTile)); } +CLayerTele::CLayerTele(const CLayerTele &Other) : + CLayerTiles(Other) +{ + str_copy(m_aName, "Tele copy"); + m_Tele = 1; + + m_pTeleTile = new CTeleTile[m_Width * m_Height]; + mem_copy(m_pTeleTile, Other.m_pTeleTile, (size_t)m_Width * m_Height * sizeof(CTeleTile)); +} + CLayerTele::~CLayerTele() { delete[] m_pTeleTile; @@ -80,48 +90,80 @@ void CLayerTele::BrushDraw(std::shared_ptr pBrush, float wx, float wy) if(!Destructive && GetTile(fx, fy).m_Index) continue; + int Index = fy * m_Width + fx; + STeleTileStateChange::SData Previous{ + m_pTeleTile[Index].m_Number, + m_pTeleTile[Index].m_Type, + m_pTiles[Index].m_Index}; + 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)) { // 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[fy * m_Width + fx].m_Number = 255; + m_pTeleTile[Index].m_Number = 255; } else if(m_pEditor->m_TeleNumber != pTeleLayer->m_TeleNum) { - m_pTeleTile[fy * m_Width + fx].m_Number = m_pEditor->m_TeleNumber; + m_pTeleTile[Index].m_Number = m_pEditor->m_TeleNumber; } else if(pTeleLayer->m_pTeleTile[y * pTeleLayer->m_Width + x].m_Number) { - m_pTeleTile[fy * m_Width + fx].m_Number = 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) { - m_pTeleTile[fy * m_Width + fx].m_Number = 0; - m_pTeleTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pTeleTile[Index].m_Number = 0; + m_pTeleTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; + + STeleTileStateChange::SData Current{ + m_pTeleTile[Index].m_Number, + m_pTeleTile[Index].m_Type, + m_pTiles[Index].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); continue; } else - m_pTeleTile[fy * m_Width + fx].m_Number = m_pEditor->m_TeleNumber; + { + m_pTeleTile[Index].m_Number = m_pEditor->m_TeleNumber; + } } - m_pTeleTile[fy * m_Width + fx].m_Type = pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Index = pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index; + m_pTeleTile[Index].m_Type = pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Index = pTeleLayer->m_pTiles[y * pTeleLayer->m_Width + x].m_Index; } else { - m_pTeleTile[fy * m_Width + fx].m_Number = 0; - m_pTeleTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pTeleTile[Index].m_Number = 0; + m_pTeleTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; } + + STeleTileStateChange::SData Current{ + m_pTeleTile[Index].m_Number, + m_pTeleTile[Index].m_Type, + m_pTiles[Index].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } FlagModified(sx, sy, pTeleLayer->m_Width, pTeleLayer->m_Height); } +void CLayerTele::RecordStateChange(int x, int y, STeleTileStateChange::SData Previous, STeleTileStateChange::SData Current) +{ + if(!m_History[y][x].m_Changed) + m_History[y][x] = STeleTileStateChange{true, Previous, Current}; + else + { + m_History[y][x].m_Current = Current; + } +} + void CLayerTele::BrushFlipX() { CLayerTiles::BrushFlipX(); @@ -202,6 +244,11 @@ void CLayerTele::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe const int SrcIndex = Empty ? 0 : (y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height); const int TgtIndex = fy * m_Width + fx; + STeleTileStateChange::SData Previous{ + m_pTeleTile[TgtIndex].m_Number, + m_pTeleTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + if(Empty || (!m_pEditor->m_AllowPlaceUnusedTiles && !IsValidTeleTile((pLt->m_pTiles[SrcIndex]).m_Index))) { m_pTiles[TgtIndex].m_Index = 0; @@ -227,6 +274,13 @@ void CLayerTele::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe m_pTeleTile[TgtIndex].m_Number = pLt->m_pTeleTile[SrcIndex].m_Number; } } + + STeleTileStateChange::SData Current{ + m_pTeleTile[TgtIndex].m_Number, + m_pTeleTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } } FlagModified(sx, sy, w, h); @@ -247,3 +301,13 @@ bool CLayerTele::ContainsElementWithId(int Id) return false; } + +std::shared_ptr CLayerTele::Duplicate() const +{ + return std::make_shared(*this); +} + +const char *CLayerTele::TypeName() const +{ + return "tele"; +} diff --git a/src/game/editor/mapitems/layer_tele.h b/src/game/editor/mapitems/layer_tele.h index d3375518535..569dd305e47 100644 --- a/src/game/editor/mapitems/layer_tele.h +++ b/src/game/editor/mapitems/layer_tele.h @@ -3,10 +3,22 @@ #include "layer_tiles.h" +struct STeleTileStateChange +{ + bool m_Changed; + struct SData + { + int m_Number; + int m_Type; + int m_Index; + } m_Previous, m_Current; +}; + class CLayerTele : public CLayerTiles { public: CLayerTele(CEditor *pEditor, int w, int h); + CLayerTele(const CLayerTele &Other); ~CLayerTele(); CTeleTile *m_pTeleTile; @@ -21,6 +33,21 @@ class CLayerTele : public CLayerTiles void BrushRotate(float Amount) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; virtual bool ContainsElementWithId(int Id); + + EditorTileStateChangeHistory m_History; + inline void ClearHistory() override + { + CLayerTiles::ClearHistory(); + m_History.clear(); + } + + std::shared_ptr Duplicate() const override; + const char *TypeName() const override; + +private: + void RecordStateChange(int x, int y, STeleTileStateChange::SData Previous, STeleTileStateChange::SData Current); + + friend class CLayerTiles; }; #endif diff --git a/src/game/editor/mapitems/layer_tiles.cpp b/src/game/editor/mapitems/layer_tiles.cpp index f2e79d5e822..a3ed05c33a1 100644 --- a/src/game/editor/mapitems/layer_tiles.cpp +++ b/src/game/editor/mapitems/layer_tiles.cpp @@ -2,10 +2,13 @@ /* If you are missing that file, acquire a complete release at teeworlds.com. */ #include "layer_tiles.h" -#include - #include #include +#include +#include + +#include +#include #include "image.h" @@ -75,10 +78,25 @@ CTile CLayerTiles::GetTile(int x, int y) } void CLayerTiles::SetTile(int x, int y, CTile Tile) +{ + auto CurrentTile = m_pTiles[y * m_Width + x]; + SetTileIgnoreHistory(x, y, Tile); + RecordStateChange(x, y, CurrentTile, Tile); +} + +void CLayerTiles::SetTileIgnoreHistory(int x, int y, CTile Tile) const { m_pTiles[y * m_Width + x] = Tile; } +void CLayerTiles::RecordStateChange(int x, int y, CTile Previous, CTile Tile) +{ + if(!m_TilesHistory[y][x].m_Changed) + m_TilesHistory[y][x] = STileStateChange{true, Previous, Tile}; + else + m_TilesHistory[y][x].m_Current = Tile; +} + void CLayerTiles::PrepareForSave() { for(int y = 0; y < m_Height; y++) @@ -93,7 +111,7 @@ void CLayerTiles::PrepareForSave() } } -void CLayerTiles::ExtractTiles(int TilemapItemVersion, const CTile *pSavedTiles, size_t SavedTilesSize) +void CLayerTiles::ExtractTiles(int TilemapItemVersion, const CTile *pSavedTiles, size_t SavedTilesSize) const { const size_t DestSize = (size_t)m_Width * m_Height; if(TilemapItemVersion >= CMapItemLayerTilemap::TILE_SKIP_MIN_VERSION) @@ -102,7 +120,7 @@ void CLayerTiles::ExtractTiles(int TilemapItemVersion, const CTile *pSavedTiles, mem_copy(m_pTiles, pSavedTiles, DestSize * sizeof(CTile)); } -void CLayerTiles::MakePalette() +void CLayerTiles::MakePalette() const { for(int y = 0; y < m_Height; y++) for(int x = 0; x < m_Width; x++) @@ -153,7 +171,7 @@ void CLayerTiles::Render(bool Tileset) int CLayerTiles::ConvertX(float x) const { return (int)(x / 32.0f); } int CLayerTiles::ConvertY(float y) const { return (int)(y / 32.0f); } -void CLayerTiles::Convert(CUIRect Rect, RECTi *pOut) +void CLayerTiles::Convert(CUIRect Rect, RECTi *pOut) const { pOut->x = ConvertX(Rect.x); pOut->y = ConvertY(Rect.y); @@ -161,7 +179,7 @@ void CLayerTiles::Convert(CUIRect Rect, RECTi *pOut) pOut->h = ConvertY(Rect.y + Rect.h + 31) - pOut->y; } -void CLayerTiles::Snap(CUIRect *pRect) +void CLayerTiles::Snap(CUIRect *pRect) const { RECTi Out; Convert(*pRect, &Out); @@ -171,7 +189,7 @@ void CLayerTiles::Snap(CUIRect *pRect) pRect->h = Out.h * 32.0f; } -void CLayerTiles::Clamp(RECTi *pRect) +void CLayerTiles::Clamp(RECTi *pRect) const { if(pRect->x < 0) { @@ -579,6 +597,11 @@ std::shared_ptr CLayerTiles::Duplicate() const return std::make_shared(*this); } +const char *CLayerTiles::TypeName() const +{ + return "tiles"; +} + void CLayerTiles::Resize(int NewW, int NewH) { CTile *pNewData = new CTile[NewW * NewH]; @@ -678,7 +701,6 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) 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) { @@ -717,17 +739,49 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) 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", + }; + + 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) { - std::shared_ptr pGLayer = m_pEditor->m_Map.m_pGameLayer; - 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++) @@ -736,40 +790,111 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) { 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)", s_apGametileOpNames[Result], Changes); + m_pEditor->m_EditorHistory.RecordAction(std::make_shared(m_pEditor, vpActions, aDisplay, true)); } else { if(!m_pEditor->m_Map.m_pTeleLayer) { - std::shared_ptr pLayer = std::make_shared(m_pEditor, m_Width, m_Height); + 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) + { + 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); + } } 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(); 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); } + 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) { - pTLayer->m_pTiles[(y + OffsetY) * pTLayer->m_Width + x + OffsetX].m_Index = TILE_AIR + Result; - pTLayer->m_pTeleTile[(y + OffsetY) * pTLayer->m_Width + x + OffsetX].m_Number = 1; - pTLayer->m_pTeleTile[(y + OffsetY) * pTLayer->m_Width + x + OffsetX].m_Type = TILE_AIR + Result; + 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); } } } + + 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)); } } } @@ -790,6 +915,12 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) { m_AutoAutoMap = !m_AutoAutoMap; FlagModified(0, 0, m_Width, m_Height); + 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)); + ClearHistory(); + } } } @@ -797,31 +928,15 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) if(m_pEditor->DoButton_Editor(&s_AutoMapperButton, "Automap", 0, &Button, 0, "Run the automapper")) { 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)); + ClearHistory(); return CUI::POPUP_CLOSE_CURRENT; } } } - enum - { - PROP_WIDTH = 0, - PROP_HEIGHT, - PROP_SHIFT, - PROP_SHIFT_BY, - PROP_IMAGE, - PROP_COLOR, - PROP_COLOR_ENV, - PROP_COLOR_ENV_OFFSET, - PROP_AUTOMAPPER, - PROP_SEED, - NUM_PROPS, - }; - - int Color = 0; - Color |= m_Color.r << 24; - Color |= m_Color.g << 16; - Color |= m_Color.b << 8; - Color |= m_Color.a; + int Color = PackColor(m_Color); CProperty aProps[] = { {"Width", m_Width, PROPTYPE_INT_SCROLL, 1, 100000}, @@ -839,21 +954,24 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) if(EntitiesLayer) // remove the image and color properties if this is a game layer { - aProps[PROP_IMAGE].m_pName = nullptr; - aProps[PROP_COLOR].m_pName = nullptr; - aProps[PROP_AUTOMAPPER].m_pName = nullptr; + aProps[(int)ETilesProp::PROP_IMAGE].m_pName = nullptr; + aProps[(int)ETilesProp::PROP_COLOR].m_pName = nullptr; + aProps[(int)ETilesProp::PROP_AUTOMAPPER].m_pName = nullptr; } if(m_Image == -1) { - aProps[PROP_AUTOMAPPER].m_pName = nullptr; - aProps[PROP_SEED].m_pName = nullptr; + aProps[(int)ETilesProp::PROP_AUTOMAPPER].m_pName = nullptr; + aProps[(int)ETilesProp::PROP_SEED].m_pName = nullptr; } - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ETilesProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = m_pEditor->DoProperties(pToolBox, aProps, s_aIds, &NewVal); + auto [State, Prop] = m_pEditor->DoPropertiesWithState(pToolBox, aProps, s_aIds, &NewVal); - if(Prop == PROP_WIDTH && NewVal > 1) + static CLayerTilesPropTracker s_Tracker(m_pEditor); + s_Tracker.Begin(this, Prop, State); + + if(Prop == ETilesProp::PROP_WIDTH && NewVal > 1) { if(NewVal > 1000 && !m_pEditor->m_LargeLayerWasWarned) { @@ -863,7 +981,7 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) } Resize(NewVal, m_Height); } - else if(Prop == PROP_HEIGHT && NewVal > 1) + else if(Prop == ETilesProp::PROP_HEIGHT && NewVal > 1) { if(NewVal > 1000 && !m_pEditor->m_LargeLayerWasWarned) { @@ -873,15 +991,15 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) } Resize(m_Width, NewVal); } - else if(Prop == PROP_SHIFT) + else if(Prop == ETilesProp::PROP_SHIFT) { Shift(NewVal); } - else if(Prop == PROP_SHIFT_BY) + else if(Prop == ETilesProp::PROP_SHIFT_BY) { m_pEditor->m_ShiftBy = NewVal; } - else if(Prop == PROP_IMAGE) + else if(Prop == ETilesProp::PROP_IMAGE) { m_Image = NewVal; if(NewVal == -1) @@ -901,14 +1019,14 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) } } } - else if(Prop == PROP_COLOR) + else if(Prop == ETilesProp::PROP_COLOR) { m_Color.r = (NewVal >> 24) & 0xff; m_Color.g = (NewVal >> 16) & 0xff; m_Color.b = (NewVal >> 8) & 0xff; m_Color.a = NewVal & 0xff; } - else if(Prop == PROP_COLOR_ENV) + else if(Prop == ETilesProp::PROP_COLOR_ENV) { int Index = clamp(NewVal - 1, -1, (int)m_pEditor->m_Map.m_vpEnvelopes.size() - 1); const int Step = (Index - m_ColorEnv) % 2; @@ -924,15 +1042,15 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) } } } - else if(Prop == PROP_COLOR_ENV_OFFSET) + else if(Prop == ETilesProp::PROP_COLOR_ENV_OFFSET) { m_ColorEnvOffset = NewVal; } - else if(Prop == PROP_SEED) + else if(Prop == ETilesProp::PROP_SEED) { m_Seed = NewVal; } - else if(Prop == PROP_AUTOMAPPER) + else if(Prop == ETilesProp::PROP_AUTOMAPPER) { if(m_Image >= 0 && m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ConfigNamesNum() > 0 && NewVal >= 0) m_AutoMapperConfig = NewVal % m_pEditor->m_Map.m_vpImages[m_Image]->m_AutoMapper.ConfigNamesNum(); @@ -940,15 +1058,17 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderProperties(CUIRect *pToolBox) m_AutoMapperConfig = -1; } - if(Prop != -1) + if(Prop != ETilesProp::PROP_NONE && Prop != ETilesProp::PROP_SHIFT_BY) { FlagModified(0, 0, m_Width, m_Height); } + s_Tracker.End(Prop, State); + return CUI::POPUP_KEEP_OPEN; } -CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector> &vpLayers) +CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropState &State, CEditor *pEditor, CUIRect *pToolbox, std::vector> &vpLayers, std::vector &vLayerIndices) { if(State.m_Modified) { @@ -957,22 +1077,77 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta static int s_CommitButton = 0; if(pEditor->DoButton_Editor(&s_CommitButton, "Commit", 0, &Commit, 0, "Applies the changes")) { + bool HasModifiedSize = (State.m_Modified & SCommonPropState::MODIFIED_SIZE) != 0; + bool HasModifiedColor = (State.m_Modified & SCommonPropState::MODIFIED_COLOR) != 0; + + std::vector> vpActions; + int j = 0; + int GroupIndex = pEditor->m_SelectedGroup; for(auto &pLayer : vpLayers) { - if((State.m_Modified & SCommonPropState::MODIFIED_SIZE) != 0) + int LayerIndex = vLayerIndices[j++]; + if(HasModifiedSize) + { + std::map> SavedLayers; + SavedLayers[LAYERTYPE_TILES] = pLayer->Duplicate(); + if(pLayer->m_Game || pLayer->m_Front || pLayer->m_Switch || pLayer->m_Speedup || pLayer->m_Tune || pLayer->m_Tele) + { // Need to save all entities layers when any entity layer + if(pEditor->m_Map.m_pFrontLayer && !pLayer->m_Front) + SavedLayers[LAYERTYPE_FRONT] = pEditor->m_Map.m_pFrontLayer->Duplicate(); + if(pEditor->m_Map.m_pTeleLayer && !pLayer->m_Tele) + SavedLayers[LAYERTYPE_TELE] = pEditor->m_Map.m_pTeleLayer->Duplicate(); + if(pEditor->m_Map.m_pSwitchLayer && !pLayer->m_Switch) + SavedLayers[LAYERTYPE_SWITCH] = pEditor->m_Map.m_pSwitchLayer->Duplicate(); + if(pEditor->m_Map.m_pSpeedupLayer && !pLayer->m_Speedup) + SavedLayers[LAYERTYPE_SPEEDUP] = pEditor->m_Map.m_pSpeedupLayer->Duplicate(); + if(pEditor->m_Map.m_pTuneLayer && !pLayer->m_Tune) + SavedLayers[LAYERTYPE_TUNE] = pEditor->m_Map.m_pTuneLayer->Duplicate(); + if(!pLayer->m_Game) + SavedLayers[LAYERTYPE_GAME] = pEditor->m_Map.m_pGameLayer->Duplicate(); + } + + int PrevW = pLayer->m_Width; + int PrevH = pLayer->m_Height; pLayer->Resize(State.m_Width, State.m_Height); - if((State.m_Modified & SCommonPropState::MODIFIED_COLOR) != 0) + if(PrevW != State.m_Width) + { + std::shared_ptr pAction; + vpActions.push_back(pAction = std::make_shared(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_WIDTH, PrevW, State.m_Width)); + pAction->SetSavedLayers(SavedLayers); + } + + if(PrevH != State.m_Height) + { + std::shared_ptr pAction; + vpActions.push_back(pAction = std::make_shared(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_HEIGHT, PrevH, State.m_Height)); + pAction->SetSavedLayers(SavedLayers); + } + } + + if(HasModifiedColor && !pLayer->IsEntitiesLayer()) { + int Color = 0; + Color |= pLayer->m_Color.r << 24; + Color |= pLayer->m_Color.g << 16; + Color |= pLayer->m_Color.b << 8; + Color |= pLayer->m_Color.a; + pLayer->m_Color.r = (State.m_Color >> 24) & 0xff; pLayer->m_Color.g = (State.m_Color >> 16) & 0xff; pLayer->m_Color.b = (State.m_Color >> 8) & 0xff; pLayer->m_Color.a = State.m_Color & 0xff; + + vpActions.push_back(std::make_shared(pEditor, GroupIndex, LayerIndex, ETilesProp::PROP_COLOR, Color, State.m_Color)); } pLayer->FlagModified(0, 0, pLayer->m_Width, pLayer->m_Height); } State.m_Modified = 0; + + char aDisplay[256]; + str_format(aDisplay, sizeof(aDisplay), "Edit %d layers common properties: %s", (int)vpLayers.size(), HasModifiedColor && HasModifiedSize ? "color, size" : (HasModifiedColor ? "color" : "size")); + pEditor->m_EditorHistory.RecordAction(std::make_shared(pEditor, vpActions, aDisplay)); } } else @@ -999,16 +1174,6 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta pToolbox->HSplitTop(2.0f, nullptr, pToolbox); } - enum - { - PROP_WIDTH = 0, - PROP_HEIGHT, - PROP_SHIFT, - PROP_SHIFT_BY, - PROP_COLOR, - NUM_PROPS, - }; - CProperty aProps[] = { {"Width", State.m_Width, PROPTYPE_INT_SCROLL, 1, 100000}, {"Height", State.m_Height, PROPTYPE_INT_SCROLL, 1, 100000}, @@ -1018,49 +1183,57 @@ CUI::EPopupMenuFunctionResult CLayerTiles::RenderCommonProperties(SCommonPropSta {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ETilesCommonProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(pToolbox, aProps, s_aIds, &NewVal); + auto [PropState, Prop] = pEditor->DoPropertiesWithState(pToolbox, aProps, s_aIds, &NewVal); + + static CLayerTilesCommonPropTracker s_Tracker(pEditor); + s_Tracker.m_vpLayers = vpLayers; + s_Tracker.m_vLayerIndices = vLayerIndices; - if(Prop == PROP_WIDTH && NewVal > 1) + s_Tracker.Begin(nullptr, Prop, PropState); + + if(Prop == ETilesCommonProp::PROP_WIDTH && NewVal > 1) { if(NewVal > 1000 && !pEditor->m_LargeLayerWasWarned) { - pEditor->m_PopupEventType = pEditor->POPEVENT_LARGELAYER; + pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER; pEditor->m_PopupEventActivated = true; pEditor->m_LargeLayerWasWarned = true; } State.m_Width = NewVal; } - else if(Prop == PROP_HEIGHT && NewVal > 1) + else if(Prop == ETilesCommonProp::PROP_HEIGHT && NewVal > 1) { if(NewVal > 1000 && !pEditor->m_LargeLayerWasWarned) { - pEditor->m_PopupEventType = pEditor->POPEVENT_LARGELAYER; + pEditor->m_PopupEventType = CEditor::POPEVENT_LARGELAYER; pEditor->m_PopupEventActivated = true; pEditor->m_LargeLayerWasWarned = true; } State.m_Height = NewVal; } - else if(Prop == PROP_SHIFT) + else if(Prop == ETilesCommonProp::PROP_SHIFT) { for(auto &pLayer : vpLayers) pLayer->Shift(NewVal); } - else if(Prop == PROP_SHIFT_BY) + else if(Prop == ETilesCommonProp::PROP_SHIFT_BY) { pEditor->m_ShiftBy = NewVal; } - else if(Prop == PROP_COLOR) + else if(Prop == ETilesCommonProp::PROP_COLOR) { State.m_Color = NewVal; } - if(Prop == PROP_WIDTH || Prop == PROP_HEIGHT) + s_Tracker.End(Prop, PropState); + + if(Prop == ETilesCommonProp::PROP_WIDTH || Prop == ETilesCommonProp::PROP_HEIGHT) { State.m_Modified |= SCommonPropState::MODIFIED_SIZE; } - else if(Prop == PROP_COLOR) + else if(Prop == ETilesCommonProp::PROP_COLOR) { State.m_Modified |= SCommonPropState::MODIFIED_COLOR; } diff --git a/src/game/editor/mapitems/layer_tiles.h b/src/game/editor/mapitems/layer_tiles.h index 48717db3ce6..ca8e84e8892 100644 --- a/src/game/editor/mapitems/layer_tiles.h +++ b/src/game/editor/mapitems/layer_tiles.h @@ -1,8 +1,21 @@ #ifndef GAME_EDITOR_MAPITEMS_LAYER_TILES_H #define GAME_EDITOR_MAPITEMS_LAYER_TILES_H +#include +#include + #include "layer.h" +struct STileStateChange +{ + bool m_Changed; + CTile m_Previous; + CTile m_Current; +}; + +template +using EditorTileStateChangeHistory = std::map>; + enum { DIRECTION_LEFT = 0, @@ -89,18 +102,19 @@ class CLayerTiles : public CLayer virtual CTile GetTile(int x, int y); virtual void SetTile(int x, int y, CTile Tile); + void SetTileIgnoreHistory(int x, int y, CTile Tile) const; virtual void Resize(int NewW, int NewH); virtual void Shift(int Direction); - void MakePalette(); + void MakePalette() const; void Render(bool Tileset = false) override; int ConvertX(float x) const; int ConvertY(float y) const; - void Convert(CUIRect Rect, RECTi *pOut); - void Snap(CUIRect *pRect); - void Clamp(RECTi *pRect); + void Convert(CUIRect Rect, RECTi *pOut) const; + void Snap(CUIRect *pRect) const; + void Clamp(RECTi *pRect) const; virtual bool IsEntitiesLayer() const override; @@ -114,6 +128,7 @@ class CLayerTiles : public CLayer void BrushRotate(float Amount) override; std::shared_ptr Duplicate() const override; + const char *TypeName() const override; virtual void ShowInfo(); CUI::EPopupMenuFunctionResult RenderProperties(CUIRect *pToolbox) override; @@ -130,13 +145,13 @@ 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); + 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; void PrepareForSave(); - void ExtractTiles(int TilemapItemVersion, const CTile *pSavedTiles, size_t SavedTilesSize); + void ExtractTiles(int TilemapItemVersion, const CTile *pSavedTiles, size_t SavedTilesSize) const; void GetSize(float *pWidth, float *pHeight) override { @@ -166,6 +181,14 @@ class CLayerTiles : public CLayer int m_Switch; int m_Tune; char m_aFileName[IO_MAX_PATH_LENGTH]; + + EditorTileStateChangeHistory m_TilesHistory; + inline virtual void ClearHistory() { m_TilesHistory.clear(); } + +protected: + void RecordStateChange(int x, int y, CTile Previous, CTile Tile); + + friend class CAutoMapper; }; #endif diff --git a/src/game/editor/mapitems/layer_tune.cpp b/src/game/editor/mapitems/layer_tune.cpp index 619336943d0..0a41fcc7b20 100644 --- a/src/game/editor/mapitems/layer_tune.cpp +++ b/src/game/editor/mapitems/layer_tune.cpp @@ -12,6 +12,16 @@ CLayerTune::CLayerTune(CEditor *pEditor, int w, int h) : mem_zero(m_pTuneTile, (size_t)w * h * sizeof(CTuneTile)); } +CLayerTune::CLayerTune(const CLayerTune &Other) : + CLayerTiles(Other) +{ + str_copy(m_aName, "Tune copy"); + m_Tune = 1; + + m_pTuneTile = new CTuneTile[m_Width * m_Height]; + mem_copy(m_pTuneTile, Other.m_pTuneTile, (size_t)m_Width * m_Height * sizeof(CTuneTile)); +} + CLayerTune::~CLayerTune() { delete[] m_pTuneTile; @@ -82,40 +92,61 @@ void CLayerTune::BrushDraw(std::shared_ptr pBrush, float wx, float wy) if(!Destructive && GetTile(fx, fy).m_Index) continue; + int Index = fy * m_Width + fx; + STuneTileStateChange::SData Previous{ + m_pTuneTile[Index].m_Number, + m_pTuneTile[Index].m_Type, + m_pTiles[Index].m_Index}; + if((m_pEditor->m_AllowPlaceUnusedTiles || IsValidTuneTile(pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index)) && pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index != TILE_AIR) { if(m_pEditor->m_TuningNum != pTuneLayer->m_TuningNumber) { - m_pTuneTile[fy * m_Width + fx].m_Number = m_pEditor->m_TuningNum; + m_pTuneTile[Index].m_Number = m_pEditor->m_TuningNum; } else if(pTuneLayer->m_pTuneTile[y * pTuneLayer->m_Width + x].m_Number) - m_pTuneTile[fy * m_Width + fx].m_Number = pTuneLayer->m_pTuneTile[y * pTuneLayer->m_Width + x].m_Number; + m_pTuneTile[Index].m_Number = pTuneLayer->m_pTuneTile[y * pTuneLayer->m_Width + x].m_Number; else { if(!m_pEditor->m_TuningNum) { - m_pTuneTile[fy * m_Width + fx].m_Number = 0; - m_pTuneTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pTuneTile[Index].m_Number = 0; + m_pTuneTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; continue; } else - m_pTuneTile[fy * m_Width + fx].m_Number = m_pEditor->m_TuningNum; + m_pTuneTile[Index].m_Number = m_pEditor->m_TuningNum; } - m_pTuneTile[fy * m_Width + fx].m_Type = pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index; - m_pTiles[fy * m_Width + fx].m_Index = pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index; + m_pTuneTile[Index].m_Type = pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index; + m_pTiles[Index].m_Index = pTuneLayer->m_pTiles[y * pTuneLayer->m_Width + x].m_Index; } else { - m_pTuneTile[fy * m_Width + fx].m_Number = 0; - m_pTuneTile[fy * m_Width + fx].m_Type = 0; - m_pTiles[fy * m_Width + fx].m_Index = 0; + m_pTuneTile[Index].m_Number = 0; + m_pTuneTile[Index].m_Type = 0; + m_pTiles[Index].m_Index = 0; } + + STuneTileStateChange::SData Current{ + m_pTuneTile[Index].m_Number, + m_pTuneTile[Index].m_Type, + m_pTiles[Index].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } FlagModified(sx, sy, pTuneLayer->m_Width, pTuneLayer->m_Height); } +void CLayerTune::RecordStateChange(int x, int y, STuneTileStateChange::SData Previous, STuneTileStateChange::SData Current) +{ + if(!m_History[y][x].m_Changed) + m_History[y][x] = STuneTileStateChange{true, Previous, Current}; + else + m_History[y][x].m_Current = Current; +} + void CLayerTune::BrushFlipX() { CLayerTiles::BrushFlipX(); @@ -194,6 +225,11 @@ void CLayerTune::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe const int SrcIndex = Empty ? 0 : (y * pLt->m_Width + x % pLt->m_Width) % (pLt->m_Width * pLt->m_Height); const int TgtIndex = fy * m_Width + fx; + STuneTileStateChange::SData Previous{ + m_pTuneTile[TgtIndex].m_Number, + m_pTuneTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + if(Empty || (!m_pEditor->m_AllowPlaceUnusedTiles && !IsValidTuneTile((pLt->m_pTiles[SrcIndex]).m_Index))) { m_pTiles[TgtIndex].m_Index = 0; @@ -213,8 +249,25 @@ void CLayerTune::FillSelection(bool Empty, std::shared_ptr pBrush, CUIRe m_pTuneTile[TgtIndex].m_Number = pLt->m_pTuneTile[SrcIndex].m_Number; } } + + STuneTileStateChange::SData Current{ + m_pTuneTile[TgtIndex].m_Number, + m_pTuneTile[TgtIndex].m_Type, + m_pTiles[TgtIndex].m_Index}; + + RecordStateChange(fx, fy, Previous, Current); } } FlagModified(sx, sy, w, h); } + +std::shared_ptr CLayerTune::Duplicate() const +{ + return std::make_shared(*this); +} + +const char *CLayerTune::TypeName() const +{ + return "tune"; +} diff --git a/src/game/editor/mapitems/layer_tune.h b/src/game/editor/mapitems/layer_tune.h index 8706778ece8..cf35b95693b 100644 --- a/src/game/editor/mapitems/layer_tune.h +++ b/src/game/editor/mapitems/layer_tune.h @@ -3,10 +3,22 @@ #include "layer_tiles.h" +struct STuneTileStateChange +{ + bool m_Changed; + struct SData + { + int m_Number; + int m_Type; + int m_Index; + } m_Previous, m_Current; +}; + class CLayerTune : public CLayerTiles { public: CLayerTune(CEditor *pEditor, int w, int h); + CLayerTune(const CLayerTune &Other); ~CLayerTune(); CTuneTile *m_pTuneTile; @@ -20,6 +32,19 @@ class CLayerTune : public CLayerTiles void BrushFlipY() override; void BrushRotate(float Amount) override; void FillSelection(bool Empty, std::shared_ptr pBrush, CUIRect Rect) override; + + EditorTileStateChangeHistory m_History; + inline void ClearHistory() override + { + CLayerTiles::ClearHistory(); + m_History.clear(); + } + + std::shared_ptr Duplicate() const override; + const char *TypeName() const override; + +private: + void RecordStateChange(int x, int y, STuneTileStateChange::SData Previous, STuneTileStateChange::SData Current); }; #endif diff --git a/src/game/editor/mapitems/map_io.cpp b/src/game/editor/mapitems/map_io.cpp index ba857df9109..78010be6840 100644 --- a/src/game/editor/mapitems/map_io.cpp +++ b/src/game/editor/mapitems/map_io.cpp @@ -148,6 +148,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); @@ -577,9 +578,7 @@ bool CEditorMap::Load(const char *pFileName, int StorageType, const std::functio } 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); diff --git a/src/game/editor/mapitems/sound.h b/src/game/editor/mapitems/sound.h index 681f9ad30b1..28b222972cb 100644 --- a/src/game/editor/mapitems/sound.h +++ b/src/game/editor/mapitems/sound.h @@ -1,8 +1,7 @@ #ifndef GAME_EDITOR_MAPITEMS_SOUND_H #define GAME_EDITOR_MAPITEMS_SOUND_H -#include - +#include #include class CEditorSound : public CEditorComponent diff --git a/src/game/editor/popups.cpp b/src/game/editor/popups.cpp index 0dd22b9b0ea..e9bcb721c47 100644 --- a/src/game/editor/popups.cpp +++ b/src/game/editor/popups.cpp @@ -16,6 +16,7 @@ #include #include "editor.h" +#include "editor_actions.h" CUI::EPopupMenuFunctionResult CEditor::PopupMenuFile(void *pContext, CUIRect View, bool Active) { @@ -327,6 +328,58 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuSettings(void *pContext, CUIRect } } + 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, "Align quads", 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", !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; + } + } + } + + 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, "Show quads bounds", 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", !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; + } + } + } + return CUI::POPUP_KEEP_OPEN; } @@ -344,6 +397,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, { if(pEditor->DoButton_Editor(&s_DeleteButton, "Delete group", 0, &Button, 0, "Delete group")) { + 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; @@ -403,8 +457,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, 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); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + 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; } } @@ -420,8 +476,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, 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); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + 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; } } @@ -437,8 +495,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, 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); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + 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; } } @@ -454,8 +514,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, 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); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + 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; } } @@ -471,8 +533,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, 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); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + 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; } } @@ -485,8 +549,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, { std::shared_ptr pQuadLayer = std::make_shared(pEditor); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pQuadLayer); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + 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; } @@ -499,8 +565,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, 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); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + 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; } @@ -512,8 +580,10 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, { std::shared_ptr pSoundLayer = std::make_shared(pEditor); pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->AddLayer(pSoundLayer); - pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_vpLayers.size() - 1); + 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; } @@ -530,21 +600,6 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, pEditor->m_Map.OnModify(); } - enum - { - PROP_ORDER = 0, - PROP_POS_X, - PROP_POS_Y, - PROP_PARA_X, - PROP_PARA_Y, - PROP_USE_CLIPPING, - PROP_CLIP_X, - PROP_CLIP_Y, - PROP_CLIP_W, - PROP_CLIP_H, - NUM_PROPS, - }; - 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}, @@ -561,17 +616,20 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, // cut the properties that aren't needed if(pEditor->GetSelectedGroup()->m_GameGroup) - aProps[PROP_POS_X].m_pName = nullptr; + aProps[(int)EGroupProp::PROP_POS_X].m_pName = nullptr; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)EGroupProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + if(Prop != EGroupProp::PROP_NONE) { pEditor->m_Map.OnModify(); } - if(Prop == PROP_ORDER) + static CLayerGroupPropTracker s_Tracker(pEditor); + s_Tracker.Begin(pEditor->GetSelectedGroup().get(), Prop, State); + + if(Prop == EGroupProp::PROP_ORDER) { pEditor->m_SelectedGroup = pEditor->m_Map.SwapGroups(pEditor->m_SelectedGroup, NewVal); } @@ -579,44 +637,46 @@ CUI::EPopupMenuFunctionResult CEditor::PopupGroup(void *pContext, CUIRect View, // these can not be changed on the game group if(!pEditor->GetSelectedGroup()->m_GameGroup) { - if(Prop == PROP_PARA_X) + if(Prop == EGroupProp::PROP_PARA_X) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ParallaxX = NewVal; } - else if(Prop == PROP_PARA_Y) + else if(Prop == EGroupProp::PROP_PARA_Y) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ParallaxY = NewVal; } - else if(Prop == PROP_POS_X) + 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 == PROP_POS_Y) + 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 == PROP_USE_CLIPPING) + else if(Prop == EGroupProp::PROP_USE_CLIPPING) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_UseClipping = NewVal; } - else if(Prop == PROP_CLIP_X) + else if(Prop == EGroupProp::PROP_CLIP_X) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipX = NewVal; } - else if(Prop == PROP_CLIP_Y) + else if(Prop == EGroupProp::PROP_CLIP_Y) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipY = NewVal; } - else if(Prop == PROP_CLIP_W) + else if(Prop == EGroupProp::PROP_CLIP_W) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipW = NewVal; } - else if(Prop == PROP_CLIP_H) + else if(Prop == EGroupProp::PROP_CLIP_H) { pEditor->m_Map.m_vpGroups[pEditor->m_SelectedGroup]->m_ClipH = NewVal; } } + s_Tracker.End(Prop, State); + return CUI::POPUP_KEEP_OPEN; } @@ -630,7 +690,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, if(pPopup->m_vpLayers.size() > 1) { - return CLayerTiles::RenderCommonProperties(pPopup->m_CommonPropState, pEditor, &View, pPopup->m_vpLayers); + return CLayerTiles::RenderCommonProperties(pPopup->m_CommonPropState, pEditor, &View, pPopup->m_vpLayers, pPopup->m_vLayerIndices); } const bool EntitiesLayer = pCurrentLayer->IsEntitiesLayer(); @@ -643,6 +703,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, static int s_DeleteButton = 0; if(pEditor->DoButton_Editor(&s_DeleteButton, "Delete layer", 0, &DeleteButton, 0, "Deletes the layer")) { + 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) @@ -654,6 +716,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, 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; } } @@ -668,6 +731,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, if(pEditor->DoButton_Editor(&s_DuplicationButton, "Duplicate layer", 0, &DuplicateButton, 0, "Duplicates the layer")) { 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; } } @@ -690,14 +754,6 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, if(!EntitiesLayer || pEditor->m_Map.m_pGameLayer != pCurrentLayer) View.HSplitBottom(10.0f, &View, nullptr); - enum - { - PROP_GROUP = 0, - PROP_ORDER, - PROP_HQ, - NUM_PROPS, - }; - 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}, @@ -718,19 +774,23 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, aProps[2].m_Type = PROPTYPE_NULL; } - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ELayerProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + if(Prop != ELayerProp::PROP_NONE) { pEditor->m_Map.OnModify(); } - if(Prop == PROP_ORDER) + static CLayerPropTracker s_Tracker(pEditor); + s_Tracker.Begin(pCurrentLayer.get(), Prop, State); + + if(Prop == ELayerProp::PROP_ORDER) { - pEditor->SelectLayer(pCurrentGroup->SwapLayers(pEditor->m_vSelectedLayers[0], NewVal)); + int NewIndex = pCurrentGroup->SwapLayers(pEditor->m_vSelectedLayers[0], NewVal); + pEditor->SelectLayer(NewIndex); } - else if(Prop == PROP_GROUP) + else if(Prop == ELayerProp::PROP_GROUP) { if(NewVal >= 0 && (size_t)NewVal < pEditor->m_Map.m_vpGroups.size()) { @@ -742,13 +802,15 @@ CUI::EPopupMenuFunctionResult CEditor::PopupLayer(void *pContext, CUIRect View, pEditor->SelectLayer(pEditor->m_Map.m_vpGroups[NewVal]->m_vpLayers.size() - 1); } } - else if(Prop == PROP_HQ) + else if(Prop == ELayerProp::PROP_HQ) { pCurrentLayer->m_Flags &= ~LAYERFLAG_DETAIL; if(NewVal) pCurrentLayer->m_Flags |= LAYERFLAG_DETAIL; } + s_Tracker.End(Prop, State); + return pCurrentLayer->RenderProperties(&View); } @@ -782,6 +844,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b static int s_AspectRatioButton = 0; if(pEditor->DoButton_Editor(&s_AspectRatioButton, "Aspect ratio", 0, &Button, 0, "Resizes the current Quad based on the aspect ratio of the image")) { + pEditor->m_QuadTracker.BeginQuadTrack(pLayer, pEditor->m_vSelectedQuads); for(auto &pQuad : vpQuads) { int Top = pQuad->m_aPoints[0].y; @@ -810,6 +873,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b pQuad->m_aPoints[3].y = Top + Height; pEditor->m_Map.OnModify(); } + pEditor->m_QuadTracker.EndQuadTrack(); + return CUI::POPUP_CLOSE_CURRENT; } } @@ -820,6 +885,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b static int s_AlignButton = 0; if(pEditor->DoButton_Editor(&s_AlignButton, "Align", 0, &Button, 0, "Aligns coordinates of the quad points")) { + pEditor->m_QuadTracker.BeginQuadTrack(pLayer, pEditor->m_vSelectedQuads); for(auto &pQuad : vpQuads) { for(int k = 1; k < 4; k++) @@ -829,6 +895,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b } pEditor->m_Map.OnModify(); } + pEditor->m_QuadTracker.EndQuadTrack(); return CUI::POPUP_CLOSE_CURRENT; } @@ -838,6 +905,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b static int s_Button = 0; if(pEditor->DoButton_Editor(&s_Button, "Square", 0, &Button, 0, "Squares the current quad")) { + pEditor->m_QuadTracker.BeginQuadTrack(pLayer, pEditor->m_vSelectedQuads); for(auto &pQuad : vpQuads) { int Top = pQuad->m_aPoints[0].y; @@ -867,6 +935,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b pQuad->m_aPoints[3].y = Bottom; pEditor->m_Map.OnModify(); } + pEditor->m_QuadTracker.EndQuadTrack(); return CUI::POPUP_CLOSE_CURRENT; } @@ -881,18 +950,6 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b return CUI::POPUP_CLOSE_CURRENT; } - enum - { - PROP_ORDER = 0, - PROP_POS_X, - PROP_POS_Y, - PROP_POS_ENV, - PROP_POS_ENV_OFFSET, - PROP_COLOR_ENV, - PROP_COLOR_ENV_OFFSET, - NUM_PROPS, - }; - const int NumQuads = pLayer ? (int)pLayer->m_vQuads.size() : 0; CProperty aProps[] = { {"Order", pEditor->m_vSelectedQuads[pEditor->m_SelectedQuadIndex], PROPTYPE_INT_STEP, 0, NumQuads}, @@ -905,18 +962,23 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)EQuadProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto PropRes = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + EQuadProp Prop = PropRes.m_Value; + if(Prop != EQuadProp::PROP_NONE) { 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); + } } const float OffsetX = i2fx(NewVal) - pCurrentQuad->m_aPoints[4].x; const float OffsetY = i2fx(NewVal) - pCurrentQuad->m_aPoints[4].y; - if(Prop == PROP_ORDER && pLayer) + if(Prop == EQuadProp::PROP_ORDER && pLayer) { const int QuadIndex = pLayer->SwapQuads(pEditor->m_vSelectedQuads[pEditor->m_SelectedQuadIndex], NewVal); pEditor->m_vSelectedQuads[pEditor->m_SelectedQuadIndex] = QuadIndex; @@ -924,17 +986,17 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b for(auto &pQuad : vpQuads) { - if(Prop == PROP_POS_X) + if(Prop == EQuadProp::PROP_POS_X) { for(auto &Point : pQuad->m_aPoints) Point.x += OffsetX; } - else if(Prop == PROP_POS_Y) + else if(Prop == EQuadProp::PROP_POS_Y) { for(auto &Point : pQuad->m_aPoints) Point.y += OffsetY; } - else if(Prop == PROP_POS_ENV) + else if(Prop == EQuadProp::PROP_POS_ENV) { int Index = clamp(NewVal - 1, -1, (int)pEditor->m_Map.m_vpEnvelopes.size() - 1); int StepDirection = Index < pQuad->m_PosEnv ? -1 : 1; @@ -950,11 +1012,11 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b } } } - else if(Prop == PROP_POS_ENV_OFFSET) + else if(Prop == EQuadProp::PROP_POS_ENV_OFFSET) { pQuad->m_PosEnvOffset = NewVal; } - else if(Prop == PROP_COLOR_ENV) + else if(Prop == EQuadProp::PROP_COLOR_ENV) { int Index = clamp(NewVal - 1, -1, (int)pEditor->m_Map.m_vpEnvelopes.size() - 1); int StepDirection = Index < pQuad->m_ColorEnv ? -1 : 1; @@ -970,12 +1032,20 @@ CUI::EPopupMenuFunctionResult CEditor::PopupQuad(void *pContext, CUIRect View, b } } } - else if(Prop == PROP_COLOR_ENV_OFFSET) + else if(Prop == EQuadProp::PROP_COLOR_ENV_OFFSET) { pQuad->m_ColorEnvOffset = NewVal; } } + if(Prop != EQuadProp::PROP_NONE) + { + if(PropRes.m_State == EEditState::END || PropRes.m_State == EEditState::ONE_GO) + { + pEditor->m_QuadTracker.EndQuadPropTrack(Prop); + } + } + return CUI::POPUP_KEEP_OPEN; } @@ -983,6 +1053,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, { CEditor *pEditor = static_cast(pContext); CSoundSource *pSource = pEditor->GetSelectedSource(); + if(!pSource) + return CUI::POPUP_CLOSE_CURRENT; CUIRect Button; @@ -994,9 +1066,7 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, std::shared_ptr pLayer = std::static_pointer_cast(pEditor->GetSelectedLayerType(0, LAYERTYPE_SOUNDS)); if(pLayer) { - pEditor->m_Map.OnModify(); - pLayer->m_vSources.erase(pLayer->m_vSources.begin() + pEditor->m_SelectedSource); - pEditor->m_SelectedSource--; + pEditor->m_EditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedGroup, pEditor->m_vSelectedLayers[0], pEditor->m_SelectedSource)); } return CUI::POPUP_CLOSE_CURRENT; } @@ -1015,40 +1085,9 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, static int s_ShapeTypeButton = 0; if(pEditor->DoButton_Editor(&s_ShapeTypeButton, s_apShapeNames[pSource->m_Shape.m_Type], 0, &ShapeButton, 0, "Change shape")) { - pSource->m_Shape.m_Type = (pSource->m_Shape.m_Type + 1) % CSoundShape::NUM_SHAPES; - - // set default values - switch(pSource->m_Shape.m_Type) - { - case CSoundShape::SHAPE_CIRCLE: - { - pSource->m_Shape.m_Circle.m_Radius = 1000.0f; - break; - } - case CSoundShape::SHAPE_RECTANGLE: - { - pSource->m_Shape.m_Rectangle.m_Width = f2fx(1000.0f); - pSource->m_Shape.m_Rectangle.m_Height = f2fx(800.0f); - break; - } - } + pEditor->m_EditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedGroup, pEditor->m_vSelectedLayers[0], pEditor->m_SelectedSource, CEditorActionEditSoundSource::EEditType::SHAPE, (pSource->m_Shape.m_Type + 1) % CSoundShape::NUM_SHAPES)); } - enum - { - PROP_POS_X = 0, - PROP_POS_Y, - PROP_LOOP, - PROP_PAN, - PROP_TIME_DELAY, - PROP_FALLOFF, - PROP_POS_ENV, - PROP_POS_ENV_OFFSET, - PROP_SOUND_ENV, - PROP_SOUND_ENV_OFFSET, - NUM_PROPS, - }; - 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}, @@ -1063,39 +1102,42 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)ESoundProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto [State, Prop] = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + if(Prop != ESoundProp::PROP_NONE) { pEditor->m_Map.OnModify(); } - if(Prop == PROP_POS_X) + static CSoundSourcePropTracker s_Tracker(pEditor); + s_Tracker.Begin(pSource, Prop, State); + + if(Prop == ESoundProp::PROP_POS_X) { pSource->m_Position.x = NewVal * 1000; } - else if(Prop == PROP_POS_Y) + else if(Prop == ESoundProp::PROP_POS_Y) { pSource->m_Position.y = NewVal * 1000; } - else if(Prop == PROP_LOOP) + else if(Prop == ESoundProp::PROP_LOOP) { pSource->m_Loop = NewVal; } - else if(Prop == PROP_PAN) + else if(Prop == ESoundProp::PROP_PAN) { pSource->m_Pan = NewVal; } - else if(Prop == PROP_TIME_DELAY) + else if(Prop == ESoundProp::PROP_TIME_DELAY) { pSource->m_TimeDelay = NewVal; } - else if(Prop == PROP_FALLOFF) + else if(Prop == ESoundProp::PROP_FALLOFF) { pSource->m_Falloff = NewVal; } - else if(Prop == PROP_POS_ENV) + else if(Prop == ESoundProp::PROP_POS_ENV) { int Index = clamp(NewVal - 1, -1, (int)pEditor->m_Map.m_vpEnvelopes.size() - 1); const int StepDirection = Index < pSource->m_PosEnv ? -1 : 1; @@ -1108,11 +1150,11 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, } } } - else if(Prop == PROP_POS_ENV_OFFSET) + else if(Prop == ESoundProp::PROP_POS_ENV_OFFSET) { pSource->m_PosEnvOffset = NewVal; } - else if(Prop == PROP_SOUND_ENV) + else if(Prop == ESoundProp::PROP_SOUND_ENV) { int Index = clamp(NewVal - 1, -1, (int)pEditor->m_Map.m_vpEnvelopes.size() - 1); const int StepDirection = Index < pSource->m_SoundEnv ? -1 : 1; @@ -1125,75 +1167,72 @@ CUI::EPopupMenuFunctionResult CEditor::PopupSource(void *pContext, CUIRect View, } } } - else if(Prop == PROP_SOUND_ENV_OFFSET) + else if(Prop == ESoundProp::PROP_SOUND_ENV_OFFSET) { pSource->m_SoundEnvOffset = NewVal; } + s_Tracker.End(Prop, State); + // source shape properties switch(pSource->m_Shape.m_Type) { case CSoundShape::SHAPE_CIRCLE: { - enum - { - PROP_CIRCLE_RADIUS = 0, - NUM_CIRCLE_PROPS, - }; - CProperty aCircleProps[] = { {"Radius", pSource->m_Shape.m_Circle.m_Radius, PROPTYPE_INT_SCROLL, 0, 1000000}, {nullptr}, }; - static int s_aCircleIds[NUM_CIRCLE_PROPS] = {0}; + static int s_aCircleIds[(int)ECircleShapeProp::NUM_CIRCLE_PROPS] = {0}; NewVal = 0; - Prop = pEditor->DoProperties(&View, aCircleProps, s_aCircleIds, &NewVal); - if(Prop != -1) + auto [LocalState, LocalProp] = pEditor->DoPropertiesWithState(&View, aCircleProps, s_aCircleIds, &NewVal); + if(LocalProp != ECircleShapeProp::PROP_NONE) { pEditor->m_Map.OnModify(); } - if(Prop == PROP_CIRCLE_RADIUS) + static CSoundSourceCircleShapePropTracker s_ShapeTracker(pEditor); + s_ShapeTracker.Begin(pSource, LocalProp, LocalState); + + if(LocalProp == ECircleShapeProp::PROP_CIRCLE_RADIUS) { pSource->m_Shape.m_Circle.m_Radius = NewVal; } + s_ShapeTracker.End(LocalProp, LocalState); break; } case CSoundShape::SHAPE_RECTANGLE: { - enum - { - PROP_RECTANGLE_WIDTH = 0, - PROP_RECTANGLE_HEIGHT, - NUM_RECTANGLE_PROPS, - }; - 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}, {nullptr}, }; - static int s_aRectangleIds[NUM_RECTANGLE_PROPS] = {0}; + static int s_aRectangleIds[(int)ERectangleShapeProp::NUM_RECTANGLE_PROPS] = {0}; NewVal = 0; - Prop = pEditor->DoProperties(&View, aRectangleProps, s_aRectangleIds, &NewVal); - if(Prop != -1) + auto [LocalState, LocalProp] = pEditor->DoPropertiesWithState(&View, aRectangleProps, s_aRectangleIds, &NewVal); + if(LocalProp != ERectangleShapeProp::PROP_NONE) { pEditor->m_Map.OnModify(); } - if(Prop == PROP_RECTANGLE_WIDTH) + static CSoundSourceRectShapePropTracker s_ShapeTracker(pEditor); + s_ShapeTracker.Begin(pSource, LocalProp, LocalState); + + if(LocalProp == ERectangleShapeProp::PROP_RECTANGLE_WIDTH) { pSource->m_Shape.m_Rectangle.m_Width = NewVal * 1024; } - else if(Prop == PROP_RECTANGLE_HEIGHT) + else if(LocalProp == ERectangleShapeProp::PROP_RECTANGLE_HEIGHT) { pSource->m_Shape.m_Rectangle.m_Height = NewVal * 1024; } + s_ShapeTracker.End(LocalProp, LocalState); break; } } @@ -1208,22 +1247,9 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, if(!in_range(pEditor->m_SelectedQuadIndex, 0, vpQuads.size() - 1)) return CUI::POPUP_CLOSE_CURRENT; CQuad *pCurrentQuad = vpQuads[pEditor->m_SelectedQuadIndex]; + std::shared_ptr pLayer = std::static_pointer_cast(pEditor->GetSelectedLayerType(0, LAYERTYPE_QUADS)); - enum - { - PROP_POS_X = 0, - PROP_POS_Y, - PROP_COLOR, - PROP_TEX_U, - PROP_TEX_V, - NUM_PROPS, - }; - - int Color = 0; - Color |= pCurrentQuad->m_aColors[pEditor->m_SelectedQuadPoint].r << 24; - Color |= pCurrentQuad->m_aColors[pEditor->m_SelectedQuadPoint].g << 16; - Color |= pCurrentQuad->m_aColors[pEditor->m_SelectedQuadPoint].b << 8; - Color |= pCurrentQuad->m_aColors[pEditor->m_SelectedQuadPoint].a; + int Color = PackColor(pCurrentQuad->m_aColors[pEditor->m_SelectedQuadPoint]); const int X = fx2i(pCurrentQuad->m_aPoints[pEditor->m_SelectedQuadPoint].x); const int Y = fx2i(pCurrentQuad->m_aPoints[pEditor->m_SelectedQuadPoint].y); @@ -1239,29 +1265,35 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, {nullptr}, }; - static int s_aIds[NUM_PROPS] = {0}; + static int s_aIds[(int)EQuadPointProp::NUM_PROPS] = {0}; int NewVal = 0; - int Prop = pEditor->DoProperties(&View, aProps, s_aIds, &NewVal); - if(Prop != -1) + auto PropRes = pEditor->DoPropertiesWithState(&View, aProps, s_aIds, &NewVal); + EQuadPointProp Prop = PropRes.m_Value; + if(Prop != EQuadPointProp::PROP_NONE) { 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); + } } for(CQuad *pQuad : vpQuads) { - if(Prop == PROP_POS_X) + if(Prop == EQuadPointProp::PROP_POS_X) { for(int v = 0; v < 4; v++) if(pEditor->IsQuadCornerSelected(v)) pQuad->m_aPoints[v].x = i2fx(fx2i(pQuad->m_aPoints[v].x) + NewVal - X); } - else if(Prop == PROP_POS_Y) + else if(Prop == EQuadPointProp::PROP_POS_Y) { for(int v = 0; v < 4; v++) if(pEditor->IsQuadCornerSelected(v)) pQuad->m_aPoints[v].y = i2fx(fx2i(pQuad->m_aPoints[v].y) + NewVal - Y); } - else if(Prop == PROP_COLOR) + else if(Prop == EQuadPointProp::PROP_COLOR) { for(int v = 0; v < 4; v++) { @@ -1274,13 +1306,13 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, } } } - else if(Prop == PROP_TEX_U) + else if(Prop == EQuadPointProp::PROP_TEX_U) { for(int v = 0; v < 4; v++) if(pEditor->IsQuadCornerSelected(v)) pQuad->m_aTexcoords[v].x = f2fx(fx2f(pQuad->m_aTexcoords[v].x) + (NewVal - TextureU) / 1024.0f); } - else if(Prop == PROP_TEX_V) + else if(Prop == EQuadPointProp::PROP_TEX_V) { for(int v = 0; v < 4; v++) if(pEditor->IsQuadCornerSelected(v)) @@ -1288,12 +1320,24 @@ CUI::EPopupMenuFunctionResult CEditor::PopupPoint(void *pContext, CUIRect View, } } + if(Prop != EQuadPointProp::PROP_NONE) + { + 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; } 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; + const float RowHeight = 12.0f; CUIRect Row, Label, EditBox; @@ -1309,14 +1353,41 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie Row.VSplitLeft(10.0f, nullptr, &EditBox); pEditor->UI()->DoLabel(&Label, "Color:", RowHeight - 2.0f, TEXTALIGN_ML); - const auto [SelectedIndex, _] = pEditor->m_vSelectedEnvelopePoints.front(); + const auto SelectedPoint = pEditor->m_vSelectedEnvelopePoints.front(); + const int SelectedIndex = SelectedPoint.first; auto *pValues = pEnvelope->m_vPoints[SelectedIndex].m_aValues; const ColorRGBA Color = ColorRGBA(fx2f(pValues[0]), fx2f(pValues[1]), fx2f(pValues[2]), fx2f(pValues[3])); const auto &&SetColor = [&](ColorRGBA NewColor) { - if(Color == NewColor) + if(Color == NewColor && pEditor->m_ColorPickerPopupContext.m_State == EEditState::EDITING) return; + + static int s_Values[4]; + + if(pEditor->m_ColorPickerPopupContext.m_State == EEditState::START || pEditor->m_ColorPickerPopupContext.m_State == EEditState::ONE_GO) + { + for(int Channel = 0; Channel < 4; ++Channel) + s_Values[Channel] = pValues[Channel]; + } + for(int Channel = 0; Channel < 4; ++Channel) + { pValues[Channel] = f2fx(NewColor[Channel]); + } + + if(pEditor->m_ColorPickerPopupContext.m_State == EEditState::END || pEditor->m_ColorPickerPopupContext.m_State == EEditState::ONE_GO) + { + std::vector> vpActions(4); + + for(int Channel = 0; Channel < 4; ++Channel) + { + vpActions[Channel] = std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, Channel, CEditorActionEnvelopeEditPoint::EEditType::VALUE, s_Values[Channel], f2fx(NewColor[Channel])); + } + + char aDisplay[256]; + str_format(aDisplay, sizeof(aDisplay), "Edit color of point %d of envelope %d", SelectedIndex, pEditor->m_SelectedEnvelope); + pEditor->m_EnvelopeEditorHistory.RecordAction(std::make_shared(pEditor, vpActions, aDisplay)); + } + pEditor->m_UpdateEnvPointInfo = true; pEditor->m_Map.OnModify(); }; @@ -1327,37 +1398,23 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie static CLineInputNumber s_CurValueInput; static CLineInputNumber s_CurTimeInput; + static float s_CurrentTime = 0; + static float s_CurrentValue = 0; + if(pEditor->m_UpdateEnvPointInfo) { pEditor->m_UpdateEnvPointInfo = false; - int CurrentTime; - int CurrentValue; - if(pEditor->IsTangentInSelected()) - { - auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentInPoint; - - CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel]; - CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaY[SelectedChannel]; - } - else if(pEditor->IsTangentOutSelected()) - { - auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentOutPoint; - - CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel]; - CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaY[SelectedChannel]; - } - else - { - auto [SelectedIndex, SelectedChannel] = pEditor->m_vSelectedEnvelopePoints.front(); - - CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time; - CurrentValue = pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]; - } + auto TimeAndValue = pEditor->EnvGetSelectedTimeAndValue(); + int CurrentTime = TimeAndValue.first; + int CurrentValue = TimeAndValue.second; // update displayed text s_CurValueInput.SetFloat(fx2f(CurrentValue)); s_CurTimeInput.SetFloat(CurrentTime / 1000.0f); + + s_CurrentTime = s_CurTimeInput.GetFloat(); + s_CurrentValue = s_CurValueInput.GetFloat(); } View.HSplitTop(RowHeight, &Row, &View); @@ -1377,53 +1434,46 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie { float CurrentTime = s_CurTimeInput.GetFloat(); float CurrentValue = s_CurValueInput.GetFloat(); - if(pEditor->IsTangentInSelected()) - { - auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentInPoint; - - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel] = minimum(CurrentTime * 1000.0f - pEnvelope->m_vPoints[SelectedIndex].m_Time, 0); - CurrentTime = (pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel]) / 1000.0f; - - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaY[SelectedChannel] = f2fx(CurrentValue) - pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]; - } - else if(pEditor->IsTangentOutSelected()) + if(!(absolute(CurrentTime - s_CurrentTime) < 0.0001f && absolute(CurrentValue - s_CurrentValue) < 0.0001f)) { - auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentOutPoint; - - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel] = maximum(CurrentTime * 1000.0f - pEnvelope->m_vPoints[SelectedIndex].m_Time, 0); - CurrentTime = (pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel]) / 1000.0f; + auto [OldTime, OldValue] = pEditor->EnvGetSelectedTimeAndValue(); - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaY[SelectedChannel] = f2fx(CurrentValue) - pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel]; - } - else - { - auto [SelectedIndex, SelectedChannel] = pEditor->m_vSelectedEnvelopePoints.front(); - if(pEnvelope->GetChannels() == 4) - CurrentValue = clamp(CurrentValue, 0.0f, 1.0f); - pEnvelope->m_vPoints[SelectedIndex].m_aValues[SelectedChannel] = f2fx(CurrentValue); - - if(SelectedIndex != 0) + if(pEditor->IsTangentInSelected()) { - pEnvelope->m_vPoints[SelectedIndex].m_Time = CurrentTime * 1000.0f; + auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentInPoint; - if(pEnvelope->m_vPoints[SelectedIndex].m_Time < pEnvelope->m_vPoints[SelectedIndex - 1].m_Time) - pEnvelope->m_vPoints[SelectedIndex].m_Time = pEnvelope->m_vPoints[SelectedIndex - 1].m_Time + 1; - if(static_cast(SelectedIndex) + 1 != pEnvelope->m_vPoints.size() && pEnvelope->m_vPoints[SelectedIndex].m_Time > pEnvelope->m_vPoints[SelectedIndex + 1].m_Time) - pEnvelope->m_vPoints[SelectedIndex].m_Time = pEnvelope->m_vPoints[SelectedIndex + 1].m_Time - 1; + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, CEditorActionEditEnvelopePointValue::EType::TANGENT_IN, static_cast(OldTime * 1000.0f), f2fx(OldValue), static_cast(CurrentTime * 1000.0f), f2fx(CurrentValue))); + CurrentTime = (pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel]) / 1000.0f; + } + else if(pEditor->IsTangentOutSelected()) + { + auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentOutPoint; - CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time / 1000.0f; + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, CEditorActionEditEnvelopePointValue::EType::TANGENT_OUT, static_cast(OldTime * 1000.0f), f2fx(OldValue), static_cast(CurrentTime * 1000.0f), f2fx(CurrentValue))); + CurrentTime = (pEnvelope->m_vPoints[SelectedIndex].m_Time + pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel]) / 1000.0f; } else { - CurrentTime = 0.0f; - pEnvelope->m_vPoints[SelectedIndex].m_Time = 0.0f; + auto [SelectedIndex, SelectedChannel] = pEditor->m_vSelectedEnvelopePoints.front(); + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, CEditorActionEditEnvelopePointValue::EType::POINT, static_cast(OldTime * 1000.0f), f2fx(OldValue), static_cast(CurrentTime * 1000.0f), f2fx(CurrentValue))); + + if(SelectedIndex != 0) + { + CurrentTime = pEnvelope->m_vPoints[SelectedIndex].m_Time / 1000.0f; + } + else + { + CurrentTime = 0.0f; + pEnvelope->m_vPoints[SelectedIndex].m_Time = 0.0f; + } } - } - s_CurTimeInput.SetFloat(static_cast(CurrentTime * 1000.0f) / 1000.0f); - s_CurValueInput.SetFloat(fx2f(f2fx(CurrentValue))); + s_CurTimeInput.SetFloat(static_cast(CurrentTime * 1000.0f) / 1000.0f); + s_CurValueInput.SetFloat(fx2f(f2fx(CurrentValue))); - pEditor->m_Map.OnModify(); + s_CurrentTime = s_CurTimeInput.GetFloat(); + s_CurrentValue = s_CurValueInput.GetFloat(); + } } View.HSplitTop(6.0f, nullptr, &View); @@ -1436,24 +1486,18 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPoint(void *pContext, CUIRect Vie if(pEditor->IsTangentInSelected()) { auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentInPoint; - - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaX[SelectedChannel] = 0.0f; - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aInTangentDeltaY[SelectedChannel] = 0.0f; + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, true)); } else if(pEditor->IsTangentOutSelected()) { auto [SelectedIndex, SelectedChannel] = pEditor->m_SelectedTangentOutPoint; - - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaX[SelectedChannel] = 0.0f; - pEnvelope->m_vPoints[SelectedIndex].m_Bezier.m_aOutTangentDeltaY[SelectedChannel] = 0.0f; + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex, SelectedChannel, false)); } else { auto [SelectedIndex, SelectedChannel] = pEditor->m_vSelectedEnvelopePoints.front(); - - pEnvelope->m_vPoints.erase(pEnvelope->m_vPoints.begin() + SelectedIndex); + pEditor->m_EnvelopeEditorHistory.Execute(std::make_shared(pEditor, pEditor->m_SelectedEnvelope, SelectedIndex)); } - pEditor->m_Map.OnModify(); return CUI::POPUP_CLOSE_CURRENT; } @@ -1515,6 +1559,8 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPointCurveType(void *pContext, CU if(pEditor->DoButton_MenuItem(&s_ButtonSmoothID, "Smooth", 0, &ButtonSmooth)) CurveType = CURVETYPE_SMOOTH; + std::vector> vpActions; + if(CurveType >= 0) { std::shared_ptr pEnvelope = pEditor->m_Map.m_vpEnvelopes.at(pEditor->m_SelectedEnvelope); @@ -1551,13 +1597,20 @@ CUI::EPopupMenuFunctionResult CEditor::PopupEnvPointCurveType(void *pContext, CU CEnvPoint &CurrentPoint = pEnvelope->m_vPoints[SelectedIndex]; ColorRGBA Channels; HelperEnvelope.Eval(CurrentPoint.m_Time / 1000.0f, Channels); + 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])); } } } } } + if(!vpActions.empty()) + { + pEditor->m_EnvelopeEditorHistory.RecordAction(std::make_shared(pEditor, vpActions, "Project points")); + } + pEditor->m_Map.OnModify(); return CUI::POPUP_CLOSE_CURRENT; } @@ -1902,6 +1955,14 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMapInfo(void *pContext, CUIRect View static int s_ConfirmButton = 0; 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; + bool CreditsDifferent = str_comp(pEditor->m_Map.m_MapInfoTmp.m_aCredits, pEditor->m_Map.m_MapInfo.m_aCredits) != 0; + bool LicenseDifferent = str_comp(pEditor->m_Map.m_MapInfoTmp.m_aLicense, pEditor->m_Map.m_MapInfo.m_aLicense) != 0; + + if(AuthorDifferent || VersionDifferent || CreditsDifferent || LicenseDifferent) + pEditor->m_Map.OnModify(); + pEditor->m_Map.m_MapInfo.Copy(pEditor->m_Map.m_MapInfoTmp); return CUI::POPUP_CLOSE_CURRENT; } diff --git a/src/game/editor/tileart.cpp b/src/game/editor/tileart.cpp index da96fdcff53..5524dce026e 100644 --- a/src/game/editor/tileart.cpp +++ b/src/game/editor/tileart.cpp @@ -1,4 +1,5 @@ #include "editor.h" +#include "editor_actions.h" #include @@ -184,10 +185,15 @@ static void SetTilelayerIndices(const std::shared_ptr &pLayer, cons } } -void CEditor::AddTileart() +void CEditor::AddTileart(bool IgnoreHistory) { + char aTileArtFileName[IO_MAX_PATH_LENGTH]; + IStorage::StripPathAndExtension(m_aTileartFilename, aTileArtFileName, sizeof(aTileArtFileName)); + std::shared_ptr pGroup = m_Map.NewGroup(); - str_copy(pGroup->m_aName, m_aTileartFilename); + str_copy(pGroup->m_aName, aTileArtFileName); + + int ImageCount = m_Map.m_vpImages.size(); auto vUniqueColors = GetUniqueColors(m_TileartImageInfo); auto vaColorGroups = GroupColors(vUniqueColors); @@ -195,11 +201,16 @@ void CEditor::AddTileart() char aImageName[IO_MAX_PATH_LENGTH]; for(size_t i = 0; i < vColorImages.size(); i++) { - str_format(aImageName, sizeof(aImageName), "%s %" PRIzu, m_aTileartFilename, i + 1); + str_format(aImageName, sizeof(aImageName), "%s %" PRIzu, aTileArtFileName, i + 1); std::shared_ptr pLayer = AddLayerWithImage(this, pGroup, m_TileartImageInfo.m_Width, m_TileartImageInfo.m_Height, vColorImages[i], aImageName); SetTilelayerIndices(pLayer, vaColorGroups[i], m_TileartImageInfo); } - SortImages(); + auto IndexMap = SortImages(); + + if(!IgnoreHistory) + { + m_EditorHistory.RecordAction(std::make_shared(this, ImageCount, m_aTileartFilename, IndexMap)); + } free(m_TileartImageInfo.m_pData); m_TileartImageInfo.m_pData = nullptr; @@ -237,7 +248,7 @@ bool CEditor::CallbackAddTileart(const char *pFilepath, int StorageType, void *p return false; } - IStorage::StripPathAndExtension(pFilepath, pEditor->m_aTileartFilename, sizeof(pEditor->m_aTileartFilename)); + str_copy(pEditor->m_aTileartFilename, pFilepath); if(pEditor->m_TileartImageInfo.m_Width * pEditor->m_TileartImageInfo.m_Height > 10'000) { pEditor->m_PopupEventType = CEditor::POPEVENT_PIXELART_BIG_IMAGE; diff --git a/src/game/gamecore.cpp b/src/game/gamecore.cpp index 409fa0780c0..3e0f921c665 100644 --- a/src/game/gamecore.cpp +++ b/src/game/gamecore.cpp @@ -6,6 +6,7 @@ #include "mapitems.h" #include "teamscore.h" +#include #include const char *CTuningParams::ms_apNames[] = @@ -546,7 +547,7 @@ void CCharacterCore::Move() m_Pos = NewPos; } -void CCharacterCore::Write(CNetObj_CharacterCore *pObjCore) +void CCharacterCore::Write(CNetObj_CharacterCore *pObjCore) const { pObjCore->m_X = round_to_int(m_Pos.x); pObjCore->m_Y = round_to_int(m_Pos.y); diff --git a/src/game/gamecore.h b/src/game/gamecore.h index 8c81c00bda9..8b16cfcaaec 100644 --- a/src/game/gamecore.h +++ b/src/game/gamecore.h @@ -3,7 +3,6 @@ #ifndef GAME_GAMECORE_H #define GAME_GAMECORE_H -#include #include #include @@ -191,7 +190,10 @@ class CWorldCore public: CWorldCore() { - mem_zero(m_apCharacters, sizeof(m_apCharacters)); + for(auto &pCharacter : m_apCharacters) + { + pCharacter = nullptr; + } m_pPrng = nullptr; } @@ -275,7 +277,7 @@ class CCharacterCore void Move(); void Read(const CNetObj_CharacterCore *pObjCore); - void Write(CNetObj_CharacterCore *pObjCore); + void Write(CNetObj_CharacterCore *pObjCore) const; void Quantize(); // DDRace diff --git a/src/game/layers.cpp b/src/game/layers.cpp index d36b1cbbc15..b00d0038c35 100644 --- a/src/game/layers.cpp +++ b/src/game/layers.cpp @@ -45,6 +45,7 @@ void CLayers::Init(class IKernel *pKernel) if(pLayer->m_Type == LAYERTYPE_TILES) { CMapItemLayerTilemap *pTilemap = reinterpret_cast(pLayer); + bool IsEntities = false; if(pTilemap->m_Flags & TILESLAYERFLAG_GAME) { @@ -66,6 +67,7 @@ void CLayers::Init(class IKernel *pKernel) m_pGameGroup->m_ClipH = 0; } + IsEntities = true; //break; } if(pTilemap->m_Flags & TILESLAYERFLAG_TELE) @@ -75,6 +77,7 @@ void CLayers::Init(class IKernel *pKernel) pTilemap->m_Tele = *((int *)(pTilemap) + 15); } m_pTeleLayer = pTilemap; + IsEntities = true; } if(pTilemap->m_Flags & TILESLAYERFLAG_SPEEDUP) { @@ -83,6 +86,7 @@ void CLayers::Init(class IKernel *pKernel) pTilemap->m_Speedup = *((int *)(pTilemap) + 16); } m_pSpeedupLayer = pTilemap; + IsEntities = true; } if(pTilemap->m_Flags & TILESLAYERFLAG_FRONT) { @@ -91,6 +95,7 @@ void CLayers::Init(class IKernel *pKernel) pTilemap->m_Front = *((int *)(pTilemap) + 17); } m_pFrontLayer = pTilemap; + IsEntities = true; } if(pTilemap->m_Flags & TILESLAYERFLAG_SWITCH) { @@ -99,6 +104,7 @@ void CLayers::Init(class IKernel *pKernel) pTilemap->m_Switch = *((int *)(pTilemap) + 18); } m_pSwitchLayer = pTilemap; + IsEntities = true; } if(pTilemap->m_Flags & TILESLAYERFLAG_TUNE) { @@ -107,6 +113,12 @@ void CLayers::Init(class IKernel *pKernel) pTilemap->m_Tune = *((int *)(pTilemap) + 19); } m_pTuneLayer = pTilemap; + IsEntities = true; + } + + if(IsEntities) + { // Ensure default color for entities layers + pTilemap->m_Color = CColor(255, 255, 255, 255); } } } diff --git a/src/game/mapitems.cpp b/src/game/mapitems.cpp index a9620bc98eb..2ccab306703 100644 --- a/src/game/mapitems.cpp +++ b/src/game/mapitems.cpp @@ -150,3 +150,13 @@ bool IsCreditsTile(int TileIndex) (TILE_CREDITS_7 == TileIndex) || (TILE_CREDITS_8 == TileIndex)); } + +int PackColor(CColor Color) +{ + int Res = 0; + Res |= Color.r << 24; + Res |= Color.g << 16; + Res |= Color.b << 8; + Res |= Color.a; + return Res; +} diff --git a/src/game/mapitems.h b/src/game/mapitems.h index c65fcfa88ee..54940501eb3 100644 --- a/src/game/mapitems.h +++ b/src/game/mapitems.h @@ -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; }; @@ -578,5 +581,6 @@ bool IsValidTuneTile(int Index); bool IsValidEntity(int Index); bool IsRotatableTile(int Index); bool IsCreditsTile(int TileIndex); +int PackColor(CColor Color); #endif diff --git a/src/game/server/ddracechat.cpp b/src/game/server/ddracechat.cpp index 8da773c9b4d..9443bdeaa41 100644 --- a/src/game/server/ddracechat.cpp +++ b/src/game/server/ddracechat.cpp @@ -871,7 +871,7 @@ void CGameContext::ConRank(IConsole::IResult *pResult, void *pUserData) pSelf->Server()->ClientName(pResult->m_ClientID)); } -void CGameContext::ConLockTeam(IConsole::IResult *pResult, void *pUserData) +void CGameContext::ConLock(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; if(!CheckClientID(pResult->m_ClientID)) @@ -917,7 +917,7 @@ void CGameContext::ConLockTeam(IConsole::IResult *pResult, void *pUserData) } } -void CGameContext::ConUnlockTeam(IConsole::IResult *pResult, void *pUserData) +void CGameContext::ConUnlock(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; if(!CheckClientID(pResult->m_ClientID)) @@ -941,7 +941,7 @@ void CGameContext::ConUnlockTeam(IConsole::IResult *pResult, void *pUserData) pSelf->UnlockTeam(pResult->m_ClientID, Team); } -void CGameContext::UnlockTeam(int ClientID, int Team) +void CGameContext::UnlockTeam(int ClientID, int Team) const { m_pController->Teams().SetTeamLock(Team, false); @@ -950,7 +950,83 @@ void CGameContext::UnlockTeam(int ClientID, int Team) SendChatTeam(Team, aBuf); } -void CGameContext::ConInviteTeam(IConsole::IResult *pResult, void *pUserData) +void CGameContext::AttemptJoinTeam(int ClientID, int Team) +{ + CPlayer *pPlayer = m_apPlayers[ClientID]; + if(!pPlayer) + return; + + 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()) + { + Console()->Print( + IConsole::OUTPUT_LEVEL_STANDARD, + "chatresp", + "You must join a team and play with somebody or else you can\'t play"); + pPlayer->GetCharacter()->m_LastStartWarning = Server()->Tick(); + } + + if(pPlayer->GetCharacter() == 0) + { + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", + "You can't change teams while you are dead/a spectator."); + } + else + { + if(Team < 0 || Team >= MAX_CLIENTS) + Team = m_pController->Teams().GetFirstEmptyTeam(); + + if(pPlayer->m_Last_Team + (int64_t)Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay > Server()->Tick()) + { + 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)) + { + 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) + { + 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)) + { + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", pError); + } + else + { + char aBuf[512]; + str_format(aBuf, sizeof(aBuf), "'%s' joined team %d", + Server()->ClientName(pPlayer->GetCID()), + Team); + SendChat(-1, CGameContext::CHAT_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!"); + } + } +} + +void CGameContext::ConInvite(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; auto *pController = pSelf->m_pController; @@ -1004,7 +1080,7 @@ void CGameContext::ConInviteTeam(IConsole::IResult *pResult, void *pUserData) 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.", pSelf->Server()->ClientName(pResult->m_ClientID), 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)); @@ -1014,10 +1090,9 @@ void CGameContext::ConInviteTeam(IConsole::IResult *pResult, void *pUserData) pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "Can't invite players to this team"); } -void CGameContext::ConJoinTeam(IConsole::IResult *pResult, void *pUserData) +void CGameContext::ConTeam(IConsole::IResult *pResult, void *pUserData) { CGameContext *pSelf = (CGameContext *)pUserData; - auto *pController = pSelf->m_pController; if(!CheckClientID(pResult->m_ClientID)) return; @@ -1025,78 +1100,9 @@ void CGameContext::ConJoinTeam(IConsole::IResult *pResult, void *pUserData) if(!pPlayer) return; - if(pSelf->m_VoteCloseTime && pSelf->m_VoteCreator == pResult->m_ClientID && (pSelf->IsKickVote() || pSelf->IsSpecVote())) - { - pSelf->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) - { - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "Teams are disabled"); - return; - } - else if(g_Config.m_SvTeam == SV_TEAM_MANDATORY && pResult->GetInteger(0) == 0 && pPlayer->GetCharacter() && pPlayer->GetCharacter()->m_LastStartWarning < pSelf->Server()->Tick() - 3 * pSelf->Server()->TickSpeed()) - { - pSelf->Console()->Print( - IConsole::OUTPUT_LEVEL_STANDARD, - "chatresp", - "You must join a team and play with somebody or else you can\'t play"); - pPlayer->GetCharacter()->m_LastStartWarning = pSelf->Server()->Tick(); - } - if(pResult->NumArguments() > 0) { - if(pPlayer->GetCharacter() == 0) - { - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "You can't change teams while you are dead/a spectator."); - } - else - { - int Team = pResult->GetInteger(0); - - if(Team < 0 || Team >= MAX_CLIENTS) - Team = pController->Teams().GetFirstEmptyTeam(); - - if(pPlayer->m_Last_Team + (int64_t)pSelf->Server()->TickSpeed() * g_Config.m_SvTeamChangeDelay > pSelf->Server()->Tick()) - { - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - "You can\'t change teams that fast!"); - } - else if(Team > 0 && Team < MAX_CLIENTS && pController->Teams().TeamLocked(Team) && !pController->Teams().IsInvited(Team, pResult->m_ClientID)) - { - pSelf->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 && pController->Teams().Count(Team) >= g_Config.m_SvMaxTeamSize) - { - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "This team already has the maximum allowed size of %d players", g_Config.m_SvMaxTeamSize); - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", aBuf); - } - else if(const char *pError = pController->Teams().SetCharacterTeam(pPlayer->GetCID(), Team)) - { - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", pError); - } - else - { - char aBuf[512]; - str_format(aBuf, sizeof(aBuf), "'%s' joined team %d", - pSelf->Server()->ClientName(pPlayer->GetCID()), - Team); - pSelf->SendChat(-1, CGameContext::CHAT_ALL, aBuf); - pPlayer->m_Last_Team = pSelf->Server()->Tick(); - - if(pController->Teams().IsPractice(Team)) - pSelf->SendChatTarget(pPlayer->GetCID(), "Practice mode enabled for your team, happy practicing!"); - } - } + pSelf->AttemptJoinTeam(pResult->m_ClientID, pResult->GetInteger(0)); } else { @@ -1115,10 +1121,39 @@ void CGameContext::ConJoinTeam(IConsole::IResult *pResult, void *pUserData) sizeof(aBuf), "You are in team %d", pSelf->GetDDRaceTeam(pResult->m_ClientID)); - pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", - aBuf); + pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", aBuf); + } + } +} + +void CGameContext::ConJoin(IConsole::IResult *pResult, void *pUserData) +{ + CGameContext *pSelf = (CGameContext *)pUserData; + if(!CheckClientID(pResult->m_ClientID)) + return; + + int Target = -1; + const char *pName = pResult->GetString(0); + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(!str_comp(pName, pSelf->Server()->ClientName(i))) + { + Target = i; + break; } } + + if(Target == -1) + { + pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "chatresp", "Player not found"); + return; + } + + int Team = pSelf->GetDDRaceTeam(Target); + if(pSelf->ProcessSpamProtection(pResult->m_ClientID, false)) + return; + + pSelf->AttemptJoinTeam(pResult->m_ClientID, Team); } void CGameContext::ConMe(IConsole::IResult *pResult, void *pUserData) diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index 56b7f3e4440..68075880d89 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -362,7 +362,7 @@ void CGameContext::CreateSound(vec2 Pos, int Sound, CClientMask Mask) } } -void CGameContext::CreateSoundGlobal(int Sound, int Target) +void CGameContext::CreateSoundGlobal(int Sound, int Target) const { if(Sound < 0) return; @@ -380,7 +380,61 @@ void CGameContext::CreateSoundGlobal(int Sound, int Target) } } -bool CGameContext::SnapLaserObject(const CSnapContext &Context, int SnapID, const vec2 &To, const vec2 &From, int StartTick, int Owner, int LaserType, int Subtype, int SwitchNumber) +void CGameContext::SnapSwitchers(int SnappingClient) +{ + if(Switchers().empty()) + return; + + 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(Team == TEAM_SUPER) + return; + + int SentTeam = Team; + if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) + SentTeam = 0; + + CNetObj_SwitchState *pSwitchState = Server()->SnapNewItem(SentTeam); + if(!pSwitchState) + return; + + pSwitchState->m_HighestSwitchNumber = clamp((int)Switchers().size() - 1, 0, 255); + mem_zero(pSwitchState->m_aStatus, sizeof(pSwitchState->m_aStatus)); + + std::vector> vEndTicks; // + + for(int i = 0; i <= pSwitchState->m_HighestSwitchNumber; i++) + { + int Status = (int)Switchers()[i].m_aStatus[Team]; + pSwitchState->m_aStatus[i / 32] |= (Status << (i % 32)); + + int EndTick = Switchers()[i].m_aEndTick[Team]; + if(EndTick > 0 && EndTick < Server()->Tick() + 3 * Server()->TickSpeed() && Switchers()[i].m_aLastUpdateTick[Team] < Server()->Tick()) + { + // only keep track of EndTicks that have less than three second left and are not currently being updated by a player being present on a switch tile, to limit how often these are sent + vEndTicks.emplace_back(Switchers()[i].m_aEndTick[Team], i); + } + } + + // send the endtick of switchers that are about to toggle back (up to four, prioritizing those with the earliest endticks) + mem_zero(pSwitchState->m_aSwitchNumbers, sizeof(pSwitchState->m_aSwitchNumbers)); + mem_zero(pSwitchState->m_aEndTicks, sizeof(pSwitchState->m_aEndTicks)); + + std::sort(vEndTicks.begin(), vEndTicks.end()); + const int NumTimedSwitchers = minimum((int)vEndTicks.size(), (int)std::size(pSwitchState->m_aEndTicks)); + + for(int i = 0; i < NumTimedSwitchers; i++) + { + pSwitchState->m_aSwitchNumbers[i] = vEndTicks[i].second; + pSwitchState->m_aEndTicks[i] = vEndTicks[i].first; + } +} + +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) { @@ -415,7 +469,7 @@ 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) +bool CGameContext::SnapPickup(const CSnapContext &Context, int SnapID, const vec2 &Pos, int Type, int SubType, int SwitchNumber) const { if(Context.IsSixup()) { @@ -489,7 +543,7 @@ void CGameContext::CallVote(int ClientID, const char *pDesc, const char *pCmd, c pPlayer->m_LastVoteCall = Now; } -void CGameContext::SendChatTarget(int To, const char *pText, int Flags) +void CGameContext::SendChatTarget(int To, const char *pText, int Flags) const { CNetMsg_Sv_Chat Msg; Msg.m_Team = 0; @@ -520,7 +574,7 @@ void CGameContext::SendChatTarget(int To, const char *pText, int Flags) } } -void CGameContext::SendChatTeam(int Team, const char *pText) +void CGameContext::SendChatTeam(int Team, const char *pText) const { for(int i = 0; i < MAX_CLIENTS; i++) if(m_apPlayers[i] != nullptr && GetDDRaceTeam(i) == Team) @@ -619,7 +673,7 @@ void CGameContext::SendStartWarning(int ClientID, const char *pMessage) } } -void CGameContext::SendEmoticon(int ClientID, int Emoticon, int TargetClientID) +void CGameContext::SendEmoticon(int ClientID, int Emoticon, int TargetClientID) const { CNetMsg_Sv_Emoticon Msg; Msg.m_ClientID = ClientID; @@ -627,21 +681,21 @@ void CGameContext::SendEmoticon(int ClientID, int Emoticon, int TargetClientID) Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, TargetClientID); } -void CGameContext::SendWeaponPickup(int ClientID, int Weapon) +void CGameContext::SendWeaponPickup(int ClientID, int Weapon) const { CNetMsg_Sv_WeaponPickup Msg; Msg.m_Weapon = Weapon; Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); } -void CGameContext::SendMotd(int ClientID) +void CGameContext::SendMotd(int ClientID) const { CNetMsg_Sv_Motd Msg; Msg.m_pMessage = g_Config.m_SvMotd; Server()->SendPackMsg(&Msg, MSGFLAG_VITAL, ClientID); } -void CGameContext::SendSettings(int ClientID) +void CGameContext::SendSettings(int ClientID) const { protocol7::CNetMsg_Sv_ServerSettings Msg; Msg.m_KickVote = g_Config.m_SvVoteKick; @@ -1297,9 +1351,9 @@ void CGameContext::OnClientPredictedEarlyInput(int ClientID, void *pInput) } } -struct CVoteOptionServer *CGameContext::GetVoteOption(int Index) +const CVoteOptionServer *CGameContext::GetVoteOption(int Index) const { - CVoteOptionServer *pCurrent; + const CVoteOptionServer *pCurrent; for(pCurrent = m_pVoteOptionFirst; Index > 0 && pCurrent; Index--, pCurrent = pCurrent->m_pNext) @@ -1350,7 +1404,7 @@ void CGameContext::ProgressVoteOptions(int ClientID) OptionMsg.m_pDescription14 = ""; // get current vote option by index - CVoteOptionServer *pCurrent = GetVoteOption(pPl->m_SendVoteIndex); + const CVoteOptionServer *pCurrent = GetVoteOption(pPl->m_SendVoteIndex); while(CurIndex < NumVotesToSend && pCurrent != NULL) { @@ -1410,15 +1464,19 @@ void CGameContext::OnClientEnter(int ClientID) Msg.m_pName = "team"; 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; - for(const IConsole::CCommandInfo *pCmd = Console()->FirstCommandInfo(IConsole::ACCESS_LEVEL_USER, CFGFLAG_CHAT); - pCmd; pCmd = pCmd->NextCommandInfo(IConsole::ACCESS_LEVEL_USER, CFGFLAG_CHAT)) + if(Server()->IsSixup(ClientID)) { - if(!str_comp_nocase(pCmd->m_pName, "w") || !str_comp_nocase(pCmd->m_pName, "whisper")) + if(!str_comp_nocase(pName, "w") || !str_comp_nocase(pName, "whisper")) continue; - const char *pName = pCmd->m_pName; - if(!str_comp_nocase(pCmd->m_pName, "r")) + if(!str_comp_nocase(pName, "r")) pName = "rescue"; protocol7::CNetMsg_Sv_CommandInfo Msg; @@ -1427,6 +1485,14 @@ void CGameContext::OnClientEnter(int ClientID) Msg.m_pHelpText = pCmd->m_pHelp; Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); } + else + { + CNetMsg_Sv_CommandInfo Msg; + Msg.m_pName = pName; + Msg.m_pArgsFormat = pCmd->m_pParams; + Msg.m_pHelpText = pCmd->m_pHelp; + Server()->SendPackMsg(&Msg, MSGFLAG_VITAL | MSGFLAG_NORECORD, ClientID); + } } { @@ -2389,24 +2455,16 @@ void CGameContext::OnSetTeamNetMessage(const CNetMsg_Cl_SetTeam *pMsg, int Clien } // Switch team on given client and kill/respawn them - if(m_pController->CanJoinTeam(pMsg->m_Team, ClientID)) + char aTeamJoinError[512]; + if(m_pController->CanJoinTeam(pMsg->m_Team, ClientID, aTeamJoinError, sizeof(aTeamJoinError))) { - if(pPlayer->IsPaused()) - SendChatTarget(ClientID, "Use /pause first then you can kill"); - else - { - if(pPlayer->GetTeam() == TEAM_SPECTATORS || pMsg->m_Team == TEAM_SPECTATORS) - m_VoteUpdate = true; - m_pController->DoTeamChange(pPlayer, pMsg->m_Team); - pPlayer->m_TeamChangeTick = Server()->Tick(); - } + if(pPlayer->GetTeam() == TEAM_SPECTATORS || pMsg->m_Team == TEAM_SPECTATORS) + m_VoteUpdate = true; + m_pController->DoTeamChange(pPlayer, pMsg->m_Team); + pPlayer->m_TeamChangeTick = Server()->Tick(); } else - { - char aBuf[128]; - str_format(aBuf, sizeof(aBuf), "Only %d active players are allowed", Server()->MaxClients() - g_Config.m_SvSpectatorSlots); - SendBroadcast(aBuf, ClientID); - } + SendBroadcast(aTeamJoinError, ClientID); } void CGameContext::OnIsDDNetLegacyNetMessage(const CNetMsg_Cl_IsDDNetLegacy *pMsg, int ClientID, CUnpacker *pUnpacker) @@ -3428,15 +3486,15 @@ 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"); @@ -4199,7 +4257,7 @@ bool CGameContext::ProcessSpamProtection(int ClientID, bool RespectChatInitialDe return false; } -int CGameContext::GetDDRaceTeam(int ClientID) +int CGameContext::GetDDRaceTeam(int ClientID) const { return m_pController->Teams().m_Core.Team(ClientID); } @@ -4468,7 +4526,7 @@ int CGameContext::GetClientVersion(int ClientID) const return Server()->GetClientVersion(ClientID); } -CClientMask CGameContext::ClientsMaskExcludeClientVersionAndHigher(int Version) +CClientMask CGameContext::ClientsMaskExcludeClientVersionAndHigher(int Version) const { CClientMask Mask; for(int i = 0; i < MAX_CLIENTS; ++i) @@ -4576,7 +4634,7 @@ bool CGameContext::RateLimitPlayerVote(int ClientID) return false; } -bool CGameContext::RateLimitPlayerMapVote(int ClientID) +bool CGameContext::RateLimitPlayerMapVote(int ClientID) const { if(!Server()->GetAuthedState(ClientID) && time_get() < m_LastMapVote + (time_freq() * g_Config.m_SvVoteMapTimeDelay)) { @@ -4653,6 +4711,7 @@ void CGameContext::OnUpdatePlayerServerInfo(char *aBuf, int BufSize, int ID) } } + const int Team = m_pController->IsTeamPlay() ? m_apPlayers[ID]->GetTeam() : m_apPlayers[ID]->GetTeam() == TEAM_SPECTATORS ? -1 : GetDDRaceTeam(ID); str_format(aBuf, BufSize, ",\"skin\":{" "%s" @@ -4661,5 +4720,5 @@ void CGameContext::OnUpdatePlayerServerInfo(char *aBuf, int BufSize, int ID) "\"team\":%d", aJsonSkin, JsonBool(m_apPlayers[ID]->IsAfk()), - m_apPlayers[ID]->GetTeam()); + Team); } diff --git a/src/game/server/gamecontext.h b/src/game/server/gamecontext.h index d521bfbcdd9..67a2a375971 100644 --- a/src/game/server/gamecontext.h +++ b/src/game/server/gamecontext.h @@ -237,10 +237,11 @@ class CGameContext : public IGameServer void CreatePlayerSpawn(vec2 Pos, CClientMask Mask = CClientMask().set()); void CreateDeath(vec2 Pos, int ClientID, CClientMask Mask = CClientMask().set()); void CreateSound(vec2 Pos, int Sound, CClientMask Mask = CClientMask().set()); - void CreateSoundGlobal(int Sound, int Target = -1); + void CreateSoundGlobal(int Sound, int Target = -1) 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); - bool SnapPickup(const CSnapContext &Context, int SnapID, const vec2 &Pos, int Type, int SubType, int SwitchNumber); + 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; enum { @@ -257,14 +258,14 @@ class CGameContext : public IGameServer // 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); - void SendChatTeam(int Team, const char *pText); + void SendChatTarget(int To, const char *pText, int Flags = CHAT_SIX | CHAT_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); - void SendWeaponPickup(int ClientID, int Weapon); - void SendMotd(int ClientID); - void SendSettings(int ClientID); + 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); @@ -273,7 +274,7 @@ class CGameContext : public IGameServer void CheckPureTuning(); void SendTuningParams(int ClientID, int Zone = 0); - struct CVoteOptionServer *GetVoteOption(int Index); + const CVoteOptionServer *GetVoteOption(int Index) const; void ProgressVoteOptions(int ClientID); // @@ -338,12 +339,12 @@ class CGameContext : public IGameServer bool OnClientDDNetVersionKnown(int ClientID); void FillAntibot(CAntibotRoundData *pData) override; bool ProcessSpamProtection(int ClientID, bool RespectChatInitialDelay = true); - int GetDDRaceTeam(int ClientID); + 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; - CClientMask ClientsMaskExcludeClientVersionAndHigher(int Version); + CClientMask ClientsMaskExcludeClientVersionAndHigher(int Version) const; bool PlayerExists(int ClientID) const override { return m_apPlayers[ClientID]; } // Returns true if someone is actively moderating. bool PlayerModerating() const; @@ -351,7 +352,7 @@ class CGameContext : public IGameServer // Checks if player can vote and notify them about the reason bool RateLimitPlayerVote(int ClientID); - bool RateLimitPlayerMapVote(int ClientID); + bool RateLimitPlayerMapVote(int ClientID) const; void OnUpdatePlayerServerInfo(char *aBuf, int BufSize, int ID) override; @@ -436,10 +437,12 @@ class CGameContext : public IGameServer static void ConMap(IConsole::IResult *pResult, void *pUserData); static void ConTeamRank(IConsole::IResult *pResult, void *pUserData); static void ConRank(IConsole::IResult *pResult, void *pUserData); - static void ConJoinTeam(IConsole::IResult *pResult, void *pUserData); - static void ConLockTeam(IConsole::IResult *pResult, void *pUserData); - static void ConUnlockTeam(IConsole::IResult *pResult, void *pUserData); - static void ConInviteTeam(IConsole::IResult *pResult, void *pUserData); + static void ConTeam(IConsole::IResult *pResult, void *pUserData); + static void ConLock(IConsole::IResult *pResult, void *pUserData); + 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 ConMe(IConsole::IResult *pResult, void *pUserData); static void ConWhisper(IConsole::IResult *pResult, void *pUserData); static void ConConverse(IConsole::IResult *pResult, void *pUserData); @@ -504,7 +507,8 @@ class CGameContext : public IGameServer 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); + void UnlockTeam(int ClientID, int Team) const; + void AttemptJoinTeam(int ClientID, int Team); enum { diff --git a/src/game/server/gamecontroller.cpp b/src/game/server/gamecontroller.cpp index 270dc6a0edf..89f4f591223 100644 --- a/src/game/server/gamecontroller.cpp +++ b/src/game/server/gamecontroller.cpp @@ -645,55 +645,7 @@ void IGameController::Snap(int SnappingClient) pRaceData->m_RaceFlags = protocol7::RACEFLAG_HIDE_KILLMSG | protocol7::RACEFLAG_KEEP_WANTED_WEAPON; } - if(!GameServer()->Switchers().empty()) - { - int Team = pPlayer && pPlayer->GetCharacter() ? pPlayer->GetCharacter()->Team() : 0; - - if(pPlayer && (pPlayer->GetTeam() == TEAM_SPECTATORS || pPlayer->IsPaused()) && pPlayer->m_SpectatorID != SPEC_FREEVIEW && GameServer()->m_apPlayers[pPlayer->m_SpectatorID] && GameServer()->m_apPlayers[pPlayer->m_SpectatorID]->GetCharacter()) - Team = GameServer()->m_apPlayers[pPlayer->m_SpectatorID]->GetCharacter()->Team(); - - if(Team == TEAM_SUPER) - return; - - int SentTeam = Team; - if(g_Config.m_SvTeam == SV_TEAM_FORCED_SOLO) - SentTeam = 0; - - CNetObj_SwitchState *pSwitchState = Server()->SnapNewItem(SentTeam); - if(!pSwitchState) - return; - - pSwitchState->m_HighestSwitchNumber = clamp((int)GameServer()->Switchers().size() - 1, 0, 255); - mem_zero(pSwitchState->m_aStatus, sizeof(pSwitchState->m_aStatus)); - - std::vector> vEndTicks; // - - for(int i = 0; i <= pSwitchState->m_HighestSwitchNumber; i++) - { - int Status = (int)GameServer()->Switchers()[i].m_aStatus[Team]; - pSwitchState->m_aStatus[i / 32] |= (Status << (i % 32)); - - int EndTick = GameServer()->Switchers()[i].m_aEndTick[Team]; - if(EndTick > 0 && EndTick < Server()->Tick() + 3 * Server()->TickSpeed() && GameServer()->Switchers()[i].m_aLastUpdateTick[Team] < Server()->Tick()) - { - // only keep track of EndTicks that have less than three second left and are not currently being updated by a player being present on a switch tile, to limit how often these are sent - vEndTicks.emplace_back(GameServer()->Switchers()[i].m_aEndTick[Team], i); - } - } - - // send the endtick of switchers that are about to toggle back (up to four, prioritizing those with the earliest endticks) - mem_zero(pSwitchState->m_aSwitchNumbers, sizeof(pSwitchState->m_aSwitchNumbers)); - mem_zero(pSwitchState->m_aEndTicks, sizeof(pSwitchState->m_aEndTicks)); - - std::sort(vEndTicks.begin(), vEndTicks.end()); - const int NumTimedSwitchers = minimum((int)vEndTicks.size(), (int)std::size(pSwitchState->m_aEndTicks)); - - for(int i = 0; i < NumTimedSwitchers; i++) - { - pSwitchState->m_aSwitchNumbers[i] = vEndTicks[i].second; - pSwitchState->m_aEndTicks[i] = vEndTicks[i].first; - } - } + GameServer()->SnapSwitchers(SnappingClient); } int IGameController::GetAutoTeam(int NotThisID) @@ -710,14 +662,21 @@ int IGameController::GetAutoTeam(int NotThisID) int Team = 0; - if(CanJoinTeam(Team, NotThisID)) + if(CanJoinTeam(Team, NotThisID, nullptr, 0)) return Team; return -1; } -bool IGameController::CanJoinTeam(int Team, int NotThisID) +bool IGameController::CanJoinTeam(int Team, int NotThisID, char *pErrorReason, int ErrorReasonSize) { - if(Team == TEAM_SPECTATORS || (GameServer()->m_apPlayers[NotThisID] && GameServer()->m_apPlayers[NotThisID]->GetTeam() != TEAM_SPECTATORS)) + const CPlayer *pPlayer = GameServer()->m_apPlayers[NotThisID]; + if(pPlayer && pPlayer->IsPaused()) + { + if(pErrorReason) + str_copy(pErrorReason, "Use /pause first then you can kill", ErrorReasonSize); + return false; + } + if(Team == TEAM_SPECTATORS || (pPlayer && pPlayer->GetTeam() != TEAM_SPECTATORS)) return true; int aNumplayers[2] = {0, 0}; @@ -730,7 +689,12 @@ bool IGameController::CanJoinTeam(int Team, int NotThisID) } } - return (aNumplayers[0] + aNumplayers[1]) < Server()->MaxClients() - g_Config.m_SvSpectatorSlots; + if((aNumplayers[0] + aNumplayers[1]) < Server()->MaxClients() - g_Config.m_SvSpectatorSlots) + return true; + + if(pErrorReason) + str_format(pErrorReason, ErrorReasonSize, "Only %d active players are allowed", Server()->MaxClients() - g_Config.m_SvSpectatorSlots); + return false; } int IGameController::ClampTeam(int Team) diff --git a/src/game/server/gamecontroller.h b/src/game/server/gamecontroller.h index 81c3fa33b89..c381c326572 100644 --- a/src/game/server/gamecontroller.h +++ b/src/game/server/gamecontroller.h @@ -145,12 +145,13 @@ class IGameController */ virtual const char *GetTeamName(int Team); virtual int GetAutoTeam(int NotThisID); - virtual bool CanJoinTeam(int Team, 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; diff --git a/src/game/server/player.cpp b/src/game/server/player.cpp index de3ae840eb5..d8e948ad853 100644 --- a/src/game/server/player.cpp +++ b/src/game/server/player.cpp @@ -379,7 +379,7 @@ void CPlayer::Snap(int SnappingClient) pPlayerInfo->m_PlayerFlags |= protocol7::PLAYERFLAG_ADMIN; // Times are in milliseconds for 0.7 - pPlayerInfo->m_Score = m_Score.has_value() ? GameServer()->Score()->PlayerData(id)->m_BestTime * 1000 : -1; + pPlayerInfo->m_Score = m_Score.has_value() ? GameServer()->Score()->PlayerData(m_ClientID)->m_BestTime * 1000 : -1; pPlayerInfo->m_Latency = Latency; } @@ -412,7 +412,7 @@ void CPlayer::Snap(int SnappingClient) if(!pDDNetPlayer) return; - pDDNetPlayer->m_AuthLevel = Server()->GetAuthedState(id); + pDDNetPlayer->m_AuthLevel = Server()->GetAuthedState(m_ClientID); pDDNetPlayer->m_Flags = 0; if(m_Afk) pDDNetPlayer->m_Flags |= EXPLAYERFLAG_AFK; @@ -626,6 +626,11 @@ void CPlayer::SetTeam(int Team, bool DoChatMsg) pPlayer->m_SpectatorID = SPEC_FREEVIEW; } } + + if(!GameServer()->m_pController->IsTeamPlay()) + { + Server()->ExpireServerInfo(); + } } bool CPlayer::SetTimerType(int TimerType) @@ -832,12 +837,12 @@ int CPlayer::ForcePause(int Time) return Pause(PAUSE_SPEC, true); } -int CPlayer::IsPaused() +int CPlayer::IsPaused() const { return m_ForcePauseTime ? m_ForcePauseTime : -1 * m_Paused; } -bool CPlayer::IsPlaying() +bool CPlayer::IsPlaying() const { return m_pCharacter && m_pCharacter->IsAlive(); } diff --git a/src/game/server/player.h b/src/game/server/player.h index 1f050cb7802..8afed78165e 100644 --- a/src/game/server/player.h +++ b/src/game/server/player.h @@ -176,9 +176,9 @@ class CPlayer void ProcessPause(); int Pause(int State, bool Force); int ForcePause(int Time); - int IsPaused(); + int IsPaused() const; - bool IsPlaying(); + bool IsPlaying() const; int64_t m_Last_KickVote; int64_t m_Last_Team; int m_ShowOthers; diff --git a/src/game/server/save.cpp b/src/game/server/save.cpp index af183a20a36..c5b1841d4d9 100644 --- a/src/game/server/save.cpp +++ b/src/game/server/save.cpp @@ -596,7 +596,7 @@ void CSaveTeam::Load(CGameContext *pGameServer, int Team, bool KeepCurrentWeakSt pGameServer->m_World.RemoveEntitiesFromPlayers(aPlayerCIDs, m_MembersCount); } -CCharacter *CSaveTeam::MatchCharacter(CGameContext *pGameServer, int ClientID, int SaveID, bool KeepCurrentCharacter) +CCharacter *CSaveTeam::MatchCharacter(CGameContext *pGameServer, int ClientID, int SaveID, bool KeepCurrentCharacter) const { if(KeepCurrentCharacter && pGameServer->m_apPlayers[ClientID]->GetCharacter()) { @@ -785,7 +785,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) +bool CSaveTeam::MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int *pClientID, int NumPlayer, char *pMessage, int MessageLen) const { if(NumPlayer > m_MembersCount) { diff --git a/src/game/server/save.h b/src/game/server/save.h index 3e079363c93..14abd7f3732 100644 --- a/src/game/server/save.h +++ b/src/game/server/save.h @@ -140,7 +140,7 @@ 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); + 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); @@ -150,7 +150,7 @@ class CSaveTeam static bool HandleSaveError(int Result, int ClientID, CGameContext *pGameContext); private: - CCharacter *MatchCharacter(CGameContext *pGameServer, int ClientID, int SaveID, bool KeepCurrentCharacter); + CCharacter *MatchCharacter(CGameContext *pGameServer, int ClientID, int SaveID, bool KeepCurrentCharacter) const; char m_aString[65536]; diff --git a/src/game/server/teehistorian.cpp b/src/game/server/teehistorian.cpp index 355916b0254..9fc9545c536 100644 --- a/src/game/server/teehistorian.cpp +++ b/src/game/server/teehistorian.cpp @@ -1,5 +1,6 @@ #include "teehistorian.h" +#include #include #include #include diff --git a/src/game/teamscore.cpp b/src/game/teamscore.cpp index 968eb8ef960..0835a9814bd 100644 --- a/src/game/teamscore.cpp +++ b/src/game/teamscore.cpp @@ -1,5 +1,6 @@ /* (c) Shereef Marzouk. See "licence DDRace.txt" and the readme.txt in the root of the distribution for more information. */ #include "teamscore.h" +#include #include CTeamsCore::CTeamsCore() diff --git a/src/game/version.h b/src/game/version.h index dba4a386ad3..2ece03c5f8e 100644 --- a/src/game/version.h +++ b/src/game/version.h @@ -3,11 +3,11 @@ #ifndef GAME_VERSION_H #define GAME_VERSION_H #ifndef GAME_RELEASE_VERSION -#define GAME_RELEASE_VERSION "17.4" +#define GAME_RELEASE_VERSION "17.4.2" #endif #define GAME_VERSION "0.6.4, " GAME_RELEASE_VERSION #define GAME_NETVERSION "0.6 626fce9a778df4d4" -#define DDNET_VERSION_NUMBER 17040 +#define DDNET_VERSION_NUMBER 17042 extern const char *GIT_SHORTREV_HASH; #define GAME_NAME "DDNet" #endif diff --git a/src/macos/notifications.mm b/src/macos/notifications.mm index 39ed0aee106..2501fe0e6b1 100644 --- a/src/macos/notifications.mm +++ b/src/macos/notifications.mm @@ -1,16 +1,9 @@ -#import - #import #import #import -void NotificationsInit() -{ -} -void NotificationsUninit() -{ -} -void NotificationsNotify(const char *pTitle, const char *pMessage) +// TODO: NSUserNotification is deprecated. Use the User Notifications framework instead: https://developer.apple.com/documentation/usernotifications?language=objc +void NotificationsNotifyMacOsInternal(const char *pTitle, const char *pMessage) { NSString* pNsTitle = [NSString stringWithCString:pTitle encoding:NSUTF8StringEncoding]; NSString* pNsMsg = [NSString stringWithCString:pMessage encoding:NSUTF8StringEncoding]; diff --git a/src/test/csv.cpp b/src/test/csv.cpp index 905983fdf32..2022f98ca28 100644 --- a/src/test/csv.cpp +++ b/src/test/csv.cpp @@ -1,6 +1,7 @@ #include "test.h" #include +#include #include static void Expect(int NumColumns, const char *const *ppColumns, const char *pExpected) diff --git a/src/test/jsonwriter.cpp b/src/test/jsonwriter.cpp index f61137c39dd..c8bcede6104 100644 --- a/src/test/jsonwriter.cpp +++ b/src/test/jsonwriter.cpp @@ -3,6 +3,7 @@ #include "test.h" #include +#include #include #include diff --git a/src/test/linereader.cpp b/src/test/linereader.cpp index cd53b38ca8b..05d93627b75 100644 --- a/src/test/linereader.cpp +++ b/src/test/linereader.cpp @@ -1,6 +1,7 @@ #include "test.h" #include +#include #include void TestFileLineReader(const char *pWritten, bool SkipBom, std::initializer_list pReads) diff --git a/src/test/score.cpp b/src/test/score.cpp index fddcefd8b3b..2a2d0ba2ea7 100644 --- a/src/test/score.cpp +++ b/src/test/score.cpp @@ -25,7 +25,7 @@ int CSaveTeam::FromString(const char *) return 1; } -bool CSaveTeam::MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int *pClientID, int NumPlayer, char *pMessage, int MessageLen) +bool CSaveTeam::MatchPlayers(const char (*paNames)[MAX_NAME_LENGTH], const int *pClientID, int NumPlayer, char *pMessage, int MessageLen) const { // Dummy implementation for testing return false; diff --git a/src/tools/demo_extract_chat.cpp b/src/tools/demo_extract_chat.cpp index 1f2d2b260ed..82f1879e3d4 100644 --- a/src/tools/demo_extract_chat.cpp +++ b/src/tools/demo_extract_chat.cpp @@ -1,7 +1,13 @@ #include #include + #include -#include +#include +#include +#include +#include + +#include static const char *TOOL_NAME = "demo_extract_chat"; @@ -190,14 +196,14 @@ class CDemoPlayerMessageListener : public CDemoPlayer::IListener } }; -int Process(const char *pDemoFilePath, IStorage *pStorage) +static int ExtractDemoChat(const char *pDemoFilePath, IStorage *pStorage) { CSnapshotDelta DemoSnapshotDelta; CDemoPlayer DemoPlayer(&DemoSnapshotDelta, false); if(DemoPlayer.Load(pStorage, nullptr, pDemoFilePath, IStorage::TYPE_ALL_OR_ABSOLUTE) == -1) { - dbg_msg(TOOL_NAME, "Demo file '%s' failed to load: %s", pDemoFilePath, DemoPlayer.ErrorMessage()); + log_error(TOOL_NAME, "Demo file '%s' failed to load: %s", pDemoFilePath, DemoPlayer.ErrorMessage()); return -1; } @@ -225,21 +231,23 @@ int Process(const char *pDemoFilePath, IStorage *pStorage) int main(int argc, const char *argv[]) { + // Create storage before setting logger to avoid log messages from storage creation IStorage *pStorage = CreateLocalStorage(); + + CCmdlineFix CmdlineFix(&argc, &argv); + log_set_global_logger_default(); + if(!pStorage) { - dbg_msg(TOOL_NAME, "Error loading storage"); + log_error(TOOL_NAME, "Error creating local storage"); return -1; } - CCmdlineFix CmdlineFix(&argc, &argv); - log_set_global_logger_default(); - if(argc != 2) { - dbg_msg(TOOL_NAME, "Usage: %s ", TOOL_NAME); + log_error(TOOL_NAME, "Usage: %s ", TOOL_NAME); return -1; } - return Process(argv[1], pStorage); + return ExtractDemoChat(argv[1], pStorage); } diff --git a/src/tools/map_convert_07.cpp b/src/tools/map_convert_07.cpp index ebd6f567871..2794ed0f827 100644 --- a/src/tools/map_convert_07.cpp +++ b/src/tools/map_convert_07.cpp @@ -219,14 +219,15 @@ int main(int argc, const char **argv) { int Type, ID; void *pItem = g_DataReader.GetItem(Index, &Type, &ID); - int Size = g_DataReader.GetItemSize(Index); - // filter ITEMTYPE_EX items, they will be automatically added again - if(Type == ITEMTYPE_EX) + // Filter items with unknown type, as we can't write them back. + // Filter ITEMTYPE_EX items, they will be automatically added again. + if(Type < 0 || Type == ITEMTYPE_EX) { continue; } + int Size = g_DataReader.GetItemSize(Index); Success &= CheckImageDimensions(pItem, Type, pSourceFileName); CMapItemImage NewImageItem; diff --git a/src/tools/map_create_pixelart.cpp b/src/tools/map_create_pixelart.cpp index 52d54d471e1..18d51b5248c 100644 --- a/src/tools/map_create_pixelart.cpp +++ b/src/tools/map_create_pixelart.cpp @@ -377,8 +377,13 @@ void SaveOutputMap(CDataFileReader &InputMap, CDataFileWriter &OutputMap, CMapIt int ID, Type; void *pItem = InputMap.GetItem(i, &Type, &ID); - if(Type == ITEMTYPE_EX) + // Filter items with unknown type, as we can't write them back. + // Filter ITEMTYPE_EX items, they will be automatically added again. + if(Type < 0 || Type == ITEMTYPE_EX) + { continue; + } + if(i == NewItemNumber) pItem = pNewItem; diff --git a/src/tools/map_extract.cpp b/src/tools/map_extract.cpp index d66bbf25eac..901e7463d12 100644 --- a/src/tools/map_extract.cpp +++ b/src/tools/map_extract.cpp @@ -102,12 +102,13 @@ bool Process(IStorage *pStorage, const char *pMapName, const char *pPathSave) continue; } + const int SoundDataSize = Reader.GetDataSize(pItem->m_SoundData); char aBuf[IO_MAX_PATH_LENGTH]; str_format(aBuf, sizeof(aBuf), "%s/%s.opus", pPathSave, pName); - dbg_msg("map_extract", "writing sound: %s (%d B)", aBuf, pItem->m_SoundDataSize); + dbg_msg("map_extract", "writing sound: %s (%d B)", aBuf, SoundDataSize); IOHANDLE Opus = io_open(aBuf, IOFLAG_WRITE); - io_write(Opus, (unsigned char *)Reader.GetData(pItem->m_SoundData), pItem->m_SoundDataSize); + io_write(Opus, Reader.GetData(pItem->m_SoundData), SoundDataSize); io_close(Opus); } diff --git a/src/tools/map_optimize.cpp b/src/tools/map_optimize.cpp index 2da28b1c092..f08334f57d5 100644 --- a/src/tools/map_optimize.cpp +++ b/src/tools/map_optimize.cpp @@ -140,13 +140,14 @@ int main(int argc, const char **argv) { int Type, ID; void *pPtr = Reader.GetItem(Index, &Type, &ID); - int Size = Reader.GetItemSize(Index); - // filter ITEMTYPE_EX items, they will be automatically added again - if(Type == ITEMTYPE_EX) + // Filter items with unknown type, as we can't write them back. + // Filter ITEMTYPE_EX items, they will be automatically added again. + if(Type < 0 || Type == ITEMTYPE_EX) { continue; } + // for all layers, check if it uses a image and set the corresponding flag if(Type == MAPITEMTYPE_LAYER) { @@ -203,6 +204,7 @@ int main(int argc, const char **argv) ++i; } + int Size = Reader.GetItemSize(Index); Writer.AddItem(Type, ID, Size, pPtr); } diff --git a/src/tools/map_replace_area.cpp b/src/tools/map_replace_area.cpp index f0d4aed3aeb..89187922c37 100644 --- a/src/tools/map_replace_area.cpp +++ b/src/tools/map_replace_area.cpp @@ -168,8 +168,13 @@ void SaveOutputMap(CDataFileReader &InputMap, CDataFileWriter &OutputMap) int ID, Type; void *pItem = InputMap.GetItem(i, &Type, &ID); - if(Type == ITEMTYPE_EX) + // Filter items with unknown type, as we can't write them back. + // Filter ITEMTYPE_EX items, they will be automatically added again. + if(Type < 0 || Type == ITEMTYPE_EX) + { continue; + } + if(g_apNewItem[i]) pItem = g_apNewItem[i]; diff --git a/src/tools/map_replace_image.cpp b/src/tools/map_replace_image.cpp index b67ce18aede..01aef691a49 100644 --- a/src/tools/map_replace_image.cpp +++ b/src/tools/map_replace_image.cpp @@ -150,9 +150,12 @@ int main(int argc, const char **argv) int Type, ID; void *pItem = g_DataReader.GetItem(Index, &Type, &ID); - // filter ITEMTYPE_EX items, they will be automatically added again - if(Type == ITEMTYPE_EX) + // Filter items with unknown type, as we can't write them back. + // Filter ITEMTYPE_EX items, they will be automatically added again. + if(Type < 0 || Type == ITEMTYPE_EX) + { continue; + } int Size = g_DataReader.GetItemSize(Index); diff --git a/src/tools/map_resave.cpp b/src/tools/map_resave.cpp index 1f6698828f6..2060851984c 100644 --- a/src/tools/map_resave.cpp +++ b/src/tools/map_resave.cpp @@ -1,24 +1,30 @@ /* (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 -int main(int argc, const char **argv) -{ - CCmdlineFix CmdlineFix(&argc, &argv); - - IStorage *pStorage = CreateStorage(IStorage::STORAGETYPE_BASIC, argc, argv); - if(!pStorage || argc != 3) - return -1; +static const char *TOOL_NAME = "map_resave"; +static int ResaveMap(const char *pSourceMap, const char *pDestinationMap, IStorage *pStorage) +{ CDataFileReader Reader; - if(!Reader.Open(pStorage, argv[1], IStorage::TYPE_ABSOLUTE)) + if(!Reader.Open(pStorage, pSourceMap, IStorage::TYPE_ABSOLUTE)) + { + log_error(TOOL_NAME, "Failed to open source map '%s' for reading", pSourceMap); return -1; + } CDataFileWriter Writer; - if(!Writer.Open(pStorage, argv[2])) + if(!Writer.Open(pStorage, pDestinationMap)) + { + log_error(TOOL_NAME, "Failed to open destination map '%s' for writing", pDestinationMap); + Reader.Close(); return -1; + } // add all items for(int Index = 0; Index < Reader.NumItems(); Index++) @@ -26,9 +32,12 @@ int main(int argc, const char **argv) int Type, ID; const void *pPtr = Reader.GetItem(Index, &Type, &ID); - // filter ITEMTYPE_EX items, they will be automatically added again - if(Type == ITEMTYPE_EX) + // Filter items with unknown type, as we can't write them back. + // Filter ITEMTYPE_EX items, they will be automatically added again. + if(Type < 0 || Type == ITEMTYPE_EX) + { continue; + } int Size = Reader.GetItemSize(Index); Writer.AddItem(Type, ID, Size, pPtr); @@ -44,5 +53,27 @@ int main(int argc, const char **argv) Reader.Close(); Writer.Finish(); + log_info(TOOL_NAME, "Resaved '%s' to '%s'", pSourceMap, pDestinationMap); return 0; } + +int main(int argc, const char **argv) +{ + CCmdlineFix CmdlineFix(&argc, &argv); + log_set_global_logger_default(); + + if(argc != 3) + { + log_error(TOOL_NAME, "Usage: %s ", TOOL_NAME); + return -1; + } + + IStorage *pStorage = CreateStorage(IStorage::STORAGETYPE_BASIC, argc, argv); + if(!pStorage) + { + log_error(TOOL_NAME, "Error creating basic storage"); + return -1; + } + + return ResaveMap(argv[1], argv[2], pStorage); +} diff --git a/src/tools/uuid.cpp b/src/tools/uuid.cpp index 0cb35aa8d10..6a6a011c214 100644 --- a/src/tools/uuid.cpp +++ b/src/tools/uuid.cpp @@ -1,4 +1,5 @@ #include +#include #include int main(int argc, const char **argv) {