From bae37ab7df8421722bc6ef0262263883ec341805 Mon Sep 17 00:00:00 2001 From: ChillerDragon Date: Sun, 16 Oct 2022 10:14:05 +0200 Subject: [PATCH 1/8] Add 0.7 client support While keeping 0.6 fully working and untouched This adds the option to connect via 0.7 0.7 servers are now mixed into the server browser list. And users can also manually connect via 0.7 using a connection url like so: ./DDNet "connect tw-0.7+udp://127.0.0.1:8303" This also adds a "Tee 0.7" section in the menu to pick a 0.7 skin. The config variables are prefixed with player7_ like for example player7_color_body --- CMakeLists.txt | 11 + data/deadtee.png | Bin 0 -> 4087 bytes src/base/system.cpp | 50 +- src/base/system.h | 21 +- src/base/types.h | 7 +- src/engine/client.h | 28 +- src/engine/client/client.cpp | 255 ++++++- src/engine/client/client.h | 6 + src/engine/client/enums.h | 11 + src/engine/client/serverbrowser.cpp | 2 +- src/engine/client/sixup_translate_system.cpp | 114 +++ src/engine/shared/config_variables.h | 42 ++ src/engine/shared/demo.cpp | 4 +- src/engine/shared/demo.h | 3 + src/engine/shared/network.cpp | 25 + src/engine/shared/network.h | 36 +- src/engine/shared/network_client.cpp | 17 +- src/engine/shared/network_conn.cpp | 152 ++-- src/engine/shared/network_server.cpp | 3 +- src/engine/shared/protocol.h | 1 + src/engine/shared/protocol7.h | 14 +- src/engine/shared/protocolglue.cpp | 75 ++ src/engine/shared/protocolglue.h | 9 + .../shared/sixup_translate_snapshot.cpp | 516 +++++++++++++ src/engine/shared/snapshot.cpp | 22 +- src/engine/shared/snapshot.h | 18 +- src/engine/shared/translation_context.h | 120 +++ src/game/client/components/broadcast.cpp | 8 +- src/game/client/components/broadcast.h | 2 + src/game/client/components/chat.cpp | 10 + src/game/client/components/hud.cpp | 4 +- src/game/client/components/mapimages.cpp | 11 +- src/game/client/components/menus.h | 13 + src/game/client/components/menus_settings.cpp | 8 +- .../client/components/menus_settings7.cpp | 548 ++++++++++++++ src/game/client/components/players.cpp | 51 ++ src/game/client/components/players.h | 3 + src/game/client/components/scoreboard.cpp | 50 +- src/game/client/components/skins7.cpp | 560 ++++++++++++++ src/game/client/components/skins7.h | 97 +++ src/game/client/components/spectator.cpp | 16 + src/game/client/components/voting.cpp | 10 + src/game/client/components/voting.h | 2 +- src/game/client/gameclient.cpp | 250 ++++++- src/game/client/gameclient.h | 17 + src/game/client/render.cpp | 251 ++++++- src/game/client/render.h | 34 + src/game/client/sixup_translate_game.cpp | 689 ++++++++++++++++++ src/game/server/gamecontext.cpp | 21 +- src/game/version.h | 6 + src/test/netaddr.cpp | 41 +- src/tools/twping.cpp | 3 +- 52 files changed, 4081 insertions(+), 186 deletions(-) create mode 100644 data/deadtee.png create mode 100644 src/engine/client/enums.h create mode 100644 src/engine/client/sixup_translate_system.cpp create mode 100644 src/engine/shared/protocolglue.cpp create mode 100644 src/engine/shared/protocolglue.h create mode 100644 src/engine/shared/sixup_translate_snapshot.cpp create mode 100644 src/engine/shared/translation_context.h create mode 100644 src/game/client/components/menus_settings7.cpp create mode 100644 src/game/client/components/skins7.cpp create mode 100644 src/game/client/components/skins7.h create mode 100644 src/game/client/sixup_translate_game.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 89cc81f6d..3c15aa4b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1386,6 +1386,7 @@ set(EXPECTED_DATA countryflags/ZW.png countryflags/default.png countryflags/index.txt + deadtee.png debug_font.png editor/audio_source.png editor/automap/basic_freeze.rules @@ -1986,10 +1987,13 @@ set_src(ENGINE_SHARED GLOB_RECURSE src/engine/shared protocol_ex.cpp protocol_ex.h protocol_ex_msgs.h + protocolglue.cpp + protocolglue.h ringbuffer.cpp ringbuffer.h serverinfo.cpp serverinfo.h + sixup_translate_snapshot.cpp snapshot.cpp snapshot.h storage.cpp @@ -1998,6 +2002,7 @@ set_src(ENGINE_SHARED GLOB_RECURSE src/engine/shared teehistorian_ex.cpp teehistorian_ex.h teehistorian_ex_chunks.h + translation_context.h uuid_manager.cpp uuid_manager.h video.cpp @@ -2148,6 +2153,7 @@ if(CLIENT) demoedit.cpp demoedit.h discord.cpp + enums.h favorites.cpp friends.cpp friends.h @@ -2169,6 +2175,7 @@ if(CLIENT) serverbrowser_http.h serverbrowser_ping_cache.cpp serverbrowser_ping_cache.h + sixup_translate_system.cpp smooth_time.cpp smooth_time.h sound.cpp @@ -2237,6 +2244,7 @@ if(CLIENT) components/menus_demo.cpp components/menus_ingame.cpp components/menus_settings.cpp + components/menus_settings7.cpp components/menus_settings_assets.cpp components/menus_start.cpp components/motd.cpp @@ -2253,6 +2261,8 @@ if(CLIENT) components/scoreboard.h components/skins.cpp components/skins.h + components/skins7.cpp + components/skins7.h components/sounds.cpp components/sounds.h components/spectator.cpp @@ -2292,6 +2302,7 @@ if(CLIENT) render.cpp render.h render_map.cpp + sixup_translate_game.cpp skin.h ui.cpp ui.h diff --git a/data/deadtee.png b/data/deadtee.png new file mode 100644 index 0000000000000000000000000000000000000000..c22df996083b28b913717f7c04d18307dab0a4a2 GIT binary patch literal 4087 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 literal 0 HcmV?d00001 diff --git a/src/base/system.cpp b/src/base/system.cpp index a3dd1c7f5..b7320fff9 100644 --- a/src/base/system.cpp +++ b/src/base/system.cpp @@ -958,7 +958,7 @@ const NETADDR NETADDR_ZEROED = {NETTYPE_INVALID, {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, static void netaddr_to_sockaddr_in(const NETADDR *src, struct sockaddr_in *dest) { mem_zero(dest, sizeof(struct sockaddr_in)); - if(src->type != NETTYPE_IPV4 && src->type != NETTYPE_WEBSOCKET_IPV4) + if(!(src->type & NETTYPE_IPV4) && !(src->type & NETTYPE_WEBSOCKET_IPV4)) { dbg_msg("system", "couldn't convert NETADDR of type %d to ipv4", src->type); return; @@ -972,9 +972,9 @@ static void netaddr_to_sockaddr_in(const NETADDR *src, struct sockaddr_in *dest) static void netaddr_to_sockaddr_in6(const NETADDR *src, struct sockaddr_in6 *dest) { mem_zero(dest, sizeof(struct sockaddr_in6)); - if(src->type != NETTYPE_IPV6) + if(!(src->type & NETTYPE_IPV6)) { - dbg_msg("system", "couldn't not convert NETADDR of type %d to ipv6", src->type); + dbg_msg("system", "couldn't convert NETADDR of type %d to ipv6", src->type); return; } @@ -1098,16 +1098,16 @@ void net_addr_str_v6(const unsigned short ip[8], int port, char *buffer, int buf } } -void net_addr_str(const NETADDR *addr, char *string, int max_length, int add_port) +bool net_addr_str(const NETADDR *addr, char *string, int max_length, int add_port) { - if(addr->type == NETTYPE_IPV4 || addr->type == NETTYPE_WEBSOCKET_IPV4) + if(addr->type & NETTYPE_IPV4 || addr->type & NETTYPE_WEBSOCKET_IPV4) { if(add_port != 0) str_format(string, max_length, "%d.%d.%d.%d:%d", addr->ip[0], addr->ip[1], addr->ip[2], addr->ip[3], addr->port); else str_format(string, max_length, "%d.%d.%d.%d", addr->ip[0], addr->ip[1], addr->ip[2], addr->ip[3]); } - else if(addr->type == NETTYPE_IPV6) + else if(addr->type & NETTYPE_IPV6) { int port = -1; unsigned short ip[8]; @@ -1123,7 +1123,27 @@ void net_addr_str(const NETADDR *addr, char *string, int max_length, int add_por net_addr_str_v6(ip, port, string, max_length); } else + { str_format(string, max_length, "unknown type %d", addr->type); + return false; + } + return true; +} + +void net_addr_url_str(const NETADDR *addr, char *string, int max_length, int add_port) +{ + char ipaddr[512]; + if(!net_addr_str(addr, ipaddr, sizeof(ipaddr), add_port)) + { + str_copy(string, ipaddr, max_length); + return; + } + str_format( + string, + max_length, + "tw-%s+udp://%s", + addr->type & NETTYPE_TW7 ? "0.7" : "0.6", + ipaddr); } static int priv_net_extract(const char *hostname, char *host, int max_host, int *port) @@ -1275,12 +1295,17 @@ static int parse_uint16(unsigned short *out, const char **str) int net_addr_from_url(NETADDR *addr, const char *string, char *host_buf, size_t host_buf_size) { + bool sixup = false; + mem_zero(addr, sizeof(*addr)); const char *str = str_startswith(string, "tw-0.6+udp://"); + if(!str && (str = str_startswith(string, "tw-0.7+udp://"))) + { + addr->type |= NETTYPE_TW7; + sixup = true; + } if(!str) return 1; - mem_zero(addr, sizeof(*addr)); - int length = str_length(str); int start = 0; int end = length; @@ -1307,7 +1332,14 @@ int net_addr_from_url(NETADDR *addr, const char *string, char *host_buf, size_t if(host_buf) str_copy(host_buf, host, host_buf_size); - return net_addr_from_str(addr, host); + int failure = net_addr_from_str(addr, host); + if(failure) + return failure; + + if(sixup) + addr->type |= NETTYPE_TW7; + + return failure; } int net_addr_from_str(NETADDR *addr, const char *string) diff --git a/src/base/system.h b/src/base/system.h index 400986a24..260b6abf7 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -812,9 +812,28 @@ int net_addr_comp_noport(const NETADDR *a, const NETADDR *b); * @param max_length Maximum size of the string. * @param add_port add port to string or not * + * @return true on success + * * @remark The string will always be zero terminated */ -void net_addr_str(const NETADDR *addr, char *string, int max_length, int add_port); +bool net_addr_str(const NETADDR *addr, char *string, int max_length, int add_port); + +/** + * Turns a network address into a url string. + * Examples: + * tw-0.6+udp://127.0.0.1:8303 + * tw-0.7+udp://127.0.0.1 + * + * @ingroup Network-General + * + * @param addr Address to turn into a string. + * @param string Buffer to fill with the url string. + * @param max_length Maximum size of the url string. + * @param add_port add port to url string or not + * + * @remark The string will always be zero terminated + */ +void net_addr_url_str(const NETADDR *addr, char *string, int max_length, int add_port); /** * Turns url string into a network address struct. diff --git a/src/base/types.h b/src/base/types.h index ffa1b47ed..13abeb0e8 100644 --- a/src/base/types.h +++ b/src/base/types.h @@ -45,9 +45,14 @@ enum NETTYPE_IPV4 = 1, NETTYPE_IPV6 = 2, NETTYPE_WEBSOCKET_IPV4 = 8, + /** + * 0.7 address. This is a flag in NETADDR to avoid introducing a parameter to every networking function + * to differenciate between 0.6 and 0.7 connections. + */ + NETTYPE_TW7 = 16, NETTYPE_ALL = NETTYPE_IPV4 | NETTYPE_IPV6 | NETTYPE_WEBSOCKET_IPV4, - NETTYPE_MASK = NETTYPE_ALL | NETTYPE_LINK_BROADCAST, + NETTYPE_MASK = NETTYPE_ALL | NETTYPE_LINK_BROADCAST | NETTYPE_TW7, }; /** diff --git a/src/engine/client.h b/src/engine/client.h index b60793192..69eafbdef 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -8,12 +8,17 @@ #include "message.h" #include +#include + #include +#include #include #include #include +#include + struct SWarning; enum @@ -23,8 +28,6 @@ enum RECORDER_RACE = 2, RECORDER_REPLAYS = 3, RECORDER_MAX = 4, - - NUM_DUMMIES = 2, }; typedef bool (*CLIENTFUNC_FILTER)(const void *pData, int DataSize, void *pUser); @@ -73,6 +76,7 @@ public: LOADING_CALLBACK_DETAIL_DEMO, }; typedef std::function TLoadingCallback; + CTranslationContext m_TranslationContext; protected: // quick access to state of the client @@ -232,19 +236,29 @@ public: virtual CSnapItem SnapGetItem(int SnapId, int Index) const = 0; virtual void SnapSetStaticsize(int ItemType, int Size) = 0; + virtual void SnapSetStaticsize7(int ItemType, int Size) = 0; virtual int SendMsg(int Conn, CMsgPacker *pMsg, int Flags) = 0; virtual int SendMsgActive(CMsgPacker *pMsg, int Flags) = 0; template - int SendPackMsgActive(T *pMsg, int Flags) + int SendPackMsgActive(T *pMsg, int Flags, bool NoTranslate = false) { - CMsgPacker Packer(T::ms_MsgId, false); + CMsgPacker Packer(T::ms_MsgId, false, NoTranslate); if(pMsg->Pack(&Packer)) return -1; return SendMsgActive(&Packer, Flags); } + template + int SendPackMsg(int Conn, T *pMsg, int Flags, bool NoTranslate = false) + { + CMsgPacker Packer(T::ms_MsgId, false, NoTranslate); + if(pMsg->Pack(&Packer)) + return -1; + return SendMsg(Conn, &Packer, Flags); + } + // virtual const char *PlayerName() const = 0; virtual const char *DummyName() = 0; @@ -265,6 +279,9 @@ public: int Points() const { return m_Points; } int64_t ReconnectTime() const { return m_ReconnectTime; } void SetReconnectTime(int64_t ReconnectTime) { m_ReconnectTime = ReconnectTime; } + + virtual bool IsSixup() const = 0; + virtual int GetCurrentRaceTime() = 0; virtual void RaceRecord_Start(const char *pFilename) = 0; @@ -368,6 +385,9 @@ public: virtual void RenderShutdownMessage() = 0; virtual CNetObjHandler *GetNetObjHandler() = 0; + virtual protocol7::CNetObjHandler *GetNetObjHandler7() = 0; + + virtual int ClientVersion7() const = 0; }; void SnapshotRemoveExtraProjectileInfo(CSnapshot *pSnap); diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index bebd80962..b637fa2b1 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -36,12 +36,18 @@ #include #include #include +#include #include #include #include #include #include +#include +#include + +#include + #include #include @@ -96,15 +102,59 @@ CClient::CClient() : for(auto &GameTime : m_aGameTime) GameTime.Init(0); m_PredictedTime.Init(0); + + m_Sixup = false; } // ----- send functions ----- -static inline bool RepackMsg(const CMsgPacker *pMsg, CPacker &Packer) +static inline bool RepackMsg(const CMsgPacker *pMsg, CPacker &Packer, bool Sixup) { + int MsgId = pMsg->m_MsgId; Packer.Reset(); + + if(Sixup && !pMsg->m_NoTranslate) + { + if(pMsg->m_System) + { + if(MsgId >= OFFSET_UUID) + ; + else if(MsgId == NETMSG_INFO || MsgId == NETMSG_REQUEST_MAP_DATA) + ; + else if(MsgId == NETMSG_READY) + MsgId = protocol7::NETMSG_READY; + else if(MsgId == NETMSG_RCON_CMD) + MsgId = protocol7::NETMSG_RCON_CMD; + else if(MsgId == NETMSG_ENTERGAME) + MsgId = protocol7::NETMSG_ENTERGAME; + else if(MsgId == NETMSG_INPUT) + MsgId = protocol7::NETMSG_INPUT; + else if(MsgId == NETMSG_RCON_AUTH) + MsgId = protocol7::NETMSG_RCON_AUTH; + else if(MsgId == NETMSGTYPE_CL_SETTEAM) + MsgId = protocol7::NETMSGTYPE_CL_SETTEAM; + else if(MsgId == NETMSGTYPE_CL_VOTE) + MsgId = protocol7::NETMSGTYPE_CL_VOTE; + else if(MsgId == NETMSG_PING) + MsgId = protocol7::NETMSG_PING; + else + { + dbg_msg("net", "0.7 DROP send sys %d", MsgId); + return true; + } + } + else + { + if(MsgId >= 0 && MsgId < OFFSET_UUID) + MsgId = Msg_SixToSeven(MsgId); + + if(MsgId < 0) + return true; + } + } + if(pMsg->m_MsgId < OFFSET_UUID) { - Packer.AddInt((pMsg->m_MsgId << 1) | (pMsg->m_System ? 1 : 0)); + Packer.AddInt((MsgId << 1) | (pMsg->m_System ? 1 : 0)); } else { @@ -125,7 +175,7 @@ int CClient::SendMsg(int Conn, CMsgPacker *pMsg, int Flags) // repack message (inefficient) CPacker Pack; - if(RepackMsg(pMsg, Pack)) + if(RepackMsg(pMsg, Pack, IsSixup())) return 0; mem_zero(&Packet, sizeof(CNetChunk)); @@ -160,6 +210,15 @@ int CClient::SendMsgActive(CMsgPacker *pMsg, int Flags) void CClient::SendInfo(int Conn) { + if(IsSixup()) + { + CMsgPacker Msg(NETMSG_INFO, true); + Msg.AddString(GAME_NETVERSION7, 128); + Msg.AddString(Config()->m_Password); + Msg.AddInt(GameClient()->ClientVersion7()); + SendMsg(Conn, &Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH); + return; + } CMsgPacker MsgVer(NETMSG_CLIENTVER, true); MsgVer.AddRaw(&m_ConnectionId, sizeof(m_ConnectionId)); MsgVer.AddInt(GameClient()->DDNetVersion()); @@ -192,9 +251,17 @@ void CClient::SendMapRequest() Storage()->RemoveFile(m_aMapdownloadFilenameTemp, IStorage::TYPE_SAVE); } m_MapdownloadFileTemp = Storage()->OpenFile(m_aMapdownloadFilenameTemp, IOFLAG_WRITE, IStorage::TYPE_SAVE); - CMsgPacker Msg(NETMSG_REQUEST_MAP_DATA, true); - Msg.AddInt(m_MapdownloadChunk); - SendMsg(CONN_MAIN, &Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH); + if(IsSixup()) + { + CMsgPacker MsgP(protocol7::NETMSG_REQUEST_MAP_DATA, true, true); + SendMsg(CONN_MAIN, &MsgP, MSGFLAG_VITAL | MSGFLAG_FLUSH); + } + else + { + CMsgPacker Msg(NETMSG_REQUEST_MAP_DATA, true); + Msg.AddInt(m_MapdownloadChunk); + SendMsg(CONN_MAIN, &Msg, MSGFLAG_VITAL | MSGFLAG_FLUSH); + } } void CClient::RconAuth(const char *pName, const char *pPassword) @@ -207,6 +274,14 @@ void CClient::RconAuth(const char *pName, const char *pPassword) if(pPassword != m_aRconPassword) str_copy(m_aRconPassword, pPassword); + if(IsSixup()) + { + CMsgPacker Msg7(protocol7::NETMSG_RCON_AUTH, true, true); + Msg7.AddString(pPassword); + SendMsgActive(&Msg7, MSGFLAG_VITAL); + return; + } + CMsgPacker Msg(NETMSG_RCON_AUTH, true); Msg.AddString(pName); Msg.AddString(pPassword); @@ -269,7 +344,18 @@ void CClient::SendInput() // pack it for(int k = 0; k < Size / 4; k++) - Msg.AddInt(m_aInputs[i][m_aCurrentInput[i]].m_aData[k]); + { + static const int FlagsOffset = offsetof(CNetObj_PlayerInput, m_PlayerFlags) / sizeof(int); + if(k == FlagsOffset && IsSixup()) + { + int PlayerFlags = m_aInputs[i][m_aCurrentInput[i]].m_aData[k]; + Msg.AddInt(PlayerFlags_SixToSeven(PlayerFlags)); + } + else + { + Msg.AddInt(m_aInputs[i][m_aCurrentInput[i]].m_aData[k]); + } + } m_aCurrentInput[i]++; m_aCurrentInput[i] %= 200; @@ -468,11 +554,13 @@ void CClient::Connect(const char *pAddress, const char *pPassword) mem_zero(aConnectAddrs, sizeof(aConnectAddrs)); const char *pNextAddr = pAddress; char aBuffer[128]; + bool OnlySixup = true; while((pNextAddr = str_next_token(pNextAddr, ",", aBuffer, sizeof(aBuffer)))) { NETADDR NextAddr; char aHost[128]; int url = net_addr_from_url(&NextAddr, aBuffer, aHost, sizeof(aHost)); + bool Sixup = NextAddr.type & NETTYPE_TW7; if(url > 0) str_copy(aHost, aBuffer); @@ -491,6 +579,10 @@ void CClient::Connect(const char *pAddress, const char *pPassword) NextAddr.port = 8303; } char aNextAddr[NETADDR_MAXSTRSIZE]; + if(Sixup) + NextAddr.type |= NETTYPE_TW7; + else + OnlySixup = false; net_addr_str(&NextAddr, aNextAddr, sizeof(aNextAddr), true); log_debug("client", "resolved connect address '%s' to %s", aBuffer, aNextAddr); aConnectAddrs[NumConnectAddrs] = NextAddr; @@ -523,7 +615,14 @@ void CClient::Connect(const char *pAddress, const char *pPassword) m_CanReceiveServerCapabilities = true; - m_aNetClient[CONN_MAIN].Connect(aConnectAddrs, NumConnectAddrs); + m_Sixup = OnlySixup; + if(m_Sixup) + { + m_aNetClient[CONN_MAIN].Connect7(aConnectAddrs, NumConnectAddrs); + } + else + m_aNetClient[CONN_MAIN].Connect(aConnectAddrs, NumConnectAddrs); + m_aNetClient[CONN_MAIN].RefreshStun(); SetState(IClient::STATE_CONNECTING); @@ -595,6 +694,10 @@ void CClient::DisconnectWithReason(const char *pReason) m_aapSnapshots[0][SNAP_PREV] = 0; m_aReceivedSnapshots[0] = 0; m_LastDummy = false; + + // 0.7 + m_TranslationContext.Reset(); + m_Sixup = false; } void CClient::Disconnect() @@ -652,7 +755,11 @@ void CClient::DummyConnect() g_Config.m_ClDummyHammer = 0; m_DummyConnecting = true; - m_aNetClient[CONN_DUMMY].Connect(m_aNetClient[CONN_MAIN].ServerAddress(), 1); + // connect to the server + if(IsSixup()) + m_aNetClient[CONN_DUMMY].Connect7(m_aNetClient[CONN_MAIN].ServerAddress(), 1); + else + m_aNetClient[CONN_DUMMY].Connect(m_aNetClient[CONN_MAIN].ServerAddress(), 1); } void CClient::DummyDisconnect(const char *pReason) @@ -738,6 +845,11 @@ void CClient::SnapSetStaticsize(int ItemType, int Size) m_SnapshotDelta.SetStaticsize(ItemType, Size); } +void CClient::SnapSetStaticsize7(int ItemType, int Size) +{ + m_SnapshotDelta.SetStaticsize7(ItemType, Size); +} + void CClient::DebugRender() { if(!g_Config.m_Debug) @@ -1335,6 +1447,20 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) SendMsg(Conn, &Packer, MSGFLAG_VITAL); } + // allocates the memory for the translated data + CPacker Packer6; + if(IsSixup()) + { + bool IsExMsg = false; + int Success = !TranslateSysMsg(&Msg, Sys, &Unpacker, &Packer6, pPacket, &IsExMsg); + if(Msg < 0) + return; + if(Success && !IsExMsg) + { + Unpacker.Reset(Packer6.Data(), Packer6.Size()); + } + } + if(Sys) { // system message @@ -1482,11 +1608,25 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) { return; } + int Last = -1; + int MapCRC = -1; + int Chunk = -1; + int Size = -1; + + if(IsSixup()) + { + MapCRC = m_MapdownloadCrc; + Chunk = m_MapdownloadChunk; + Size = minimum(m_TranslationContext.m_MapDownloadChunkSize, m_TranslationContext.m_MapdownloadTotalsize - m_MapdownloadAmount); + } + else + { + Last = Unpacker.GetInt(); + MapCRC = Unpacker.GetInt(); + Chunk = Unpacker.GetInt(); + Size = Unpacker.GetInt(); + } - int Last = Unpacker.GetInt(); - int MapCRC = Unpacker.GetInt(); - int Chunk = Unpacker.GetInt(); - int Size = Unpacker.GetInt(); const unsigned char *pData = Unpacker.GetRaw(Size); if(Unpacker.Error() || Size <= 0 || MapCRC != m_MapdownloadCrc || Chunk != m_MapdownloadChunk) { @@ -1497,6 +1637,9 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) m_MapdownloadAmount += Size; + if(IsSixup()) + Last = m_MapdownloadAmount == m_TranslationContext.m_MapdownloadTotalsize; + if(Last) { if(m_MapdownloadFileTemp) @@ -1511,9 +1654,17 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) // request new chunk m_MapdownloadChunk++; - CMsgPacker MsgP(NETMSG_REQUEST_MAP_DATA, true); - MsgP.AddInt(m_MapdownloadChunk); - SendMsg(CONN_MAIN, &MsgP, MSGFLAG_VITAL | MSGFLAG_FLUSH); + if(IsSixup() && (m_MapdownloadChunk % m_TranslationContext.m_MapDownloadChunksPerRequest == 0)) + { + CMsgPacker MsgP(protocol7::NETMSG_REQUEST_MAP_DATA, true, true); + SendMsg(CONN_MAIN, &MsgP, MSGFLAG_VITAL | MSGFLAG_FLUSH); + } + else + { + CMsgPacker MsgP(NETMSG_REQUEST_MAP_DATA, true); + MsgP.AddInt(m_MapdownloadChunk); + SendMsg(CONN_MAIN, &MsgP, MSGFLAG_VITAL | MSGFLAG_FLUSH); + } if(g_Config.m_Debug) { @@ -1781,7 +1932,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) } // unpack delta - const int SnapSize = m_SnapshotDelta.UnpackDelta(pDeltaShot, pTmpBuffer3, pDeltaData, DeltaSize); + const int SnapSize = m_SnapshotDelta.UnpackDelta(pDeltaShot, pTmpBuffer3, pDeltaData, DeltaSize, IsSixup()); if(SnapSize < 0) { dbg_msg("client", "delta unpack failed. error=%d", SnapSize); @@ -1828,9 +1979,30 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) m_aSnapshotStorage[Conn].PurgeUntil(PurgeTick); // create a verified and unpacked snapshot + int AltSnapSize = -1; unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE]; CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer; - const int AltSnapSize = UnpackAndValidateSnapshot(pTmpBuffer3, pAltSnapBuffer); + + if(IsSixup()) + { + unsigned char aTmpTransSnapBuffer[CSnapshot::MAX_SIZE]; + CSnapshot *pTmpTransSnapBuffer = (CSnapshot *)aTmpTransSnapBuffer; + mem_copy(pTmpTransSnapBuffer, pTmpBuffer3, CSnapshot::MAX_SIZE); + AltSnapSize = pTmpTransSnapBuffer->TranslateSevenToSix( + pAltSnapBuffer, + m_TranslationContext, + LocalTime(), + CClient::GameTick(g_Config.m_ClDummy), + Conn, + Dummy, + GameClient()->GetNetObjHandler7(), + GameClient()->GetNetObjHandler()); + } + else + { + AltSnapSize = UnpackAndValidateSnapshot(pTmpBuffer3, pAltSnapBuffer); + } + if(AltSnapSize < 0) { dbg_msg("client", "unpack snapshot and validate failed. error=%d", AltSnapSize); @@ -1902,8 +2074,6 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) { if(m_ServerCapabilities.m_ChatTimeoutCode) { - CNetMsg_Cl_Say MsgP; - MsgP.m_Team = 0; char aBuf[128]; char aBufMsg[256]; if(!g_Config.m_ClRunOnJoin[0] && !g_Config.m_ClDummyDefaultEyes && !g_Config.m_ClPlayerDefaultEyes) @@ -1947,10 +2117,23 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) str_append(aBufMsg, aBuf); } } - MsgP.m_pMessage = aBufMsg; - CMsgPacker PackerTimeout(&MsgP); - MsgP.Pack(&PackerTimeout); - SendMsg(Conn, &PackerTimeout, MSGFLAG_VITAL); + if(IsSixup()) + { + protocol7::CNetMsg_Cl_Say Msg7; + Msg7.m_Mode = protocol7::CHAT_ALL; + Msg7.m_Target = -1; + Msg7.m_pMessage = aBufMsg; + SendPackMsg(Conn, &Msg7, MSGFLAG_VITAL, true); + } + else + { + CNetMsg_Cl_Say MsgP; + MsgP.m_Team = 0; + MsgP.m_pMessage = aBufMsg; + CMsgPacker PackerTimeout(&MsgP); + MsgP.Pack(&PackerTimeout); + SendMsg(Conn, &PackerTimeout, MSGFLAG_VITAL); + } } m_aCodeRunAfterJoin[Conn] = true; } @@ -2313,9 +2496,10 @@ void CClient::PumpNetwork() // process packets CNetChunk Packet; + SECURITY_TOKEN ResponseToken; for(int Conn = 0; Conn < NUM_CONNS; Conn++) { - while(m_aNetClient[Conn].Recv(&Packet)) + while(m_aNetClient[Conn].Recv(&Packet, &ResponseToken, IsSixup())) { if(Packet.m_ClientId == -1) { @@ -2347,11 +2531,29 @@ void CClient::OnDemoPlayerSnapshot(void *pData, int Size) return; } + unsigned char aTmpTranslateBuffer[CSnapshot::MAX_SIZE]; + CSnapshot *pTmpTranslateBuffer = nullptr; + int TranslatedSize = 0; + if(IsSixup()) + { + pTmpTranslateBuffer = (CSnapshot *)aTmpTranslateBuffer; + TranslatedSize = ((CSnapshot *)pData)->TranslateSevenToSix(pTmpTranslateBuffer, m_TranslationContext, LocalTime(), GameTick(g_Config.m_ClDummy), CONN_MAIN, false, GameClient()->GetNetObjHandler7(), GameClient()->GetNetObjHandler()); + if(TranslatedSize < 0) + { + dbg_msg("sixup", "failed to translate snapshot. error=%d", TranslatedSize); + pTmpTranslateBuffer = nullptr; + TranslatedSize = 0; + } + } + // handle snapshots after validation std::swap(m_aapSnapshots[0][SNAP_PREV], m_aapSnapshots[0][SNAP_CURRENT]); mem_copy(m_aapSnapshots[0][SNAP_CURRENT]->m_pSnap, pData, Size); mem_copy(m_aapSnapshots[0][SNAP_CURRENT]->m_pAltSnap, pAltSnapBuffer, AltSnapSize); + if(pTmpTranslateBuffer && TranslatedSize > 0) + mem_copy(m_aapSnapshots[0][SNAP_CURRENT]->m_pAltSnap, pTmpTranslateBuffer, TranslatedSize); + GameClient()->OnNewSnapshot(); } @@ -3591,6 +3793,9 @@ const char *CClient::DemoPlayer_Play(const char *pFilename, int StorageType) return m_DemoPlayer.ErrorMessage(); } + m_Sixup = str_startswith(m_DemoPlayer.Info()->m_Header.m_aNetversion, "0.7"); + m_DemoPlayer.SetSixup(m_Sixup); + // load map const CMapInfo *pMapInfo = m_DemoPlayer.GetMapInfo(); int Crc = pMapInfo->m_Crc; diff --git a/src/engine/client/client.h b/src/engine/client/client.h index 7ee46e270..a72d95173 100644 --- a/src/engine/client/client.h +++ b/src/engine/client/client.h @@ -93,6 +93,7 @@ class CClient : public IClient, public CDemoPlayer::IListener char m_aConnectAddressStr[MAX_SERVER_ADDRESSES * NETADDR_MAXSTRSIZE] = ""; CUuid m_ConnectionId = UUID_ZEROED; + bool m_Sixup; bool m_HaveGlobalTcpAddr = false; NETADDR m_GlobalTcpAddr = NETADDR_ZEROED; @@ -334,6 +335,7 @@ public: const void *SnapFindItem(int SnapId, int Type, int Id) const override; int SnapNumItems(int SnapId) const override; void SnapSetStaticsize(int ItemType, int Size) override; + void SnapSetStaticsize7(int ItemType, int Size) override; void Render(); void DebugRender(); @@ -348,6 +350,8 @@ public: const char *LoadMap(const char *pName, const char *pFilename, SHA256_DIGEST *pWantedSha256, unsigned WantedCrc); const char *LoadMapSearch(const char *pMapName, SHA256_DIGEST *pWantedSha256, int WantedCrc); + int TranslateSysMsg(int *pMsgId, bool System, CUnpacker *pUnpacker, CPacker *pPacker, CNetChunk *pPacket, bool *pIsExMsg); + void ProcessConnlessPacket(CNetChunk *pPacket); void ProcessServerInfo(int Type, NETADDR *pFrom, const void *pData, int DataSize); void ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy); @@ -362,6 +366,8 @@ public: void FinishDDNetInfo(); void LoadDDNetInfo(); + bool IsSixup() const override { return m_Sixup; } + const NETADDR &ServerAddress() const override { return *m_aNetClient[CONN_MAIN].ServerAddress(); } int ConnectNetTypes() const override; const char *ConnectAddressString() const override { return m_aConnectAddressStr; } diff --git a/src/engine/client/enums.h b/src/engine/client/enums.h new file mode 100644 index 000000000..45a475fa1 --- /dev/null +++ b/src/engine/client/enums.h @@ -0,0 +1,11 @@ +/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ +/* If you are missing that file, acquire a complete release at teeworlds.com. */ +#ifndef ENGINE_CLIENT_ENUMS_H +#define ENGINE_CLIENT_ENUMS_H + +enum +{ + NUM_DUMMIES = 2, +}; + +#endif diff --git a/src/engine/client/serverbrowser.cpp b/src/engine/client/serverbrowser.cpp index 393105c3e..4da2676df 100644 --- a/src/engine/client/serverbrowser.cpp +++ b/src/engine/client/serverbrowser.cpp @@ -704,7 +704,7 @@ void ServerBrowserFormatAddresses(char *pBuffer, int BufferSize, NETADDR *pAddrs { return; } - net_addr_str(&pAddrs[i], pBuffer, BufferSize, true); + net_addr_url_str(&pAddrs[i], pBuffer, BufferSize, true); int Length = str_length(pBuffer); pBuffer += Length; BufferSize -= Length; diff --git a/src/engine/client/sixup_translate_system.cpp b/src/engine/client/sixup_translate_system.cpp new file mode 100644 index 000000000..f6398cfcb --- /dev/null +++ b/src/engine/client/sixup_translate_system.cpp @@ -0,0 +1,114 @@ +#include + +int CClient::TranslateSysMsg(int *pMsgId, bool System, CUnpacker *pUnpacker, CPacker *pPacker, CNetChunk *pPacket, bool *pIsExMsg) +{ + *pIsExMsg = false; + if(!System) + return -1; + + // ddnet ex + if(*pMsgId > NETMSG_WHATIS && *pMsgId < NETMSG_RCON_CMD_GROUP_END) + { + *pIsExMsg = true; + return 0; + } + + pPacker->Reset(); + + if(*pMsgId == protocol7::NETMSG_MAP_CHANGE) + { + *pMsgId = NETMSG_MAP_CHANGE; + const char *pMapName = pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES); + int MapCrc = pUnpacker->GetInt(); + int Size = pUnpacker->GetInt(); + m_TranslationContext.m_MapDownloadChunksPerRequest = pUnpacker->GetInt(); + int ChunkSize = pUnpacker->GetInt(); + // void *pSha256 = pUnpacker->GetRaw(); // probably safe to ignore + pPacker->AddString(pMapName, 0); + pPacker->AddInt(MapCrc); + pPacker->AddInt(Size); + m_TranslationContext.m_MapdownloadTotalsize = Size; + m_TranslationContext.m_MapDownloadChunkSize = ChunkSize; + return 0; + } + else if(*pMsgId == protocol7::NETMSG_SERVERINFO) + { + // side effect only + // this is a 0.7 only message and not handled in 0.6 code + *pMsgId = -1; + net_addr_str(&pPacket->m_Address, m_CurrentServerInfo.m_aAddress, sizeof(m_CurrentServerInfo.m_aAddress), true); + str_copy(m_CurrentServerInfo.m_aVersion, pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES)); + str_copy(m_CurrentServerInfo.m_aName, pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES)); + str_clean_whitespaces(m_CurrentServerInfo.m_aName); + pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES); // Hostname + str_copy(m_CurrentServerInfo.m_aMap, pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES)); + str_copy(m_CurrentServerInfo.m_aGameType, pUnpacker->GetString(CUnpacker::SANITIZE_CC | CUnpacker::SKIP_START_WHITESPACES)); + int Flags = pUnpacker->GetInt(); + if(Flags & SERVER_FLAG_PASSWORD) + m_CurrentServerInfo.m_Flags |= SERVER_FLAG_PASSWORD; + // ddnets http master server handles timescore for us already + // if(Flags&SERVER_FLAG_TIMESCORE) + // m_CurrentServerInfo.m_Flags |= SERVER_FLAG_TIMESCORE; + pUnpacker->GetInt(); // Server level + m_CurrentServerInfo.m_NumPlayers = pUnpacker->GetInt(); + m_CurrentServerInfo.m_MaxPlayers = pUnpacker->GetInt(); + m_CurrentServerInfo.m_NumClients = pUnpacker->GetInt(); + m_CurrentServerInfo.m_MaxClients = pUnpacker->GetInt(); + return 0; + } + else if(*pMsgId == protocol7::NETMSG_RCON_AUTH_ON) + { + *pMsgId = NETMSG_RCON_AUTH_STATUS; + pPacker->AddInt(1); // authed + pPacker->AddInt(1); // cmdlist + return 0; + } + else if(*pMsgId == protocol7::NETMSG_RCON_AUTH_OFF) + { + *pMsgId = NETMSG_RCON_AUTH_STATUS; + pPacker->AddInt(0); // authed + pPacker->AddInt(0); // cmdlist + return 0; + } + else if(*pMsgId == protocol7::NETMSG_MAP_DATA) + { + // not binary compatible but translation happens on unpack + *pMsgId = NETMSG_MAP_DATA; + } + else if(*pMsgId >= protocol7::NETMSG_CON_READY && *pMsgId <= protocol7::NETMSG_INPUTTIMING) + { + *pMsgId = *pMsgId - 1; + } + else if(*pMsgId == protocol7::NETMSG_RCON_LINE) + { + *pMsgId = NETMSG_RCON_LINE; + } + else if(*pMsgId == protocol7::NETMSG_RCON_CMD_ADD) + { + *pMsgId = NETMSG_RCON_CMD_ADD; + } + else if(*pMsgId == protocol7::NETMSG_RCON_CMD_REM) + { + *pMsgId = NETMSG_RCON_CMD_REM; + } + else if(*pMsgId == protocol7::NETMSG_PING_REPLY) + { + *pMsgId = NETMSG_PING_REPLY; + } + else if(*pMsgId == protocol7::NETMSG_MAPLIST_ENTRY_ADD || *pMsgId == protocol7::NETMSG_MAPLIST_ENTRY_REM) + { + // This is just a nice to have so silently dropping that is fine + return -1; + } + else if(*pMsgId >= NETMSG_INFO && *pMsgId <= NETMSG_MAP_DATA) + { + return -1; // same in 0.6 and 0.7 + } + else if(*pMsgId < OFFSET_UUID) + { + dbg_msg("sixup", "drop unknown sys msg=%d", *pMsgId); + return -1; + } + + return -1; +} diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index f9df0aad1..417807e84 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -136,6 +136,48 @@ MACRO_CONFIG_INT(ClPlayerDefaultEyes, player_default_eyes, 0, 0, 5, CFGFLAG_CLIE MACRO_CONFIG_STR(ClSkinPrefix, cl_skin_prefix, 12, "", CFGFLAG_CLIENT | CFGFLAG_SAVE, "Replace the skins by skins with this prefix (e.g. kitty, santa)") MACRO_CONFIG_INT(ClFatSkins, cl_fat_skins, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable fat skins") +MACRO_CONFIG_COL(ClPlayer7ColorBody, player7_color_body, 0x1B6F74, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Player body color") +MACRO_CONFIG_COL(ClPlayer7ColorFeet, player7_color_feet, 0x1C873E, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Player feet color") + +MACRO_CONFIG_INT(ClPlayer7ColorMarking, player7_color_marking, 0xFF0000FF, 0, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player marking color") +MACRO_CONFIG_COL(ClPlayer7ColorDecoration, player7_color_decoration, 0x1B6F74, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Player decoration color") +MACRO_CONFIG_COL(ClPlayer7ColorHands, player7_color_hands, 0x1B759E, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Player hands color") +MACRO_CONFIG_COL(ClPlayer7ColorEyes, player7_color_eyes, 0x0000FF, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Player eyes color") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorBody, player7_use_custom_color_body, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for body") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorMarking, player7_use_custom_color_marking, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for marking") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorDecoration, player7_use_custom_color_decoration, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for decoration") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorHands, player7_use_custom_color_hands, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for hands") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorFeet, player7_use_custom_color_feet, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for feet") +MACRO_CONFIG_INT(ClPlayer7UseCustomColorEyes, player7_use_custom_color_eyes, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for eyes") +MACRO_CONFIG_STR(ClPlayer7Skin, player7_skin, protocol7::MAX_SKIN_ARRAY_SIZE, "default", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin") +MACRO_CONFIG_STR(ClPlayer7SkinBody, player7_skin_body, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin body") +MACRO_CONFIG_STR(ClPlayer7SkinMarking, player7_skin_marking, protocol7::MAX_SKIN_ARRAY_SIZE, "", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin marking") +MACRO_CONFIG_STR(ClPlayer7SkinDecoration, player7_skin_decoration, protocol7::MAX_SKIN_ARRAY_SIZE, "", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin decoration") +MACRO_CONFIG_STR(ClPlayer7SkinHands, player7_skin_hands, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin hands") +MACRO_CONFIG_STR(ClPlayer7SkinFeet, player7_skin_feet, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin feet") +MACRO_CONFIG_STR(ClPlayer7SkinEyes, player7_skin_eyes, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Player skin eyes") + +MACRO_CONFIG_COL(ClDummy7ColorBody, dummy7_color_body, 0x1B6F74, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Dummy body color") +MACRO_CONFIG_COL(ClDummy7ColorFeet, dummy7_color_feet, 0x1C873E, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Dummy feet color") + +MACRO_CONFIG_COL(ClDummy7ColorMarking, dummy7_color_marking, 0, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Dummy marking color") +MACRO_CONFIG_COL(ClDummy7ColorDecoration, dummy7_color_decoration, 0x1B6F74, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Dummy decoration color") +MACRO_CONFIG_COL(ClDummy7ColorHands, dummy7_color_hands, 0x1B759E, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Dummy hands color") +MACRO_CONFIG_COL(ClDummy7ColorEyes, dummy7_color_eyes, 0x0000FF, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_COLLIGHT | CFGFLAG_INSENSITIVE, "Dummy eyes color") +MACRO_CONFIG_INT(ClDummy7UseCustomColorBody, dummy7_use_custom_color_body, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for body") +MACRO_CONFIG_INT(ClDummy7UseCustomColorMarking, dummy7_use_custom_color_marking, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for marking") +MACRO_CONFIG_INT(ClDummy7UseCustomColorDecoration, dummy7_use_custom_color_decoration, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for decoration") +MACRO_CONFIG_INT(ClDummy7UseCustomColorHands, dummy7_use_custom_color_hands, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for hands") +MACRO_CONFIG_INT(ClDummy7UseCustomColorFeet, dummy7_use_custom_color_feet, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for feet") +MACRO_CONFIG_INT(ClDummy7UseCustomColorEyes, dummy7_use_custom_color_eyes, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Toggles usage of custom colors for eyes") +MACRO_CONFIG_STR(ClDummy7Skin, dummy7_skin, protocol7::MAX_SKIN_ARRAY_SIZE, "default", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin") +MACRO_CONFIG_STR(ClDummy7SkinBody, dummy7_skin_body, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin body") +MACRO_CONFIG_STR(ClDummy7SkinMarking, dummy7_skin_marking, protocol7::MAX_SKIN_ARRAY_SIZE, "", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin marking") +MACRO_CONFIG_STR(ClDummy7SkinDecoration, dummy7_skin_decoration, protocol7::MAX_SKIN_ARRAY_SIZE, "", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin decoration") +MACRO_CONFIG_STR(ClDummy7SkinHands, dummy7_skin_hands, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin hands") +MACRO_CONFIG_STR(ClDummy7SkinFeet, dummy7_skin_feet, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin feet") +MACRO_CONFIG_STR(ClDummy7SkinEyes, dummy7_skin_eyes, protocol7::MAX_SKIN_ARRAY_SIZE, "standard", CFGFLAG_CLIENT | CFGFLAG_SAVE | CFGFLAG_INSENSITIVE, "Dummy skin eyes") + MACRO_CONFIG_INT(UiPage, ui_page, 6, 6, 13, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interface page") MACRO_CONFIG_INT(UiSettingsPage, ui_settings_page, 0, 0, 9, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Interface settings page") MACRO_CONFIG_INT(UiToolboxPage, ui_toolbox_page, 0, 0, 2, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Toolbox page") diff --git a/src/engine/shared/demo.cpp b/src/engine/shared/demo.cpp index 5e7566b24..21678de73 100644 --- a/src/engine/shared/demo.cpp +++ b/src/engine/shared/demo.cpp @@ -677,7 +677,7 @@ void CDemoPlayer::DoTick() { // process delta snapshot CSnapshot *pNewsnap = (CSnapshot *)m_aSnapshot; - DataSize = m_pSnapshotDelta->UnpackDelta((CSnapshot *)m_aLastSnapshotData, pNewsnap, m_aChunkData, DataSize); + DataSize = m_pSnapshotDelta->UnpackDelta((CSnapshot *)m_aLastSnapshotData, pNewsnap, m_aChunkData, DataSize, IsSixup()); if(DataSize < 0) { @@ -1243,7 +1243,7 @@ bool CDemoEditor::Slice(const char *pDemo, const char *pDst, int StartTick, int CDemoRecorder DemoRecorder(m_pSnapshotDelta); unsigned char *pMapData = DemoPlayer.GetMapData(m_pStorage); - const int Result = DemoRecorder.Start(m_pStorage, m_pConsole, pDst, m_pNetVersion, pMapInfo->m_aName, Sha256, pMapInfo->m_Crc, pInfo->m_Header.m_aType, pMapInfo->m_Size, pMapData, nullptr, pfnFilter, pUser) == -1; + const int Result = DemoRecorder.Start(m_pStorage, m_pConsole, pDst, pInfo->m_Header.m_aNetversion, pMapInfo->m_aName, Sha256, pMapInfo->m_Crc, pInfo->m_Header.m_aType, pMapInfo->m_Size, pMapData, nullptr, pfnFilter, pUser) == -1; free(pMapData); if(Result != 0) { diff --git a/src/engine/shared/demo.h b/src/engine/shared/demo.h index f1ecc21d4..5792429f1 100644 --- a/src/engine/shared/demo.h +++ b/src/engine/shared/demo.h @@ -145,6 +145,7 @@ private: bool ScanFile(); int64_t Time(); + bool m_Sixup; public: CDemoPlayer(class CSnapshotDelta *pSnapshotDelta, bool UseVideo); @@ -176,6 +177,8 @@ public: const char *ErrorMessage() const override { return m_aErrorMessage; } int Update(bool RealTime = true); + bool IsSixup() const { return m_Sixup; } + void SetSixup(bool Sixup) { m_Sixup = Sixup; } const CPlaybackInfo *Info() const { return &m_Info; } bool IsPlaying() const override { return m_File != nullptr; } diff --git a/src/engine/shared/network.cpp b/src/engine/shared/network.cpp index 40e2b5dff..d427c153d 100644 --- a/src/engine/shared/network.cpp +++ b/src/engine/shared/network.cpp @@ -298,6 +298,18 @@ int CNetBase::UnpackPacket(unsigned char *pBuffer, int Size, CNetPacketConstruct return -1; } + // set the response token (a bit hacky because this function shouldn't know about control packets) + if(pPacket->m_Flags & NET_PACKETFLAG_CONTROL) + { + if(pPacket->m_DataSize >= 5) // control byte + token + { + if(pPacket->m_aChunkData[0] == NET_CTRLMSG_CONNECT || pPacket->m_aChunkData[0] == NET_CTRLMSG_TOKEN) + { + *pResponseToken = ToSecurityToken(&pPacket->m_aChunkData[1]); + } + } + } + // log the data if(ms_DataLogRecv) { @@ -327,6 +339,19 @@ void CNetBase::SendControlMsg(NETSOCKET Socket, NETADDR *pAddr, int Ack, int Con CNetBase::SendPacket(Socket, pAddr, &Construct, SecurityToken, Sixup, true); } +void CNetBase::SendControlMsgWithToken7(NETSOCKET Socket, NETADDR *pAddr, TOKEN Token, int Ack, int ControlMsg, TOKEN MyToken, bool Extended) +{ + dbg_assert((Token & ~NET_TOKEN_MASK) == 0, "token out of range"); + dbg_assert((MyToken & ~NET_TOKEN_MASK) == 0, "resp token out of range"); + + unsigned char s_aRequestTokenBuf[NET_TOKENREQUEST_DATASIZE]; + s_aRequestTokenBuf[0] = (MyToken >> 24) & 0xff; + s_aRequestTokenBuf[1] = (MyToken >> 16) & 0xff; + s_aRequestTokenBuf[2] = (MyToken >> 8) & 0xff; + s_aRequestTokenBuf[3] = (MyToken)&0xff; + CNetBase::SendControlMsg(Socket, pAddr, 0, ControlMsg, s_aRequestTokenBuf, Extended ? sizeof(s_aRequestTokenBuf) : 4, Token, true); +} + unsigned char *CNetChunkHeader::Pack(unsigned char *pData, int Split) const { pData[0] = ((m_Flags & 3) << 6) | ((m_Size >> Split) & 0x3f); diff --git a/src/engine/shared/network.h b/src/engine/shared/network.h index 2e2ec65c6..7cea700c1 100644 --- a/src/engine/shared/network.h +++ b/src/engine/shared/network.h @@ -65,10 +65,11 @@ enum NET_SEQUENCE_MASK = NET_MAX_SEQUENCE - 1, NET_CONNSTATE_OFFLINE = 0, - NET_CONNSTATE_CONNECT = 1, - NET_CONNSTATE_PENDING = 2, - NET_CONNSTATE_ONLINE = 3, - NET_CONNSTATE_ERROR = 4, + NET_CONNSTATE_TOKEN = 1, + NET_CONNSTATE_CONNECT = 2, + NET_CONNSTATE_PENDING = 3, + NET_CONNSTATE_ONLINE = 4, + NET_CONNSTATE_ERROR = 5, NET_PACKETFLAG_UNUSED = 1 << 0, NET_PACKETFLAG_TOKEN = 1 << 1, @@ -87,6 +88,7 @@ enum NET_CTRLMSG_CONNECTACCEPT = 2, NET_CTRLMSG_ACCEPT = 3, NET_CTRLMSG_CLOSE = 4, + NET_CTRLMSG_TOKEN = 5, NET_CONN_BUFFERSIZE = 1024 * 32, @@ -94,8 +96,17 @@ enum NET_ENUM_TERMINATOR }; +enum +{ + NET_TOKEN_MAX = 0xffffffff, + NET_TOKEN_NONE = NET_TOKEN_MAX, + NET_TOKEN_MASK = NET_TOKEN_MAX, + + NET_TOKENREQUEST_DATASIZE = 512, +}; typedef int SECURITY_TOKEN; +typedef unsigned int TOKEN; SECURITY_TOKEN ToSecurityToken(const unsigned char *pData); void WriteSecurityToken(unsigned char *pData, SECURITY_TOKEN Token); @@ -217,7 +228,10 @@ private: unsigned short m_PeerAck; unsigned m_State; +public: SECURITY_TOKEN m_SecurityToken; + +private: int m_RemoteClosed; bool m_BlockCloseMsg; bool m_UnknownSeq; @@ -239,6 +253,10 @@ private: NETSTATS m_Stats; char m_aPeerAddrStr[NETADDR_MAXSTRSIZE]; + // client 0.7 + static TOKEN GenerateToken7(const NETADDR *pPeerAddr); + class CNetBase *m_pNetBase; + bool IsSixup() { return m_Sixup; } // void ResetStats(); @@ -248,6 +266,7 @@ private: int QueueChunkEx(int Flags, int DataSize, const void *pData, int Sequence); void SendConnect(); void SendControl(int ControlMsg, const void *pExtra, int ExtraSize); + void SendControlWithToken7(int ControlMsg, SECURITY_TOKEN ResponseToken); void ResendChunk(CNetChunkResend *pResend); void Resend(); @@ -255,15 +274,18 @@ public: bool m_TimeoutProtected; bool m_TimeoutSituation; + void SetToken7(TOKEN Token); + void Reset(bool Rejoin = false); void Init(NETSOCKET Socket, bool BlockCloseMsg); int Connect(const NETADDR *pAddr, int NumAddrs); + int Connect7(const NETADDR *pAddr, int NumAddrs); void Disconnect(const char *pReason); int Update(); int Flush(); - int Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_TOKEN SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED); + int Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_TOKEN SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED, SECURITY_TOKEN ResponseToken = NET_SECURITY_TOKEN_UNSUPPORTED); int QueueChunk(int Flags, int DataSize, const void *pData); const char *ErrorString(); @@ -501,9 +523,10 @@ public: // connection state int Disconnect(const char *pReason); int Connect(const NETADDR *pAddr, int NumAddrs); + int Connect7(const NETADDR *pAddr, int NumAddrs); // communication - int Recv(CNetChunk *pChunk); + int Recv(CNetChunk *pChunk, SECURITY_TOKEN *pResponseToken, bool Sixup); int Send(CNetChunk *pChunk); // pumping @@ -541,6 +564,7 @@ public: static int Decompress(const void *pData, int DataSize, void *pOutput, int OutputSize); static void SendControlMsg(NETSOCKET Socket, NETADDR *pAddr, int Ack, int ControlMsg, const void *pExtra, int ExtraSize, SECURITY_TOKEN SecurityToken, bool Sixup = false); + static void SendControlMsgWithToken7(NETSOCKET Socket, NETADDR *pAddr, TOKEN Token, int Ack, int ControlMsg, TOKEN MyToken, bool Extended); static void SendPacketConnless(NETSOCKET Socket, NETADDR *pAddr, const void *pData, int DataSize, bool Extended, unsigned char aExtra[4]); static void SendPacket(NETSOCKET Socket, NETADDR *pAddr, CNetPacketConstruct *pPacket, SECURITY_TOKEN SecurityToken, bool Sixup = false, bool NoCompress = false); diff --git a/src/engine/shared/network_client.cpp b/src/engine/shared/network_client.cpp index 934305908..7d8027215 100644 --- a/src/engine/shared/network_client.cpp +++ b/src/engine/shared/network_client.cpp @@ -56,13 +56,19 @@ int CNetClient::Connect(const NETADDR *pAddr, int NumAddrs) return 0; } +int CNetClient::Connect7(const NETADDR *pAddr, int NumAddrs) +{ + m_Connection.Connect7(pAddr, NumAddrs); + return 0; +} + int CNetClient::ResetErrorString() { m_Connection.ResetErrorString(); return 0; } -int CNetClient::Recv(CNetChunk *pChunk) +int CNetClient::Recv(CNetChunk *pChunk, SECURITY_TOKEN *pResponseToken, bool Sixup) { while(true) { @@ -83,9 +89,12 @@ int CNetClient::Recv(CNetChunk *pChunk) { continue; } + if(Sixup) + Addr.type |= NETTYPE_TW7; - bool Sixup = false; - if(CNetBase::UnpackPacket(pData, Bytes, &m_RecvUnpacker.m_Data, Sixup) == 0) + SECURITY_TOKEN Token; + *pResponseToken = NET_SECURITY_TOKEN_UNKNOWN; + if(CNetBase::UnpackPacket(pData, Bytes, &m_RecvUnpacker.m_Data, Sixup, &Token, pResponseToken) == 0) { if(m_RecvUnpacker.m_Data.m_Flags & NET_PACKETFLAG_CONNLESS) { @@ -103,7 +112,7 @@ int CNetClient::Recv(CNetChunk *pChunk) } else { - if(m_Connection.State() != NET_CONNSTATE_OFFLINE && m_Connection.State() != NET_CONNSTATE_ERROR && m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr)) + if(m_Connection.State() != NET_CONNSTATE_OFFLINE && m_Connection.State() != NET_CONNSTATE_ERROR && m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr, Token, *pResponseToken)) m_RecvUnpacker.Start(&Addr, &m_Connection, 0); } } diff --git a/src/engine/shared/network_conn.cpp b/src/engine/shared/network_conn.cpp index 594152353..2826b1a6e 100644 --- a/src/engine/shared/network_conn.cpp +++ b/src/engine/shared/network_conn.cpp @@ -210,6 +210,51 @@ int CNetConnection::Connect(const NETADDR *pAddr, int NumAddrs) return 0; } +void CNetConnection::SendControlWithToken7(int ControlMsg, SECURITY_TOKEN ResponseToken) +{ + m_LastSendTime = time_get(); + + CNetBase::SendControlMsgWithToken7(m_Socket, &m_PeerAddr, ResponseToken, 0, ControlMsg, m_Token, true); +} + +int CNetConnection::Connect7(const NETADDR *pAddr, int NumAddrs) +{ + if(State() != NET_CONNSTATE_OFFLINE) + return -1; + + // init connection + Reset(); + mem_zero(&m_PeerAddr, sizeof(m_PeerAddr)); + for(int i = 0; i < NumAddrs; i++) + { + m_aConnectAddrs[i] = pAddr[i]; + } + m_LastRecvTime = time_get(); + m_NumConnectAddrs = NumAddrs; + m_PeerAddr = *pAddr; + SetToken7(GenerateToken7(pAddr)); + mem_zero(m_aErrorString, sizeof(m_aErrorString)); + m_State = NET_CONNSTATE_TOKEN; + SendControlWithToken7(NET_CTRLMSG_TOKEN, NET_TOKEN_NONE); + m_Sixup = true; + return 0; +} + +void CNetConnection::SetToken7(TOKEN Token) +{ + if(State() != NET_CONNSTATE_OFFLINE) + return; + + m_Token = Token; +} + +TOKEN CNetConnection::GenerateToken7(const NETADDR *pPeerAddr) +{ + TOKEN Token; + secure_random_fill(&Token, sizeof(Token)); + return Token; +} + void CNetConnection::Disconnect(const char *pReason) { if(State() == NET_CONNSTATE_OFFLINE) @@ -256,7 +301,7 @@ void CNetConnection::DirectInit(const NETADDR &Addr, SECURITY_TOKEN SecurityToke m_Sixup = Sixup; } -int CNetConnection::Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_TOKEN SecurityToken) +int CNetConnection::Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_TOKEN SecurityToken, SECURITY_TOKEN ResponseToken) { // Disregard packets from the wrong address, unless we don't know our peer yet. if(State() != NET_CONNSTATE_OFFLINE && State() != NET_CONNSTATE_CONNECT && *pAddr != m_PeerAddr) @@ -346,63 +391,80 @@ int CNetConnection::Feed(CNetPacketConstruct *pPacket, NETADDR *pAddr, SECURITY_ } else { - if(State() == NET_CONNSTATE_OFFLINE) + if(CtrlMsg == NET_CTRLMSG_TOKEN) { - if(CtrlMsg == NET_CTRLMSG_CONNECT) + if(State() == NET_CONNSTATE_TOKEN) { - if(net_addr_comp_noport(&m_PeerAddr, pAddr) == 0 && time_get() - m_LastUpdateTime < time_freq() * 3) - return 0; - - // send response and init connection - Reset(); - m_State = NET_CONNSTATE_PENDING; - m_PeerAddr = *pAddr; - net_addr_str(pAddr, m_aPeerAddrStr, sizeof(m_aPeerAddrStr), true); - mem_zero(m_aErrorString, sizeof(m_aErrorString)); - m_LastSendTime = Now; m_LastRecvTime = Now; - m_LastUpdateTime = Now; - if(m_SecurityToken == NET_SECURITY_TOKEN_UNKNOWN && pPacket->m_DataSize >= (int)(1 + sizeof(SECURITY_TOKEN_MAGIC) + sizeof(m_SecurityToken)) && !mem_comp(&pPacket->m_aChunkData[1], SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC))) - { - m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; - if(g_Config.m_Debug) - dbg_msg("security", "generated token %d", m_SecurityToken); - } - else - { - if(g_Config.m_Debug) - dbg_msg("security", "token not supported by client (packet size %d)", pPacket->m_DataSize); - m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; - } - SendControl(NET_CTRLMSG_CONNECTACCEPT, SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC)); - if(g_Config.m_Debug) - dbg_msg("connection", "got connection, sending connect+accept"); + m_State = NET_CONNSTATE_CONNECT; + m_SecurityToken = ResponseToken; + SendControlWithToken7(NET_CTRLMSG_CONNECT, m_SecurityToken); + dbg_msg("connection", "got token, replying, token=%x mytoken=%x", m_SecurityToken, m_Token); } + else if(g_Config.m_Debug) + dbg_msg("connection", "got token, token=%x", ResponseToken); } - else if(State() == NET_CONNSTATE_CONNECT) + else { - // connection made - if(CtrlMsg == NET_CTRLMSG_CONNECTACCEPT) + if(State() == NET_CONNSTATE_OFFLINE) { - m_PeerAddr = *pAddr; - net_addr_str(pAddr, m_aPeerAddrStr, sizeof(m_aPeerAddrStr), true); - if(m_SecurityToken == NET_SECURITY_TOKEN_UNKNOWN && pPacket->m_DataSize >= (int)(1 + sizeof(SECURITY_TOKEN_MAGIC) + sizeof(m_SecurityToken)) && !mem_comp(&pPacket->m_aChunkData[1], SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC))) + if(CtrlMsg == NET_CTRLMSG_CONNECT) { - m_SecurityToken = ToSecurityToken(&pPacket->m_aChunkData[1 + sizeof(SECURITY_TOKEN_MAGIC)]); + if(net_addr_comp_noport(&m_PeerAddr, pAddr) == 0 && time_get() - m_LastUpdateTime < time_freq() * 3) + return 0; + + // send response and init connection + Reset(); + m_State = NET_CONNSTATE_PENDING; + m_PeerAddr = *pAddr; + net_addr_str(pAddr, m_aPeerAddrStr, sizeof(m_aPeerAddrStr), true); + mem_zero(m_aErrorString, sizeof(m_aErrorString)); + m_LastSendTime = Now; + m_LastRecvTime = Now; + m_LastUpdateTime = Now; + if(m_SecurityToken == NET_SECURITY_TOKEN_UNKNOWN && pPacket->m_DataSize >= (int)(1 + sizeof(SECURITY_TOKEN_MAGIC) + sizeof(m_SecurityToken)) && !mem_comp(&pPacket->m_aChunkData[1], SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC))) + { + m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; + if(g_Config.m_Debug) + dbg_msg("security", "generated token %d", m_SecurityToken); + } + else + { + if(g_Config.m_Debug) + dbg_msg("security", "token not supported by client (packet size %d)", pPacket->m_DataSize); + m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; + } + SendControl(NET_CTRLMSG_CONNECTACCEPT, SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC)); if(g_Config.m_Debug) - dbg_msg("security", "got token %d", m_SecurityToken); + dbg_msg("connection", "got connection, sending connect+accept"); } - else + } + else if(State() == NET_CONNSTATE_CONNECT) + { + // connection made + if(CtrlMsg == NET_CTRLMSG_CONNECTACCEPT) { - m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; + m_PeerAddr = *pAddr; + net_addr_str(pAddr, m_aPeerAddrStr, sizeof(m_aPeerAddrStr), true); + if(m_SecurityToken == NET_SECURITY_TOKEN_UNKNOWN && pPacket->m_DataSize >= (int)(1 + sizeof(SECURITY_TOKEN_MAGIC) + sizeof(m_SecurityToken)) && !mem_comp(&pPacket->m_aChunkData[1], SECURITY_TOKEN_MAGIC, sizeof(SECURITY_TOKEN_MAGIC))) + { + m_SecurityToken = ToSecurityToken(&pPacket->m_aChunkData[1 + sizeof(SECURITY_TOKEN_MAGIC)]); + if(g_Config.m_Debug) + dbg_msg("security", "got token %d", m_SecurityToken); + } + else if(!IsSixup()) + { + m_SecurityToken = NET_SECURITY_TOKEN_UNSUPPORTED; + if(g_Config.m_Debug) + dbg_msg("security", "token not supported by server"); + } + if(!IsSixup()) + SendControl(NET_CTRLMSG_ACCEPT, 0, 0); + m_LastRecvTime = Now; + m_State = NET_CONNSTATE_ONLINE; if(g_Config.m_Debug) - dbg_msg("security", "token not supported by server"); + dbg_msg("connection", "got connect+accept, sending accept. connection online"); } - m_LastRecvTime = Now; - SendControl(NET_CTRLMSG_ACCEPT, 0, 0); - m_State = NET_CONNSTATE_ONLINE; - if(g_Config.m_Debug) - dbg_msg("connection", "got connect+accept, sending accept. connection online"); } } } diff --git a/src/engine/shared/network_server.cpp b/src/engine/shared/network_server.cpp index 682d2351d..e72bc2000 100644 --- a/src/engine/shared/network_server.cpp +++ b/src/engine/shared/network_server.cpp @@ -640,7 +640,6 @@ int CNetServer::Recv(CNetChunk *pChunk, SECURITY_TOKEN *pResponseToken) SECURITY_TOKEN Token; bool Sixup = false; - *pResponseToken = NET_SECURITY_TOKEN_UNKNOWN; if(CNetBase::UnpackPacket(pData, Bytes, &m_RecvUnpacker.m_Data, Sixup, &Token, pResponseToken) == 0) { if(m_RecvUnpacker.m_Data.m_Flags & NET_PACKETFLAG_CONNLESS) @@ -685,7 +684,7 @@ int CNetServer::Recv(CNetChunk *pChunk, SECURITY_TOKEN *pResponseToken) if(m_RecvUnpacker.m_Data.m_Flags & NET_PACKETFLAG_CONTROL) OnConnCtrlMsg(Addr, Slot, m_RecvUnpacker.m_Data.m_aChunkData[0], m_RecvUnpacker.m_Data); - if(m_aSlots[Slot].m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr, Token)) + if(m_aSlots[Slot].m_Connection.Feed(&m_RecvUnpacker.m_Data, &Addr, Token, *pResponseToken)) { if(m_RecvUnpacker.m_Data.m_DataSize) m_RecvUnpacker.Start(&Addr, &m_aSlots[Slot].m_Connection, Slot); diff --git a/src/engine/shared/protocol.h b/src/engine/shared/protocol.h index 6c00a487b..e0f807b10 100644 --- a/src/engine/shared/protocol.h +++ b/src/engine/shared/protocol.h @@ -4,6 +4,7 @@ #define ENGINE_SHARED_PROTOCOL_H #include +#include /* Connection diagram - How the initialization works. diff --git a/src/engine/shared/protocol7.h b/src/engine/shared/protocol7.h index c8524ef78..73bbe92fd 100644 --- a/src/engine/shared/protocol7.h +++ b/src/engine/shared/protocol7.h @@ -3,6 +3,8 @@ #ifndef ENGINE_SHARED_PROTOCOL7_H #define ENGINE_SHARED_PROTOCOL7_H +#include + namespace protocol7 { enum @@ -52,6 +54,16 @@ enum NETMSG_MAPLIST_ENTRY_REM, }; -} +enum +{ + MAX_NAME_LENGTH = 16, + MAX_NAME_ARRAY_SIZE = MAX_NAME_LENGTH * UTF8_BYTE_LENGTH + 1, + MAX_CLAN_LENGTH = 12, + MAX_CLAN_ARRAY_SIZE = MAX_CLAN_LENGTH * UTF8_BYTE_LENGTH + 1, + MAX_SKIN_LENGTH = 24, + MAX_SKIN_ARRAY_SIZE = MAX_SKIN_LENGTH * UTF8_BYTE_LENGTH + 1, +}; + +} // namespace protocol7 #endif diff --git a/src/engine/shared/protocolglue.cpp b/src/engine/shared/protocolglue.cpp new file mode 100644 index 000000000..e6bc23f95 --- /dev/null +++ b/src/engine/shared/protocolglue.cpp @@ -0,0 +1,75 @@ +#include +#include +#include + +#include "protocolglue.h" + +int PlayerFlags_SevenToSix(int Flags) +{ + int Six = 0; + if(Flags & protocol7::PLAYERFLAG_CHATTING) + Six |= PLAYERFLAG_CHATTING; + if(Flags & protocol7::PLAYERFLAG_SCOREBOARD) + Six |= PLAYERFLAG_SCOREBOARD; + if(Flags & protocol7::PLAYERFLAG_AIM) + Six |= PLAYERFLAG_AIM; + return Six; +} + +int PlayerFlags_SixToSeven(int Flags) +{ + int Seven = 0; + if(Flags & PLAYERFLAG_CHATTING) + Seven |= protocol7::PLAYERFLAG_CHATTING; + if(Flags & PLAYERFLAG_SCOREBOARD) + Seven |= protocol7::PLAYERFLAG_SCOREBOARD; + if(Flags & PLAYERFLAG_AIM) + Seven |= protocol7::PLAYERFLAG_AIM; + return Seven; +} + +void PickupType_SevenToSix(int Type7, int &Type6, int &SubType6) +{ + SubType6 = 0; + Type6 = POWERUP_WEAPON; + switch(Type7) + { + case protocol7::PICKUP_HEALTH: + case protocol7::PICKUP_ARMOR: + Type6 = Type7; + break; + case protocol7::PICKUP_GRENADE: + SubType6 = WEAPON_GRENADE; + break; + case protocol7::PICKUP_SHOTGUN: + SubType6 = WEAPON_SHOTGUN; + break; + case protocol7::PICKUP_LASER: + SubType6 = WEAPON_LASER; + break; + case protocol7::PICKUP_GUN: + SubType6 = WEAPON_GUN; + break; + case protocol7::PICKUP_HAMMER: + SubType6 = WEAPON_HAMMER; + break; + case protocol7::PICKUP_NINJA: + SubType6 = WEAPON_NINJA; + Type6 = POWERUP_NINJA; + break; + default: + // dbg_msg("sixup", "ERROR: failed to translate weapon=%d to 0.6", Type7); + break; + } +} + +int PickupType_SixToSeven(int Type6, int SubType6) +{ + if(Type6 == POWERUP_WEAPON) + return SubType6 == WEAPON_SHOTGUN ? protocol7::PICKUP_SHOTGUN : SubType6 == WEAPON_GRENADE ? protocol7::PICKUP_GRENADE : protocol7::PICKUP_LASER; + else if(Type6 == POWERUP_NINJA) + return protocol7::PICKUP_NINJA; + else if(Type6 == POWERUP_ARMOR) + return protocol7::PICKUP_ARMOR; + return 0; +} diff --git a/src/engine/shared/protocolglue.h b/src/engine/shared/protocolglue.h new file mode 100644 index 000000000..8fa2f13dd --- /dev/null +++ b/src/engine/shared/protocolglue.h @@ -0,0 +1,9 @@ +#ifndef ENGINE_SHARED_PROTOCOLGLUE_H +#define ENGINE_SHARED_PROTOCOLGLUE_H + +int PlayerFlags_SevenToSix(int Flags); +int PlayerFlags_SixToSeven(int Flags); +void PickupType_SevenToSix(int Type7, int &Type6, int &SubType6); +int PickupType_SixToSeven(int Type6, int SubType6); + +#endif diff --git a/src/engine/shared/sixup_translate_snapshot.cpp b/src/engine/shared/sixup_translate_snapshot.cpp new file mode 100644 index 000000000..d90925d1c --- /dev/null +++ b/src/engine/shared/sixup_translate_snapshot.cpp @@ -0,0 +1,516 @@ +#include "compression.h" +#include "snapshot.h" +#include "uuid_manager.h" + +#include +#include + +#include +#include + +#include +#include + +#include +#include +#include + +#include + +int CSnapshot::TranslateSevenToSix( + CSnapshot *pSixSnapDest, + CTranslationContext &TranslationContext, + float LocalTime, + int GameTick, + int Conn, + bool Dummy, + protocol7::CNetObjHandler *pNetObjHandler, + CNetObjHandler *pNetObjHandler6) +{ + CSnapshotBuilder Builder; + Builder.Init(); + + for(auto &PlayerInfosRace : TranslationContext.m_apPlayerInfosRace) + PlayerInfosRace = nullptr; + + int SpectatorId = -3; + + for(int i = 0; i < m_NumItems; i++) + { + const CSnapshotItem *pItem7 = (CSnapshotItem *)GetItem(i); + const int Size = GetItemSize(i); + const int ItemType = GetItemType(i); + + // ddnet ex snap items + if((ItemType > __NETOBJTYPE_UUID_HELPER && ItemType < OFFSET_NETMSGTYPE_UUID) || pItem7->Type() == NETOBJTYPE_EX) + { + CUnpacker Unpacker; + Unpacker.Reset(pItem7->Data(), Size); + + void *pRawObj = pNetObjHandler6->SecureUnpackObj(ItemType, &Unpacker); + if(!pRawObj) + { + if(ItemType != UUID_UNKNOWN) + dbg_msg("sixup", "dropped weird ddnet ex object '%s' (%d), failed on '%s'", pNetObjHandler6->GetObjName(ItemType), ItemType, pNetObjHandler6->FailedObjOn()); + continue; + } + const int ItemSize = pNetObjHandler6->GetUnpackedObjSize(ItemType); + + void *pObj = Builder.NewItem(pItem7->Type(), pItem7->Id(), ItemSize); + if(!pObj) + return -17; + + mem_copy(pObj, pRawObj, ItemSize); + continue; + } + + if(pNetObjHandler->ValidateObj(pItem7->Type(), pItem7->Data(), Size) != 0) + { + if(pItem7->Type() > 0 && pItem7->Type() < OFFSET_UUID_TYPE) + { + dbg_msg( + "sixup", + "invalidated index=%d type=%d (%s) size=%d id=%d", + i, + pItem7->Type(), + pNetObjHandler->GetObjName(pItem7->Type()), + Size, + pItem7->Id()); + } + InvalidateItem(i); + } + + if(pItem7->Type() == protocol7::NETOBJTYPE_PLAYERINFORACE) + { + const protocol7::CNetObj_PlayerInfoRace *pInfo = (const protocol7::CNetObj_PlayerInfoRace *)pItem7->Data(); + int ClientId = pItem7->Id(); + if(ClientId < MAX_CLIENTS && TranslationContext.m_aClients[ClientId].m_Active) + { + TranslationContext.m_apPlayerInfosRace[ClientId] = pInfo; + } + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_SPECTATORINFO) + { + const protocol7::CNetObj_SpectatorInfo *pSpec7 = (const protocol7::CNetObj_SpectatorInfo *)pItem7->Data(); + SpectatorId = pSpec7->m_SpectatorId; + if(pSpec7->m_SpecMode == protocol7::SPEC_FREEVIEW) + SpectatorId = SPEC_FREEVIEW; + } + } + + // hack to put game info in the snap + // even though in 0.7 we get it as a game message + // this message has to be on the top + // the client looks at the items in order and needs the gameinfo at the top + // otherwise it will not render skins with team colors + if(TranslationContext.m_ShouldSendGameInfo) + { + void *pObj = Builder.NewItem(NETOBJTYPE_GAMEINFO, 0, sizeof(CNetObj_GameInfo)); + if(!pObj) + return -1; + + int GameStateFlagsSix = 0; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_GAMEOVER) + GameStateFlagsSix |= GAMESTATEFLAG_GAMEOVER; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_SUDDENDEATH) + GameStateFlagsSix |= GAMESTATEFLAG_SUDDENDEATH; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_PAUSED) + GameStateFlagsSix |= GAMESTATEFLAG_PAUSED; + + /* + This is a 0.7 only flag that we just ignore for now + + GAMESTATEFLAG_ROUNDOVER + */ + + CNetObj_GameInfo Info6 = {}; + Info6.m_GameFlags = TranslationContext.m_GameFlags; + Info6.m_GameStateFlags = GameStateFlagsSix; + Info6.m_RoundStartTick = TranslationContext.m_GameStartTick7; + Info6.m_WarmupTimer = 0; // removed in 0.7 + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_WARMUP) + Info6.m_WarmupTimer = TranslationContext.m_GameStateEndTick7 - GameTick; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_STARTCOUNTDOWN) + Info6.m_WarmupTimer = TranslationContext.m_GameStateEndTick7 - GameTick; + + // hack to port 0.7 race timer to ddnet warmup gametimer hack + int TimerClientId = clamp(TranslationContext.m_aLocalClientId[Conn], 0, (int)MAX_CLIENTS); + if(SpectatorId >= 0) + TimerClientId = SpectatorId; + const protocol7::CNetObj_PlayerInfoRace *pRaceInfo = TranslationContext.m_apPlayerInfosRace[TimerClientId]; + if(pRaceInfo) + { + Info6.m_WarmupTimer = -pRaceInfo->m_RaceStartTick; + Info6.m_GameStateFlags |= GAMESTATEFLAG_RACETIME; + } + + Info6.m_ScoreLimit = TranslationContext.m_ScoreLimit; + Info6.m_TimeLimit = TranslationContext.m_TimeLimit; + Info6.m_RoundNum = TranslationContext.m_MatchNum; + Info6.m_RoundCurrent = TranslationContext.m_MatchCurrent; + + mem_copy(pObj, &Info6, sizeof(CNetObj_GameInfo)); + } + + for(int i = 0; i < MAX_CLIENTS; i++) + { + const CTranslationContext::CClientData &Client = TranslationContext.m_aClients[i]; + if(!Client.m_Active) + continue; + + void *pObj = Builder.NewItem(NETOBJTYPE_CLIENTINFO, i, sizeof(CNetObj_ClientInfo)); + if(!pObj) + return -2; + + CNetObj_ClientInfo Info6 = {}; + StrToInts(&Info6.m_Name0, 4, Client.m_aName); + StrToInts(&Info6.m_Clan0, 3, Client.m_aClan); + Info6.m_Country = Client.m_Country; + StrToInts(&Info6.m_Skin0, 6, Client.m_aSkinName); + Info6.m_UseCustomColor = Client.m_UseCustomColor; + Info6.m_ColorBody = Client.m_ColorBody; + Info6.m_ColorFeet = Client.m_ColorFeet; + mem_copy(pObj, &Info6, sizeof(CNetObj_ClientInfo)); + } + + bool NewGameData = false; + + for(int i = 0; i < m_NumItems; i++) + { + const CSnapshotItem *pItem7 = GetItem(i); + const int Size = GetItemSize(i); + // the first few items are a full match + // no translation needed + if(pItem7->Type() == protocol7::NETOBJTYPE_PROJECTILE || + pItem7->Type() == protocol7::NETOBJTYPE_LASER || + pItem7->Type() == protocol7::NETOBJTYPE_FLAG) + { + void *pObj = Builder.NewItem(pItem7->Type(), pItem7->Id(), Size); + if(!pObj) + return -4; + + mem_copy(pObj, pItem7->Data(), Size); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_PICKUP) + { + void *pObj = Builder.NewItem(NETOBJTYPE_PICKUP, pItem7->Id(), sizeof(CNetObj_Pickup)); + if(!pObj) + return -5; + + const protocol7::CNetObj_Pickup *pPickup7 = (const protocol7::CNetObj_Pickup *)pItem7->Data(); + CNetObj_Pickup Pickup6 = {}; + Pickup6.m_X = pPickup7->m_X; + Pickup6.m_Y = pPickup7->m_Y; + PickupType_SevenToSix(pPickup7->m_Type, Pickup6.m_Type, Pickup6.m_Subtype); + + mem_copy(pObj, &Pickup6, sizeof(CNetObj_Pickup)); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATA) + { + const protocol7::CNetObj_GameData *pGameData = (const protocol7::CNetObj_GameData *)pItem7->Data(); + TranslationContext.m_GameStateFlags7 = pGameData->m_GameStateFlags; + TranslationContext.m_GameStartTick7 = pGameData->m_GameStartTick; + TranslationContext.m_GameStateEndTick7 = pGameData->m_GameStateEndTick; + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATATEAM) + { + // 0.7 added GameDataTeam and GameDataFlag + // both items merged together have all fields of the 0.6 GameData + // so if we get either one of them we store the details in the translation context + // and build one GameData snap item after this loop + const protocol7::CNetObj_GameDataTeam *pTeam7 = (const protocol7::CNetObj_GameDataTeam *)pItem7->Data(); + + TranslationContext.m_TeamscoreRed = pTeam7->m_TeamscoreRed; + TranslationContext.m_TeamscoreBlue = pTeam7->m_TeamscoreBlue; + NewGameData = true; + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATAFLAG) + { + const protocol7::CNetObj_GameDataFlag *pFlag7 = (const protocol7::CNetObj_GameDataFlag *)pItem7->Data(); + + TranslationContext.m_FlagCarrierRed = pFlag7->m_FlagCarrierRed; + TranslationContext.m_FlagCarrierBlue = pFlag7->m_FlagCarrierBlue; + NewGameData = true; + + // used for blinking the flags in the hud + // but that already works the 0.6 way + // and is covered by the NETOBJTYPE_GAMEDATA translation + // pFlag7->m_FlagDropTickRed; + // pFlag7->m_FlagDropTickBlue; + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_CHARACTER) + { + void *pObj = Builder.NewItem(NETOBJTYPE_CHARACTER, pItem7->Id(), sizeof(CNetObj_Character)); + if(!pObj) + return -6; + + const protocol7::CNetObj_Character *pChar7 = (const protocol7::CNetObj_Character *)pItem7->Data(); + + CNetObj_Character Char6 = {}; + // character core is unchanged + mem_copy(&Char6, pItem7->Data(), sizeof(CNetObj_CharacterCore)); + + Char6.m_PlayerFlags = 0; + if(pItem7->Id() >= 0 && pItem7->Id() < MAX_CLIENTS) + Char6.m_PlayerFlags = PlayerFlags_SevenToSix(TranslationContext.m_aClients[pItem7->Id()].m_PlayerFlags7); + Char6.m_Health = pChar7->m_Health; + Char6.m_Armor = pChar7->m_Armor; + Char6.m_AmmoCount = pChar7->m_AmmoCount; + Char6.m_Weapon = pChar7->m_Weapon; + Char6.m_Emote = pChar7->m_Emote; + Char6.m_AttackTick = pChar7->m_AttackTick; + + if(TranslationContext.m_aLocalClientId[Conn] != pItem7->Id()) + { + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_GROUND_JUMP) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_PLAYER_JUMP; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_ATTACH_PLAYER) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_HOOK_ATTACH_PLAYER; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_ATTACH_GROUND) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_HOOK_ATTACH_GROUND; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_HIT_NOHOOK) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_HOOK_NOATTACH; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + } + + mem_copy(pObj, &Char6, sizeof(CNetObj_Character)); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_PLAYERINFO) + { + void *pObj = Builder.NewItem(NETOBJTYPE_PLAYERINFO, pItem7->Id(), sizeof(CNetObj_PlayerInfo)); + if(!pObj) + return -8; + + const protocol7::CNetObj_PlayerInfo *pInfo7 = (const protocol7::CNetObj_PlayerInfo *)pItem7->Data(); + CNetObj_PlayerInfo Info6 = {}; + Info6.m_Local = TranslationContext.m_aLocalClientId[Conn] == pItem7->Id(); + Info6.m_ClientId = pItem7->Id(); + Info6.m_Team = 0; + if(pItem7->Id() >= 0 && pItem7->Id() < MAX_CLIENTS) + { + Info6.m_Team = TranslationContext.m_aClients[pItem7->Id()].m_Team; + TranslationContext.m_aClients[pItem7->Id()].m_PlayerFlags7 = pInfo7->m_PlayerFlags; + } + Info6.m_Score = pInfo7->m_Score; + Info6.m_Latency = pInfo7->m_Latency; + mem_copy(pObj, &Info6, sizeof(CNetObj_PlayerInfo)); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_SPECTATORINFO) + { + void *pObj = Builder.NewItem(NETOBJTYPE_SPECTATORINFO, pItem7->Id(), sizeof(CNetObj_SpectatorInfo)); + if(!pObj) + return -9; + + const protocol7::CNetObj_SpectatorInfo *pSpec7 = (const protocol7::CNetObj_SpectatorInfo *)pItem7->Data(); + CNetObj_SpectatorInfo Spec6 = {}; + Spec6.m_SpectatorId = pSpec7->m_SpectatorId; + if(pSpec7->m_SpecMode == protocol7::SPEC_FREEVIEW) + Spec6.m_SpectatorId = SPEC_FREEVIEW; + Spec6.m_X = pSpec7->m_X; + Spec6.m_Y = pSpec7->m_Y; + mem_copy(pObj, &Spec6, sizeof(CNetObj_SpectatorInfo)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_EXPLOSION) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_EXPLOSION, pItem7->Id(), sizeof(CNetEvent_Explosion)); + if(!pEvent) + return -10; + + const protocol7::CNetEvent_Explosion *pExplosion7 = (const protocol7::CNetEvent_Explosion *)pItem7->Data(); + CNetEvent_Explosion Explosion6 = {}; + Explosion6.m_X = pExplosion7->m_X; + Explosion6.m_Y = pExplosion7->m_Y; + mem_copy(pEvent, &Explosion6, sizeof(CNetEvent_Explosion)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_SPAWN) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SPAWN, pItem7->Id(), sizeof(CNetEvent_Spawn)); + if(!pEvent) + return -11; + + const protocol7::CNetEvent_Spawn *pSpawn7 = (const protocol7::CNetEvent_Spawn *)pItem7->Data(); + CNetEvent_Spawn Spawn6 = {}; + Spawn6.m_X = pSpawn7->m_X; + Spawn6.m_Y = pSpawn7->m_Y; + mem_copy(pEvent, &Spawn6, sizeof(CNetEvent_Spawn)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_HAMMERHIT) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_HAMMERHIT, pItem7->Id(), sizeof(CNetEvent_HammerHit)); + if(!pEvent) + return -12; + + const protocol7::CNetEvent_HammerHit *pHammerHit7 = (const protocol7::CNetEvent_HammerHit *)pItem7->Data(); + CNetEvent_HammerHit HammerHit6 = {}; + HammerHit6.m_X = pHammerHit7->m_X; + HammerHit6.m_Y = pHammerHit7->m_Y; + mem_copy(pEvent, &HammerHit6, sizeof(CNetEvent_HammerHit)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_DEATH) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_DEATH, pItem7->Id(), sizeof(CNetEvent_Death)); + if(!pEvent) + return -13; + + const protocol7::CNetEvent_Death *pDeath7 = (const protocol7::CNetEvent_Death *)pItem7->Data(); + CNetEvent_Death Death6 = {}; + Death6.m_X = pDeath7->m_X; + Death6.m_Y = pDeath7->m_Y; + Death6.m_ClientId = pDeath7->m_ClientId; + mem_copy(pEvent, &Death6, sizeof(CNetEvent_Death)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_SOUNDWORLD) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -14; + + const protocol7::CNetEvent_SoundWorld *pSoundWorld7 = (const protocol7::CNetEvent_SoundWorld *)pItem7->Data(); + CNetEvent_SoundWorld SoundWorld6 = {}; + SoundWorld6.m_X = pSoundWorld7->m_X; + SoundWorld6.m_Y = pSoundWorld7->m_Y; + SoundWorld6.m_SoundId = pSoundWorld7->m_SoundId; + mem_copy(pEvent, &SoundWorld6, sizeof(CNetEvent_SoundWorld)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_DAMAGE) + { + // 0.7 introduced amount for damage indicators + // so for one 0.7 item we might create multiple 0.6 ones + const protocol7::CNetEvent_Damage *pDmg7 = (const protocol7::CNetEvent_Damage *)pItem7->Data(); + + int Amount = pDmg7->m_HealthAmount + pDmg7->m_ArmorAmount; + if(Amount < 1) + continue; + + int ClientId = pDmg7->m_ClientId; + TranslationContext.m_aDamageTaken[ClientId]++; + + float Angle; + // create healthmod indicator + if(LocalTime < TranslationContext.m_aDamageTakenTick[ClientId] + 0.5f) + { + // make sure that the damage indicators don't group together + Angle = TranslationContext.m_aDamageTaken[ClientId] * 0.25f; + } + else + { + TranslationContext.m_aDamageTaken[ClientId] = 0; + Angle = 0; + } + + TranslationContext.m_aDamageTakenTick[ClientId] = LocalTime; + + float a = 3 * pi / 2 + Angle; + float s = a - pi / 3; + float e = a + pi / 3; + for(int k = 0; k < Amount; k++) + { + // pItem7->Id() is reused that is technically wrong + // but the client implementation does not look at the ids + // and renders the damage indicators just fine + void *pEvent = Builder.NewItem(NETEVENTTYPE_DAMAGEIND, pItem7->Id(), sizeof(CNetEvent_DamageInd)); + if(!pEvent) + return -16; + + CNetEvent_DamageInd Dmg6 = {}; + Dmg6.m_X = pDmg7->m_X; + Dmg6.m_Y = pDmg7->m_Y; + float f = mix(s, e, float(k + 1) / float(Amount + 2)); + Dmg6.m_Angle = (int)(f * 256.0f); + mem_copy(pEvent, &Dmg6, sizeof(CNetEvent_DamageInd)); + } + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_DE_CLIENTINFO) + { + const protocol7::CNetObj_De_ClientInfo *pInfo = (const protocol7::CNetObj_De_ClientInfo *)pItem7->Data(); + + const int ClientId = pItem7->Id(); + + if(ClientId < 0 || ClientId >= MAX_CLIENTS) + { + dbg_msg("sixup", "De_ClientInfo got invalid ClientId: %d", ClientId); + return -17; + } + + if(pInfo->m_Local) + { + TranslationContext.m_aLocalClientId[Conn] = ClientId; + } + CTranslationContext::CClientData &Client = TranslationContext.m_aClients[ClientId]; + Client.m_Active = true; + Client.m_Team = pInfo->m_Team; + IntsToStr(pInfo->m_aName, 4, Client.m_aName, std::size(Client.m_aName)); + IntsToStr(pInfo->m_aClan, 3, Client.m_aClan, std::size(Client.m_aClan)); + Client.m_Country = pInfo->m_Country; + + // TODO: get this method into scope + // ApplySkin7InfoFromGameMsg(pInfo, ClientId); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_DE_GAMEINFO) + { + const protocol7::CNetObj_De_GameInfo *pInfo = (const protocol7::CNetObj_De_GameInfo *)pItem7->Data(); + + TranslationContext.m_GameFlags = pInfo->m_GameFlags; + TranslationContext.m_ScoreLimit = pInfo->m_ScoreLimit; + TranslationContext.m_TimeLimit = pInfo->m_TimeLimit; + TranslationContext.m_MatchNum = pInfo->m_MatchNum; + TranslationContext.m_MatchCurrent = pInfo->m_MatchCurrent; + TranslationContext.m_ShouldSendGameInfo = true; + } + } + + if(NewGameData) + { + void *pObj = Builder.NewItem(NETOBJTYPE_GAMEDATA, 0, sizeof(CNetObj_GameData)); + if(!pObj) + return -17; + + CNetObj_GameData GameData = {}; + GameData.m_TeamscoreRed = TranslationContext.m_TeamscoreRed; + GameData.m_TeamscoreBlue = TranslationContext.m_TeamscoreBlue; + GameData.m_FlagCarrierRed = TranslationContext.m_FlagCarrierRed; + GameData.m_FlagCarrierBlue = TranslationContext.m_FlagCarrierBlue; + mem_copy(pObj, &GameData, sizeof(CNetObj_GameData)); + } + + return Builder.Finish(pSixSnapDest); +} diff --git a/src/engine/shared/snapshot.cpp b/src/engine/shared/snapshot.cpp index 1201d94d5..76e920d6a 100644 --- a/src/engine/shared/snapshot.cpp +++ b/src/engine/shared/snapshot.cpp @@ -10,6 +10,7 @@ #include #include +#include #include // CSnapshot @@ -65,6 +66,11 @@ int CSnapshot::GetItemIndex(int Key) const return -1; } +void CSnapshot::InvalidateItem(int Index) +{ + ((CSnapshotItem *)(DataStart() + Offsets()[Index]))->Invalidate(); +} + const void *CSnapshot::FindItem(int Type, int Id) const { int InternalType = Type; @@ -243,6 +249,7 @@ void CSnapshotDelta::UndiffItem(const int *pPast, const int *pDiff, int *pOut, i CSnapshotDelta::CSnapshotDelta() { mem_zero(m_aItemSizes, sizeof(m_aItemSizes)); + mem_zero(m_aItemSizes7, sizeof(m_aItemSizes7)); mem_zero(m_aSnapshotDataRate, sizeof(m_aSnapshotDataRate)); mem_zero(m_aSnapshotDataUpdates, sizeof(m_aSnapshotDataUpdates)); mem_zero(&m_Empty, sizeof(m_Empty)); @@ -251,6 +258,7 @@ CSnapshotDelta::CSnapshotDelta() CSnapshotDelta::CSnapshotDelta(const CSnapshotDelta &Old) { mem_copy(m_aItemSizes, Old.m_aItemSizes, sizeof(m_aItemSizes)); + mem_copy(m_aItemSizes7, Old.m_aItemSizes7, sizeof(m_aItemSizes7)); mem_copy(m_aSnapshotDataRate, Old.m_aSnapshotDataRate, sizeof(m_aSnapshotDataRate)); mem_copy(m_aSnapshotDataUpdates, Old.m_aSnapshotDataUpdates, sizeof(m_aSnapshotDataUpdates)); mem_zero(&m_Empty, sizeof(m_Empty)); @@ -263,6 +271,13 @@ void CSnapshotDelta::SetStaticsize(int ItemType, size_t Size) m_aItemSizes[ItemType] = Size; } +void CSnapshotDelta::SetStaticsize7(int ItemType, size_t Size) +{ + dbg_assert(ItemType >= 0 && ItemType < MAX_NETOBJSIZES, "ItemType invalid"); + dbg_assert(Size <= (size_t)std::numeric_limits::max(), "Size invalid"); + m_aItemSizes7[ItemType] = Size; +} + const CSnapshotDelta::CData *CSnapshotDelta::EmptyDelta() const { return &m_Empty; @@ -484,7 +499,7 @@ int CSnapshotDelta::DebugDumpDelta(const void *pSrcData, int DataSize) return 0; } -int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const void *pSrcData, int DataSize) +int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const void *pSrcData, int DataSize, bool Sixup) { CData *pDelta = (CData *)pSrcData; int *pData = (int *)pDelta->m_aData; @@ -542,8 +557,9 @@ int CSnapshotDelta::UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const vo return -203; int ItemSize; - if(Type < MAX_NETOBJSIZES && m_aItemSizes[Type]) - ItemSize = m_aItemSizes[Type]; + const short *pItemSizes = Sixup ? m_aItemSizes7 : m_aItemSizes; + if(Type < MAX_NETOBJSIZES && pItemSizes[Type]) + ItemSize = pItemSizes[Type]; else { if(pData + 1 > pEnd) diff --git a/src/engine/shared/snapshot.h b/src/engine/shared/snapshot.h index c745dc089..4060b9938 100644 --- a/src/engine/shared/snapshot.h +++ b/src/engine/shared/snapshot.h @@ -6,6 +6,9 @@ #include #include +#include +#include + // CSnapshot class CSnapshotItem @@ -21,6 +24,7 @@ public: int Type() const { return m_TypeAndId >> 16; } int Id() const { return m_TypeAndId & 0xffff; } int Key() const { return m_TypeAndId; } + void Invalidate() { m_TypeAndId = -1; } }; class CSnapshot @@ -52,12 +56,22 @@ public: const CSnapshotItem *GetItem(int Index) const; int GetItemSize(int Index) const; int GetItemIndex(int Key) const; + void InvalidateItem(int Index); int GetItemType(int Index) const; int GetExternalItemType(int InternalType) const; const void *FindItem(int Type, int Id) const; unsigned Crc() const; void DebugDump() const; + int TranslateSevenToSix( + CSnapshot *pSixSnapDest, + class CTranslationContext &TranslationContext, + float LocalTime, + int GameTick, + int Conn, + bool Dummy, + protocol7::CNetObjHandler *pNetObjHandler, + CNetObjHandler *pNetObjHandler6); bool IsValid(size_t ActualSize) const; static const CSnapshot *EmptySnapshot() { return &ms_EmptySnapshot; } @@ -83,6 +97,7 @@ private: MAX_NETOBJSIZES = 64 }; short m_aItemSizes[MAX_NETOBJSIZES]; + short m_aItemSizes7[MAX_NETOBJSIZES]; int m_aSnapshotDataRate[CSnapshot::MAX_TYPE + 1]; int m_aSnapshotDataUpdates[CSnapshot::MAX_TYPE + 1]; CData m_Empty; @@ -96,9 +111,10 @@ public: int GetDataRate(int Index) const { return m_aSnapshotDataRate[Index]; } int GetDataUpdates(int Index) const { return m_aSnapshotDataUpdates[Index]; } void SetStaticsize(int ItemType, size_t Size); + void SetStaticsize7(int ItemType, size_t Size); const CData *EmptyDelta() const; int CreateDelta(const CSnapshot *pFrom, const CSnapshot *pTo, void *pDstData); - int UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const void *pSrcData, int DataSize); + int UnpackDelta(const CSnapshot *pFrom, CSnapshot *pTo, const void *pSrcData, int DataSize, bool Sixup); int DebugDumpDelta(const void *pSrcData, int DataSize); }; diff --git a/src/engine/shared/translation_context.h b/src/engine/shared/translation_context.h new file mode 100644 index 000000000..60cae63c9 --- /dev/null +++ b/src/engine/shared/translation_context.h @@ -0,0 +1,120 @@ +#ifndef ENGINE_SHARED_TRANSLATION_CONTEXT_H +#define ENGINE_SHARED_TRANSLATION_CONTEXT_H + +#include +#include + +#include + +class CTranslationContext +{ +public: + CTranslationContext() + { + Reset(); + } + + void Reset() + { + for(int &LocalClientId : m_aLocalClientId) + LocalClientId = -1; + m_ShouldSendGameInfo = false; + m_GameFlags = 0; + m_ScoreLimit = 0; + m_TimeLimit = 0; + m_MatchNum = 0; + m_MatchCurrent = 0; + mem_zero(m_aDamageTaken, sizeof(m_aDamageTaken)); + mem_zero(m_aDamageTakenTick, sizeof(m_aDamageTakenTick)); + m_FlagCarrierBlue = 0; + m_FlagCarrierRed = 0; + m_TeamscoreRed = 0; + m_TeamscoreBlue = 0; + } + + // this class is not used + // it could be used in the in game menu + // to grey out buttons and similar + // + // but that can not be done without mixing it + // into the 0.6 code so it is out of scope for ddnet + class CServerSettings + { + public: + bool m_KickVote = false; + int m_KickMin = 0; + bool m_SpecVote = false; + bool m_TeamLock = false; + bool m_TeamBalance = false; + int m_PlayerSlots = 0; + } m_ServerSettings; + + class CClientData + { + public: + CClientData() + { + Reset(); + } + + void Reset() + { + m_Active = false; + + m_UseCustomColor = 0; + m_ColorBody = 0; + m_ColorFeet = 0; + + m_aName[0] = '\0'; + m_aClan[0] = '\0'; + m_Country = 0; + m_aSkinName[0] = '\0'; + m_SkinColor = 0; + m_Team = 0; + m_PlayerFlags7 = 0; + } + + bool m_Active; + + int m_UseCustomColor; + int m_ColorBody; + int m_ColorFeet; + + char m_aName[MAX_NAME_LENGTH]; + char m_aClan[MAX_CLAN_LENGTH]; + int m_Country; + char m_aSkinName[protocol7::MAX_SKIN_LENGTH]; + int m_SkinColor; + int m_Team; + int m_PlayerFlags7; + }; + + const protocol7::CNetObj_PlayerInfoRace *m_apPlayerInfosRace[MAX_CLIENTS]; + CClientData m_aClients[MAX_CLIENTS]; + int m_aDamageTaken[MAX_CLIENTS]; + float m_aDamageTakenTick[MAX_CLIENTS]; + + int m_aLocalClientId[NUM_DUMMIES]; + + bool m_ShouldSendGameInfo; + int m_GameStateFlags7; + int m_GameFlags; + int m_ScoreLimit; + int m_TimeLimit; + int m_MatchNum; + int m_MatchCurrent; + + int m_MapdownloadTotalsize; + int m_MapDownloadChunkSize; + int m_MapDownloadChunksPerRequest; + + int m_FlagCarrierBlue; + int m_FlagCarrierRed; + int m_TeamscoreRed; + int m_TeamscoreBlue; + + int m_GameStartTick7; + int m_GameStateEndTick7; +}; + +#endif diff --git a/src/game/client/components/broadcast.cpp b/src/game/client/components/broadcast.cpp index 5683705da..87a1c0f60 100644 --- a/src/game/client/components/broadcast.cpp +++ b/src/game/client/components/broadcast.cpp @@ -74,20 +74,20 @@ void CBroadcast::OnMessage(int MsgType, void *pRawMsg) { if(MsgType == NETMSGTYPE_SV_BROADCAST) { - OnBroadcastMessage((CNetMsg_Sv_Broadcast *)pRawMsg); + const CNetMsg_Sv_Broadcast *pMsg = (CNetMsg_Sv_Broadcast *)pRawMsg; + DoBroadcast(pMsg->m_pMessage); } } -void CBroadcast::OnBroadcastMessage(const CNetMsg_Sv_Broadcast *pMsg) +void CBroadcast::DoBroadcast(const char *pText) { - str_copy(m_aBroadcastText, pMsg->m_pMessage); + str_copy(m_aBroadcastText, pText); m_BroadcastTick = Client()->GameTick(g_Config.m_ClDummy) + Client()->GameTickSpeed() * 10; m_BroadcastRenderOffset = -1.0f; TextRender()->DeleteTextContainer(m_TextContainerIndex); if(g_Config.m_ClPrintBroadcasts) { - const char *pText = m_aBroadcastText; char aLine[sizeof(m_aBroadcastText)]; while((pText = str_next_token(pText, "\n", aLine, sizeof(aLine)))) { diff --git a/src/game/client/components/broadcast.h b/src/game/client/components/broadcast.h index e8bfaa41f..11942764d 100644 --- a/src/game/client/components/broadcast.h +++ b/src/game/client/components/broadcast.h @@ -25,6 +25,8 @@ public: virtual void OnWindowResize() override; virtual void OnRender() override; virtual void OnMessage(int MsgType, void *pRawMsg) override; + + void DoBroadcast(const char *pText); }; #endif diff --git a/src/game/client/components/chat.cpp b/src/game/client/components/chat.cpp index 35a7257ad..d4a25f2c4 100644 --- a/src/game/client/components/chat.cpp +++ b/src/game/client/components/chat.cpp @@ -1318,6 +1318,16 @@ void CChat::SendChat(int Team, const char *pLine) m_LastChatSend = time(); + if(m_pClient->Client()->IsSixup()) + { + protocol7::CNetMsg_Cl_Say Msg7; + Msg7.m_Mode = Team == 1 ? protocol7::CHAT_TEAM : protocol7::CHAT_ALL; + Msg7.m_Target = -1; + Msg7.m_pMessage = pLine; + Client()->SendPackMsgActive(&Msg7, MSGFLAG_VITAL, true); + return; + } + // send chat message CNetMsg_Cl_Say Msg; Msg.m_Team = Team; diff --git a/src/game/client/components/hud.cpp b/src/game/client/components/hud.cpp index 517482188..9ebb7afca 100644 --- a/src/game/client/components/hud.cpp +++ b/src/game/client/components/hud.cpp @@ -329,7 +329,9 @@ void CHud::RenderScoreHud() { if(apPlayerInfo[t]) { - if(m_pClient->m_GameInfo.m_TimeScore) + if(Client()->IsSixup() && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & protocol7::GAMEFLAG_RACE) + str_time((int64_t)absolute(apPlayerInfo[t]->m_Score) / 10, TIME_MINS_CENTISECS, aScore[t], sizeof(aScore[t])); + else if(m_pClient->m_GameInfo.m_TimeScore) { if(apPlayerInfo[t]->m_Score != -9999) str_time((int64_t)absolute(apPlayerInfo[t]->m_Score) * 100, TIME_HOURS, aScore[t], sizeof(aScore[t])); diff --git a/src/game/client/components/mapimages.cpp b/src/game/client/components/mapimages.cpp index 84568e793..10e703f3d 100644 --- a/src/game/client/components/mapimages.cpp +++ b/src/game/client/components/mapimages.cpp @@ -132,7 +132,16 @@ void CMapImages::OnMapLoadImpl(class CLayers *pLayers, IMap *pMap) if(pImg->m_External) { char aPath[IO_MAX_PATH_LENGTH]; - str_format(aPath, sizeof(aPath), "mapres/%s.png", pName); + bool Translated = false; + if(Client()->IsSixup()) + { + Translated = + !str_comp(pName, "grass_doodads") || + !str_comp(pName, "grass_main") || + !str_comp(pName, "winter_main") || + !str_comp(pName, "generic_unhookable"); + } + str_format(aPath, sizeof(aPath), "mapres/%s%s.png", pName, Translated ? "_0.7" : ""); m_aTextures[i] = Graphics()->LoadTexture(aPath, IStorage::TYPE_ALL, LoadFlag); } else diff --git a/src/game/client/components/menus.h b/src/game/client/components/menus.h index 3e0b8cf73..1064aca7c 100644 --- a/src/game/client/components/menus.h +++ b/src/game/client/components/menus.h @@ -23,10 +23,13 @@ #include #include +#include #include #include #include +#include + struct CServerProcess { PROCESS m_Process; @@ -244,6 +247,11 @@ protected: bool m_NeedSendDummyinfo; int m_SettingPlayerPage; + // 0.7 skins + int m_TeePartSelected = protocol7::SKINPART_BODY; + const CSkins7::CSkin *m_pSelectedSkin = nullptr; + CLineInputBuffered m_SkinNameInput; + // for map download popup int64_t m_DownloadLastCheckTime; int m_DownloadLastCheckSize; @@ -586,6 +594,11 @@ protected: void RenderSettingsPlayer(CUIRect MainView); void RenderSettingsDummyPlayer(CUIRect MainView); void RenderSettingsTee(CUIRect MainView); + void RenderSettingsTee7(CUIRect MainView); + void RenderSettingsTeeCustom7(CUIRect MainView); + void RenderSettingsTeeBasic7(CUIRect MainView); + void RenderSkinSelection7(CUIRect MainView); + void RenderSkinPartSelection7(CUIRect MainView); void RenderSettingsControls(CUIRect MainView); void ResetSettingsControls(); void RenderSettingsGraphics(CUIRect MainView); diff --git a/src/game/client/components/menus_settings.cpp b/src/game/client/components/menus_settings.cpp index 8172b46a5..b22d50ec2 100644 --- a/src/game/client/components/menus_settings.cpp +++ b/src/game/client/components/menus_settings.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -2021,7 +2022,7 @@ void CMenus::RenderSettings(CUIRect MainView) Localize("Language"), Localize("General"), Localize("Player"), - Localize("Tee"), + Client()->IsSixup() ? "Tee 0.7" : Localize("Tee"), Localize("Appearance"), Localize("Controls"), Localize("Graphics"), @@ -2056,7 +2057,10 @@ void CMenus::RenderSettings(CUIRect MainView) else if(g_Config.m_UiSettingsPage == SETTINGS_TEE) { GameClient()->m_MenuBackground.ChangePosition(CMenuBackground::POS_SETTINGS_TEE); - RenderSettingsTee(MainView); + if(Client()->IsSixup()) + RenderSettingsTee7(MainView); + else + RenderSettingsTee(MainView); } else if(g_Config.m_UiSettingsPage == SETTINGS_APPEARANCE) { diff --git a/src/game/client/components/menus_settings7.cpp b/src/game/client/components/menus_settings7.cpp new file mode 100644 index 000000000..d077e3aff --- /dev/null +++ b/src/game/client/components/menus_settings7.cpp @@ -0,0 +1,548 @@ +/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ +/* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "menus.h" +#include "skins7.h" + +#include + +void CMenus::RenderSettingsTee7(CUIRect MainView) +{ + static bool s_CustomSkinMenu = false; + // static int s_PlayerCountry = 0; + // static char s_aPlayerName[64] = {0}; + // static char s_aPlayerClan[64] = {0}; + + // if(m_pClient->m_IdentityState < 0) + // { + // s_PlayerCountry = Config()->m_PlayerCountry; + // str_copy(s_aPlayerName, Config()->m_PlayerName, sizeof(s_aPlayerName)); + // str_copy(s_aPlayerClan, Config()->m_PlayerClan, sizeof(s_aPlayerClan)); + // m_pClient->m_IdentityState = 0; + // } + + CUIRect Label, TopView, BottomView, Left, Right; + + // cut view + MainView.HSplitBottom(40.0f, &MainView, &BottomView); + BottomView.HSplitTop(20.f, 0, &BottomView); + + CUIRect QuickSearch, Buttons; + CUIRect ButtonLeft, ButtonMiddle, ButtonRight; + + BottomView.VSplitMid(&QuickSearch, &Buttons); + + const float ButtonSize = Buttons.w / 3; + Buttons.VSplitLeft(ButtonSize, &ButtonLeft, &Buttons); + Buttons.VSplitLeft(ButtonSize, &ButtonMiddle, &Buttons); + Buttons.VSplitLeft(ButtonSize, &ButtonRight, &Buttons); + + // HotCuiRects(MainView, BottomView, QuickSearch, ButtonLeft, ButtonMiddle, ButtonRight); + + // render skin preview background + const float SpacingH = 2.0f; + const float SpacingW = 3.0f; + const float ButtonHeight = 20.0f; + const float SkinHeight = 50.0f; + const float BackgroundHeight = (ButtonHeight + SpacingH) + SkinHeight * 2; + const vec2 MousePosition = vec2(Ui()->MouseX(), Ui()->MouseY()); + + MainView.HSplitTop(20.0f, 0, &MainView); + MainView.HSplitTop(BackgroundHeight, &TopView, &MainView); + TopView.VSplitMid(&Left, &Right, 3.0f); + Left.Draw(vec4(0.0f, 0.0f, 0.0f, 0.25f), 15, 5.0F); + Right.Draw(vec4(0.0f, 0.0f, 0.0f, 0.25f), 15, 5.0F); + + Left.HSplitTop(ButtonHeight, &Label, &Left); + Ui()->DoLabel(&Label, Localize("Tee"), ButtonHeight * CUi::ms_FontmodHeight * 0.8f, TEXTALIGN_MC); + + // Preview + { + CUIRect Top, Bottom, TeeLeft, TeeRight; + + Left.HSplitTop(SpacingH, 0, &Left); + Left.HSplitTop(SkinHeight * 2, &Top, &Left); + + // split the menu in 2 parts + Top.HSplitMid(&Top, &Bottom, SpacingH); + + // handle left + + // validate skin parts for solo mode + CTeeRenderInfo OwnSkinInfo; + OwnSkinInfo.m_Size = 50.0f; + + char aSkinParts[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_ARRAY_SIZE]; + char *apSkinPartsPtr[protocol7::NUM_SKINPARTS]; + int aUCCVars[protocol7::NUM_SKINPARTS]; + int aColorVars[protocol7::NUM_SKINPARTS]; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + str_copy(aSkinParts[Part], CSkins7::ms_apSkinVariables[(int)m_Dummy][Part], protocol7::MAX_SKIN_ARRAY_SIZE); + apSkinPartsPtr[Part] = aSkinParts[Part]; + aUCCVars[Part] = *CSkins7::ms_apUCCVariables[(int)m_Dummy][Part]; + aColorVars[Part] = *CSkins7::ms_apColorVariables[(int)m_Dummy][Part]; + } + + m_pClient->m_Skins7.ValidateSkinParts(apSkinPartsPtr, aUCCVars, aColorVars, 0); + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + int SkinPart = m_pClient->m_Skins7.FindSkinPart(Part, apSkinPartsPtr[Part], false); + const CSkins7::CSkinPart *pSkinPart = m_pClient->m_Skins7.GetSkinPart(Part, SkinPart); + if(aUCCVars[Part]) + { + OwnSkinInfo.m_Sixup.m_aTextures[Part] = pSkinPart->m_ColorTexture; + OwnSkinInfo.m_Sixup.m_aColors[Part] = m_pClient->m_Skins7.GetColor(aColorVars[Part], Part == protocol7::SKINPART_MARKING); + } + else + { + OwnSkinInfo.m_Sixup.m_aTextures[Part] = pSkinPart->m_OrgTexture; + OwnSkinInfo.m_Sixup.m_aColors[Part] = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + } + + // draw preview + Top.Draw(vec4(0.0f, 0.0f, 0.0f, 0.25f), 15, 5.0F); + + Top.VSplitLeft(Top.w / 3.0f + SpacingW / 2.0f, &Label, &Top); + Label.y += 17.0f; + Ui()->DoLabel(&Label, Localize("Normal:"), ButtonHeight * CUi::ms_FontmodHeight * 0.8f, TEXTALIGN_CENTER); + + Top.Draw(vec4(0.0f, 0.0f, 0.0f, 0.25f), 15, 5.0F); + + { + // interactive tee: tee looking towards cursor, and it is happy when you touch it + vec2 TeePosition = vec2(Top.x + Top.w / 2.0f, Top.y + Top.h / 2.0f + 6.0f); + vec2 DeltaPosition = MousePosition - TeePosition; + float Distance = length(DeltaPosition); + vec2 TeeDirection = Distance < 20.0f ? normalize(vec2(DeltaPosition.x, maximum(DeltaPosition.y, 0.5f))) : normalize(DeltaPosition); + int TeeEmote = Distance < 20.0f ? EMOTE_HAPPY : EMOTE_NORMAL; + RenderTools()->RenderTee(CAnimState::GetIdle(), &OwnSkinInfo, TeeEmote, TeeDirection, TeePosition); + if(Distance < 20.0f && Ui()->MouseButtonClicked(0)) + m_pClient->m_Sounds.Play(CSounds::CHN_GUI, SOUND_PLAYER_SPAWN, 0); + } + + // handle right (team skins) + + // validate skin parts for team game mode + CTeeRenderInfo TeamSkinInfo = OwnSkinInfo; + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + str_copy(aSkinParts[Part], CSkins7::ms_apSkinVariables[(int)m_Dummy][Part], protocol7::MAX_SKIN_ARRAY_SIZE); + apSkinPartsPtr[Part] = aSkinParts[Part]; + aUCCVars[Part] = *CSkins7::ms_apUCCVariables[(int)m_Dummy][Part]; + aColorVars[Part] = *CSkins7::ms_apColorVariables[(int)m_Dummy][Part]; + } + + m_pClient->m_Skins7.ValidateSkinParts(apSkinPartsPtr, aUCCVars, aColorVars, GAMEFLAG_TEAMS); + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + int SkinPart = m_pClient->m_Skins7.FindSkinPart(Part, apSkinPartsPtr[Part], false); + const CSkins7::CSkinPart *pSkinPart = m_pClient->m_Skins7.GetSkinPart(Part, SkinPart); + if(aUCCVars[Part]) + { + TeamSkinInfo.m_Sixup.m_aTextures[Part] = pSkinPart->m_ColorTexture; + TeamSkinInfo.m_Sixup.m_aColors[Part] = m_pClient->m_Skins7.GetColor(aColorVars[Part], Part == protocol7::SKINPART_MARKING); + } + else + { + TeamSkinInfo.m_Sixup.m_aTextures[Part] = pSkinPart->m_OrgTexture; + TeamSkinInfo.m_Sixup.m_aColors[Part] = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + } + + // draw preview + Bottom.Draw(vec4(0.0f, 0.0f, 0.0f, 0.25f), 15, 5.0F); + + Bottom.VSplitLeft(Bottom.w / 3.0f + SpacingW / 2.0f, &Label, &Bottom); + Label.y += 17.0f; + Ui()->DoLabel(&Label, Localize("Team:"), ButtonHeight * CUi::ms_FontmodHeight * 0.8f, TEXTALIGN_CENTER); + + Bottom.VSplitMid(&TeeLeft, &TeeRight, SpacingW); + + TeeLeft.Draw(vec4(0.0f, 0.0f, 0.0f, 0.25f), 15, 5.0F); + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + ColorRGBA TeamColor = m_pClient->m_Skins7.GetTeamColor(aUCCVars[Part], aColorVars[Part], TEAM_RED, Part); + TeamSkinInfo.m_Sixup.m_aColors[Part] = TeamColor; + } + RenderTools()->RenderTee(CAnimState::GetIdle(), &TeamSkinInfo, 0, vec2(1, 0), vec2(TeeLeft.x + TeeLeft.w / 2.0f, TeeLeft.y + TeeLeft.h / 2.0f + 6.0f)); + + TeeRight.Draw(vec4(0.0f, 0.0f, 0.0f, 0.25f), 15, 5.0F); + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + ColorRGBA TeamColor = m_pClient->m_Skins7.GetTeamColor(aUCCVars[Part], aColorVars[Part], TEAM_BLUE, Part); + TeamSkinInfo.m_Sixup.m_aColors[Part] = TeamColor; + } + RenderTools()->RenderTee(CAnimState::GetIdle(), &TeamSkinInfo, 0, vec2(-1, 0), vec2(TeeRight.x + TeeRight.w / 2.0f, TeeRight.y + TeeRight.h / 2.0f + 6.0f)); + } + + Right.HSplitTop(ButtonHeight, &Label, &Right); + Ui()->DoLabel(&Label, Localize("Settings"), ButtonHeight * CUi::ms_FontmodHeight * 0.8f, TEXTALIGN_MC); + + // Settings + { + CUIRect Top, Bottom, Dummy, DummyLabel; + Right.HSplitTop(SpacingH, 0, &Right); + Right.HSplitMid(&Top, &Bottom, SpacingH); + + Right.HSplitTop(20.0f, &Dummy, &Right); + Dummy.HSplitTop(20.0f, &DummyLabel, &Dummy); + + if(DoButton_CheckBox(&m_Dummy, Localize("Dummy settings"), m_Dummy, &DummyLabel)) + { + m_Dummy ^= 1; + } + GameClient()->m_Tooltips.DoToolTip(&m_Dummy, &DummyLabel, Localize("Toggle to edit your dummy settings")); + } + + MainView.HSplitTop(10.0f, 0, &MainView); + + if(s_CustomSkinMenu) + RenderSettingsTeeCustom7(MainView); + else + RenderSettingsTeeBasic7(MainView); + + // bottom buttons + if(s_CustomSkinMenu) + { + static CButtonContainer s_RandomizeSkinButton; + if(DoButton_Menu(&s_RandomizeSkinButton, Localize("Randomize"), 0, &ButtonMiddle)) + { + m_pClient->m_Skins7.RandomizeSkin(m_Dummy); + Config()->m_ClPlayer7Skin[0] = 0; + SetNeedSendInfo(); + } + } + else if(m_pSelectedSkin && (m_pSelectedSkin->m_Flags & CSkins7::SKINFLAG_STANDARD) == 0) + { + static CButtonContainer s_CustomSkinDeleteButton; + if(DoButton_Menu(&s_CustomSkinDeleteButton, Localize("Delete"), 0, &ButtonMiddle)) + { + // char aBuf[128]; + // str_format(aBuf, sizeof(aBuf), Localize("Are you sure that you want to delete the skin '%s'?"), m_pSelectedSkin->m_aName); + // PopupConfirm(Localize("Delete skin"), aBuf, Localize("Yes"), Localize("No"), &CMenus::PopupConfirmDeleteSkin); + } + } + + static CButtonContainer s_CustomSwitchButton; + if(DoButton_Menu(&s_CustomSwitchButton, s_CustomSkinMenu ? Localize("Basic") : Localize("Custom"), 0, &ButtonRight)) + { + s_CustomSkinMenu = !s_CustomSkinMenu; + if(s_CustomSkinMenu && m_pSelectedSkin) + { + if(m_pSelectedSkin->m_Flags & CSkins7::SKINFLAG_STANDARD) + { + m_SkinNameInput.Set("copy_"); + m_SkinNameInput.Append(m_pSelectedSkin->m_aName); + } + else + m_SkinNameInput.Set(m_pSelectedSkin->m_aName); + } + } + + // Quick search + { + TextRender()->SetFontPreset(EFontPreset::ICON_FONT); + TextRender()->SetRenderFlags(ETextRenderFlags::TEXT_RENDER_FLAG_ONLY_ADVANCE_WIDTH | ETextRenderFlags::TEXT_RENDER_FLAG_NO_X_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_Y_BEARING | ETextRenderFlags::TEXT_RENDER_FLAG_NO_PIXEL_ALIGMENT | ETextRenderFlags::TEXT_RENDER_FLAG_NO_OVERSIZE); + Ui()->DoLabel(&QuickSearch, FontIcons::FONT_ICON_MAGNIFYING_GLASS, 14.0f, TEXTALIGN_ML); + float SearchWidth = TextRender()->TextWidth(14.0f, FontIcons::FONT_ICON_MAGNIFYING_GLASS, -1, -1.0f); + TextRender()->SetRenderFlags(0); + TextRender()->SetFontPreset(EFontPreset::DEFAULT_FONT); + QuickSearch.VSplitLeft(SearchWidth + 5.0f, nullptr, &QuickSearch); + static CLineInput s_SkinFilterInput(g_Config.m_ClSkinFilterString, sizeof(g_Config.m_ClSkinFilterString)); + if(Input()->KeyPress(KEY_F) && Input()->ModifierIsPressed()) + { + Ui()->SetActiveItem(&s_SkinFilterInput); + s_SkinFilterInput.SelectAll(); + } + s_SkinFilterInput.SetEmptyText(Localize("Search")); + if(Ui()->DoClearableEditBox(&s_SkinFilterInput, &QuickSearch, 14.0f)) + m_SkinListNeedsUpdate = true; + } +} + +void CMenus::RenderSettingsTeeBasic7(CUIRect MainView) +{ + RenderSkinSelection7(MainView); // yes thats all here ^^ +} + +void CMenus::RenderSettingsTeeCustom7(CUIRect MainView) +{ + CUIRect Label, Patterns, Button, Left, Right, Picker, Palette; + + // render skin preview background + float SpacingH = 2.0f; + float SpacingW = 3.0f; + float ButtonHeight = 20.0f; + + MainView.Draw(vec4(0.0f, 0.0f, 0.0f, 0.25f), 15, 5.0F); + + MainView.HSplitTop(ButtonHeight, &Label, &MainView); + Ui()->DoLabel(&Label, Localize("Customize"), ButtonHeight * CUi::ms_FontmodHeight * 0.8f, TEXTALIGN_MC); + + // skin part selection + MainView.HSplitTop(SpacingH, 0, &MainView); + MainView.HSplitTop(ButtonHeight, &Patterns, &MainView); + Patterns.Draw(vec4(0.0f, 0.0f, 0.0f, 0.25f), 15, 5.0F); + + float ButtonWidth = (Patterns.w / 6.0f) - (SpacingW * 5.0) / 6.0f; + + static CButtonContainer s_aPatternButtons[protocol7::NUM_SKINPARTS]; + for(int i = 0; i < protocol7::NUM_SKINPARTS; i++) + { + Patterns.VSplitLeft(ButtonWidth, &Button, &Patterns); + if(DoButton_MenuTab(&s_aPatternButtons[i], Localize(CSkins7::ms_apSkinPartNames[i], "skins"), m_TeePartSelected == i, &Button, IGraphics::CORNER_ALL)) + { + m_TeePartSelected = i; + } + Patterns.VSplitLeft(SpacingW, 0, &Patterns); + } + + MainView.HSplitTop(SpacingH, 0, &MainView); + MainView.VSplitMid(&Left, &Right, SpacingW); + + // part selection + RenderSkinPartSelection7(Left); + + // use custom color checkbox + Right.HSplitTop(ButtonHeight, &Button, &Right); + Right.HSplitBottom(45.0f, &Picker, &Palette); + static CButtonContainer s_ColorPicker; + DoLine_ColorPicker( + &s_ColorPicker, + 25.0f, // LineSize + 13.0f, // LabelSize + 5.0f, // BottomMargin + &Right, + Localize("Custom colors"), + (unsigned int *)CSkins7::ms_apColorVariables[(int)m_Dummy][m_TeePartSelected], + ColorRGBA(1.0f, 1.0f, 0.5f), // DefaultColor + true, // CheckBoxSpacing + CSkins7::ms_apUCCVariables[(int)m_Dummy][m_TeePartSelected], // CheckBoxValue + m_TeePartSelected == protocol7::SKINPART_MARKING); // use alpha + static int s_OldColor = *CSkins7::ms_apColorVariables[(int)m_Dummy][m_TeePartSelected]; + int NewColor = *CSkins7::ms_apColorVariables[(int)m_Dummy][m_TeePartSelected]; + if(s_OldColor != NewColor) + { + s_OldColor = NewColor; + SetNeedSendInfo(); + } +} + +void CMenus::RenderSkinSelection7(CUIRect MainView) +{ + static float s_LastSelectionTime = -10.0f; + static std::vector s_vpSkinList; + static CListBox s_ListBox; + static int s_SkinCount = 0; + if(m_SkinListNeedsUpdate || m_pClient->m_Skins7.Num() != s_SkinCount) + { + s_vpSkinList.clear(); + s_SkinCount = m_pClient->m_Skins7.Num(); + for(int i = 0; i < s_SkinCount; ++i) + { + const CSkins7::CSkin *s = m_pClient->m_Skins7.Get(i); + + if(g_Config.m_ClSkinFilterString[0] != '\0' && !str_utf8_find_nocase(s->m_aName, g_Config.m_ClSkinFilterString)) + continue; + + // no special skins + if((s->m_Flags & CSkins7::SKINFLAG_SPECIAL) == 0) + { + s_vpSkinList.emplace_back(s); + } + } + m_SkinListNeedsUpdate = false; + } + + m_pSelectedSkin = 0; + int s_OldSelected = -1; + s_ListBox.DoStart(60.0f, s_vpSkinList.size(), 10, 1, s_OldSelected, &MainView); + + for(int i = 0; i < (int)s_vpSkinList.size(); ++i) + { + const CSkins7::CSkin *s = s_vpSkinList[i]; + if(s == 0) + continue; + if(!str_comp(s->m_aName, Config()->m_ClPlayer7Skin)) + { + m_pSelectedSkin = s; + s_OldSelected = i; + } + + CListboxItem Item = s_ListBox.DoNextItem(&s_vpSkinList[i], s_OldSelected == i); + if(Item.m_Visible) + { + CTeeRenderInfo Info; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + if(s->m_aUseCustomColors[Part]) + { + Info.m_Sixup.m_aTextures[Part] = s->m_apParts[Part]->m_ColorTexture; + Info.m_Sixup.m_aColors[Part] = m_pClient->m_Skins7.GetColor(s->m_aPartColors[Part], Part == protocol7::SKINPART_MARKING); + } + else + { + Info.m_Sixup.m_aTextures[Part] = s->m_apParts[Part]->m_OrgTexture; + Info.m_Sixup.m_aColors[Part] = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + } + + Info.m_Size = 50.0f; + Item.m_Rect.HSplitTop(5.0f, 0, &Item.m_Rect); // some margin from the top + + { + // interactive tee: tee is happy to be selected + int TeeEmote = (Item.m_Selected && s_LastSelectionTime + 0.75f > Client()->LocalTime()) ? EMOTE_HAPPY : EMOTE_NORMAL; + RenderTools()->RenderTee(CAnimState::GetIdle(), &Info, TeeEmote, vec2(1.0f, 0.0f), vec2(Item.m_Rect.x + Item.m_Rect.w / 2, Item.m_Rect.y + Item.m_Rect.h / 2)); + } + + CUIRect Label; + Item.m_Rect.Margin(5.0f, &Item.m_Rect); + Item.m_Rect.HSplitBottom(10.0f, &Item.m_Rect, &Label); + + Ui()->DoLabel(&Label, s->m_aName, 10.0f, TEXTALIGN_MC); + } + } + + const int NewSelected = s_ListBox.DoEnd(); + if(NewSelected != -1 && NewSelected != s_OldSelected) + { + s_LastSelectionTime = Client()->LocalTime(); + m_pSelectedSkin = s_vpSkinList[NewSelected]; + str_copy(Config()->m_ClPlayer7Skin, m_pSelectedSkin->m_aName); + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + str_copy(CSkins7::ms_apSkinVariables[(int)m_Dummy][Part], m_pSelectedSkin->m_apParts[Part]->m_aName, protocol7::MAX_SKIN_ARRAY_SIZE); + *CSkins7::ms_apUCCVariables[(int)m_Dummy][Part] = m_pSelectedSkin->m_aUseCustomColors[Part]; + *CSkins7::ms_apColorVariables[(int)m_Dummy][Part] = m_pSelectedSkin->m_aPartColors[Part]; + } + SetNeedSendInfo(); + } +} + +void CMenus::RenderSkinPartSelection7(CUIRect MainView) +{ + static bool s_InitSkinPartList = true; + static std::vector s_paList[protocol7::NUM_SKINPARTS]; + static CListBox s_ListBox; + if(s_InitSkinPartList) + { + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + s_paList[Part].clear(); + for(int i = 0; i < m_pClient->m_Skins7.NumSkinPart(Part); ++i) + { + const CSkins7::CSkinPart *s = m_pClient->m_Skins7.GetSkinPart(Part, i); + // no special skins + if((s->m_Flags & CSkins7::SKINFLAG_SPECIAL) == 0) + { + s_paList[Part].emplace_back(s); + } + } + } + s_InitSkinPartList = false; + } + + static int s_OldSelected = -1; + s_ListBox.DoBegin(&MainView); + s_ListBox.DoStart(60.0f, s_paList[m_TeePartSelected].size(), 5, 1, s_OldSelected); + + for(int i = 0; i < (int)s_paList[m_TeePartSelected].size(); ++i) + { + const CSkins7::CSkinPart *s = s_paList[m_TeePartSelected][i]; + if(s == 0) + continue; + if(!str_comp(s->m_aName, CSkins7::ms_apSkinVariables[(int)m_Dummy][m_TeePartSelected])) + s_OldSelected = i; + + CListboxItem Item = s_ListBox.DoNextItem(&s_paList[m_TeePartSelected][i], s_OldSelected == i); + if(Item.m_Visible) + { + CTeeRenderInfo Info; + for(int j = 0; j < protocol7::NUM_SKINPARTS; j++) + { + int SkinPart = m_pClient->m_Skins7.FindSkinPart(j, CSkins7::ms_apSkinVariables[(int)m_Dummy][j], false); + const CSkins7::CSkinPart *pSkinPart = m_pClient->m_Skins7.GetSkinPart(j, SkinPart); + if(*CSkins7::ms_apUCCVariables[(int)m_Dummy][j]) + { + if(m_TeePartSelected == j) + Info.m_Sixup.m_aTextures[j] = s->m_ColorTexture; + else + Info.m_Sixup.m_aTextures[j] = pSkinPart->m_ColorTexture; + Info.m_Sixup.m_aColors[j] = m_pClient->m_Skins7.GetColor(*CSkins7::ms_apColorVariables[(int)m_Dummy][j], j == protocol7::SKINPART_MARKING); + } + else + { + if(m_TeePartSelected == j) + Info.m_Sixup.m_aTextures[j] = s->m_OrgTexture; + else + Info.m_Sixup.m_aTextures[j] = pSkinPart->m_OrgTexture; + Info.m_Sixup.m_aColors[j] = vec4(1.0f, 1.0f, 1.0f, 1.0f); + } + } + Info.m_Size = 50.0f; + Item.m_Rect.HSplitTop(5.0f, 0, &Item.m_Rect); // some margin from the top + const vec2 TeePos(Item.m_Rect.x + Item.m_Rect.w / 2, Item.m_Rect.y + Item.m_Rect.h / 2); + + if(m_TeePartSelected == protocol7::SKINPART_HANDS) + { + // RenderTools()->RenderTeeHand(&Info, TeePos, vec2(1.0f, 0.0f), -pi*0.5f, vec2(18, 0)); + } + int TeePartEmote = EMOTE_NORMAL; + if(m_TeePartSelected == protocol7::SKINPART_EYES) + { + float LocalTime = Client()->LocalTime(); + TeePartEmote = (int)(LocalTime * 0.5f) % NUM_EMOTES; + } + RenderTools()->RenderTee(CAnimState::GetIdle(), &Info, TeePartEmote, vec2(1.0f, 0.0f), TeePos); + + CUIRect Label; + Item.m_Rect.Margin(5.0f, &Item.m_Rect); + Item.m_Rect.HSplitBottom(10.0f, &Item.m_Rect, &Label); + + Ui()->DoLabel(&Label, s->m_aName, 10.0f, TEXTALIGN_MC); + } + } + + const int NewSelected = s_ListBox.DoEnd(); + if(NewSelected != -1 && NewSelected != s_OldSelected) + { + const CSkins7::CSkinPart *s = s_paList[m_TeePartSelected][NewSelected]; + str_copy(CSkins7::ms_apSkinVariables[(int)m_Dummy][m_TeePartSelected], s->m_aName, protocol7::MAX_SKIN_ARRAY_SIZE); + Config()->m_ClPlayer7Skin[0] = 0; + SetNeedSendInfo(); + } + s_OldSelected = NewSelected; +} diff --git a/src/game/client/components/players.cpp b/src/game/client/components/players.cpp index be642f967..b8487b259 100644 --- a/src/game/client/components/players.cpp +++ b/src/game/client/components/players.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,54 @@ #include void CPlayers::RenderHand(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha) +{ + if(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_BODY].IsValid()) + RenderHand7(pInfo, CenterPos, Dir, AngleOffset, PostRotOffset, Alpha); + else + RenderHand6(pInfo, CenterPos, Dir, AngleOffset, PostRotOffset, Alpha); +} + +void CPlayers::RenderHand7(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha) +{ + // in-game hand size is 15 when tee size is 64 + float BaseSize = 15.0f * (pInfo->m_Size / 64.0f); + + vec2 HandPos = CenterPos + Dir; + float Angle = angle(Dir); + if(Dir.x < 0) + Angle -= AngleOffset; + else + Angle += AngleOffset; + + vec2 DirX = Dir; + vec2 DirY(-Dir.y, Dir.x); + + if(Dir.x < 0) + DirY = -DirY; + + HandPos += DirX * PostRotOffset.x; + HandPos += DirY * PostRotOffset.y; + + ColorRGBA Color = pInfo->m_Sixup.m_aColors[protocol7::SKINPART_HANDS]; + Color.a = Alpha; + IGraphics::CQuadItem QuadOutline(HandPos.x, HandPos.y, 2 * BaseSize, 2 * BaseSize); + IGraphics::CQuadItem QuadHand = QuadOutline; + + Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_HANDS]); + Graphics()->QuadsBegin(); + Graphics()->SetColor(Color); + Graphics()->QuadsSetRotation(Angle); + + RenderTools()->SelectSprite7(client_data7::SPRITE_TEE_HAND_OUTLINE, 0, 0, 0); + Graphics()->QuadsDraw(&QuadOutline, 1); + RenderTools()->SelectSprite7(client_data7::SPRITE_TEE_HAND, 0, 0, 0); + Graphics()->QuadsDraw(&QuadHand, 1); + + Graphics()->QuadsSetRotation(0); + Graphics()->QuadsEnd(); +} + +void CPlayers::RenderHand6(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha) { vec2 HandPos = CenterPos + Dir; float Angle = angle(Dir); @@ -779,6 +828,8 @@ void CPlayers::OnRender() const auto *pSkin = m_pClient->m_Skins.FindOrNullptr("x_ninja"); if(pSkin != nullptr) { + aRenderInfo[i].m_Sixup.Reset(); + aRenderInfo[i].m_OriginalRenderSkin = pSkin->m_OriginalSkin; aRenderInfo[i].m_ColorableRenderSkin = pSkin->m_ColorableSkin; aRenderInfo[i].m_BloodColor = pSkin->m_BloodColor; diff --git a/src/game/client/components/players.h b/src/game/client/components/players.h index 5c8d668b6..b20df3518 100644 --- a/src/game/client/components/players.h +++ b/src/game/client/components/players.h @@ -11,6 +11,9 @@ class CPlayers : public CComponent { friend class CGhost; + void RenderHand6(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha = 1.0f); + void RenderHand7(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha = 1.0f); + void RenderHand(const CTeeRenderInfo *pInfo, vec2 CenterPos, vec2 Dir, float AngleOffset, vec2 PostRotOffset, float Alpha = 1.0f); void RenderPlayer( const CNetObj_Character *pPrevChar, diff --git a/src/game/client/components/scoreboard.cpp b/src/game/client/components/scoreboard.cpp index a51d56caf..de08db6a8 100644 --- a/src/game/client/components/scoreboard.cpp +++ b/src/game/client/components/scoreboard.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include CScoreboard::CScoreboard() @@ -227,6 +228,8 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, const int NumPlayers = CountEnd - CountStart; const bool LowScoreboardWidth = Scoreboard.w < 700.0f; + bool Race7 = Client()->IsSixup() && m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & protocol7::GAMEFLAG_RACE; + // calculate measurements float LineHeight; float TeeSizeMod; @@ -336,6 +339,11 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, int DDTeam = GameClient()->m_Teams.Team(pInfo->m_ClientId); int NextDDTeam = 0; + bool RenderDead = Client()->m_TranslationContext.m_aClients[pInfo->m_ClientId].m_PlayerFlags7 & protocol7::PLAYERFLAG_DEAD; + + ColorRGBA TextColor = TextRender()->DefaultTextColor(); + TextColor.a = RenderDead ? 0.5f : 1.0f; + TextRender()->TextColor(TextColor); for(int j = i + 1; j < MAX_CLIENTS; j++) { @@ -420,7 +428,21 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, } // score - if(TimeScore) + if(Race7) + { + if(pInfo->m_Score == -1) + { + aBuf[0] = '\0'; + } + else + { + // 0.7 uses milliseconds and ddnets str_time wants centiseconds + // 0.7 servers can also send the amount of precision the client should use + // we ignore that and always show 3 digit precision + str_time((int64_t)absolute(pInfo->m_Score / 10), TIME_MINS_CENTISECS, aBuf, sizeof(aBuf)); + } + } + else if(TimeScore) { if(pInfo->m_Score == -9999) { @@ -453,6 +475,23 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, const CGameClient::CClientData &ClientData = GameClient()->m_aClients[pInfo->m_ClientId]; // skin + if(RenderDead) + { + Graphics()->BlendNormal(); + Graphics()->TextureSet(client_data7::g_pData->m_aImages[client_data7::IMAGE_DEADTEE].m_Id); + Graphics()->QuadsBegin(); + if(m_pClient->m_Snap.m_pGameInfoObj->m_GameFlags & GAMEFLAG_TEAMS) + { + ColorRGBA Color = m_pClient->m_Skins7.GetTeamColor(true, 0, m_pClient->m_aClients[pInfo->m_ClientId].m_Team, protocol7::SKINPART_BODY); + Graphics()->SetColor(Color.r, Color.g, Color.b, Color.a); + } + CTeeRenderInfo TeeInfo = m_pClient->m_aClients[pInfo->m_ClientId].m_RenderInfo; + TeeInfo.m_Size *= TeeSizeMod; + IGraphics::CQuadItem QuadItem(TeeOffset, Row.y, TeeInfo.m_Size, TeeInfo.m_Size); + Graphics()->QuadsDrawTL(&QuadItem, 1); + Graphics()->QuadsEnd(); + } + else { CTeeRenderInfo TeeInfo = ClientData.m_RenderInfo; TeeInfo.m_Size *= TeeSizeMod; @@ -480,6 +519,13 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, { TextRender()->TextEx(&Cursor, ClientData.m_aName); } + + // ready / watching + if(Client()->IsSixup() && Client()->m_TranslationContext.m_aClients[pInfo->m_ClientId].m_PlayerFlags7 & protocol7::PLAYERFLAG_READY) + { + TextRender()->TextColor(0.1f, 1.0f, 0.1f, TextColor.a); + TextRender()->TextEx(&Cursor, "✓"); + } } // clan @@ -490,7 +536,7 @@ void CScoreboard::RenderScoreboard(CUIRect Scoreboard, int Team, int CountStart, } else { - TextRender()->TextColor(TextRender()->DefaultTextColor()); + TextRender()->TextColor(TextColor); } CTextCursor Cursor; TextRender()->SetCursor(&Cursor, ClanOffset + (ClanLength - minimum(TextRender()->TextWidth(FontSize, ClientData.m_aClan), ClanLength)) / 2.0f, Row.y + (Row.h - FontSize) / 2.0f, FontSize, TEXTFLAG_RENDER | TEXTFLAG_ELLIPSIS_AT_END); diff --git a/src/game/client/components/skins7.cpp b/src/game/client/components/skins7.cpp new file mode 100644 index 000000000..183ce886a --- /dev/null +++ b/src/game/client/components/skins7.cpp @@ -0,0 +1,560 @@ +/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ +/* If you are missing that file, acquire a complete release at teeworlds.com. */ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "menus.h" +#include "skins7.h" + +const char *const CSkins7::ms_apSkinPartNames[protocol7::NUM_SKINPARTS] = {"body", "marking", "decoration", "hands", "feet", "eyes"}; /* Localize("body","skins");Localize("marking","skins");Localize("decoration","skins");Localize("hands","skins");Localize("feet","skins");Localize("eyes","skins"); */ +const char *const CSkins7::ms_apColorComponents[NUM_COLOR_COMPONENTS] = {"hue", "sat", "lgt", "alp"}; + +char *CSkins7::ms_apSkinVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{0}}; +int *CSkins7::ms_apUCCVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{0}}; +int *CSkins7::ms_apColorVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{0}}; + +// TODO: uncomment +// const float MIN_EYE_BODY_COLOR_DIST = 80.f; // between body and eyes (LAB color space) + +int CSkins7::SkinPartScan(const char *pName, int IsDir, int DirType, void *pUser) +{ + CSkins7 *pSelf = (CSkins7 *)pUser; + if(IsDir || !str_endswith(pName, ".png")) + return 0; + + size_t PartNameSize, PartNameCount; + str_utf8_stats(pName, str_length(pName) - str_length(".png") + 1, IO_MAX_PATH_LENGTH, &PartNameSize, &PartNameCount); + if(PartNameSize >= protocol7::MAX_SKIN_ARRAY_SIZE || PartNameCount > protocol7::MAX_SKIN_LENGTH) + { + log_error("skins7", "Failed to load skin part '%s': name too long", pName); + return 0; + } + + CSkinPart Part; + str_copy(Part.m_aName, pName, minimum(PartNameSize + 1, sizeof(Part.m_aName))); + if(pSelf->FindSkinPart(pSelf->m_ScanningPart, Part.m_aName, true) != -1) + return 0; + + char aFilename[IO_MAX_PATH_LENGTH]; + str_format(aFilename, sizeof(aFilename), "skins/%s/%s", CSkins7::ms_apSkinPartNames[pSelf->m_ScanningPart], pName); + CImageInfo Info; + if(!pSelf->Graphics()->LoadPng(Info, aFilename, DirType)) + { + log_error("skins7", "Failed to load skin part '%s'", pName); + return 0; + } + if(Info.m_Format != CImageInfo::FORMAT_RGBA) + { + log_error("skins7", "Failed to load skin part '%s': must be RGBA format", pName); + return 0; + } + + Part.m_OrgTexture = pSelf->Graphics()->LoadTextureRaw(Info, 0, aFilename); + Part.m_BloodColor = ColorRGBA(1.0f, 1.0f, 1.0f); + + int Step = Info.m_Format == CImageInfo::FORMAT_RGBA ? 4 : 3; + unsigned char *pData = (unsigned char *)Info.m_pData; + + // dig out blood color + if(pSelf->m_ScanningPart == protocol7::SKINPART_BODY) + { + int Pitch = Info.m_Width * Step; + int PartX = Info.m_Width / 2; + int PartY = 0; + int PartWidth = Info.m_Width / 2; + int PartHeight = Info.m_Height / 2; + + int aColors[3] = {0}; + for(int y = PartY; y < PartY + PartHeight; y++) + for(int x = PartX; x < PartX + PartWidth; x++) + if(pData[y * Pitch + x * Step + 3] > 128) + for(int c = 0; c < 3; c++) + aColors[c] += pData[y * Pitch + x * Step + c]; + + Part.m_BloodColor = ColorRGBA(normalize(vec3(aColors[0], aColors[1], aColors[2]))); + } + + // create colorless version + for(size_t i = 0; i < Info.m_Width * Info.m_Height; i++) + { + const int Average = (pData[i * Step] + pData[i * Step + 1] + pData[i * Step + 2]) / 3; + pData[i * Step] = Average; + pData[i * Step + 1] = Average; + pData[i * Step + 2] = Average; + } + + Part.m_ColorTexture = pSelf->Graphics()->LoadTextureRawMove(Info, 0, aFilename); + + // set skin part data + Part.m_Flags = 0; + if(pName[0] == 'x' && pName[1] == '_') + Part.m_Flags |= SKINFLAG_SPECIAL; + if(DirType != IStorage::TYPE_SAVE) + Part.m_Flags |= SKINFLAG_STANDARD; + log_debug("skins7", "load skin part %s", Part.m_aName); + pSelf->m_avSkinParts[pSelf->m_ScanningPart].emplace_back(Part); + + return 0; +} + +int CSkins7::SkinScan(const char *pName, int IsDir, int DirType, void *pUser) +{ + if(IsDir || !str_endswith(pName, ".json")) + return 0; + + CSkins7 *pSelf = (CSkins7 *)pUser; + + // read file data into buffer + char aFilename[IO_MAX_PATH_LENGTH]; + str_format(aFilename, sizeof(aFilename), "skins/%s", pName); + void *pFileData; + unsigned JsonFileSize; + if(!pSelf->Storage()->ReadFile(aFilename, IStorage::TYPE_ALL, &pFileData, &JsonFileSize)) + { + return 0; + } + + // init + CSkin Skin = pSelf->m_DummySkin; + int NameLength = str_length(pName); + str_copy(Skin.m_aName, pName, 1 + NameLength - str_length(".json")); + bool SpecialSkin = pName[0] == 'x' && pName[1] == '_'; + + // parse json data + json_settings JsonSettings; + mem_zero(&JsonSettings, sizeof(JsonSettings)); + char aError[256]; + json_value *pJsonData = json_parse_ex(&JsonSettings, static_cast(pFileData), JsonFileSize, aError); + free(pFileData); + + if(pJsonData == nullptr) + { + log_error("skins7", "Failed to parse skin json file '%s': %s", aFilename, aError); + return 0; + } + + // extract data + const json_value &Start = (*pJsonData)["skin"]; + if(Start.type == json_object) + { + for(int PartIndex = 0; PartIndex < protocol7::NUM_SKINPARTS; ++PartIndex) + { + const json_value &Part = Start[(const char *)ms_apSkinPartNames[PartIndex]]; + if(Part.type != json_object) + continue; + + // filename + const json_value &Filename = Part["filename"]; + if(Filename.type == json_string) + { + int SkinPart = pSelf->FindSkinPart(PartIndex, (const char *)Filename, SpecialSkin); + if(SkinPart > -1) + Skin.m_apParts[PartIndex] = pSelf->GetSkinPart(PartIndex, SkinPart); + } + + // use custom colors + bool UseCustomColors = false; + const json_value &Color = Part["custom_colors"]; + if(Color.type == json_string) + UseCustomColors = str_comp((const char *)Color, "true") == 0; + else if(Color.type == json_boolean) + UseCustomColors = Color.u.boolean; + Skin.m_aUseCustomColors[PartIndex] = UseCustomColors; + + // color components + if(!UseCustomColors) + continue; + + for(int i = 0; i < NUM_COLOR_COMPONENTS; i++) + { + if(PartIndex != protocol7::SKINPART_MARKING && i == 3) + continue; + + const json_value &Component = Part[(const char *)ms_apColorComponents[i]]; + if(Component.type == json_integer) + { + switch(i) + { + case 0: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0xFF00FFFF) | (Component.u.integer << 16); break; + case 1: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0xFFFF00FF) | (Component.u.integer << 8); break; + case 2: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0xFFFFFF00) | Component.u.integer; break; + case 3: Skin.m_aPartColors[PartIndex] = (Skin.m_aPartColors[PartIndex] & 0x00FFFFFF) | (Component.u.integer << 24); break; + } + } + } + } + } + + // clean up + json_value_free(pJsonData); + + // set skin data + Skin.m_Flags = SpecialSkin ? SKINFLAG_SPECIAL : 0; + if(DirType != IStorage::TYPE_SAVE) + Skin.m_Flags |= SKINFLAG_STANDARD; + if(pSelf->Config()->m_Debug) + { + log_debug("skins7", "load skin %s", Skin.m_aName); + } + pSelf->m_vSkins.insert(std::lower_bound(pSelf->m_vSkins.begin(), pSelf->m_vSkins.end(), Skin), Skin); + + return 0; +} + +int CSkins7::GetInitAmount() const +{ + return protocol7::NUM_SKINPARTS * 5 + 8; +} + +void CSkins7::OnInit() +{ + int Dummy = 0; + ms_apSkinVariables[Dummy][protocol7::SKINPART_BODY] = Config()->m_ClPlayer7SkinBody; + ms_apSkinVariables[Dummy][protocol7::SKINPART_MARKING] = Config()->m_ClPlayer7SkinMarking; + ms_apSkinVariables[Dummy][protocol7::SKINPART_DECORATION] = Config()->m_ClPlayer7SkinDecoration; + ms_apSkinVariables[Dummy][protocol7::SKINPART_HANDS] = Config()->m_ClPlayer7SkinHands; + ms_apSkinVariables[Dummy][protocol7::SKINPART_FEET] = Config()->m_ClPlayer7SkinFeet; + ms_apSkinVariables[Dummy][protocol7::SKINPART_EYES] = Config()->m_ClPlayer7SkinEyes; + ms_apUCCVariables[Dummy][protocol7::SKINPART_BODY] = &Config()->m_ClPlayer7UseCustomColorBody; + ms_apUCCVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClPlayer7UseCustomColorMarking; + ms_apUCCVariables[Dummy][protocol7::SKINPART_DECORATION] = &Config()->m_ClPlayer7UseCustomColorDecoration; + ms_apUCCVariables[Dummy][protocol7::SKINPART_HANDS] = &Config()->m_ClPlayer7UseCustomColorHands; + ms_apUCCVariables[Dummy][protocol7::SKINPART_FEET] = &Config()->m_ClPlayer7UseCustomColorFeet; + ms_apUCCVariables[Dummy][protocol7::SKINPART_EYES] = &Config()->m_ClPlayer7UseCustomColorEyes; + ms_apColorVariables[Dummy][protocol7::SKINPART_BODY] = (int *)&Config()->m_ClPlayer7ColorBody; + ms_apColorVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClPlayer7ColorMarking; + ms_apColorVariables[Dummy][protocol7::SKINPART_DECORATION] = (int *)&Config()->m_ClPlayer7ColorDecoration; + ms_apColorVariables[Dummy][protocol7::SKINPART_HANDS] = (int *)&Config()->m_ClPlayer7ColorHands; + ms_apColorVariables[Dummy][protocol7::SKINPART_FEET] = (int *)&Config()->m_ClPlayer7ColorFeet; + ms_apColorVariables[Dummy][protocol7::SKINPART_EYES] = (int *)&Config()->m_ClPlayer7ColorEyes; + + Dummy = 1; + ms_apSkinVariables[Dummy][protocol7::SKINPART_BODY] = Config()->m_ClDummy7SkinBody; + ms_apSkinVariables[Dummy][protocol7::SKINPART_MARKING] = Config()->m_ClDummy7SkinMarking; + ms_apSkinVariables[Dummy][protocol7::SKINPART_DECORATION] = Config()->m_ClDummy7SkinDecoration; + ms_apSkinVariables[Dummy][protocol7::SKINPART_HANDS] = Config()->m_ClDummy7SkinHands; + ms_apSkinVariables[Dummy][protocol7::SKINPART_FEET] = Config()->m_ClDummy7SkinFeet; + ms_apSkinVariables[Dummy][protocol7::SKINPART_EYES] = Config()->m_ClDummy7SkinEyes; + ms_apUCCVariables[Dummy][protocol7::SKINPART_BODY] = &Config()->m_ClDummy7UseCustomColorBody; + ms_apUCCVariables[Dummy][protocol7::SKINPART_MARKING] = &Config()->m_ClDummy7UseCustomColorMarking; + ms_apUCCVariables[Dummy][protocol7::SKINPART_DECORATION] = &Config()->m_ClDummy7UseCustomColorDecoration; + ms_apUCCVariables[Dummy][protocol7::SKINPART_HANDS] = &Config()->m_ClDummy7UseCustomColorHands; + ms_apUCCVariables[Dummy][protocol7::SKINPART_FEET] = &Config()->m_ClDummy7UseCustomColorFeet; + ms_apUCCVariables[Dummy][protocol7::SKINPART_EYES] = &Config()->m_ClDummy7UseCustomColorEyes; + ms_apColorVariables[Dummy][protocol7::SKINPART_BODY] = (int *)&Config()->m_ClDummy7ColorBody; + ms_apColorVariables[Dummy][protocol7::SKINPART_MARKING] = (int *)&Config()->m_ClDummy7ColorMarking; + ms_apColorVariables[Dummy][protocol7::SKINPART_DECORATION] = (int *)&Config()->m_ClDummy7ColorDecoration; + ms_apColorVariables[Dummy][protocol7::SKINPART_HANDS] = (int *)&Config()->m_ClDummy7ColorHands; + ms_apColorVariables[Dummy][protocol7::SKINPART_FEET] = (int *)&Config()->m_ClDummy7ColorFeet; + ms_apColorVariables[Dummy][protocol7::SKINPART_EYES] = (int *)&Config()->m_ClDummy7ColorEyes; + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + m_avSkinParts[Part].clear(); + + // add none part + if(Part == protocol7::SKINPART_MARKING || Part == protocol7::SKINPART_DECORATION) + { + CSkinPart NoneSkinPart; + NoneSkinPart.m_Flags = SKINFLAG_STANDARD; + NoneSkinPart.m_aName[0] = '\0'; + NoneSkinPart.m_BloodColor = vec3(1.0f, 1.0f, 1.0f); + m_avSkinParts[Part].emplace_back(NoneSkinPart); + } + + // load skin parts + char aBuf[64]; + str_format(aBuf, sizeof(aBuf), "skins/%s", ms_apSkinPartNames[Part]); + m_ScanningPart = Part; + Storage()->ListDirectory(IStorage::TYPE_ALL, aBuf, SkinPartScan, this); + + // add dummy skin part + if(m_avSkinParts[Part].empty()) + { + CSkinPart DummySkinPart; + DummySkinPart.m_Flags = SKINFLAG_STANDARD; + str_copy(DummySkinPart.m_aName, "dummy"); + DummySkinPart.m_BloodColor = vec3(1.0f, 1.0f, 1.0f); + m_avSkinParts[Part].emplace_back(DummySkinPart); + } + + GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); + } + + // create dummy skin + m_DummySkin.m_Flags = SKINFLAG_STANDARD; + str_copy(m_DummySkin.m_aName, "dummy"); + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + int Default; + if(Part == protocol7::SKINPART_MARKING || Part == protocol7::SKINPART_DECORATION) + Default = FindSkinPart(Part, "", false); + else + Default = FindSkinPart(Part, "standard", false); + if(Default < 0) + Default = 0; + m_DummySkin.m_apParts[Part] = GetSkinPart(Part, Default); + m_DummySkin.m_aPartColors[Part] = Part == protocol7::SKINPART_MARKING ? (255 << 24) + 65408 : 65408; + m_DummySkin.m_aUseCustomColors[Part] = 0; + } + GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); + + // load skins + m_vSkins.clear(); + Storage()->ListDirectory(IStorage::TYPE_ALL, "skins", SkinScan, this); + GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); + + // add dummy skin + if(m_vSkins.empty()) + m_vSkins.emplace_back(m_DummySkin); + + { + // add xmas hat + const char *pFileName = "skins/xmas_hat.png"; + CImageInfo Info; + if(!Graphics()->LoadPng(Info, pFileName, IStorage::TYPE_ALL) || Info.m_Width != 128 || Info.m_Height != 512) + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "failed to load xmas hat '%s'", pFileName); + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "skins7", aBuf); + } + else + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "loaded xmas hat '%s'", pFileName); + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "skins7", aBuf); + m_XmasHatTexture = Graphics()->LoadTextureRawMove(Info, 0, pFileName); + } + } + GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); + + { + // add bot decoration + const char *pFileName = "skins/bot.png"; + CImageInfo Info; + if(!Graphics()->LoadPng(Info, pFileName, IStorage::TYPE_ALL) || Info.m_Width != 384 || Info.m_Height != 160) + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "failed to load bot '%s'", pFileName); + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "skins7", aBuf); + } + else + { + char aBuf[128]; + str_format(aBuf, sizeof(aBuf), "loaded bot '%s'", pFileName); + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "skins7", aBuf); + m_BotTexture = Graphics()->LoadTextureRawMove(Info, 0, pFileName); + } + } + GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); +} + +void CSkins7::AddSkin(const char *pSkinName, int Dummy) +{ + CSkin Skin = m_DummySkin; + Skin.m_Flags = 0; + str_copy(Skin.m_aName, pSkinName, sizeof(Skin.m_aName)); + for(int PartIndex = 0; PartIndex < protocol7::NUM_SKINPARTS; ++PartIndex) + { + int SkinPart = FindSkinPart(PartIndex, ms_apSkinVariables[Dummy][PartIndex], false); + if(SkinPart > -1) + Skin.m_apParts[PartIndex] = GetSkinPart(PartIndex, SkinPart); + Skin.m_aUseCustomColors[PartIndex] = *ms_apUCCVariables[Dummy][PartIndex]; + Skin.m_aPartColors[PartIndex] = *ms_apColorVariables[Dummy][PartIndex]; + } + int SkinIndex = Find(Skin.m_aName, false); + if(SkinIndex != -1) + m_vSkins[SkinIndex] = Skin; + else + m_vSkins.emplace_back(Skin); +} + +void CSkins7::RemoveSkin(const CSkin *pSkin) +{ + auto Position = std::find(m_vSkins.begin(), m_vSkins.end(), *pSkin); + m_vSkins.erase(Position); +} + +int CSkins7::Num() +{ + return m_vSkins.size(); +} + +int CSkins7::NumSkinPart(int Part) +{ + return m_avSkinParts[Part].size(); +} + +const CSkins7::CSkin *CSkins7::Get(int Index) +{ + return &m_vSkins[maximum((std::size_t)0, Index % m_vSkins.size())]; +} + +int CSkins7::Find(const char *pName, bool AllowSpecialSkin) +{ + for(unsigned int i = 0; i < m_vSkins.size(); i++) + { + if(str_comp(m_vSkins[i].m_aName, pName) == 0 && ((m_vSkins[i].m_Flags & SKINFLAG_SPECIAL) == 0 || AllowSpecialSkin)) + return i; + } + return -1; +} + +const CSkins7::CSkinPart *CSkins7::GetSkinPart(int Part, int Index) +{ + int Size = m_avSkinParts[Part].size(); + return &m_avSkinParts[Part][maximum(0, Index % Size)]; +} + +int CSkins7::FindSkinPart(int Part, const char *pName, bool AllowSpecialPart) +{ + for(unsigned int i = 0; i < m_avSkinParts[Part].size(); i++) + { + if(str_comp(m_avSkinParts[Part][i].m_aName, pName) == 0 && ((m_avSkinParts[Part][i].m_Flags & SKINFLAG_SPECIAL) == 0 || AllowSpecialPart)) + return i; + } + return -1; +} + +void CSkins7::RandomizeSkin(int Dummy) +{ + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + int Hue = rand() % 255; + int Sat = rand() % 255; + int Lgt = rand() % 255; + int Alp = 0; + if(Part == protocol7::SKINPART_MARKING) + Alp = rand() % 255; + int ColorVariable = (Alp << 24) | (Hue << 16) | (Sat << 8) | Lgt; + *CSkins7::ms_apUCCVariables[Dummy][Part] = true; + *CSkins7::ms_apColorVariables[Dummy][Part] = ColorVariable; + } + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + const CSkins7::CSkinPart *pSkinPart = GetSkinPart(Part, rand() % NumSkinPart(Part)); + while(pSkinPart->m_Flags & CSkins7::SKINFLAG_SPECIAL) + pSkinPart = GetSkinPart(Part, rand() % NumSkinPart(Part)); + mem_copy(CSkins7::ms_apSkinVariables[Dummy][Part], pSkinPart->m_aName, protocol7::MAX_SKIN_ARRAY_SIZE); + } +} + +ColorRGBA CSkins7::GetColor(int Value, bool UseAlpha) const +{ + float Dark = DARKEST_COLOR_LGT / 255.0f; + ColorRGBA Color = color_cast(ColorHSLA(Value).UnclampLighting(Dark)); + float Alpha = UseAlpha ? ((Value >> 24) & 0xff) / 255.0f : 1.0f; + Color.a = Alpha; + return Color; +} + +ColorRGBA CSkins7::GetTeamColor(int UseCustomColors, int PartColor, int Team, int Part) const +{ + static const int s_aTeamColors[3] = {0xC4C34E, 0x00FF6B, 0x9BFF6B}; + + int TeamHue = (s_aTeamColors[Team + 1] >> 16) & 0xff; + int TeamSat = (s_aTeamColors[Team + 1] >> 8) & 0xff; + int TeamLgt = s_aTeamColors[Team + 1] & 0xff; + int PartSat = (PartColor >> 8) & 0xff; + int PartLgt = PartColor & 0xff; + + if(!UseCustomColors) + { + PartSat = 255; + PartLgt = 255; + } + + int MinSat = 160; + int MaxSat = 255; + + int h = TeamHue; + int s = clamp(mix(TeamSat, PartSat, 0.2), MinSat, MaxSat); + int l = clamp(mix(TeamLgt, PartLgt, 0.2), (int)DARKEST_COLOR_LGT, 200); + + int ColorVal = (h << 16) + (s << 8) + l; + + return GetColor(ColorVal, Part == protocol7::SKINPART_MARKING); +} + +bool CSkins7::ValidateSkinParts(char *apPartNames[protocol7::NUM_SKINPARTS], int *pUseCustomColors, int *pPartColors, int GameFlags) const +{ + // force standard (black) eyes on team skins + if(GameFlags & GAMEFLAG_TEAMS) + { + // TODO: adjust eye color here as well? + if(str_comp(apPartNames[protocol7::SKINPART_EYES], "colorable") == 0 || str_comp(apPartNames[protocol7::SKINPART_EYES], "negative") == 0) + { + str_copy(apPartNames[protocol7::SKINPART_EYES], "standard", protocol7::MAX_SKIN_ARRAY_SIZE); + return false; + } + } + return true; +} + +bool CSkins7::SaveSkinfile(const char *pSaveSkinName, int Dummy) +{ + char aBuf[IO_MAX_PATH_LENGTH]; + str_format(aBuf, sizeof(aBuf), "skins/%s.json", pSaveSkinName); + IOHANDLE File = Storage()->OpenFile(aBuf, IOFLAG_WRITE, IStorage::TYPE_SAVE); + if(!File) + return false; + + CJsonFileWriter Writer(File); + + Writer.BeginObject(); + Writer.WriteAttribute("skin"); + Writer.BeginObject(); + for(int PartIndex = 0; PartIndex < protocol7::NUM_SKINPARTS; PartIndex++) + { + if(!ms_apSkinVariables[Dummy][PartIndex][0]) + continue; + + // part start + Writer.WriteAttribute(ms_apSkinPartNames[PartIndex]); + Writer.BeginObject(); + { + Writer.WriteAttribute("filename"); + Writer.WriteStrValue(ms_apSkinVariables[Dummy][PartIndex]); + + const bool CustomColors = *ms_apUCCVariables[PartIndex]; + Writer.WriteAttribute("custom_colors"); + Writer.WriteBoolValue(CustomColors); + + if(CustomColors) + { + for(int ColorComponent = 0; ColorComponent < NUM_COLOR_COMPONENTS - 1; ColorComponent++) + { + int Val = (*ms_apColorVariables[Dummy][PartIndex] >> (2 - ColorComponent) * 8) & 0xff; + Writer.WriteAttribute(ms_apColorComponents[ColorComponent]); + Writer.WriteIntValue(Val); + } + if(PartIndex == protocol7::SKINPART_MARKING) + { + int Val = (*ms_apColorVariables[Dummy][PartIndex] >> 24) & 0xff; + Writer.WriteAttribute(ms_apColorComponents[3]); + Writer.WriteIntValue(Val); + } + } + } + Writer.EndObject(); + } + Writer.EndObject(); + Writer.EndObject(); + + // add new skin to the skin list + AddSkin(pSaveSkinName, Dummy); + return true; +} diff --git a/src/game/client/components/skins7.h b/src/game/client/components/skins7.h new file mode 100644 index 000000000..4809a76e4 --- /dev/null +++ b/src/game/client/components/skins7.h @@ -0,0 +1,97 @@ +/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */ +/* If you are missing that file, acquire a complete release at teeworlds.com. */ +#ifndef GAME_CLIENT_COMPONENTS_SKINS7_H +#define GAME_CLIENT_COMPONENTS_SKINS7_H +#include +#include +#include +#include +#include +#include + +#include +#include + +class CSkins7 : public CComponent +{ +public: + enum + { + SKINFLAG_SPECIAL = 1 << 0, + SKINFLAG_STANDARD = 1 << 1, + + DARKEST_COLOR_LGT = 61, + + NUM_COLOR_COMPONENTS = 4, + + HAT_NUM = 2, + HAT_OFFSET_SIDE = 2, + }; + + struct CSkinPart + { + int m_Flags; + char m_aName[24]; + IGraphics::CTextureHandle m_OrgTexture; + IGraphics::CTextureHandle m_ColorTexture; + ColorRGBA m_BloodColor; + + bool operator<(const CSkinPart &Other) { return str_comp_nocase(m_aName, Other.m_aName) < 0; } + }; + + struct CSkin + { + int m_Flags; + char m_aName[24]; + const CSkinPart *m_apParts[protocol7::NUM_SKINPARTS]; + int m_aPartColors[protocol7::NUM_SKINPARTS]; + int m_aUseCustomColors[protocol7::NUM_SKINPARTS]; + + bool operator<(const CSkin &Other) const { return str_comp_nocase(m_aName, Other.m_aName) < 0; } + bool operator==(const CSkin &Other) const { return !str_comp(m_aName, Other.m_aName); } + }; + + static const char *const ms_apSkinPartNames[protocol7::NUM_SKINPARTS]; + static const char *const ms_apColorComponents[NUM_COLOR_COMPONENTS]; + + static char *ms_apSkinVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS]; + static int *ms_apUCCVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS]; // use custom color + static int *ms_apColorVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS]; + IGraphics::CTextureHandle m_XmasHatTexture; + IGraphics::CTextureHandle m_BotTexture; + + int GetInitAmount() const; + void OnInit() override; + + void AddSkin(const char *pSkinName, int Dummy); + void RemoveSkin(const CSkin *pSkin); + + int Num(); + int NumSkinPart(int Part); + const CSkin *Get(int Index); + int Find(const char *pName, bool AllowSpecialSkin); + const CSkinPart *GetSkinPart(int Part, int Index); + int FindSkinPart(int Part, const char *pName, bool AllowSpecialPart); + void RandomizeSkin(int Dummy); + + ColorRGBA GetColor(int Value, bool UseAlpha) const; + ColorRGBA GetTeamColor(int UseCustomColors, int PartColor, int Team, int Part) const; + + // returns true if everything was valid and nothing changed + bool ValidateSkinParts(char *apPartNames[protocol7::NUM_SKINPARTS], int *pUseCustomColors, int *pPartColors, int GameFlags) const; + + bool SaveSkinfile(const char *pSaveSkinName, int Dummy); + + virtual int Sizeof() const override { return sizeof(*this); } + +private: + int m_ScanningPart; + std::vector m_avSkinParts[protocol7::NUM_SKINPARTS]; + std::vector m_vSkins; + CSkin m_DummySkin; + + static int SkinPartScan(const char *pName, int IsDir, int DirType, void *pUser); + static int SkinScan(const char *pName, int IsDir, int DirType, void *pUser); +}; + +#endif diff --git a/src/game/client/components/spectator.cpp b/src/game/client/components/spectator.cpp index 345d43cbe..11b4fd383 100644 --- a/src/game/client/components/spectator.cpp +++ b/src/game/client/components/spectator.cpp @@ -558,6 +558,22 @@ void CSpectator::Spectate(int SpectatorId) if(m_pClient->m_Snap.m_SpecInfo.m_SpectatorId == SpectatorId) return; + if(Client()->IsSixup()) + { + protocol7::CNetMsg_Cl_SetSpectatorMode Msg; + if(SpectatorId == SPEC_FREEVIEW) + { + Msg.m_SpecMode = protocol7::SPEC_FREEVIEW; + Msg.m_SpectatorId = -1; + } + else + { + Msg.m_SpecMode = protocol7::SPEC_PLAYER; + Msg.m_SpectatorId = SpectatorId; + } + Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL, true); + return; + } CNetMsg_Cl_SetSpectatorMode Msg; Msg.m_SpectatorId = SpectatorId; Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL); diff --git a/src/game/client/components/voting.cpp b/src/game/client/components/voting.cpp index 0c2fbb57e..8a0282fdb 100644 --- a/src/game/client/components/voting.cpp +++ b/src/game/client/components/voting.cpp @@ -31,6 +31,16 @@ void CVoting::ConVote(IConsole::IResult *pResult, void *pUserData) void CVoting::Callvote(const char *pType, const char *pValue, const char *pReason) { + if(Client()->IsSixup()) + { + protocol7::CNetMsg_Cl_CallVote Msg; + Msg.m_pType = pType; + Msg.m_pValue = pValue; + Msg.m_pReason = pReason; + Msg.m_Force = false; + Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL, true); + return; + } CNetMsg_Cl_CallVote Msg = {0}; Msg.m_pType = pType; Msg.m_pValue = pValue; diff --git a/src/game/client/components/voting.h b/src/game/client/components/voting.h index 2d3d36d15..87abcb1eb 100644 --- a/src/game/client/components/voting.h +++ b/src/game/client/components/voting.h @@ -26,7 +26,6 @@ class CVoting : public CComponent int m_Yes, m_No, m_Pass, m_Total; bool m_ReceivingOptions; - void AddOption(const char *pDescription); void RemoveOption(const char *pDescription); void ClearOptions(); void Callvote(const char *pType, const char *pValue, const char *pReason); @@ -54,6 +53,7 @@ public: void CallvoteOption(int OptionId, const char *pReason, bool ForceVote = false); void RemovevoteOption(int OptionId); void AddvoteOption(const char *pDescription, const char *pCommand); + void AddOption(const char *pDescription); void Vote(int v); // -1 = no, 1 = yes diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index fdd6b5161..5cc75cb63 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -37,6 +37,9 @@ #include #include +#include +#include + #include "components/background.h" #include "components/binds.h" #include "components/broadcast.h" @@ -66,6 +69,7 @@ #include "components/race_demo.h" #include "components/scoreboard.h" #include "components/skins.h" +#include "components/skins7.h" #include "components/sounds.h" #include "components/spectator.h" #include "components/statboard.h" @@ -79,6 +83,7 @@ const char *CGameClient::Version() const { return GAME_VERSION; } const char *CGameClient::NetVersion() const { return GAME_NETVERSION; } int CGameClient::DDNetVersion() const { return DDNET_VERSION_NUMBER; } const char *CGameClient::DDNetVersionStr() const { return m_aDDNetVersionStr; } +int CGameClient::ClientVersion7() const { return CLIENT_VERSION7; } const char *CGameClient::GetItemName(int Type) const { return m_NetObjHandler.GetObjName(Type); } void CGameClient::OnConsoleInit() @@ -105,6 +110,7 @@ void CGameClient::OnConsoleInit() // make a list of all the systems, make sure to add them in the correct render order m_vpAll.insert(m_vpAll.end(), {&m_Skins, + &m_Skins7, &m_CountryFlags, &m_MapImages, &m_Effects, // doesn't render anything, just updates effects @@ -161,6 +167,7 @@ void CGameClient::OnConsoleInit() // add basic console commands Console()->Register("team", "i[team-id]", CFGFLAG_CLIENT, ConTeam, this, "Switch team"); Console()->Register("kill", "", CFGFLAG_CLIENT, ConKill, this, "Kill yourself to restart"); + Console()->Register("ready_change", "", CFGFLAG_CLIENT, ConReadyChange7, this, "Change ready state (0.7 only)"); // register tune zone command to allow the client prediction to load tunezones from the map Console()->Register("tune_zone", "i[zone] s[tuning] f[value]", CFGFLAG_GAME, ConTuneZone, this, "Tune in zone a variable to value"); @@ -272,6 +279,11 @@ void CGameClient::OnInit() // setup item sizes for(int i = 0; i < NUM_NETOBJTYPES; i++) Client()->SnapSetStaticsize(i, m_NetObjHandler.GetObjSize(i)); + // HACK: only set static size for items, which were available in the first 0.7 release + // so new items don't break the snapshot delta + static const int OLD_NUM_NETOBJTYPES = 23; + for(int i = 0; i < OLD_NUM_NETOBJTYPES; i++) + Client()->SnapSetStaticsize7(i, m_NetObjHandler7.GetObjSize(i)); TextRender()->LoadFonts(); TextRender()->SetFontLanguageVariant(g_Config.m_ClLanguagefile); @@ -330,6 +342,14 @@ void CGameClient::OnInit() g_pData->m_aImages[i].m_Id = Graphics()->LoadTexture(g_pData->m_aImages[i].m_pFilename, IStorage::TYPE_ALL); m_Menus.RenderLoading(pLoadingDDNetCaption, pLoadingMessageAssets, 1); } + for(int i = 0; i < client_data7::g_pData->m_NumImages; i++) + { + if(client_data7::g_pData->m_aImages[i].m_pFilename[0] == '\0') // handle special null image without filename + client_data7::g_pData->m_aImages[i].m_Id = IGraphics::CTextureHandle(); + else if(i == client_data7::IMAGE_DEADTEE) + client_data7::g_pData->m_aImages[i].m_Id = Graphics()->LoadTexture(client_data7::g_pData->m_aImages[i].m_pFilename, IStorage::TYPE_ALL, 0); + m_Menus.RenderLoading(pLoadingDDNetCaption, Localize("Initializing assets"), 1); + } m_GameWorld.m_pCollision = Collision(); m_GameWorld.m_pTuningList = m_aTuningList; @@ -726,17 +746,27 @@ void CGameClient::OnRender() { if(m_aCheckInfo[0] == 0) { - if( - str_comp(m_aClients[m_aLocalIds[0]].m_aName, Client()->PlayerName()) || - str_comp(m_aClients[m_aLocalIds[0]].m_aClan, g_Config.m_PlayerClan) || - m_aClients[m_aLocalIds[0]].m_Country != g_Config.m_PlayerCountry || - str_comp(m_aClients[m_aLocalIds[0]].m_aSkinName, g_Config.m_ClPlayerSkin) || - m_aClients[m_aLocalIds[0]].m_UseCustomColor != g_Config.m_ClPlayerUseCustomColor || - m_aClients[m_aLocalIds[0]].m_ColorBody != (int)g_Config.m_ClPlayerColorBody || - m_aClients[m_aLocalIds[0]].m_ColorFeet != (int)g_Config.m_ClPlayerColorFeet) - SendInfo(false); + if(m_pClient->IsSixup()) + { + if(!GotWantedSkin7(false)) + SendSkinChange7(false); + else + m_aCheckInfo[0] = -1; + } else - m_aCheckInfo[0] = -1; + { + if( + str_comp(m_aClients[m_aLocalIds[0]].m_aName, Client()->PlayerName()) || + str_comp(m_aClients[m_aLocalIds[0]].m_aClan, g_Config.m_PlayerClan) || + m_aClients[m_aLocalIds[0]].m_Country != g_Config.m_PlayerCountry || + str_comp(m_aClients[m_aLocalIds[0]].m_aSkinName, g_Config.m_ClPlayerSkin) || + m_aClients[m_aLocalIds[0]].m_UseCustomColor != g_Config.m_ClPlayerUseCustomColor || + m_aClients[m_aLocalIds[0]].m_ColorBody != (int)g_Config.m_ClPlayerColorBody || + m_aClients[m_aLocalIds[0]].m_ColorFeet != (int)g_Config.m_ClPlayerColorFeet) + SendInfo(false); + else + m_aCheckInfo[0] = -1; + } } if(m_aCheckInfo[0] > 0) @@ -746,17 +776,27 @@ void CGameClient::OnRender() { if(m_aCheckInfo[1] == 0) { - if( - str_comp(m_aClients[m_aLocalIds[1]].m_aName, Client()->DummyName()) || - str_comp(m_aClients[m_aLocalIds[1]].m_aClan, g_Config.m_ClDummyClan) || - m_aClients[m_aLocalIds[1]].m_Country != g_Config.m_ClDummyCountry || - str_comp(m_aClients[m_aLocalIds[1]].m_aSkinName, g_Config.m_ClDummySkin) || - m_aClients[m_aLocalIds[1]].m_UseCustomColor != g_Config.m_ClDummyUseCustomColor || - m_aClients[m_aLocalIds[1]].m_ColorBody != (int)g_Config.m_ClDummyColorBody || - m_aClients[m_aLocalIds[1]].m_ColorFeet != (int)g_Config.m_ClDummyColorFeet) - SendDummyInfo(false); + if(m_pClient->IsSixup()) + { + if(!GotWantedSkin7(true)) + SendSkinChange7(true); + else + m_aCheckInfo[1] = -1; + } else - m_aCheckInfo[1] = -1; + { + if( + str_comp(m_aClients[m_aLocalIds[1]].m_aName, Client()->DummyName()) || + str_comp(m_aClients[m_aLocalIds[1]].m_aClan, g_Config.m_ClDummyClan) || + m_aClients[m_aLocalIds[1]].m_Country != g_Config.m_ClDummyCountry || + str_comp(m_aClients[m_aLocalIds[1]].m_aSkinName, g_Config.m_ClDummySkin) || + m_aClients[m_aLocalIds[1]].m_UseCustomColor != g_Config.m_ClDummyUseCustomColor || + m_aClients[m_aLocalIds[1]].m_ColorBody != (int)g_Config.m_ClDummyColorBody || + m_aClients[m_aLocalIds[1]].m_ColorFeet != (int)g_Config.m_ClDummyColorFeet) + SendDummyInfo(false); + else + m_aCheckInfo[1] = -1; + } } if(m_aCheckInfo[1] > 0) @@ -815,16 +855,18 @@ void CGameClient::OnRelease() void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dummy) { // special messages + static_assert((int)NETMSGTYPE_SV_TUNEPARAMS == (int)protocol7::NETMSGTYPE_SV_TUNEPARAMS, "0.6 and 0.7 tune message id do not match"); if(MsgId == NETMSGTYPE_SV_TUNEPARAMS) { // unpack the new tuning CTuningParams NewTuning; int *pParams = (int *)&NewTuning; - // No jetpack on DDNet incompatible servers: - NewTuning.m_JetpackStrength = 0; for(unsigned i = 0; i < sizeof(CTuningParams) / sizeof(int); i++) { - int value = pUnpacker->GetInt(); + // 31 is the magic number index of laser_damage + // which was removed in 0.7 + // also in 0.6 it is unsed so we just set it to 0 + int value = (Client()->IsSixup() && i == 30) ? 0 : pUnpacker->GetInt(); // check for unpacking errors if(pUnpacker->Error()) @@ -833,6 +875,9 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm pParams[i] = value; } + // No jetpack on DDNet incompatible servers: + NewTuning.m_JetpackStrength = 0; + m_ServerMode = SERVERMODE_PURE; m_aReceivedTuning[Conn] = true; @@ -841,12 +886,18 @@ void CGameClient::OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dumm return; } - void *pRawMsg = m_NetObjHandler.SecureUnpackMsg(MsgId, pUnpacker); + void *pRawMsg = TranslateGameMsg(&MsgId, pUnpacker, Conn); + if(!pRawMsg) { - char aBuf[256]; - str_format(aBuf, sizeof(aBuf), "dropped weird message '%s' (%d), failed on '%s'", m_NetObjHandler.GetMsgName(MsgId), MsgId, m_NetObjHandler.FailedMsgOn()); - Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf); + // the 0.7 version of this error message is printed on translation + // in sixup/translate_game.cpp + if(!Client()->IsSixup()) + { + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "dropped weird message '%s' (%d), failed on '%s'", m_NetObjHandler.GetMsgName(MsgId), MsgId, m_NetObjHandler.FailedMsgOn()); + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf); + } return; } @@ -1591,6 +1642,10 @@ void CGameClient::OnNewSnapshot() m_Snap.m_pSpectatorInfo = (const CNetObj_SpectatorInfo *)Item.m_pData; m_Snap.m_pPrevSpectatorInfo = (const CNetObj_SpectatorInfo *)Client()->SnapFindItem(IClient::SNAP_PREV, NETOBJTYPE_SPECTATORINFO, Item.m_Id); + // needed for 0.7 survival + // to auto spec players when dead + if(Client()->IsSixup()) + m_Snap.m_SpecInfo.m_Active = true; m_Snap.m_SpecInfo.m_SpectatorId = m_Snap.m_pSpectatorInfo->m_SpectatorId; } else if(Item.m_Type == NETOBJTYPE_GAMEINFO) @@ -1728,7 +1783,11 @@ void CGameClient::OnNewSnapshot() if(Client()->State() == IClient::STATE_DEMOPLAYBACK) { if(m_Snap.m_LocalClientId == -1 && m_DemoSpecId == SPEC_FOLLOW) - m_DemoSpecId = SPEC_FREEVIEW; + { + // TODO: can this be done in the translation layer? + if(!Client()->IsSixup()) + m_DemoSpecId = SPEC_FREEVIEW; + } if(m_DemoSpecId != SPEC_FOLLOW) { m_Snap.m_SpecInfo.m_Active = true; @@ -1770,18 +1829,30 @@ void CGameClient::OnNewSnapshot() }); bool TimeScore = m_GameInfo.m_TimeScore; + bool Race7 = Client()->IsSixup() && m_Snap.m_pGameInfoObj && m_Snap.m_pGameInfoObj->m_GameFlags & protocol7::GAMEFLAG_RACE; // sort player infos by score mem_copy(m_Snap.m_apInfoByScore, m_Snap.m_apInfoByName, sizeof(m_Snap.m_apInfoByScore)); - std::stable_sort(m_Snap.m_apInfoByScore, m_Snap.m_apInfoByScore + MAX_CLIENTS, - [TimeScore](const CNetObj_PlayerInfo *p1, const CNetObj_PlayerInfo *p2) -> bool { - if(!p2) - return static_cast(p1); - if(!p1) - return false; - return (((TimeScore && p1->m_Score == -9999) ? std::numeric_limits::min() : p1->m_Score) > - ((TimeScore && p2->m_Score == -9999) ? std::numeric_limits::min() : p2->m_Score)); - }); + if(Race7) + std::stable_sort(m_Snap.m_apInfoByScore, m_Snap.m_apInfoByScore + MAX_CLIENTS, + [](const CNetObj_PlayerInfo *p1, const CNetObj_PlayerInfo *p2) -> bool { + if(!p2) + return static_cast(p1); + if(!p1) + return false; + return (((p1->m_Score == -1) ? std::numeric_limits::max() : p1->m_Score) < + ((p2->m_Score == -1) ? std::numeric_limits::max() : p2->m_Score)); + }); + else + std::stable_sort(m_Snap.m_apInfoByScore, m_Snap.m_apInfoByScore + MAX_CLIENTS, + [TimeScore](const CNetObj_PlayerInfo *p1, const CNetObj_PlayerInfo *p2) -> bool { + if(!p2) + return static_cast(p1); + if(!p1) + return false; + return (((TimeScore && p1->m_Score == -9999) ? std::numeric_limits::min() : p1->m_Score) > + ((TimeScore && p2->m_Score == -9999) ? std::numeric_limits::min() : p2->m_Score)); + }); // sort player infos by DDRace Team (and score between) int Index = 0; @@ -2226,11 +2297,28 @@ void CGameClient::CClientData::UpdateRenderInfo(bool IsTeamPlay) { m_RenderInfo.m_ColorBody = color_cast(ColorHSLA(aTeamColors[m_Team])); m_RenderInfo.m_ColorFeet = color_cast(ColorHSLA(aTeamColors[m_Team])); + + // 0.7 + { + const ColorRGBA aTeamColorsSixup[2] = { + ColorRGBA(0.753f, 0.318f, 0.318f, 1.0f), + ColorRGBA(0.318f, 0.471f, 0.753f, 1.0f)}; + const ColorRGBA aMarkingColorsSixup[2] = { + ColorRGBA(0.824f, 0.345f, 0.345f, 1.0f), + ColorRGBA(0.345f, 0.514f, 0.824f, 1.0f)}; + float MarkingAlpha = m_RenderInfo.m_Sixup.m_aColors[protocol7::SKINPART_MARKING].a; + for(auto &Color : m_RenderInfo.m_Sixup.m_aColors) + Color = aTeamColorsSixup[m_Team]; + if(MarkingAlpha > 0.1f) + m_RenderInfo.m_Sixup.m_aColors[protocol7::SKINPART_MARKING] = aMarkingColorsSixup[m_Team]; + } } else { m_RenderInfo.m_ColorBody = color_cast(ColorHSLA(12829350)); m_RenderInfo.m_ColorFeet = color_cast(ColorHSLA(12829350)); + for(auto &Color : m_RenderInfo.m_Sixup.m_aColors) + Color = color_cast(ColorHSLA(12829350)); } } } @@ -2315,8 +2403,63 @@ void CGameClient::SendSwitchTeam(int Team) m_Camera.OnReset(); } +void CGameClient::SendStartInfo7(bool Dummy) const +{ + protocol7::CNetMsg_Cl_StartInfo Msg; + Msg.m_pName = Dummy ? Client()->DummyName() : Config()->m_PlayerName; + Msg.m_pClan = Dummy ? Config()->m_ClDummyClan : Config()->m_PlayerClan; + Msg.m_Country = Dummy ? Config()->m_ClDummyCountry : Config()->m_PlayerCountry; + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) + { + Msg.m_apSkinPartNames[p] = CSkins7::ms_apSkinVariables[(int)Dummy][p]; + Msg.m_aUseCustomColors[p] = *CSkins7::ms_apUCCVariables[(int)Dummy][p]; + Msg.m_aSkinPartColors[p] = *CSkins7::ms_apColorVariables[(int)Dummy][p]; + } + CMsgPacker Packer(&Msg, false, true); + if(Msg.Pack(&Packer)) + return; + Client()->SendMsg((int)Dummy, &Packer, MSGFLAG_VITAL | MSGFLAG_FLUSH); +} + +void CGameClient::SendSkinChange7(bool Dummy) +{ + protocol7::CNetMsg_Cl_SkinChange Msg; + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) + { + Msg.m_apSkinPartNames[p] = CSkins7::ms_apSkinVariables[(int)Dummy][p]; + Msg.m_aUseCustomColors[p] = *CSkins7::ms_apUCCVariables[(int)Dummy][p]; + Msg.m_aSkinPartColors[p] = *CSkins7::ms_apColorVariables[(int)Dummy][p]; + } + CMsgPacker Packer(&Msg, false, true); + if(Msg.Pack(&Packer)) + return; + Client()->SendMsg((int)Dummy, &Packer, MSGFLAG_VITAL | MSGFLAG_FLUSH); + m_aCheckInfo[(int)Dummy] = Client()->GameTickSpeed(); +} + +bool CGameClient::GotWantedSkin7(bool Dummy) +{ + // TODO: add name change ddnet extension to 0.7 protocol + // if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_aName, Dummy ? Client()->DummyName() : Client()->PlayerName())) + // return false; + // if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_aClan, Dummy ? g_Config.m_ClDummyClan : g_Config.m_PlayerClan)) + // return false; + // if(m_aClients[m_aLocalIds[(int)Dummy]].m_Country != (Dummy ? g_Config.m_ClDummyCountry : g_Config.m_PlayerCountry)) + // return false; + + return true; +} + void CGameClient::SendInfo(bool Start) { + if(m_pClient->IsSixup()) + { + if(Start) + SendStartInfo7(false); + else + SendSkinChange7(false); + return; + } if(Start) { CNetMsg_Cl_StartInfo Msg; @@ -2351,6 +2494,14 @@ void CGameClient::SendInfo(bool Start) void CGameClient::SendDummyInfo(bool Start) { + if(m_pClient->IsSixup()) + { + if(Start) + SendStartInfo7(true); + else + SendSkinChange7(true); + return; + } if(Start) { CNetMsg_Cl_StartInfo Msg; @@ -2395,6 +2546,17 @@ void CGameClient::SendKill(int ClientId) const } } +void CGameClient::SendReadyChange7() +{ + if(!Client()->IsSixup()) + { + Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "client", "Error you have to be connected to a 0.7 server to use ready_change"); + return; + } + protocol7::CNetMsg_Cl_ReadyChange Msg; + Client()->SendPackMsgActive(&Msg, MSGFLAG_VITAL, true); +} + void CGameClient::ConTeam(IConsole::IResult *pResult, void *pUserData) { ((CGameClient *)pUserData)->SendSwitchTeam(pResult->GetInteger(0)); @@ -2405,6 +2567,13 @@ void CGameClient::ConKill(IConsole::IResult *pResult, void *pUserData) ((CGameClient *)pUserData)->SendKill(-1); } +void CGameClient::ConReadyChange7(IConsole::IResult *pResult, void *pUserData) +{ + CGameClient *pClient = static_cast(pUserData); + if(pClient->Client()->State() == IClient::STATE_ONLINE) + pClient->SendReadyChange7(); +} + void CGameClient::ConchainLanguageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) { CGameClient *pThis = static_cast(pUserData); @@ -2434,8 +2603,10 @@ void CGameClient::ConchainSpecialDummy(IConsole::IResult *pResult, void *pUserDa { pfnCallback(pResult, pCallbackUserData); if(pResult->NumArguments()) + { if(g_Config.m_ClDummy && !((CGameClient *)pUserData)->Client()->DummyConnected()) g_Config.m_ClDummy = 0; + } } void CGameClient::ConchainClTextEntitiesSize(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData) @@ -3565,6 +3736,11 @@ CNetObjHandler *CGameClient::GetNetObjHandler() return &m_NetObjHandler; } +protocol7::CNetObjHandler *CGameClient::GetNetObjHandler7() +{ + return &m_NetObjHandler7; +} + void CGameClient::SnapCollectEntities() { int NumSnapItems = Client()->SnapNumItems(IClient::SNAP_CURRENT); diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 0d4eae079..6a2732bb9 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -17,6 +17,9 @@ #include +#include +#include + // components #include "components/background.h" #include "components/binds.h" @@ -48,6 +51,7 @@ #include "components/race_demo.h" #include "components/scoreboard.h" #include "components/skins.h" +#include "components/skins7.h" #include "components/sounds.h" #include "components/spectator.h" #include "components/statboard.h" @@ -121,6 +125,7 @@ public: CParticles m_Particles; CMenus m_Menus; CSkins m_Skins; + CSkins7 m_Skins7; CCountryFlags m_CountryFlags; CFlow m_Flow; CHud m_Hud; @@ -157,6 +162,7 @@ private: std::vector m_vpAll; std::vector m_vpInput; CNetObjHandler m_NetObjHandler; + protocol7::CNetObjHandler m_NetObjHandler7; class IEngine *m_pEngine; class IInput *m_pInput; @@ -203,6 +209,7 @@ private: static void ConTeam(IConsole::IResult *pResult, void *pUserData); static void ConKill(IConsole::IResult *pResult, void *pUserData); + static void ConReadyChange7(IConsole::IResult *pResult, void *pUserData); static void ConchainLanguageUpdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); static void ConchainSpecialInfoupdate(IConsole::IResult *pResult, void *pUserData, IConsole::FCommandCallback pfnCallback, void *pCallbackUserData); @@ -478,6 +485,7 @@ public: void OnInit() override; void OnConsoleInit() override; void OnStateChange(int NewState, int OldState) override; + void *TranslateGameMsg(int *pMsgId, CUnpacker *pUnpacker, int Conn); void OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dummy) override; void InvalidateSnapshot() override; void OnNewSnapshot() override; @@ -508,13 +516,21 @@ public: const char *NetVersion() const override; int DDNetVersion() const override; const char *DDNetVersionStr() const override; + virtual int ClientVersion7() const override; + + void DoTeamChangeMessage7(const char *pName, int ClientId, int Team, const char *pPrefix = ""); // actions // TODO: move these void SendSwitchTeam(int Team); + void SendStartInfo7(bool Dummy) const; + void SendSkinChange7(bool Dummy); + // Returns true if the requested skin change got applied by the server + bool GotWantedSkin7(bool Dummy); void SendInfo(bool Start); void SendDummyInfo(bool Start) override; void SendKill(int ClientId) const; + void SendReadyChange7(); int m_NextChangeInfo; @@ -557,6 +573,7 @@ public: bool IsLocalCharSuper() const; bool CanDisplayWarning() const override; CNetObjHandler *GetNetObjHandler() override; + protocol7::CNetObjHandler *GetNetObjHandler7() override; void LoadGameSkin(const char *pPath, bool AsDir = false); void LoadEmoticonsSkin(const char *pPath, bool AsDir = false); diff --git a/src/game/client/render.cpp b/src/game/client/render.cpp index 998cfec4e..ed2974a7a 100644 --- a/src/game/client/render.cpp +++ b/src/game/client/render.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include @@ -89,6 +90,13 @@ void CRenderTools::SelectSprite(int Id, int Flags, int sx, int sy) const SelectSprite(&g_pData->m_aSprites[Id], Flags, sx, sy); } +void CRenderTools::SelectSprite7(int Id, int Flags, int sx, int sy) const +{ + if(Id < 0 || Id >= client_data7::g_pData->m_NumSprites) + return; + SelectSprite(&client_data7::g_pData->m_aSprites[Id], Flags, sx, sy); +} + void CRenderTools::GetSpriteScale(const CDataSprite *pSprite, float &ScaleX, float &ScaleY) const { int w = pSprite->m_W; @@ -263,6 +271,234 @@ void CRenderTools::GetRenderTeeOffsetToRenderedTee(const CAnimState *pAnim, cons } void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha) const +{ + if(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_BODY].IsValid()) + RenderTee7(pAnim, pInfo, Emote, Dir, Pos); + else + RenderTee6(pAnim, pInfo, Emote, Dir, Pos, Alpha); + + Graphics()->SetColor(1.f, 1.f, 1.f, 1.f); + Graphics()->QuadsSetRotation(0); +} + +void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos) const +{ + vec2 Direction = Dir; + vec2 Position = Pos; + bool IsBot = false; + + // first pass we draw the outline + // second pass we draw the filling + for(int Pass = 0; Pass < 2; Pass++) + { + bool OutLine = Pass == 0; + + for(int Filling = 0; Filling < 2; Filling++) + { + float AnimScale = pInfo->m_Size * 1.0f / 64.0f; + float BaseSize = pInfo->m_Size; + if(Filling == 1) + { + vec2 BodyPos = Position + vec2(pAnim->GetBody()->m_X, pAnim->GetBody()->m_Y) * AnimScale; + IGraphics::CQuadItem BodyItem(BodyPos.x, BodyPos.y, BaseSize, BaseSize); + IGraphics::CQuadItem BotItem(BodyPos.x + (2.f / 3.f) * AnimScale, BodyPos.y + (-16 + 2.f / 3.f) * AnimScale, BaseSize, BaseSize); // x+0.66, y+0.66 to correct some rendering bug + IGraphics::CQuadItem Item; + + // draw bot visuals (background) + if(IsBot && !OutLine) + { + Graphics()->TextureSet(pInfo->m_Sixup.m_BotTexture); + Graphics()->QuadsBegin(); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + SelectSprite7(client_data7::SPRITE_TEE_BOT_BACKGROUND, 0, 0, 0); + Item = BotItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + } + + // draw bot visuals (foreground) + if(IsBot && !OutLine) + { + Graphics()->TextureSet(pInfo->m_Sixup.m_BotTexture); + Graphics()->QuadsBegin(); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + SelectSprite7(client_data7::SPRITE_TEE_BOT_FOREGROUND, 0, 0, 0); + Item = BotItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->SetColor(pInfo->m_Sixup.m_BotColor.r, pInfo->m_Sixup.m_BotColor.g, pInfo->m_Sixup.m_BotColor.b, pInfo->m_Sixup.m_BotColor.a); + SelectSprite7(client_data7::SPRITE_TEE_BOT_GLOW, 0, 0, 0); + Item = BotItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + } + + // draw decoration + if(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_DECORATION].IsValid()) + { + Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_DECORATION]); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION].r, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION].g, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION].b, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION].a); + SelectSprite7(OutLine ? client_data7::SPRITE_TEE_DECORATION_OUTLINE : client_data7::SPRITE_TEE_DECORATION, 0, 0, 0); + Item = BodyItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + } + + // draw body (behind marking) + Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_BODY]); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + if(OutLine) + { + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + SelectSprite7(client_data7::SPRITE_TEE_BODY_OUTLINE, 0, 0, 0); + } + else + { + Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY].r, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY].g, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY].b, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY].a); + SelectSprite7(client_data7::SPRITE_TEE_BODY, 0, 0, 0); + } + Item = BodyItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + + // draw marking + if(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_MARKING].IsValid() && !OutLine) + { + Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_MARKING]); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].r * pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].a, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].g * pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].a, + pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].b * pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].a, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].a); + SelectSprite7(client_data7::SPRITE_TEE_MARKING, 0, 0, 0); + Item = BodyItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + } + + // draw body (in front of marking) + if(!OutLine) + { + Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_BODY]); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + for(int t = 0; t < 2; t++) + { + SelectSprite7(t == 0 ? client_data7::SPRITE_TEE_BODY_SHADOW : client_data7::SPRITE_TEE_BODY_UPPER_OUTLINE, 0, 0, 0); + Item = BodyItem; + Graphics()->QuadsDraw(&Item, 1); + } + Graphics()->QuadsEnd(); + } + + // draw eyes + Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_EYES]); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + if(IsBot) + { + Graphics()->SetColor(pInfo->m_Sixup.m_BotColor.r, pInfo->m_Sixup.m_BotColor.g, pInfo->m_Sixup.m_BotColor.b, pInfo->m_Sixup.m_BotColor.a); + Emote = EMOTE_SURPRISE; + } + else + Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES].r, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES].g, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES].b, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES].a); + if(Pass == 1) + { + switch(Emote) + { + case EMOTE_PAIN: + SelectSprite7(client_data7::SPRITE_TEE_EYES_PAIN, 0, 0, 0); + break; + case EMOTE_HAPPY: + SelectSprite7(client_data7::SPRITE_TEE_EYES_HAPPY, 0, 0, 0); + break; + case EMOTE_SURPRISE: + SelectSprite7(client_data7::SPRITE_TEE_EYES_SURPRISE, 0, 0, 0); + break; + case EMOTE_ANGRY: + SelectSprite7(client_data7::SPRITE_TEE_EYES_ANGRY, 0, 0, 0); + break; + default: + SelectSprite7(client_data7::SPRITE_TEE_EYES_NORMAL, 0, 0, 0); + break; + } + + float EyeScale = BaseSize * 0.60f; + float h = Emote == EMOTE_BLINK ? BaseSize * 0.15f / 2.0f : EyeScale / 2.0f; + vec2 Offset = vec2(Direction.x * 0.125f, -0.05f + Direction.y * 0.10f) * BaseSize; + IGraphics::CQuadItem QuadItem(BodyPos.x + Offset.x, BodyPos.y + Offset.y, EyeScale, h); + Graphics()->QuadsDraw(&QuadItem, 1); + } + Graphics()->QuadsEnd(); + + // TODO: ddnet has own hats + // draw xmas hat + // if(!OutLine && pInfo->m_HatTexture.IsValid()) + // { + // Graphics()->TextureSet(pInfo->m_HatTexture); + // Graphics()->QuadsBegin(); + // Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle*pi * 2); + // Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + // int Flag = Direction.x < 0.0f ? SPRITE_FLAG_FLIP_X : 0; + // switch(pInfo->m_HatSpriteIndex) + // { + // case 0: + // SelectSprite7(SPRITE_TEE_HATS_TOP1, Flag, 0, 0); + // break; + // case 1: + // SelectSprite7(SPRITE_TEE_HATS_TOP2, Flag, 0, 0); + // break; + // case 2: + // SelectSprite7(SPRITE_TEE_HATS_SIDE1, Flag, 0, 0); + // break; + // case 3: + // SelectSprite7(SPRITE_TEE_HATS_SIDE2, Flag, 0, 0); + // } + // Item = BodyItem; + // Graphics()->QuadsDraw(&Item, 1); + // Graphics()->QuadsEnd(); + // } + } + + // draw feet + Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_FEET]); + Graphics()->QuadsBegin(); + const CAnimKeyframe *pFoot = Filling ? pAnim->GetFrontFoot() : pAnim->GetBackFoot(); + + float w = BaseSize / 2.1f; + float h = w; + + Graphics()->QuadsSetRotation(pFoot->m_Angle * pi * 2); + + if(OutLine) + { + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + SelectSprite7(client_data7::SPRITE_TEE_FOOT_OUTLINE, 0, 0, 0); + } + else + { + bool Indicate = !pInfo->m_GotAirJump && g_Config.m_ClAirjumpindicator; + float ColorScale = 1.0f; + if(Indicate) + ColorScale = 0.5f; + Graphics()->SetColor( + pInfo->m_Sixup.m_aColors[protocol7::SKINPART_FEET].r * ColorScale, + pInfo->m_Sixup.m_aColors[protocol7::SKINPART_FEET].g * ColorScale, + pInfo->m_Sixup.m_aColors[protocol7::SKINPART_FEET].b * ColorScale, + pInfo->m_Sixup.m_aColors[protocol7::SKINPART_FEET].a); + SelectSprite7(client_data7::SPRITE_TEE_FOOT, 0, 0, 0); + } + + IGraphics::CQuadItem QuadItem(Position.x + pFoot->m_X * AnimScale, Position.y + pFoot->m_Y * AnimScale, w, h); + Graphics()->QuadsDraw(&QuadItem, 1); + Graphics()->QuadsEnd(); + } + } +} + +void CRenderTools::RenderTee6(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha) const { vec2 Direction = Dir; vec2 Position = Pos; @@ -271,15 +507,15 @@ void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInf // first pass we draw the outline // second pass we draw the filling - for(int p = 0; p < 2; p++) + for(int Pass = 0; Pass < 2; Pass++) { - int OutLine = p == 0 ? 1 : 0; + int OutLine = Pass == 0 ? 1 : 0; - for(int f = 0; f < 2; f++) + for(int Filling = 0; Filling < 2; Filling++) { float AnimScale, BaseSize; GetRenderTeeAnimScaleAndBaseSize(pInfo, AnimScale, BaseSize); - if(f == 1) + if(Filling == 1) { Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); @@ -292,7 +528,7 @@ void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInf Graphics()->RenderQuadContainerAsSprite(m_TeeQuadContainerIndex, OutLine, BodyPos.x, BodyPos.y, BodyScale, BodyScale); // draw eyes - if(p == 1) + if(Pass == 1) { int QuadOffset = 2; int EyeQuadOffset = 0; @@ -333,7 +569,7 @@ void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInf } // draw feet - const CAnimKeyframe *pFoot = f ? pAnim->GetFrontFoot() : pAnim->GetBackFoot(); + const CAnimKeyframe *pFoot = Filling ? pAnim->GetFrontFoot() : pAnim->GetBackFoot(); float w = BaseSize; float h = BaseSize / 2; @@ -362,9 +598,6 @@ void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInf Graphics()->RenderQuadContainerAsSprite(m_TeeQuadContainerIndex, QuadOffset, Position.x + pFoot->m_X * AnimScale, Position.y + pFoot->m_Y * AnimScale, w / 64.f, h / 32.f); } } - - Graphics()->SetColor(1.f, 1.f, 1.f, 1.f); - Graphics()->QuadsSetRotation(0); } void CRenderTools::CalcScreenParams(float Aspect, float Zoom, float *pWidth, float *pHeight) diff --git a/src/game/client/render.h b/src/game/client/render.h index 0bbbbbacb..ed5a31a9f 100644 --- a/src/game/client/render.h +++ b/src/game/client/render.h @@ -8,6 +8,7 @@ #include #include +#include class CAnimState; class CSpeedupTile; @@ -25,6 +26,8 @@ struct CEnvPointBezier_upstream; struct CMapItemGroup; struct CQuad; +#include + class CTeeRenderInfo { public: @@ -46,6 +49,8 @@ public: m_GotAirJump = true; m_TeeRenderFlags = 0; m_FeetFlipped = false; + + m_Sixup.Reset(); } CSkin::SSkinTextures m_OriginalRenderSkin; @@ -67,6 +72,31 @@ public: { return m_CustomColoredSkin ? m_ColorableRenderSkin.m_Body.IsValid() : m_OriginalRenderSkin.m_Body.IsValid(); } + + class CSixup + { + public: + void Reset() + { + for(auto &Texture : m_aTextures) + Texture = IGraphics::CTextureHandle(); + m_BotTexture = IGraphics::CTextureHandle(); + } + bool Valid() const + { + for(const auto &Texture : m_aTextures) + if(!Texture.IsValid()) + return false; + return true; + } + + IGraphics::CTextureHandle m_aTextures[protocol7::NUM_SKINPARTS]; + vec4 m_aColors[protocol7::NUM_SKINPARTS]; + IGraphics::CTextureHandle m_BotTexture; + vec4 m_BotColor; + }; + + CSixup m_Sixup; }; // Tee Render Flags @@ -129,6 +159,9 @@ class CRenderTools static void GetRenderTeeBodyScale(float BaseSize, float &BodyScale); static void GetRenderTeeFeetScale(float BaseSize, float &FeetScaleWidth, float &FeetScaleHeight); + void RenderTee6(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha = 1.0f) const; + void RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos) const; + public: class IGraphics *Graphics() const { return m_pGraphics; } class ITextRender *TextRender() const { return m_pTextRender; } @@ -137,6 +170,7 @@ public: 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 SelectSprite7(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; diff --git a/src/game/client/sixup_translate_game.cpp b/src/game/client/sixup_translate_game.cpp new file mode 100644 index 000000000..bd918b33d --- /dev/null +++ b/src/game/client/sixup_translate_game.cpp @@ -0,0 +1,689 @@ +#include + +#include + +enum +{ + STR_TEAM_GAME, + STR_TEAM_RED, + STR_TEAM_BLUE, + STR_TEAM_SPECTATORS, +}; + +static int GetStrTeam7(int Team, bool Teamplay) +{ + if(Teamplay) + { + if(Team == TEAM_RED) + return STR_TEAM_RED; + else if(Team == TEAM_BLUE) + return STR_TEAM_BLUE; + } + else if(Team == 0) + return STR_TEAM_GAME; + + return STR_TEAM_SPECTATORS; +} + +enum +{ + DO_CHAT = 0, + DO_BROADCAST, + DO_SPECIAL, + + PARA_NONE = 0, + PARA_I, + PARA_II, + PARA_III, +}; + +struct CGameMsg7 +{ + int m_Action; + int m_ParaType; + const char *m_pText; +}; + +static CGameMsg7 gs_GameMsgList7[protocol7::NUM_GAMEMSGS] = { + {/*GAMEMSG_TEAM_SWAP*/ DO_CHAT, PARA_NONE, "Teams were swapped"}, // Localize("Teams were swapped") + {/*GAMEMSG_SPEC_INVALIDID*/ DO_CHAT, PARA_NONE, "Invalid spectator id used"}, //! + {/*GAMEMSG_TEAM_SHUFFLE*/ DO_CHAT, PARA_NONE, "Teams were shuffled"}, // Localize("Teams were shuffled") + {/*GAMEMSG_TEAM_BALANCE*/ DO_CHAT, PARA_NONE, "Teams have been balanced"}, // Localize("Teams have been balanced") + {/*GAMEMSG_CTF_DROP*/ DO_SPECIAL, PARA_NONE, ""}, // special - play ctf drop sound + {/*GAMEMSG_CTF_RETURN*/ DO_SPECIAL, PARA_NONE, ""}, // special - play ctf return sound + + {/*GAMEMSG_TEAM_ALL*/ DO_SPECIAL, PARA_I, ""}, // special - add team name + {/*GAMEMSG_TEAM_BALANCE_VICTIM*/ DO_SPECIAL, PARA_I, ""}, // special - add team name + {/*GAMEMSG_CTF_GRAB*/ DO_SPECIAL, PARA_I, ""}, // special - play ctf grab sound based on team + + {/*GAMEMSG_CTF_CAPTURE*/ DO_SPECIAL, PARA_III, ""}, // special - play ctf capture sound + capture chat message + + {/*GAMEMSG_GAME_PAUSED*/ DO_SPECIAL, PARA_I, ""}, // special - add player name +}; + +void CGameClient::DoTeamChangeMessage7(const char *pName, int ClientId, int Team, const char *pPrefix) +{ + char aBuf[128]; + switch(GetStrTeam7(Team, m_pClient->m_TranslationContext.m_GameFlags & protocol7::GAMEFLAG_TEAMS)) + { + case STR_TEAM_GAME: str_format(aBuf, sizeof(aBuf), Localize("'%s' %sjoined the game"), pName, pPrefix); break; + case STR_TEAM_RED: str_format(aBuf, sizeof(aBuf), Localize("'%s' %sjoined the red team"), pName, pPrefix); break; + case STR_TEAM_BLUE: str_format(aBuf, sizeof(aBuf), Localize("'%s' %sjoined the blue team"), pName, pPrefix); break; + case STR_TEAM_SPECTATORS: str_format(aBuf, sizeof(aBuf), Localize("'%s' %sjoined the spectators"), pName, pPrefix); break; + } + m_Chat.AddLine(-1, 0, aBuf); +} + +void *CGameClient::TranslateGameMsg(int *pMsgId, CUnpacker *pUnpacker, int Conn) +{ + if(!m_pClient->IsSixup()) + { + return m_NetObjHandler.SecureUnpackMsg(*pMsgId, pUnpacker); + } + + void *pRawMsg = m_NetObjHandler7.SecureUnpackMsg(*pMsgId, pUnpacker); + if(!pRawMsg) + { + if(*pMsgId > __NETMSGTYPE_UUID_HELPER && *pMsgId < OFFSET_MAPITEMTYPE_UUID) + { + void *pDDNetExMsg = m_NetObjHandler.SecureUnpackMsg(*pMsgId, pUnpacker); + if(pDDNetExMsg) + return pDDNetExMsg; + } + + dbg_msg("sixup", "dropped weird message '%s' (%d), failed on '%s'", m_NetObjHandler7.GetMsgName(*pMsgId), *pMsgId, m_NetObjHandler7.FailedMsgOn()); + return nullptr; + } + static char s_aRawMsg[1024]; + + if(*pMsgId == protocol7::NETMSGTYPE_SV_MOTD) + { + protocol7::CNetMsg_Sv_Motd *pMsg7 = (protocol7::CNetMsg_Sv_Motd *)pRawMsg; + ::CNetMsg_Sv_Motd *pMsg = (::CNetMsg_Sv_Motd *)s_aRawMsg; + + pMsg->m_pMessage = pMsg7->m_pMessage; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_BROADCAST) + { + protocol7::CNetMsg_Sv_Broadcast *pMsg7 = (protocol7::CNetMsg_Sv_Broadcast *)pRawMsg; + ::CNetMsg_Sv_Broadcast *pMsg = (::CNetMsg_Sv_Broadcast *)s_aRawMsg; + + pMsg->m_pMessage = pMsg7->m_pMessage; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_CL_SETTEAM) + { + protocol7::CNetMsg_Cl_SetTeam *pMsg7 = (protocol7::CNetMsg_Cl_SetTeam *)pRawMsg; + ::CNetMsg_Cl_SetTeam *pMsg = (::CNetMsg_Cl_SetTeam *)s_aRawMsg; + + pMsg->m_Team = pMsg7->m_Team; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_TEAM) + { + protocol7::CNetMsg_Sv_Team *pMsg7 = (protocol7::CNetMsg_Sv_Team *)pRawMsg; + + if(Client()->State() != IClient::STATE_DEMOPLAYBACK) + { + m_aClients[pMsg7->m_ClientId].m_Team = pMsg7->m_Team; + m_pClient->m_TranslationContext.m_aClients[pMsg7->m_ClientId].m_Team = pMsg7->m_Team; + m_aClients[pMsg7->m_ClientId].UpdateRenderInfo(IsTeamPlay()); + + // if(pMsg7->m_ClientId == m_LocalClientId) + // { + // m_TeamCooldownTick = pMsg7->m_CooldownTick; + // m_TeamChangeTime = Client()->LocalTime(); + // } + } + + if(Conn != g_Config.m_ClDummy) + return nullptr; + + if(pMsg7->m_Silent == 0) + { + DoTeamChangeMessage7(m_aClients[pMsg7->m_ClientId].m_aName, pMsg7->m_ClientId, pMsg7->m_Team); + } + + // we drop the message and add the new team + // info to the playerinfo snap item + // using translation context + return nullptr; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_WEAPONPICKUP) + { + protocol7::CNetMsg_Sv_WeaponPickup *pMsg7 = (protocol7::CNetMsg_Sv_WeaponPickup *)pRawMsg; + ::CNetMsg_Sv_WeaponPickup *pMsg = (::CNetMsg_Sv_WeaponPickup *)s_aRawMsg; + + pMsg->m_Weapon = pMsg7->m_Weapon; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_SERVERSETTINGS) + { + // 0.7 only message for ui enrichment like locked teams + protocol7::CNetMsg_Sv_ServerSettings *pMsg = (protocol7::CNetMsg_Sv_ServerSettings *)pRawMsg; + + if(!m_pClient->m_TranslationContext.m_ServerSettings.m_TeamLock && pMsg->m_TeamLock) + m_Chat.AddLine(-1, 0, Localize("Teams were locked")); + else if(m_pClient->m_TranslationContext.m_ServerSettings.m_TeamLock && !pMsg->m_TeamLock) + m_Chat.AddLine(-1, 0, Localize("Teams were unlocked")); + + m_pClient->m_TranslationContext.m_ServerSettings.m_KickVote = pMsg->m_KickVote; + m_pClient->m_TranslationContext.m_ServerSettings.m_KickMin = pMsg->m_KickMin; + m_pClient->m_TranslationContext.m_ServerSettings.m_SpecVote = pMsg->m_SpecVote; + m_pClient->m_TranslationContext.m_ServerSettings.m_TeamLock = pMsg->m_TeamLock; + m_pClient->m_TranslationContext.m_ServerSettings.m_TeamBalance = pMsg->m_TeamBalance; + m_pClient->m_TranslationContext.m_ServerSettings.m_PlayerSlots = pMsg->m_PlayerSlots; + return nullptr; // There is no 0.6 equivalent + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_RACEFINISH) + { + *pMsgId = NETMSGTYPE_SV_RACEFINISH; + protocol7::CNetMsg_Sv_RaceFinish *pMsg7 = (protocol7::CNetMsg_Sv_RaceFinish *)pRawMsg; + ::CNetMsg_Sv_RaceFinish *pMsg = (::CNetMsg_Sv_RaceFinish *)s_aRawMsg; + + pMsg->m_ClientId = pMsg7->m_ClientId; + pMsg->m_Time = pMsg7->m_Time; + pMsg->m_Diff = pMsg7->m_Diff; + pMsg->m_RecordPersonal = pMsg7->m_RecordPersonal; + pMsg->m_RecordServer = pMsg7->m_RecordServer; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_COMMANDINFOREMOVE) + { + *pMsgId = NETMSGTYPE_SV_COMMANDINFOREMOVE; + protocol7::CNetMsg_Sv_CommandInfoRemove *pMsg7 = (protocol7::CNetMsg_Sv_CommandInfoRemove *)pRawMsg; + ::CNetMsg_Sv_CommandInfoRemove *pMsg = (::CNetMsg_Sv_CommandInfoRemove *)s_aRawMsg; + + pMsg->m_pName = pMsg7->m_pName; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_COMMANDINFO) + { + *pMsgId = NETMSGTYPE_SV_COMMANDINFO; + protocol7::CNetMsg_Sv_CommandInfo *pMsg7 = (protocol7::CNetMsg_Sv_CommandInfo *)pRawMsg; + ::CNetMsg_Sv_CommandInfo *pMsg = (::CNetMsg_Sv_CommandInfo *)s_aRawMsg; + + pMsg->m_pName = pMsg7->m_pName; + pMsg->m_pArgsFormat = pMsg7->m_pArgsFormat; + pMsg->m_pHelpText = pMsg7->m_pHelpText; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_SKINCHANGE) + { + protocol7::CNetMsg_Sv_SkinChange *pMsg7 = (protocol7::CNetMsg_Sv_SkinChange *)pRawMsg; + + if(pMsg7->m_ClientId < 0 || pMsg7->m_ClientId >= MAX_CLIENTS) + { + dbg_msg("sixup", "Sv_SkinChange got invalid ClientId: %d", pMsg7->m_ClientId); + return nullptr; + } + + CTranslationContext::CClientData &Client = m_pClient->m_TranslationContext.m_aClients[pMsg7->m_ClientId]; + Client.m_Active = true; + CClientData *pClient = &m_aClients[pMsg7->m_ClientId]; + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) + { + int Id = m_Skins7.FindSkinPart(p, pMsg7->m_apSkinPartNames[p], false); + const CSkins7::CSkinPart *pSkinPart = m_Skins7.GetSkinPart(p, Id); + if(pMsg7->m_aUseCustomColors[p]) + { + pClient->m_SkinInfo.m_Sixup.m_aTextures[p] = pSkinPart->m_ColorTexture; + pClient->m_SkinInfo.m_Sixup.m_aColors[p] = m_Skins7.GetColor(pMsg7->m_aSkinPartColors[p], p == protocol7::SKINPART_MARKING); + } + else + { + pClient->m_SkinInfo.m_Sixup.m_aTextures[p] = pSkinPart->m_OrgTexture; + pClient->m_SkinInfo.m_Sixup.m_aColors[p] = vec4(1.0f, 1.0f, 1.0f, 1.0f); + } + } + // skin will be moved to the 0.6 snap by the translation context + // and we drop the game message + return nullptr; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTECLEAROPTIONS) + { + *pMsgId = NETMSGTYPE_SV_VOTECLEAROPTIONS; + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTEOPTIONADD) + { + *pMsgId = NETMSGTYPE_SV_VOTEOPTIONADD; + protocol7::CNetMsg_Sv_VoteOptionAdd *pMsg7 = (protocol7::CNetMsg_Sv_VoteOptionAdd *)pRawMsg; + ::CNetMsg_Sv_VoteOptionAdd *pMsg = (::CNetMsg_Sv_VoteOptionAdd *)s_aRawMsg; + pMsg->m_pDescription = pMsg7->m_pDescription; + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTEOPTIONREMOVE) + { + *pMsgId = NETMSGTYPE_SV_VOTEOPTIONREMOVE; + protocol7::CNetMsg_Sv_VoteOptionRemove *pMsg7 = (protocol7::CNetMsg_Sv_VoteOptionRemove *)pRawMsg; + ::CNetMsg_Sv_VoteOptionRemove *pMsg = (::CNetMsg_Sv_VoteOptionRemove *)s_aRawMsg; + pMsg->m_pDescription = pMsg7->m_pDescription; + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTEOPTIONLISTADD) + { + ::CNetMsg_Sv_VoteOptionListAdd *pMsg = (::CNetMsg_Sv_VoteOptionListAdd *)s_aRawMsg; + int NumOptions = pUnpacker->GetInt(); + if(NumOptions > 14) + { + for(int i = 0; i < NumOptions; i++) + { + const char *pDescription = pUnpacker->GetString(CUnpacker::SANITIZE_CC); + if(pUnpacker->Error()) + continue; + + m_Voting.AddOption(pDescription); + } + // 0.7 can send more vote options than + // the 0.6 protocol fit + // in that case we do not translate it but just + // reimplement what 0.6 would do + return nullptr; + } + pMsg->m_NumOptions = 0; + for(int i = 0; i < NumOptions; i++) + { + const char *pDescription = pUnpacker->GetString(CUnpacker::SANITIZE_CC); + if(pUnpacker->Error()) + continue; + + pMsg->m_NumOptions++; + switch(i) + { + case 0: (pMsg->m_pDescription0 = pDescription); break; + case 1: (pMsg->m_pDescription1 = pDescription); break; + case 2: (pMsg->m_pDescription2 = pDescription); break; + case 3: (pMsg->m_pDescription3 = pDescription); break; + case 4: (pMsg->m_pDescription4 = pDescription); break; + case 5: (pMsg->m_pDescription5 = pDescription); break; + case 6: (pMsg->m_pDescription6 = pDescription); break; + case 7: (pMsg->m_pDescription7 = pDescription); break; + case 8: (pMsg->m_pDescription8 = pDescription); break; + case 9: (pMsg->m_pDescription9 = pDescription); break; + case 10: (pMsg->m_pDescription10 = pDescription); break; + case 11: (pMsg->m_pDescription11 = pDescription); break; + case 12: (pMsg->m_pDescription12 = pDescription); break; + case 13: (pMsg->m_pDescription13 = pDescription); break; + case 14: (pMsg->m_pDescription14 = pDescription); + } + } + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTESET) + { + *pMsgId = NETMSGTYPE_SV_VOTESET; + protocol7::CNetMsg_Sv_VoteSet *pMsg7 = (protocol7::CNetMsg_Sv_VoteSet *)pRawMsg; + ::CNetMsg_Sv_VoteSet *pMsg = (::CNetMsg_Sv_VoteSet *)s_aRawMsg; + + pMsg->m_Timeout = pMsg7->m_Timeout; + pMsg->m_pDescription = pMsg7->m_pDescription; + pMsg->m_pReason = pMsg7->m_pReason; + + char aBuf[128]; + if(pMsg7->m_Timeout) + { + if(pMsg7->m_ClientId != -1) + { + const char *pName = m_aClients[pMsg7->m_ClientId].m_aName; + switch(pMsg7->m_Type) + { + case protocol7::VOTE_START_OP: + str_format(aBuf, sizeof(aBuf), Localize("'%s' called vote to change server option '%s' (%s)"), pName, pMsg7->m_pDescription, pMsg7->m_pReason); + m_Chat.AddLine(-1, 0, aBuf); + break; + case protocol7::VOTE_START_KICK: + { + str_format(aBuf, sizeof(aBuf), Localize("'%s' called for vote to kick '%s' (%s)"), pName, pMsg7->m_pDescription, pMsg7->m_pReason); + m_Chat.AddLine(-1, 0, aBuf); + break; + } + case protocol7::VOTE_START_SPEC: + { + str_format(aBuf, sizeof(aBuf), Localize("'%s' called for vote to move '%s' to spectators (%s)"), pName, pMsg7->m_pDescription, pMsg7->m_pReason); + m_Chat.AddLine(-1, 0, aBuf); + } + } + } + } + else + { + switch(pMsg7->m_Type) + { + case protocol7::VOTE_START_OP: + str_format(aBuf, sizeof(aBuf), Localize("Admin forced server option '%s' (%s)"), pMsg7->m_pDescription, pMsg7->m_pReason); + m_Chat.AddLine(-1, 0, aBuf); + break; + case protocol7::VOTE_START_SPEC: + str_format(aBuf, sizeof(aBuf), Localize("Admin moved '%s' to spectator (%s)"), pMsg7->m_pDescription, pMsg7->m_pReason); + m_Chat.AddLine(-1, 0, aBuf); + break; + case protocol7::VOTE_END_ABORT: + m_Voting.OnReset(); + m_Chat.AddLine(-1, 0, Localize("Vote aborted")); + break; + case protocol7::VOTE_END_PASS: + m_Voting.OnReset(); + m_Chat.AddLine(-1, 0, pMsg7->m_ClientId == -1 ? Localize("Admin forced vote yes") : Localize("Vote passed")); + break; + case protocol7::VOTE_END_FAIL: + m_Voting.OnReset(); + m_Chat.AddLine(-1, 0, pMsg7->m_ClientId == -1 ? Localize("Admin forced vote no") : Localize("Vote failed")); + } + } + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_VOTESTATUS) + { + *pMsgId = NETMSGTYPE_SV_VOTESTATUS; + protocol7::CNetMsg_Sv_VoteStatus *pMsg7 = (protocol7::CNetMsg_Sv_VoteStatus *)pRawMsg; + ::CNetMsg_Sv_VoteStatus *pMsg = (::CNetMsg_Sv_VoteStatus *)s_aRawMsg; + + pMsg->m_Yes = pMsg7->m_Yes; + pMsg->m_No = pMsg7->m_No; + pMsg->m_Pass = pMsg7->m_Pass; + pMsg->m_Total = pMsg7->m_Total; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_READYTOENTER) + { + *pMsgId = NETMSGTYPE_SV_READYTOENTER; + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_CLIENTDROP) + { + protocol7::CNetMsg_Sv_ClientDrop *pMsg7 = (protocol7::CNetMsg_Sv_ClientDrop *)pRawMsg; + if(pMsg7->m_ClientId < 0 || pMsg7->m_ClientId >= MAX_CLIENTS) + { + dbg_msg("sixup", "Sv_ClientDrop got invalid ClientId: %d", pMsg7->m_ClientId); + return nullptr; + } + CTranslationContext::CClientData &Client = m_pClient->m_TranslationContext.m_aClients[pMsg7->m_ClientId]; + Client.Reset(); + + if(pMsg7->m_Silent) + return nullptr; + + if(Conn != g_Config.m_ClDummy) + return nullptr; + + static char s_aBuf[128]; + if(pMsg7->m_pReason[0]) + str_format(s_aBuf, sizeof(s_aBuf), "'%s' has left the game (%s)", m_aClients[pMsg7->m_ClientId].m_aName, pMsg7->m_pReason); + else + str_format(s_aBuf, sizeof(s_aBuf), "'%s' has left the game", m_aClients[pMsg7->m_ClientId].m_aName); + m_Chat.AddLine(-1, 0, s_aBuf); + + return nullptr; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_CLIENTINFO) + { + protocol7::CNetMsg_Sv_ClientInfo *pMsg7 = (protocol7::CNetMsg_Sv_ClientInfo *)pRawMsg; + if(pMsg7->m_ClientId < 0 || pMsg7->m_ClientId >= MAX_CLIENTS) + { + dbg_msg("sixup", "Sv_ClientInfo got invalid ClientId: %d", pMsg7->m_ClientId); + return nullptr; + } + + if(pMsg7->m_Local) + { + m_pClient->m_TranslationContext.m_aLocalClientId[Conn] = pMsg7->m_ClientId; + } + CTranslationContext::CClientData &Client = m_pClient->m_TranslationContext.m_aClients[pMsg7->m_ClientId]; + Client.m_Active = true; + Client.m_Team = pMsg7->m_Team; + str_copy(Client.m_aName, pMsg7->m_pName); + str_copy(Client.m_aClan, pMsg7->m_pClan); + Client.m_Country = pMsg7->m_Country; + CClientData *pClient = &m_aClients[pMsg7->m_ClientId]; + for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) + { + int Id = m_Skins7.FindSkinPart(p, pMsg7->m_apSkinPartNames[p], false); + const CSkins7::CSkinPart *pSkinPart = m_Skins7.GetSkinPart(p, Id); + if(pMsg7->m_aUseCustomColors[p]) + { + pClient->m_SkinInfo.m_Sixup.m_aTextures[p] = pSkinPart->m_ColorTexture; + pClient->m_SkinInfo.m_Sixup.m_aColors[p] = m_Skins7.GetColor(pMsg7->m_aSkinPartColors[p], p == protocol7::SKINPART_MARKING); + } + else + { + pClient->m_SkinInfo.m_Sixup.m_aTextures[p] = pSkinPart->m_OrgTexture; + pClient->m_SkinInfo.m_Sixup.m_aColors[p] = vec4(1.0f, 1.0f, 1.0f, 1.0f); + } + } + + if(m_pClient->m_TranslationContext.m_aLocalClientId[Conn] == -1) + return nullptr; + if(pMsg7->m_Silent || pMsg7->m_Local) + return nullptr; + + if(Conn != g_Config.m_ClDummy) + return nullptr; + + DoTeamChangeMessage7( + pMsg7->m_pName, + pMsg7->m_ClientId, + pMsg7->m_Team, + "entered and "); + + return nullptr; // we only do side effects and add stuff to the snap here + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_GAMEINFO) + { + protocol7::CNetMsg_Sv_GameInfo *pMsg7 = (protocol7::CNetMsg_Sv_GameInfo *)pRawMsg; + m_pClient->m_TranslationContext.m_GameFlags = pMsg7->m_GameFlags; + m_pClient->m_TranslationContext.m_ScoreLimit = pMsg7->m_ScoreLimit; + m_pClient->m_TranslationContext.m_TimeLimit = pMsg7->m_TimeLimit; + m_pClient->m_TranslationContext.m_MatchNum = pMsg7->m_MatchNum; + m_pClient->m_TranslationContext.m_MatchCurrent = pMsg7->m_MatchCurrent; + m_pClient->m_TranslationContext.m_ShouldSendGameInfo = true; + return nullptr; // Added to snap by translation context + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_EMOTICON) + { + *pMsgId = NETMSGTYPE_SV_EMOTICON; + protocol7::CNetMsg_Sv_Emoticon *pMsg7 = (protocol7::CNetMsg_Sv_Emoticon *)pRawMsg; + ::CNetMsg_Sv_Emoticon *pMsg = (::CNetMsg_Sv_Emoticon *)s_aRawMsg; + + pMsg->m_ClientId = pMsg7->m_ClientId; + pMsg->m_Emoticon = pMsg7->m_Emoticon; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_KILLMSG) + { + *pMsgId = NETMSGTYPE_SV_KILLMSG; + + protocol7::CNetMsg_Sv_KillMsg *pMsg7 = (protocol7::CNetMsg_Sv_KillMsg *)pRawMsg; + ::CNetMsg_Sv_KillMsg *pMsg = (::CNetMsg_Sv_KillMsg *)s_aRawMsg; + + pMsg->m_Killer = pMsg7->m_Killer; + pMsg->m_Victim = pMsg7->m_Victim; + pMsg->m_Weapon = pMsg7->m_Weapon; + pMsg->m_ModeSpecial = pMsg7->m_ModeSpecial; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_CHAT) + { + *pMsgId = NETMSGTYPE_SV_CHAT; + + protocol7::CNetMsg_Sv_Chat *pMsg7 = (protocol7::CNetMsg_Sv_Chat *)pRawMsg; + ::CNetMsg_Sv_Chat *pMsg = (::CNetMsg_Sv_Chat *)s_aRawMsg; + + if(pMsg7->m_Mode == protocol7::CHAT_WHISPER) + { + bool Receive = pMsg7->m_TargetId == m_pClient->m_TranslationContext.m_aLocalClientId[Conn]; + + pMsg->m_Team = Receive ? 3 : 2; + pMsg->m_ClientId = Receive ? pMsg7->m_ClientId : pMsg7->m_TargetId; + } + else + { + pMsg->m_Team = pMsg7->m_Mode == protocol7::CHAT_TEAM ? 1 : 0; + pMsg->m_ClientId = pMsg7->m_ClientId; + } + + pMsg->m_pMessage = pMsg7->m_pMessage; + + return s_aRawMsg; + } + else if(*pMsgId == protocol7::NETMSGTYPE_SV_GAMEMSG) + { + int GameMsgId = pUnpacker->GetInt(); + + /** + * Prints chat message only once + * even if it is being sent to main tee and dummy + */ + auto SendChat = [Conn, GameMsgId, this](const char *pText) -> void { + if(GameMsgId != protocol7::GAMEMSG_TEAM_BALANCE_VICTIM && GameMsgId != protocol7::GAMEMSG_SPEC_INVALIDID) + { + if(Conn != g_Config.m_ClDummy) + return; + } + m_Chat.AddLine(-1, 0, pText); + }; + + // check for valid gamemsgid + if(GameMsgId < 0 || GameMsgId >= protocol7::NUM_GAMEMSGS) + return nullptr; + + int aParaI[3] = {0}; + int NumParaI = 0; + + // get paras + switch(gs_GameMsgList7[GameMsgId].m_ParaType) + { + case PARA_I: NumParaI = 1; break; + case PARA_II: NumParaI = 2; break; + case PARA_III: NumParaI = 3; break; + } + for(int i = 0; i < NumParaI; i++) + { + aParaI[i] = pUnpacker->GetInt(); + } + + // check for unpacking errors + if(pUnpacker->Error()) + return nullptr; + + // handle special messages + char aBuf[256]; + bool TeamPlay = m_pClient->m_TranslationContext.m_GameFlags & protocol7::GAMEFLAG_TEAMS; + if(gs_GameMsgList7[GameMsgId].m_Action == DO_SPECIAL) + { + switch(GameMsgId) + { + case protocol7::GAMEMSG_CTF_DROP: + m_Sounds.Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_DROP); + break; + case protocol7::GAMEMSG_CTF_RETURN: + m_Sounds.Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_RETURN); + break; + case protocol7::GAMEMSG_TEAM_ALL: + { + const char *pMsg = ""; + switch(GetStrTeam7(aParaI[0], TeamPlay)) + { + case STR_TEAM_GAME: pMsg = Localize("All players were moved to the game"); break; + case STR_TEAM_RED: pMsg = Localize("All players were moved to the red team"); break; + case STR_TEAM_BLUE: pMsg = Localize("All players were moved to the blue team"); break; + case STR_TEAM_SPECTATORS: pMsg = Localize("All players were moved to the spectators"); break; + } + m_Broadcast.DoBroadcast(pMsg); // client side broadcast + } + break; + case protocol7::GAMEMSG_TEAM_BALANCE_VICTIM: + { + const char *pMsg = ""; + switch(GetStrTeam7(aParaI[0], TeamPlay)) + { + case STR_TEAM_RED: pMsg = Localize("You were moved to the red team due to team balancing"); break; + case STR_TEAM_BLUE: pMsg = Localize("You were moved to the blue team due to team balancing"); break; + } + m_Broadcast.DoBroadcast(pMsg); // client side broadcast + } + break; + case protocol7::GAMEMSG_CTF_GRAB: + m_Sounds.Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_GRAB_EN); + break; + case protocol7::GAMEMSG_GAME_PAUSED: + { + int ClientId = clamp(aParaI[0], 0, MAX_CLIENTS - 1); + str_format(aBuf, sizeof(aBuf), Localize("'%s' initiated a pause"), m_aClients[ClientId].m_aName); + SendChat(aBuf); + } + break; + case protocol7::GAMEMSG_CTF_CAPTURE: + m_Sounds.Enqueue(CSounds::CHN_GLOBAL, SOUND_CTF_CAPTURE); + int ClientId = clamp(aParaI[1], 0, MAX_CLIENTS - 1); + m_aStats[ClientId].m_FlagCaptures++; + + float Time = aParaI[2] / (float)Client()->GameTickSpeed(); + if(Time <= 60) + { + if(aParaI[0]) + { + str_format(aBuf, sizeof(aBuf), Localize("The blue flag was captured by '%s' (%.2f seconds)"), m_aClients[ClientId].m_aName, Time); + } + else + { + str_format(aBuf, sizeof(aBuf), Localize("The red flag was captured by '%s' (%.2f seconds)"), m_aClients[ClientId].m_aName, Time); + } + } + else + { + if(aParaI[0]) + { + str_format(aBuf, sizeof(aBuf), Localize("The blue flag was captured by '%s'"), m_aClients[ClientId].m_aName); + } + else + { + str_format(aBuf, sizeof(aBuf), Localize("The red flag was captured by '%s'"), m_aClients[ClientId].m_aName); + } + } + SendChat(aBuf); + } + return nullptr; + } + + // build message + const char *pText = ""; + if(NumParaI == 0) + { + pText = Localize(gs_GameMsgList7[GameMsgId].m_pText); + } + + // handle message + switch(gs_GameMsgList7[GameMsgId].m_Action) + { + case DO_CHAT: + SendChat(pText); + break; + case DO_BROADCAST: + m_Broadcast.DoBroadcast(pText); // client side broadcast + break; + } + + // no need to handle it in 0.6 since we printed + // the message already + return nullptr; + } + + char aBuf[256]; + str_format(aBuf, sizeof(aBuf), "dropped weird message '%s' (%d), failed on '%s'", m_NetObjHandler7.GetMsgName(*pMsgId), *pMsgId, m_NetObjHandler7.FailedMsgOn()); + Console()->Print(IConsole::OUTPUT_LEVEL_ADDINFO, "client", aBuf); + + return nullptr; +} diff --git a/src/game/server/gamecontext.cpp b/src/game/server/gamecontext.cpp index ec8b0fb26..f98d6e387 100644 --- a/src/game/server/gamecontext.cpp +++ b/src/game/server/gamecontext.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -490,13 +491,7 @@ bool CGameContext::SnapPickup(const CSnapContext &Context, int SnapId, const vec pPickup->m_X = (int)Pos.x; pPickup->m_Y = (int)Pos.y; - - if(Type == POWERUP_WEAPON) - pPickup->m_Type = SubType == WEAPON_SHOTGUN ? protocol7::PICKUP_SHOTGUN : SubType == WEAPON_GRENADE ? protocol7::PICKUP_GRENADE : protocol7::PICKUP_LASER; - else if(Type == POWERUP_NINJA) - pPickup->m_Type = protocol7::PICKUP_NINJA; - else if(Type == POWERUP_ARMOR) - pPickup->m_Type = protocol7::PICKUP_ARMOR; + pPickup->m_Type = PickupType_SixToSeven(Type, SubType); } else if(Context.GetClientVersion() >= VERSION_DDNET_ENTITY_NETOBJS) { @@ -1291,18 +1286,6 @@ void CGameContext::OnTick() // Warning: do not put code in this function directly above or below this comment } -static int PlayerFlags_SevenToSix(int Flags) -{ - int Six = 0; - if(Flags & protocol7::PLAYERFLAG_CHATTING) - Six |= PLAYERFLAG_CHATTING; - if(Flags & protocol7::PLAYERFLAG_SCOREBOARD) - Six |= PLAYERFLAG_SCOREBOARD; - if(Flags & protocol7::PLAYERFLAG_AIM) - Six |= PLAYERFLAG_AIM; - return Six; -} - // Server hooks void CGameContext::OnClientPrepareInput(int ClientId, void *pInput) { diff --git a/src/game/version.h b/src/game/version.h index d53d9bd1e..793ff5b2f 100644 --- a/src/game/version.h +++ b/src/game/version.h @@ -5,8 +5,14 @@ #ifndef GAME_RELEASE_VERSION #define GAME_RELEASE_VERSION "18.4" #endif + +// teeworlds +#define CLIENT_VERSION7 0x0705 #define GAME_VERSION "0.6.4, " GAME_RELEASE_VERSION #define GAME_NETVERSION "0.6 626fce9a778df4d4" +#define GAME_NETVERSION7 "0.7 802f1be60a05665f" + +// ddnet #define DDNET_VERSION_NUMBER 18040 extern const char *GIT_SHORTREV_HASH; #define GAME_NAME "DDNet" diff --git a/src/test/netaddr.cpp b/src/test/netaddr.cpp index ccbf09648..ecc6450b5 100644 --- a/src/test/netaddr.cpp +++ b/src/test/netaddr.cpp @@ -2,18 +2,32 @@ #include -TEST(NetAddr, FromUrlString) +TEST(NetAddr, FromUrlStringInvalid) { NETADDR Addr; - char aBuf1[NETADDR_MAXSTRSIZE]; - char aBuf2[NETADDR_MAXSTRSIZE]; EXPECT_EQ(net_addr_from_url(&Addr, "tw-0.6+udp://127.0", nullptr, 0), -1); // invalid ip EXPECT_EQ(net_addr_from_url(&Addr, "tw-0.6+udp://ddnet.org", nullptr, 0), -1); // invalid ip EXPECT_EQ(net_addr_from_url(&Addr, "127.0.0.1", nullptr, 0), 1); // not a URL EXPECT_EQ(net_addr_from_url(&Addr, "tw-0.9+udp://127.0.0.1", nullptr, 0), 1); // invalid tw protocol - EXPECT_EQ(net_addr_from_url(&Addr, "tw-0.7+udp://127.0.0.1", nullptr, 0), 1); // unsupported tw protocol EXPECT_EQ(net_addr_from_url(&Addr, "tw-0.6+tcp://127.0.0.1", nullptr, 0), 1); // invalid internet protocol +} + +TEST(NetAddr, FromUrlStringValid) +{ + NETADDR Addr; + char aBuf1[NETADDR_MAXSTRSIZE]; + char aBuf2[NETADDR_MAXSTRSIZE]; + + EXPECT_EQ(net_addr_from_url(&Addr, "tw-0.7+udp://127.0.0.1", nullptr, 0), 0); + net_addr_str(&Addr, aBuf1, sizeof(aBuf1), true); + net_addr_str(&Addr, aBuf2, sizeof(aBuf2), false); + EXPECT_STREQ(aBuf1, "127.0.0.1:0"); + EXPECT_STREQ(aBuf2, "127.0.0.1"); + net_addr_url_str(&Addr, aBuf1, sizeof(aBuf1), true); + net_addr_url_str(&Addr, aBuf2, sizeof(aBuf2), false); + EXPECT_STREQ(aBuf1, "tw-0.7+udp://127.0.0.1:0"); + EXPECT_STREQ(aBuf2, "tw-0.7+udp://127.0.0.1"); EXPECT_EQ(net_addr_from_url(&Addr, "tw-0.6+udp://127.0.0.1", nullptr, 0), 0); net_addr_str(&Addr, aBuf1, sizeof(aBuf1), true); @@ -26,12 +40,20 @@ TEST(NetAddr, FromUrlString) net_addr_str(&Addr, aBuf2, sizeof(aBuf2), false); EXPECT_STREQ(aBuf1, "127.0.0.1:0"); EXPECT_STREQ(aBuf2, "127.0.0.1"); + net_addr_url_str(&Addr, aBuf1, sizeof(aBuf1), true); + net_addr_url_str(&Addr, aBuf2, sizeof(aBuf2), false); + EXPECT_STREQ(aBuf1, "tw-0.6+udp://127.0.0.1:0"); + EXPECT_STREQ(aBuf2, "tw-0.6+udp://127.0.0.1"); EXPECT_EQ(net_addr_from_url(&Addr, "tw-0.6+udp://[0123:4567:89ab:cdef:1:2:3:4]:5678", nullptr, 0), 0); net_addr_str(&Addr, aBuf1, sizeof(aBuf1), true); net_addr_str(&Addr, aBuf2, sizeof(aBuf2), false); EXPECT_STREQ(aBuf1, "[123:4567:89ab:cdef:1:2:3:4]:5678"); EXPECT_STREQ(aBuf2, "[123:4567:89ab:cdef:1:2:3:4]"); + net_addr_url_str(&Addr, aBuf1, sizeof(aBuf1), true); + net_addr_url_str(&Addr, aBuf2, sizeof(aBuf2), false); + EXPECT_STREQ(aBuf1, "tw-0.6+udp://[123:4567:89ab:cdef:1:2:3:4]:5678"); + EXPECT_STREQ(aBuf2, "tw-0.6+udp://[123:4567:89ab:cdef:1:2:3:4]"); char aHost[128]; EXPECT_EQ(net_addr_from_url(&Addr, "tw-0.6+udp://ger10.ddnet.org:5678", aHost, sizeof(aHost)), -1); @@ -134,3 +156,14 @@ TEST(NetAddr, StrInvalid) net_addr_str(&Addr, aBuf2, sizeof(aBuf2), false); EXPECT_STREQ(aBuf2, "unknown type 0"); } + +TEST(NetAddr, UrlStrInvalid) +{ + NETADDR Addr = {0}; + char aBuf1[NETADDR_MAXSTRSIZE]; + char aBuf2[NETADDR_MAXSTRSIZE]; + net_addr_url_str(&Addr, aBuf1, sizeof(aBuf1), true); + EXPECT_STREQ(aBuf1, "unknown type 0"); + net_addr_url_str(&Addr, aBuf2, sizeof(aBuf2), false); + EXPECT_STREQ(aBuf2, "unknown type 0"); +} diff --git a/src/tools/twping.cpp b/src/tools/twping.cpp index b48f8d4a6..f24541a74 100644 --- a/src/tools/twping.cpp +++ b/src/tools/twping.cpp @@ -64,7 +64,8 @@ int main(int argc, const char **argv) NetClient.Update(); - while(NetClient.Recv(&Packet)) + SECURITY_TOKEN ResponseToken; + while(NetClient.Recv(&Packet, &ResponseToken, false)) { if(Packet.m_DataSize >= (int)sizeof(SERVERBROWSE_INFO) && mem_comp(Packet.m_pData, SERVERBROWSE_INFO, sizeof(SERVERBROWSE_INFO)) == 0) { From fe99a346dd230bc88585d24687f3c78a1e4726f8 Mon Sep 17 00:00:00 2001 From: Alexander Akulich Date: Sun, 24 Dec 2023 01:59:31 +0300 Subject: [PATCH 2/8] Improve 0.7 skins skins7: Extract the skins dir to a macro skins7: Look for the skins at 'skins7' dir Data: Add 0.7 skins (diliated) CMenus::RenderSkinSelection7: Fix the skins number (use 0.7 getter) CSkins7::GetColorV3: Fix missing UnclampLighting CRenderTools: Use stronger type for 0.7 colors CSkins7::RandomizeSkin: Use enum value instead of hardcode+comment CMenus::RenderSettingsTee7: Enable team skins validation system: Add str_utf8_copy_num() GameClient: Extract ApplySkin7InfoFromGameMsg() GameClient: Add skin7 to CClientData (also process and validate it) --- CMakeLists.txt | 122 +++++++++++++++++++++++ data/skins7/beaver.json | 38 +++++++ data/skins7/bluekitty.json | 40 ++++++++ data/skins7/bluestripe.json | 31 ++++++ data/skins7/body/bat.png | Bin 0 -> 10412 bytes data/skins7/body/bear.png | Bin 0 -> 8452 bytes data/skins7/body/beaver.png | Bin 0 -> 8401 bytes data/skins7/body/dog.png | Bin 0 -> 8919 bytes data/skins7/body/force.png | Bin 0 -> 7922 bytes data/skins7/body/fox.png | Bin 0 -> 10513 bytes data/skins7/body/hippo.png | Bin 0 -> 10002 bytes data/skins7/body/kitty.png | Bin 0 -> 8284 bytes data/skins7/body/koala.png | Bin 0 -> 9648 bytes data/skins7/body/monkey.png | Bin 0 -> 8795 bytes data/skins7/body/mouse.png | Bin 0 -> 11729 bytes data/skins7/body/piglet.png | Bin 0 -> 10070 bytes data/skins7/body/raccoon.png | Bin 0 -> 11102 bytes data/skins7/body/spiky.png | Bin 0 -> 8530 bytes data/skins7/body/standard.png | Bin 0 -> 7107 bytes data/skins7/body/x_ninja.png | Bin 0 -> 6848 bytes data/skins7/bot.png | Bin 0 -> 4442 bytes data/skins7/brownbear.json | 39 ++++++++ data/skins7/bumbler.json | 38 +++++++ data/skins7/cammo.json | 35 +++++++ data/skins7/cammostripes.json | 35 +++++++ data/skins7/cavebat.json | 38 +++++++ data/skins7/decoration/hair.png | Bin 0 -> 1651 bytes data/skins7/decoration/twinbopp.png | Bin 0 -> 3887 bytes data/skins7/decoration/twinmello.png | Bin 0 -> 1873 bytes data/skins7/decoration/twinpen.png | Bin 0 -> 4095 bytes data/skins7/decoration/unibop.png | Bin 0 -> 2525 bytes data/skins7/decoration/unimelo.png | Bin 0 -> 1835 bytes data/skins7/decoration/unipento.png | Bin 0 -> 2667 bytes data/skins7/default.json | 27 +++++ data/skins7/eyes/colorable.png | Bin 0 -> 4689 bytes data/skins7/eyes/negative.png | Bin 0 -> 4333 bytes data/skins7/eyes/standard.png | Bin 0 -> 2736 bytes data/skins7/eyes/standardreal.png | Bin 0 -> 2842 bytes data/skins7/eyes/x_ninja.png | Bin 0 -> 2196 bytes data/skins7/feet/standard.png | Bin 0 -> 1955 bytes data/skins7/force.json | 38 +++++++ data/skins7/fox.json | 38 +++++++ data/skins7/greycoon.json | 38 +++++++ data/skins7/greyfox.json | 38 +++++++ data/skins7/hands/standard.png | Bin 0 -> 1794 bytes data/skins7/hippo.json | 42 ++++++++ data/skins7/koala.json | 35 +++++++ data/skins7/limedog.json | 40 ++++++++ data/skins7/limekitty.json | 40 ++++++++ data/skins7/marking/bear.png | Bin 0 -> 920 bytes data/skins7/marking/belly1.png | Bin 0 -> 957 bytes data/skins7/marking/belly2.png | Bin 0 -> 1631 bytes data/skins7/marking/blush.png | Bin 0 -> 651 bytes data/skins7/marking/bug.png | Bin 0 -> 1790 bytes data/skins7/marking/cammo1.png | Bin 0 -> 4195 bytes data/skins7/marking/cammo2.png | Bin 0 -> 4770 bytes data/skins7/marking/cammostripes.png | Bin 0 -> 1461 bytes data/skins7/marking/coonfluff.png | Bin 0 -> 2014 bytes data/skins7/marking/donny.png | Bin 0 -> 834 bytes data/skins7/marking/downdony.png | Bin 0 -> 999 bytes data/skins7/marking/duodonny.png | Bin 0 -> 1014 bytes data/skins7/marking/fox.png | Bin 0 -> 1176 bytes data/skins7/marking/hipbel.png | Bin 0 -> 1263 bytes data/skins7/marking/lowcross.png | Bin 0 -> 1872 bytes data/skins7/marking/lowpaint.png | Bin 0 -> 1934 bytes data/skins7/marking/marksman.png | Bin 0 -> 1644 bytes data/skins7/marking/mice.png | Bin 0 -> 1012 bytes data/skins7/marking/mixture1.png | Bin 0 -> 3316 bytes data/skins7/marking/mixture2.png | Bin 0 -> 3362 bytes data/skins7/marking/monkey.png | Bin 0 -> 1919 bytes data/skins7/marking/panda1.png | Bin 0 -> 2189 bytes data/skins7/marking/panda2.png | Bin 0 -> 1769 bytes data/skins7/marking/purelove.png | Bin 0 -> 2838 bytes data/skins7/marking/saddo.png | Bin 0 -> 1983 bytes data/skins7/marking/setisu.png | Bin 0 -> 2189 bytes data/skins7/marking/sidemarks.png | Bin 0 -> 1168 bytes data/skins7/marking/singu.png | Bin 0 -> 1837 bytes data/skins7/marking/stripe.png | Bin 0 -> 1191 bytes data/skins7/marking/striped.png | Bin 0 -> 3352 bytes data/skins7/marking/stripes.png | Bin 0 -> 2059 bytes data/skins7/marking/stripes2.png | Bin 0 -> 1335 bytes data/skins7/marking/thunder.png | Bin 0 -> 2397 bytes data/skins7/marking/tiger1.png | Bin 0 -> 3599 bytes data/skins7/marking/tiger2.png | Bin 0 -> 3373 bytes data/skins7/marking/toptri.png | Bin 0 -> 1069 bytes data/skins7/marking/triangular.png | Bin 0 -> 1485 bytes data/skins7/marking/tricircular.png | Bin 0 -> 2174 bytes data/skins7/marking/tripledon.png | Bin 0 -> 1206 bytes data/skins7/marking/tritri.png | Bin 0 -> 1932 bytes data/skins7/marking/twinbelly.png | Bin 0 -> 1159 bytes data/skins7/marking/twincross.png | Bin 0 -> 2978 bytes data/skins7/marking/twintri.png | Bin 0 -> 1198 bytes data/skins7/marking/uppy.png | Bin 0 -> 779 bytes data/skins7/marking/warpaint.png | Bin 0 -> 2077 bytes data/skins7/marking/warstripes.png | Bin 0 -> 1098 bytes data/skins7/marking/whisker.png | Bin 0 -> 924 bytes data/skins7/marking/wildpaint.png | Bin 0 -> 3137 bytes data/skins7/marking/wildpatch.png | Bin 0 -> 2372 bytes data/skins7/marking/yinyang.png | Bin 0 -> 1897 bytes data/skins7/monkey.json | 42 ++++++++ data/skins7/paintgre.json | 35 +++++++ data/skins7/pandabear.json | 42 ++++++++ data/skins7/panther.json | 38 +++++++ data/skins7/pento.json | 42 ++++++++ data/skins7/piggy.json | 38 +++++++ data/skins7/pinky.json | 35 +++++++ data/skins7/raccoon.json | 38 +++++++ data/skins7/redbopp.json | 42 ++++++++ data/skins7/redstripe.json | 31 ++++++ data/skins7/saddo.json | 35 +++++++ data/skins7/setisu.json | 42 ++++++++ data/skins7/snowti.json | 38 +++++++ data/skins7/spiky.json | 38 +++++++ data/skins7/swardy.json | 38 +++++++ data/skins7/tiger.json | 38 +++++++ data/skins7/tooxy.json | 42 ++++++++ data/skins7/toptri.json | 31 ++++++ data/skins7/twinbop.json | 42 ++++++++ data/skins7/twintri.json | 35 +++++++ data/skins7/warmouse.json | 38 +++++++ data/skins7/warpaint.json | 31 ++++++ data/skins7/x_ninja.json | 29 ++++++ data/skins7/xmas_hat.png | Bin 0 -> 17887 bytes src/base/system.cpp | 18 ++++ src/base/system.h | 16 +++ src/game/client/components/skins7.cpp | 14 +-- src/game/client/gameclient.cpp | 6 ++ src/game/client/gameclient.h | 8 ++ src/game/client/render.cpp | 65 ++++++------ src/game/client/render.h | 12 ++- src/game/client/sixup_translate_game.cpp | 81 +++++++++------ 131 files changed, 1758 insertions(+), 74 deletions(-) create mode 100644 data/skins7/beaver.json create mode 100644 data/skins7/bluekitty.json create mode 100644 data/skins7/bluestripe.json create mode 100644 data/skins7/body/bat.png create mode 100644 data/skins7/body/bear.png create mode 100644 data/skins7/body/beaver.png create mode 100644 data/skins7/body/dog.png create mode 100644 data/skins7/body/force.png create mode 100644 data/skins7/body/fox.png create mode 100644 data/skins7/body/hippo.png create mode 100644 data/skins7/body/kitty.png create mode 100644 data/skins7/body/koala.png create mode 100644 data/skins7/body/monkey.png create mode 100644 data/skins7/body/mouse.png create mode 100644 data/skins7/body/piglet.png create mode 100644 data/skins7/body/raccoon.png create mode 100644 data/skins7/body/spiky.png create mode 100644 data/skins7/body/standard.png create mode 100644 data/skins7/body/x_ninja.png create mode 100644 data/skins7/bot.png create mode 100644 data/skins7/brownbear.json create mode 100644 data/skins7/bumbler.json create mode 100644 data/skins7/cammo.json create mode 100644 data/skins7/cammostripes.json create mode 100644 data/skins7/cavebat.json create mode 100644 data/skins7/decoration/hair.png create mode 100644 data/skins7/decoration/twinbopp.png create mode 100644 data/skins7/decoration/twinmello.png create mode 100644 data/skins7/decoration/twinpen.png create mode 100644 data/skins7/decoration/unibop.png create mode 100644 data/skins7/decoration/unimelo.png create mode 100644 data/skins7/decoration/unipento.png create mode 100644 data/skins7/default.json create mode 100644 data/skins7/eyes/colorable.png create mode 100644 data/skins7/eyes/negative.png create mode 100644 data/skins7/eyes/standard.png create mode 100644 data/skins7/eyes/standardreal.png create mode 100644 data/skins7/eyes/x_ninja.png create mode 100644 data/skins7/feet/standard.png create mode 100644 data/skins7/force.json create mode 100644 data/skins7/fox.json create mode 100644 data/skins7/greycoon.json create mode 100644 data/skins7/greyfox.json create mode 100644 data/skins7/hands/standard.png create mode 100644 data/skins7/hippo.json create mode 100644 data/skins7/koala.json create mode 100644 data/skins7/limedog.json create mode 100644 data/skins7/limekitty.json create mode 100644 data/skins7/marking/bear.png create mode 100644 data/skins7/marking/belly1.png create mode 100644 data/skins7/marking/belly2.png create mode 100644 data/skins7/marking/blush.png create mode 100644 data/skins7/marking/bug.png create mode 100644 data/skins7/marking/cammo1.png create mode 100644 data/skins7/marking/cammo2.png create mode 100644 data/skins7/marking/cammostripes.png create mode 100644 data/skins7/marking/coonfluff.png create mode 100644 data/skins7/marking/donny.png create mode 100644 data/skins7/marking/downdony.png create mode 100644 data/skins7/marking/duodonny.png create mode 100644 data/skins7/marking/fox.png create mode 100644 data/skins7/marking/hipbel.png create mode 100644 data/skins7/marking/lowcross.png create mode 100644 data/skins7/marking/lowpaint.png create mode 100644 data/skins7/marking/marksman.png create mode 100644 data/skins7/marking/mice.png create mode 100644 data/skins7/marking/mixture1.png create mode 100644 data/skins7/marking/mixture2.png create mode 100644 data/skins7/marking/monkey.png create mode 100644 data/skins7/marking/panda1.png create mode 100644 data/skins7/marking/panda2.png create mode 100644 data/skins7/marking/purelove.png create mode 100644 data/skins7/marking/saddo.png create mode 100644 data/skins7/marking/setisu.png create mode 100644 data/skins7/marking/sidemarks.png create mode 100644 data/skins7/marking/singu.png create mode 100644 data/skins7/marking/stripe.png create mode 100644 data/skins7/marking/striped.png create mode 100644 data/skins7/marking/stripes.png create mode 100644 data/skins7/marking/stripes2.png create mode 100644 data/skins7/marking/thunder.png create mode 100644 data/skins7/marking/tiger1.png create mode 100644 data/skins7/marking/tiger2.png create mode 100644 data/skins7/marking/toptri.png create mode 100644 data/skins7/marking/triangular.png create mode 100644 data/skins7/marking/tricircular.png create mode 100644 data/skins7/marking/tripledon.png create mode 100644 data/skins7/marking/tritri.png create mode 100644 data/skins7/marking/twinbelly.png create mode 100644 data/skins7/marking/twincross.png create mode 100644 data/skins7/marking/twintri.png create mode 100644 data/skins7/marking/uppy.png create mode 100644 data/skins7/marking/warpaint.png create mode 100644 data/skins7/marking/warstripes.png create mode 100644 data/skins7/marking/whisker.png create mode 100644 data/skins7/marking/wildpaint.png create mode 100644 data/skins7/marking/wildpatch.png create mode 100644 data/skins7/marking/yinyang.png create mode 100644 data/skins7/monkey.json create mode 100644 data/skins7/paintgre.json create mode 100644 data/skins7/pandabear.json create mode 100644 data/skins7/panther.json create mode 100644 data/skins7/pento.json create mode 100644 data/skins7/piggy.json create mode 100644 data/skins7/pinky.json create mode 100644 data/skins7/raccoon.json create mode 100644 data/skins7/redbopp.json create mode 100644 data/skins7/redstripe.json create mode 100644 data/skins7/saddo.json create mode 100644 data/skins7/setisu.json create mode 100644 data/skins7/snowti.json create mode 100644 data/skins7/spiky.json create mode 100644 data/skins7/swardy.json create mode 100644 data/skins7/tiger.json create mode 100644 data/skins7/tooxy.json create mode 100644 data/skins7/toptri.json create mode 100644 data/skins7/twinbop.json create mode 100644 data/skins7/twintri.json create mode 100644 data/skins7/warmouse.json create mode 100644 data/skins7/warpaint.json create mode 100644 data/skins7/x_ninja.json create mode 100644 data/skins7/xmas_hat.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 3c15aa4b3..adfd29853 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1700,6 +1700,128 @@ set(EXPECTED_DATA skins/whis.png skins/x_ninja.png skins/x_spec.png + skins7/beaver.json + skins7/bluekitty.json + skins7/bluestripe.json + skins7/body/bat.png + skins7/body/bear.png + skins7/body/beaver.png + skins7/body/dog.png + skins7/body/force.png + skins7/body/fox.png + skins7/body/hippo.png + skins7/body/kitty.png + skins7/body/koala.png + skins7/body/monkey.png + skins7/body/mouse.png + skins7/body/piglet.png + skins7/body/raccoon.png + skins7/body/spiky.png + skins7/body/standard.png + skins7/body/x_ninja.png + skins7/bot.png + skins7/brownbear.json + skins7/bumbler.json + skins7/cammo.json + skins7/cammostripes.json + skins7/cavebat.json + skins7/decoration/hair.png + skins7/decoration/twinbopp.png + skins7/decoration/twinmello.png + skins7/decoration/twinpen.png + skins7/decoration/unibop.png + skins7/decoration/unimelo.png + skins7/decoration/unipento.png + skins7/default.json + skins7/eyes/colorable.png + skins7/eyes/negative.png + skins7/eyes/standard.png + skins7/eyes/standardreal.png + skins7/eyes/x_ninja.png + skins7/feet/standard.png + skins7/force.json + skins7/fox.json + skins7/greycoon.json + skins7/greyfox.json + skins7/hands/standard.png + skins7/hippo.json + skins7/koala.json + skins7/limedog.json + skins7/limekitty.json + skins7/marking/bear.png + skins7/marking/belly1.png + skins7/marking/belly2.png + skins7/marking/blush.png + skins7/marking/bug.png + skins7/marking/cammo1.png + skins7/marking/cammo2.png + skins7/marking/cammostripes.png + skins7/marking/coonfluff.png + skins7/marking/donny.png + skins7/marking/downdony.png + skins7/marking/duodonny.png + skins7/marking/fox.png + skins7/marking/hipbel.png + skins7/marking/lowcross.png + skins7/marking/lowpaint.png + skins7/marking/marksman.png + skins7/marking/mice.png + skins7/marking/mixture1.png + skins7/marking/mixture2.png + skins7/marking/monkey.png + skins7/marking/panda1.png + skins7/marking/panda2.png + skins7/marking/purelove.png + skins7/marking/saddo.png + skins7/marking/setisu.png + skins7/marking/sidemarks.png + skins7/marking/singu.png + skins7/marking/stripe.png + skins7/marking/striped.png + skins7/marking/stripes.png + skins7/marking/stripes2.png + skins7/marking/thunder.png + skins7/marking/tiger1.png + skins7/marking/tiger2.png + skins7/marking/toptri.png + skins7/marking/triangular.png + skins7/marking/tricircular.png + skins7/marking/tripledon.png + skins7/marking/tritri.png + skins7/marking/twinbelly.png + skins7/marking/twincross.png + skins7/marking/twintri.png + skins7/marking/uppy.png + skins7/marking/warpaint.png + skins7/marking/warstripes.png + skins7/marking/whisker.png + skins7/marking/wildpaint.png + skins7/marking/wildpatch.png + skins7/marking/yinyang.png + skins7/monkey.json + skins7/paintgre.json + skins7/pandabear.json + skins7/panther.json + skins7/pento.json + skins7/piggy.json + skins7/pinky.json + skins7/raccoon.json + skins7/redbopp.json + skins7/redstripe.json + skins7/saddo.json + skins7/setisu.json + skins7/snowti.json + skins7/spiky.json + skins7/swardy.json + skins7/tiger.json + skins7/tooxy.json + skins7/toptri.json + skins7/twinbop.json + skins7/twintri.json + skins7/warmouse.json + skins7/warpaint.json + skins7/x_ninja.json + skins7/xmas_hat.png strong_weak.png themes/auto.png themes/autumn.png diff --git a/data/skins7/beaver.json b/data/skins7/beaver.json new file mode 100644 index 000000000..a9a3c4bfe --- /dev/null +++ b/data/skins7/beaver.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "beaver", + "custom_colors": "true", + "hue": 19, + "sat": 105, + "lgt": 85 + }, + "marking": { + "filename": "twinbelly", + "custom_colors": "true", + "hue": 32, + "sat": 59, + "lgt": 209, + "alp": 198 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 16, + "sat": 133, + "lgt": 121 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 17, + "sat": 129, + "lgt": 38 + }, + "eyes": { + "filename": "colorable", + "custom_colors": "true", + "hue": 23, + "sat": 202, + "lgt": 45 + }} +} diff --git a/data/skins7/bluekitty.json b/data/skins7/bluekitty.json new file mode 100644 index 000000000..6951315b9 --- /dev/null +++ b/data/skins7/bluekitty.json @@ -0,0 +1,40 @@ +{ + "skin": { + "body": { + "filename": "kitty", + "custom_colors": true, + "hue": 132, + "sat": 118, + "lgt": 184 + }, + "marking": { + "filename": "whisker", + "custom_colors": true, + "hue": 130, + "sat": 109, + "lgt": 219, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": true, + "hue": 120, + "sat": 82, + "lgt": 235 + }, + "feet": { + "filename": "standard", + "custom_colors": true, + "hue": 135, + "sat": 82, + "lgt": 233 + }, + "eyes": { + "filename": "negative", + "custom_colors": true, + "hue": 137, + "sat": 255, + "lgt": 0 + } + } +} diff --git a/data/skins7/bluestripe.json b/data/skins7/bluestripe.json new file mode 100644 index 000000000..f2f90f48c --- /dev/null +++ b/data/skins7/bluestripe.json @@ -0,0 +1,31 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 155, + "sat": 116, + "lgt": 122 + }, + "marking": { + "filename": "stripes", + "custom_colors": "false" + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 11, + "sat": 117, + "lgt": 0 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 29, + "sat": 173, + "lgt": 87 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/body/bat.png b/data/skins7/body/bat.png new file mode 100644 index 0000000000000000000000000000000000000000..1e9da2647fe9fb079f1bac532f184df599744503 GIT binary patch literal 10412 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

-ZE-NZh|(F zGss4=GVPu1PHq;s`q7P&=w%)Qu0D{33iU(tOC;943?+8g%^+}oB=w`)TfKkq+xBN$ zMpY9#J8XxliC|Akh7?UCr0#k5E%VX8e_eVVydODo!j>SVpHbdrR&t5qB8ZIjk29uS zrfw`$(K%Yb86ac#+xaUqjr<<3+?YF@mboLm#TzL&<8D`~Ru2aw9KbG6(8A7aKG>{8aN z>5+q#H>zhK%;zzz1Liq~9Zy=w4VKO$`x6(2<4FUbQlGj~FTU>$ud8n+wNwe9ofCD( z0NKB)PDo%YD9wz^4_bC8$<0d{D$c{JAE}821WR~bQ|FO%!)K<2E6gEkY_7e>#tcum z<{cwVyx)|u5APqb9I39GMbfilukTBOg#TROL^7Zf4RXtzJIF zrrl-A=sY~A341rXdT^WKRi`1vuqr-ZswZ-rZ6K=RnXX%SlG)Yf6^r7&AMUtD znM2@}ej>wYU?fGhgcjEi-w|Om`EwhBk*czrYW*DqTM|BSukovZ zO6u|J*BLy~*Z56t+KpYAQ9)yub#`9@`3W8o0g%FcB{ErxEin(j$k1YE@no8Gn0PzB z>kBa*nW!BV0Q;G&jV;8aht`_{2x5GWT=Y`qN*NUJr%}oGE5j9T-sVU=rKBo|mtjrc$fTE@y_1l36E)#Ed2^gA>;zYO{8Q{xmKuV<@m^~a3-BMqRn ztisiCVI`x|b+CVxejdV(usDVd8Wnu3it$=kwgw^A5)ostnXJksYyH#)|rUOoc1&J1>4u^nL*1< zw&Q4ll&YBfyi`MujgXB0ec(<-8qLK1UnM47xk#8c_5a@aDoRPNLb9`mT?uy!5ztmQ KP^(w5kNH2um}&d~ literal 0 HcmV?d00001 diff --git a/data/skins7/body/bear.png b/data/skins7/body/bear.png new file mode 100644 index 0000000000000000000000000000000000000000..f4ad63ed51ac749586dd3ed2a0c6b620a90307b6 GIT binary patch literal 8452 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 E0IhPR=l}o! literal 0 HcmV?d00001 diff --git a/data/skins7/body/beaver.png b/data/skins7/body/beaver.png new file mode 100644 index 0000000000000000000000000000000000000000..4e9e7c492a4e57a5dd36f6b3cdda95ac3286f833 GIT binary patch literal 8401 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 R;~uO4IvNJ*bq{SL{|D1F&$j>o literal 0 HcmV?d00001 diff --git a/data/skins7/body/force.png b/data/skins7/body/force.png new file mode 100644 index 0000000000000000000000000000000000000000..00fe21c8629ad58f3a035155dea4ab3930d5750a GIT binary patch literal 7922 zcma)BWn5I}dLJ%n_3H_|wC3aEsFbVx`_moNiLry?OKEg&T!pfI3Fhk%0A47?y9 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 literal 0 HcmV?d00001 diff --git a/data/skins7/body/hippo.png b/data/skins7/body/hippo.png new file mode 100644 index 0000000000000000000000000000000000000000..35370aaef952ec66905e45b68d8b29a364bc1378 GIT binary patch literal 10002 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 literal 0 HcmV?d00001 diff --git a/data/skins7/body/kitty.png b/data/skins7/body/kitty.png new file mode 100644 index 0000000000000000000000000000000000000000..d1bfaf742066ae95d2abe845c59cdde9f64beacb GIT binary patch literal 8284 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 literal 0 HcmV?d00001 diff --git a/data/skins7/body/koala.png b/data/skins7/body/koala.png new file mode 100644 index 0000000000000000000000000000000000000000..85d58d255933300d97e447ed1ee36bc21dc30946 GIT binary patch literal 9648 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 J2Q^s4{{oD557+zNb$e-SRw0QV%zU+3DEp`8= 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 Pstu^V&{C|Cw|Vtn_o+bl_%8J3;L5NOkg8*-) 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 vT*?>B?N|^C{a--_)KUV1q%_D%u5^fi)CwXc zCEd^5&)4_;^30dn>o>F4>2v<)%oYDgUz3E0kq7_)5^XItBLIMaKOq1NAG}%kmOB9e zJD{zmVjPgWlNXr9tr=LhT(!0Pv3mDoewx&&a}+)|375+_j1Xd%B!o+W}q?;2V_QQAVZE33p5JI$K?`+HnwR2SoHhv^JX)wC%?%r!_PaeKrt^E7( zi=+fs&BuTOzJia{qlw_Pmr0REvHV;vIr6K`hJl37bF__EZ!856S(qXYwGlfK*8qrP z6#HR_uj2#6y)W&C>J@fo;9hqwHqA*kyI;3W2HuT{)=EGg_1t9)iqyhmkFy=-iH|AT zzIYSh5wpZC0U39>TI}@@>I||p6c-mqI-1EQN3O~yCMEUdm5$5#ZIJi$^!)aflgJK; z{l&?E0~^w6o6>)5+V6JJDFUUUqB;y?Vq<$A5Rm26pj|m#^i-%nRWg+Mo?o1W$N(A* z%(=f>x;x**QVEL^0c@FcP6LgJojkupFf-A13PIrvx2kW49PtSVT%;H)C?YpEU6=<7 zO)3L_D(O?eo&ZG>Z+8!0G9?b4Xa1adD7m%mGdAbOiJd-MHxkLL6H1tvFdUJae9&s) z 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 literal 0 HcmV?d00001 diff --git a/data/skins7/body/spiky.png b/data/skins7/body/spiky.png new file mode 100644 index 0000000000000000000000000000000000000000..2fa3cf85756075f7f6a8839d1221833ba2374e8b GIT binary patch literal 8530 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 Nm>6C+Xw*Z+{tw`OnYI7` literal 0 HcmV?d00001 diff --git a/data/skins7/body/standard.png b/data/skins7/body/standard.png new file mode 100644 index 0000000000000000000000000000000000000000..6f0bcfcf6f1b19279742b5a054d79becc8ae6f8c GIT binary patch literal 7107 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

T@g2vu!tdtdW$d{f5-)Og0z zK7`jS&!^MiOXEn)>z!bAU*rFgDz zj{5h`+A%nvE^FKg_?O{6qbg6(M2%VU_C1Q)%>9r*KKPe}ywTs{!XQA-RXvlzu_E4) zI6FEgQF^rr9<(GvS>BhDE$k)YM&cOM-`!!h&ipG1yfm}&YrK6NQsWhT^I59>!Hdgn z-;D#LdT;L?&d6o03}$L;86-z=J_XR@cc;d2yc==E?Qd%q4iCoGNCWoxZy(@C_f|2hT*m)LUu_?=@Dn8>8f8k-Iba&18 z(E(o%0^WGp_+6E8$~#euvj8rWscZe0VE^wH3b&tbN{~73RmvBi#>H@s_Wmycb#ty? zg-o<7zOhtLweGmYY_@fIv6l=3A5&+uW=m1Q>!s_1ydh4qkY(BLS0pUG%gm}qVmT|a zA4oF*3uibh4pc3D?`^(!%Rd8`sqrli(a7t1RHoNJj(GgG)pKds6$JQ=bO kw>&@d>OcJdXm)6Upr_AfCORYNKj#2jOGk@pGi2ib0Bbu9vH$=8 literal 0 HcmV?d00001 diff --git a/data/skins7/body/x_ninja.png b/data/skins7/body/x_ninja.png new file mode 100644 index 0000000000000000000000000000000000000000..aff5afbf36fe9504b1f2d986faf0853ae2cb84f7 GIT binary patch literal 6848 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 WbND`V#DH@$1)MgsHhp1Cj{Ps!vwgS# literal 0 HcmV?d00001 diff --git a/data/skins7/bot.png b/data/skins7/bot.png new file mode 100644 index 0000000000000000000000000000000000000000..8b8ac416b8c9d0984746a9d224dc27cb6bdb6943 GIT binary patch literal 4442 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 YkD^ZH-i@L#-%J1lT@#)6T24{_1wN2Z0{{R3 literal 0 HcmV?d00001 diff --git a/data/skins7/brownbear.json b/data/skins7/brownbear.json new file mode 100644 index 000000000..09bbd0332 --- /dev/null +++ b/data/skins7/brownbear.json @@ -0,0 +1,39 @@ +{"skin": { + "body": { + "filename": "bear", + "custom_colors": "true", + "hue": 16, + "sat": 133, + "lgt": 121 + }, + "marking": { + "filename": "bear", + "custom_colors": "true", + "hue": 17, + "sat": 110, + "lgt": 168, + "alp": 255 + }, + "decoration": { + "filename": "hair", + "custom_colors": "false" + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 16, + "sat": 133, + "lgt": 121 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 17, + "sat": 129, + "lgt": 38 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/bumbler.json b/data/skins7/bumbler.json new file mode 100644 index 000000000..069d54589 --- /dev/null +++ b/data/skins7/bumbler.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "raccoon", + "custom_colors": "true", + "hue": 150, + "sat": 16, + "lgt": 29 + }, + "marking": { + "filename": "setisu", + "custom_colors": "true", + "hue": 34, + "sat": 183, + "lgt": 220, + "alp": 96 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 27, + "sat": 138, + "lgt": 158 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 28, + "sat": 103, + "lgt": 143 + }, + "eyes": { + "filename": "colorable", + "custom_colors": "true", + "hue": 28, + "sat": 107, + "lgt": 54 + }} +} diff --git a/data/skins7/cammo.json b/data/skins7/cammo.json new file mode 100644 index 000000000..c28689c1b --- /dev/null +++ b/data/skins7/cammo.json @@ -0,0 +1,35 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 81, + "sat": 101, + "lgt": 70 + }, + "marking": { + "filename": "cammo2", + "custom_colors": "true", + "hue": 76, + "sat": 97, + "lgt": 45, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 11, + "sat": 117, + "lgt": 0 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 29, + "sat": 173, + "lgt": 87 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/cammostripes.json b/data/skins7/cammostripes.json new file mode 100644 index 000000000..71d6d4186 --- /dev/null +++ b/data/skins7/cammostripes.json @@ -0,0 +1,35 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 81, + "sat": 101, + "lgt": 70 + }, + "marking": { + "filename": "cammostripes", + "custom_colors": "true", + "hue": 29, + "sat": 142, + "lgt": 0, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 11, + "sat": 117, + "lgt": 0 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 29, + "sat": 173, + "lgt": 87 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/cavebat.json b/data/skins7/cavebat.json new file mode 100644 index 000000000..e2d72dafb --- /dev/null +++ b/data/skins7/cavebat.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "bat", + "custom_colors": "true", + "hue": 16, + "sat": 167, + "lgt": 185 + }, + "marking": { + "filename": "belly2", + "custom_colors": "true", + "hue": 10, + "sat": 39, + "lgt": 45, + "alp": 152 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 10, + "sat": 45, + "lgt": 72 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 14, + "sat": 123, + "lgt": 156 + }, + "eyes": { + "filename": "colorable", + "custom_colors": "true", + "hue": 8, + "sat": 112, + "lgt": 47 + }} +} diff --git a/data/skins7/decoration/hair.png b/data/skins7/decoration/hair.png new file mode 100644 index 0000000000000000000000000000000000000000..d7b97d5d4391f9f29812cad652d21ae31afaa02b GIT binary patch literal 1651 zcmeHI`#02i82-$6G#F!b!MIJ*#cj4p7}qel-*TzkiZLAxiphkx-!~XJ~=Q;0rpYuM?dCz&XygZ!b(Q0S_fV_*d z{c!*g5(t<>wXyb@%rZ0(B4Qc@rGf zuz|<-I_Pc!*8c3@+^{O|*8&PEkGf~bMNjzY-gi!0e(){7LjyZkINw#6)Wzr{M=Xgh zwJrG-NvN1CFq+#q!da;btBKLZTD9a!o1AHFv|8C*05zdu~ z*F!)#dWD?G#Z6yD->H+~$_`9-(HrDBr$pYFCX*!*mc(4P7oM4o{vg^iO{#L9E;q4r zug;zwbh0GouniF1(>XNG(UQfC&9}|IJ=VjIX@%)BOzZdzqgK(0w1T`|Vm5nr6}Y0_ z`Sz#U1?{WobQZ+SH5q=~j4Gc6`C&be=5S=964#M6z4|j{m=ts;ZX2Bs4Fx7nj+;>H}C%Me8_X_e5nM`b|T%y?`5srXWD}&Vm-=ss2Q=?;rN_0&+~z=S(o%0<&SBv0L-; zc*t{C?4Vy-dqGXN`{yTBG{r?}LO$b?sZgUng_4ew_K_kX(;m#tOtW93$M{=p zO{grHLL(rXWh|8|WSKvO%4aOVNaLs-qaOADv}h*8j;^#TTh-p=Gk2ch*-im7nQskm zS0%0x!AOND;rCjH8kdQ8OYGj<7c88d&o~g?5ZFP)AhZP zibUs`mKf2V!gTnEtjn~75V77Nvf6u&s zIRRMASo|19Ad5>e2)fEz!tpd|OobCdoNiqNtCg6mPulmrESYv*#?K(j}{) zNYX*jp2%69y4+BOaB^EuYw3vVF)qUH4Zyp%b4Zq`lQGj&~9jL|I*qUj>$`& z>owD^Xp_osEQdb%&xCjQrqzkco_Ha^D{M)ht&{QXQ`bs0gVN=IO7$Kh)}n&ie|B&| zfIkaJTdc>=yKfw89}YG+^_@oG;cE=gJB%c`c7p%u cf794d!0DBjBwY2k{P9y<96ao+Z7G-k1$*(yWB>pF literal 0 HcmV?d00001 diff --git a/data/skins7/decoration/twinbopp.png b/data/skins7/decoration/twinbopp.png new file mode 100644 index 0000000000000000000000000000000000000000..5c1de1a87dfcd3980cb8eafa4c14682d1a436e54 GIT binary patch literal 3887 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 aR1S;H)}$9Wz0O{pmtiJ&Qrfnqv literal 0 HcmV?d00001 diff --git a/data/skins7/decoration/twinpen.png b/data/skins7/decoration/twinpen.png new file mode 100644 index 0000000000000000000000000000000000000000..9c640f37fcf48d0fc357082afb81ae2021f4254f GIT binary patch literal 4095 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%0A$Ds5t~iL&&SBObpcjdzM{y zr?%F>-QB%^V8Dk!ctj?X>m23>$+{_rU8_Pl%x6os0yQe%rEZ6#1><2Ita|?%xcdeN z20z3E+zs+>nE{E^C?N{b3;bSL37hU!clYq%xUaET3MC~ac8-qOUh4%@P0Q=Q9sk96 z=moY4oL!#s_URVL`ix>2`mVoVYZ_yja|{AHOcg4hJRx;;IT#xo z*H+DV;BbDivANre^DDWxq(!gP>PFe;Cjgi!>FdWCU7_ZSf$q_kQzdOT%G){sXb7$p z36^2|Mt*Hj=E98}H#Nk9kjj&S4Efac=0}SofgsULZ6q8;E$xa!?W2YWkUfL(6_5Dm z_Z!y<3>J3l-d?|7udk9X3EI(c&w2B{*_T79q`4usb%YdcDf*;PF^4CYRy9+xVc=9n z7M_HGS7H#>U)U9AoB`GFO3~42+Jb(Em;nF|%E@mg_?uWi{4=n6I3(I3kXB$EQ zS@UKmeU&Ux^P)&1{>M3hp$dgH_M<<(I)MIjTtTm9h7^7EK@;q0p_~}_`Wxo5 BiRS7;q7%xRh0W}Ff}bN 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 B(C`2N literal 0 HcmV?d00001 diff --git a/data/skins7/eyes/negative.png b/data/skins7/eyes/negative.png new file mode 100644 index 0000000000000000000000000000000000000000..411188be7335ce5cf38384ee8edc15f0f9ebe7dc GIT binary patch literal 4333 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 aOBJL;*u4Fvb$aoQ0T>{R;5By~WB&(1{WYEd literal 0 HcmV?d00001 diff --git a/data/skins7/eyes/standard.png b/data/skins7/eyes/standard.png new file mode 100644 index 0000000000000000000000000000000000000000..eebee95a9fcc37c4268cfcde6931485631ff0561 GIT binary patch literal 2736 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 S$h$vJ6o5JBY*UN&Px>Dm#47dx literal 0 HcmV?d00001 diff --git a/data/skins7/eyes/standardreal.png b/data/skins7/eyes/standardreal.png new file mode 100644 index 0000000000000000000000000000000000000000..2c1fa80bd96019060f7ed5d6b14d23853402dd22 GIT binary patch literal 2842 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 D;g1+> literal 0 HcmV?d00001 diff --git a/data/skins7/feet/standard.png b/data/skins7/feet/standard.png new file mode 100644 index 0000000000000000000000000000000000000000..0bbfe75e3ce71164a53ef8a01cb078b707f15acb GIT binary patch literal 1955 zcmV;U2VD4xP) zU2GIp6vuxHozjo34Q^W_Akb~ZfX2jVj0TZDNHi)04G$7|Fg{@FgCQ{y8wrU%AiSVP zUo@JiXd*U|m{>8!AYx*TU@#VFg25tR8lY_3ZC2?nbA7m-&FnBcUpv#8+1+1qvOClH zxaXYzy)$#?-fcn%L7m!VPS^#g6E1ZD)Hrnk)Hrnk)Hn@c7ob{6BoeOo3FC%5QGW`k z952asLy$SD3&0~mC%qhg=gW5i_z{>Wi!!Xlo8d>Cd=WsLn?R>TDe*=siH~OiR7(@+ zgeWIpkC)iE7C@AOF&kotlD|%I6Bkzkh-xr`5<*n{b%N8FI1)fy0^z3+;^MO%b}_N- z35-)9{3>gle73{R7t0nP&Vh*A&27+>i$_@jR6`&_zQMf-=$WxwfJVdL1uzDT0)GOh zfq(rhfDyBJm$e?zNP=fKOzOKBiHjG>udym+zLwQH9c8ygdTGdw&jwr$%cmMvRm)&yPy_5*hX zB@LE13T<~0yYkk6W; zqoYHdI&~`SoSQN*M2X$oq@16X&OW;xQJy4TwY{Ar2in6n361nV6Um zYuB#TJADSY(Sy(L`W*bI*fVYe(Sf|KigoMOg`HnVsH!TqZr!SPdY?<56UY}dVJ9mH z%i^-!Gj0G&aEp9_XT|}e7+?Di3-wQbrB1;fo%S#rl!KqsVm0E$3=H{x90R1 z&Zoewi9nKqw8KV+tpal^z=8z}Xl?c7CDg3Z-rmmk?c24*8-Y6=?z%Xb5=Dl)fTVtf zl>p`eB$G*tO`=P0Z?AswL5I7=p}V&d<^uQWS6B(a_6<#xS-Ny73l|pO-Q4YPHxW>A zqQnyYf|UR+0bIOzG0@TkiLUjU=Wv%R0Tm@ml*|aX62Jw3OeRAvmkYE+L2>!=W$o*P z!(Gk=RFo(&rC+cW0Kn+zXkeuYigV}AF*Y`)E&k1p!$yy+)ELqh|KqM#@Wjg5^cic+#}&K&nYVxp=l{r&y=Ro^(=^#Zc< ztHt$66qQB{@w`}ebB#}1Ai zJEm;k*fYTU$Y)5py1H1sdNt{EnvRYRQmGWFREnl1 zZ zcGO%kd_Mu4^!a=m_W+*)%K|!nkmP_@fHy6-n(dpn7}A2goLnIM+U&%R1Fs`zFxexj zcut_tG$5~|9|FH3Cvp}?Nrz}TBpj@7M!4JZPfhqf?aIt|1iRaIE%nZZtDVYNL+;AHlK9?R6tZjQArN#0;qB7 p|2MG4sSBXSsSBXSsSBXS`5*R;lXMWGTJ!(_002ovPDHLkV1g>TpdtVO literal 0 HcmV?d00001 diff --git a/data/skins7/force.json b/data/skins7/force.json new file mode 100644 index 000000000..da511e6c4 --- /dev/null +++ b/data/skins7/force.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "force", + "custom_colors": "true", + "hue": 24, + "sat": 19, + "lgt": 52 + }, + "marking": { + "filename": "wildpaint", + "custom_colors": "true", + "hue": 30, + "sat": 54, + "lgt": 3, + "alp": 54 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 27, + "sat": 0, + "lgt": 63 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 28, + "sat": 0, + "lgt": 0 + }, + "eyes": { + "filename": "standard", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 0 + }} +} diff --git a/data/skins7/fox.json b/data/skins7/fox.json new file mode 100644 index 000000000..c54419959 --- /dev/null +++ b/data/skins7/fox.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "fox", + "custom_colors": "true", + "hue": 16, + "sat": 210, + "lgt": 107 + }, + "marking": { + "filename": "fox", + "custom_colors": "true", + "hue": 16, + "sat": 255, + "lgt": 242, + "alp": 227 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 16, + "sat": 180, + "lgt": 99 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 16, + "sat": 210, + "lgt": 114 + }, + "eyes": { + "filename": "colorable", + "custom_colors": "true", + "hue": 21, + "sat": 255, + "lgt": 96 + }} +} diff --git a/data/skins7/greycoon.json b/data/skins7/greycoon.json new file mode 100644 index 000000000..44a9fa825 --- /dev/null +++ b/data/skins7/greycoon.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "raccoon", + "custom_colors": "true", + "hue": 14, + "sat": 0, + "lgt": 147 + }, + "marking": { + "filename": "coonfluff", + "custom_colors": "true", + "hue": 23, + "sat": 0, + "lgt": 105, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 27, + "sat": 0, + "lgt": 171 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 19, + "sat": 0, + "lgt": 152 + }, + "eyes": { + "filename": "standard", + "custom_colors": "true", + "hue": 16, + "sat": 143, + "lgt": 50 + }} +} diff --git a/data/skins7/greyfox.json b/data/skins7/greyfox.json new file mode 100644 index 000000000..24068f7f0 --- /dev/null +++ b/data/skins7/greyfox.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "fox", + "custom_colors": "true", + "hue": 16, + "sat": 10, + "lgt": 67 + }, + "marking": { + "filename": "cammostripes", + "custom_colors": "true", + "hue": 23, + "sat": 21, + "lgt": 191, + "alp": 32 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 156, + "sat": 28, + "lgt": 19 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 32, + "sat": 12, + "lgt": 56 + }, + "eyes": { + "filename": "colorable", + "custom_colors": "true", + "hue": 28, + "sat": 90, + "lgt": 65 + }} +} diff --git a/data/skins7/hands/standard.png b/data/skins7/hands/standard.png new file mode 100644 index 0000000000000000000000000000000000000000..791ad0b3f7abe0fa4631549082857d5c06a35fc1 GIT binary patch literal 1794 zcmV+d2mSboP) zU1(fI7>1wSG|5_68*NIAiGL^+s@D431T9`jL<9wMmDUQjH)2bnSG^Dks0}RzFBEfO z6boK@ClqDTq9l+~%o4h2Lc6h&HZ4h$rsQXvq?_b;FlLppp=YL>!efIZ=;0hl7guOjfuFV%XD2*5NMeiMP${;1TiT>x&8;WgD8rObqp?D`;0{#Zlz*RsxIJfNy|qVAXCxTYOlS<@oR;RN~PbZxnbRm_+no9mns$ zvpV^3JOViE`I~_@;6dO%#1L3rRr1PXf=&RO(^Re*+<4UA?~4u}cRZ(j$OmPiO@`2VN*V5s$}d zY-}uQZ+d!~$;rvWU#|jPK&RcxJ(%FW3w(<^c@_h}i%3imi}GQxC+sJ$$2T=KSx1f> zvHJV_t(ln_s~pasKW}w+cUv7D9aeR9bx!rGxHu9>;xS+Wv%S9X*sIEgDKZY_oQ%a{ zR%d6YmC0nvHhLk3hK8&}B4PFP^yG`6Pc>*mF`vf0hR+?psB&R~h{u6Nz^bpWx6Yh7 zBi-;hOifK$iA2ImBobD8dwYK1jjDlbj`=;@YxMkMM3slKD}j>tzzFaaP#cX#Id$q3 zEiElF<=3IEu8wF_xVU}$_WV;1IW}qok}+=qJ~QM>IXr;i+!l((rF%1IKky2`!Gj0c zxpSwIv8t*n>gvS0Nkc%{bP8aQH8PUPq_VNGu`%Z6=9KNoLUIu( zgXCh2-*86(NRlyw*uqs7KsK9YWF#=W4tni?3Uto_k^pDVo+X>jIyRC{r*A92w6v7} z^_FAf`r)+zY-b1+gKh!z0*sH3bM)v@>B5#}F+4oX*x1-@=Pq5kl>haDQwI&eO99*j zrc?~*7C;YcqrkCa$2fQHob7_EtE;3^DK1~WT=aaN0G9EW00t0AoL*oU#T>!10+5^s zEF*TtZe_Ds_U+qe>sGM5yi78gWMX2XFev`#*G^#4W?2l1_lOlU)3Km^y38H`1Pl% z0oy5Il1ro;fZu?ZKx_^jKYpA&d-f2GMwywJVRUqqiHV7_;+mbEJNT#O5;oIpuJVu;Mxf76zlo11BEZDq%f9cOmMlC!KTmIOFDFl)%wG{a1bk`N0hPwjK{Do#SR4IZz)y$-R9h&;h3TYF5Oac$ z5vkzoP*YRGrcIlutzEyQzO=MdEGhH{#NNAF5~L&?jC?;LQr}Ht4tN46K4UI|l3ciu zE(Ky(JOK0~pBjD@=#Z_@NnD!ZtwLl7nM?)yKo=KozZe2y3-wjt5kxxG7T~UeYqE&h zehT;%klGc}(en%8Al`R?FVLSY^gW_O=otxJ>=~{=XV2FV@7;)b!FAMo{TSjFsFjcK zItOi)r9H#0xP}yJL3v2wx9(01fdz3N#>YfA!q{e&h<`_3$CY z97r0rAMbS$=cNd|xE=XH^raxfhqk={ujT}mR&P1T`lWR<$Zb-*ZUnFEFN70+wC4o6 zMT%d@54s;ll0r?B;uq3$@Y)}x{SMR4`~Ax35*L*>VKA3r;6j3A6p(nA4MHhleZXbAHN k1yJD#1yJD#1yJGm2m7(6a)~*_H~;_u07*qoM6N<$f`~&tQ~&?~ literal 0 HcmV?d00001 diff --git a/data/skins7/hippo.json b/data/skins7/hippo.json new file mode 100644 index 000000000..0d2f41a9d --- /dev/null +++ b/data/skins7/hippo.json @@ -0,0 +1,42 @@ +{"skin": { + "body": { + "filename": "hippo", + "custom_colors": "true", + "hue": 174, + "sat": 176, + "lgt": 183 + }, + "marking": { + "filename": "hipbel", + "custom_colors": "true", + "hue": 191, + "sat": 0, + "lgt": 255, + "alp": 178 + }, + "decoration": { + "filename": "hair", + "custom_colors": "false" + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 189, + "sat": 72, + "lgt": 160 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 209, + "sat": 50, + "lgt": 36 + }, + "eyes": { + "filename": "standard", + "custom_colors": "true", + "hue": 198, + "sat": 196, + "lgt": 45 + }} +} diff --git a/data/skins7/koala.json b/data/skins7/koala.json new file mode 100644 index 000000000..08b3cadb7 --- /dev/null +++ b/data/skins7/koala.json @@ -0,0 +1,35 @@ +{"skin": { + "body": { + "filename": "koala", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 184 + }, + "marking": { + "filename": "twinbelly", + "custom_colors": "true", + "hue": 21, + "sat": 12, + "lgt": 226, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 184 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 149, + "sat": 4, + "lgt": 71 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/limedog.json b/data/skins7/limedog.json new file mode 100644 index 000000000..e9e772797 --- /dev/null +++ b/data/skins7/limedog.json @@ -0,0 +1,40 @@ +{ + "skin": { + "body": { + "filename": "dog", + "custom_colors": true, + "hue": 36, + "sat": 185, + "lgt": 169 + }, + "marking": { + "filename": "whisker", + "custom_colors": true, + "hue": 0, + "sat": 153, + "lgt": 255, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": true, + "hue": 12, + "sat": 178, + "lgt": 136 + }, + "feet": { + "filename": "standard", + "custom_colors": true, + "hue": 14, + "sat": 205, + "lgt": 112 + }, + "eyes": { + "filename": "negative", + "custom_colors": true, + "hue": 18, + "sat": 180, + "lgt": 118 + } + } +} diff --git a/data/skins7/limekitty.json b/data/skins7/limekitty.json new file mode 100644 index 000000000..ae66e4ff6 --- /dev/null +++ b/data/skins7/limekitty.json @@ -0,0 +1,40 @@ +{ + "skin": { + "body": { + "filename": "kitty", + "custom_colors": true, + "hue": 70, + "sat": 98, + "lgt": 195 + }, + "marking": { + "filename": "whisker", + "custom_colors": true, + "hue": 69, + "sat": 98, + "lgt": 224, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": true, + "hue": 58, + "sat": 104, + "lgt": 239 + }, + "feet": { + "filename": "standard", + "custom_colors": true, + "hue": 58, + "sat": 104, + "lgt": 239 + }, + "eyes": { + "filename": "negative", + "custom_colors": true, + "hue": 125, + "sat": 250, + "lgt": 0 + } + } +} diff --git a/data/skins7/marking/bear.png b/data/skins7/marking/bear.png new file mode 100644 index 0000000000000000000000000000000000000000..06c805ed995458a1fb53c0b49cc3705f6d7fe4ad GIT binary patch literal 920 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV2<~6aSW-L^Y+&MED=YB;~(F< zDmyASE$G$g$Ve^h5_`1Y?f!xNtqOLHm2G<*x@;N}X4*71iLtqeBwsn@vesvzf=AMZ zjx|SnDwgax{buf&bN}k(Im%_sw>v*OeJ<5}?y6M=vB^8t6Dlnk3i%x7Fbh0qI5Je2 zx%>$-GbED!xcuq4mKDvAd>}VLtb%b3qx1o-4;(vK-@G!eli4A8f49`}bw|(7>Yaag z)&0ojt5)49mYAA+fce3phVQkE?-=~I@2TUg;E<6tsAJs0UE?z^k$LOVXVnLUn#JWG z2z-#3@v1VHtK!guqv~?WnsS>faxIwWFwbMO7CIh!VC#<>xfd#Z|9N9r=Bf8(Pr1Z= zgLe;O-*uV&a`NigeerC&THbTHYSnVjSoZIcbHtBb-TpS;z6dh<8O|7J+b9abQgI4JKAp0p7+l4N@x*7d$Iol_sP#DEqTIl z@98{;ezmizo+s}!mI-~ko4D?4{~`TD-zT0u^LfS6C&mZVKA3%1;V9N9`nd2714CMo ze?!`th1(`=Z`l9rN4cl@)!#oVHa+`M`Jn3T#@-8U415A&pAsJJcz@vLk3Xs#>En zmOKH!_wJnC|GGB#>M+^=KfT5{Gnrx90^R6_t54{c1n+ZA9WfI&U!Fr+5Bnz^^UAUE@R${`L@eu|Q&LivLB`<)iY`(Y zlXv)Ep6nLycm8zk|Ek#eOES-0n76uepRKRGSSEFz7KC|;Xc?}{zf6YDj)-yq-g0&{^cKii3*{NQt-`mn6*yphO zF}OF(6T5p}{Q>I-kqU-CSr`9q@%_T^jWN8j`#_u7{qOt<96NaTWPh$T_r1b!jcI#> z_RjW@yNv4?_ZRc83_rr~jrn`y-kt4dKFaN2*mKr)iQGjd3mzNhT}AvekBbz@RLrbC z5#-91ApK$4ue~Y42M)ga{>asm{{kmZ@V|rYTMB1v*;x_DWZ_*EFYhX!S^RMkOM&M1 zhE$G?mWQ$%R$CF{Rq$YW4#*gIU!cdzEaNu1K*~dCq5*IK3lz&8~&h z8`$L^*)d&trDA)39&_KNU)~%4F`RE~Kd|@J+YMn(3|q~ZmNyz7&`hYET=(Ap(&yjG z4-7x-_1tg1d#aejTD?o|NBx*~dx&S3F-JFE{ru-l`~&WnBJWr}2v#`#(unxD{>AiU zRd$2pAa)g$Iif5*vX4;_=c~E)fxoRF%nkk^GY1$xpkktyB(?b8mP>MFY^^C|Om#B65LIQzKJm4lbktAH) zi_@ZexZ5Vv&YrwuZWCcZFGSzZk~f~n36fQB4Al+aQnR#9p3`okQH9KH7X_O_FK_H2 z3VA)B{Z$i!qe{W1_w=E2M;~wdI(7V0OZsb}G%pVYZI$dpXbr4}I#{YP0+6EuEzr@F zeb^09o&b7K5CQf-A8z(K0k9zsA6ze%&C1F^t5FTc;JS_2@G$mDF>XMuKDPG0jIF+9 zyZy@V?L4iA>}(JLX8ITPCLG!{_RgP&jAJl*@!74#X;Mzw6~1)l|aBaE-D|WwwN- z7azi`a)8!*t(MB2xTTH=vNs*?ngC6KL_INBTC}%?j<~xi)vS!6PF!ru?&)J(sis+y ze-=+|z^f%rRn(g;*@6njP%Rg`=alX{@*-?}-xyJinRH%$VR(dR0IT(uhFMNktA|1I zfoH{%m)cVpDxQgbV1Js*!aWIQlSciX{$3Z?W=e&5PN8g0z1n_p&Xw64N4fg~*@#?N zBjb~6lC}31w9VSpVZ@@z?+JZWqNa2;RTk826rL5x8Y-dA}*oh>{fd(6pg8Y4bC`8~sCC$s9L6sX`$ zbz)u^#iJX;@3t6)HH4j$0(Jke-fgOVBbUzOyTe?Z8JwXCd$m6t8ucyES}M47#@Zh;_U$JCU+1Te7l&UXsvkHfyrsRXlTrx8KP|<`s^!F}4u!w_-a_Cuu%qkkoEQ6QNgO(azG23I@sQkcM`xi)z{sb~GbXnx zKI#?CTFsJ=*A8D#TdOi2*>URe8u&+>t%AX(4AKnW@z{iRV^NB;2|=3VU0-B(R?uZw zhoPFzpdZ{mAw!Z+{4&iHBNocXZl`*&E7*Si1=aA~c+i|02>WERR(&2aX9fn63vF*%)@|%kO`mXlm*%9xATsp2QjrM$z z+PZBR5)(a{aW9}UN{{~>>o3{f72Qp*tR&}k!2XEEltfEX|F=%6 z{+^99WcWzV$QhC)9|I6f1I=VHp=E^^4pTEeM0*>SO&;_11IoWef(a<*Zx^ebzSv%A z*|S+c7z@NCgl^!puNTDH>Z@Xuk4fVc%bgIaZ&uJOj>_|JHhHQ85NL+$;x6RqC`(9$ z!9{MLjaav!M0QG94~yrTD>N(7eVpfb1jHg9FVc4;z8 literal 0 HcmV?d00001 diff --git a/data/skins7/marking/blush.png b/data/skins7/marking/blush.png new file mode 100644 index 0000000000000000000000000000000000000000..e579d53038b4956bf937957a910a4dec9f951672 GIT binary patch literal 651 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU<&edaSW-L^Y(_VS4^Nx+sE=P z*G`?|O_651l*+DmFliIx_X)l!>@Qm))VxoytzpyIGHvo9pd}YZLf8rvIVbSe8DJnn&ANt!@0pv@EJC2uG3n%BU8Cu;DG0(*DCub-BFMHT6{l# z_q+EiH@$qmz;}1%+EBajwOd1@^RM3bzPshKq+P<<-h0>XnD2SC%rpIBl=NTKFZ$x1 zd6Q)FEAIwR^7qX9zHh~|WhYnJF~_9+mY!R8_UXLsdsgho+_Z|ZMyBL#%zW$XKv}j0 z=kcSPEJ>uKe@)E=vI8a$0zHP-&Nu#+Z-}~`$T~0$L-4h)#fEfqeabg tp9nBtkPEHlxMFR{V8PsAOc!E@{lpRqo8vXs|J|@%UV2|qzN^XOsDJqv{GzMFult{yl)Q~Z5k@(S29G51QXmr`G zyKOtJ?FNmZWH$B;$Ic~8Q)U~5W5UL6O2>K5`EdS#b3UwhJ?q1}p7&WF-j(C)L)X;M z(*OWKli@-0+hFKlz@RqfbLY@L06=v!Xl}LV|SzDd*`&4voA#*2#6Ah+79VX*^ZZj&7$HK9_?v2)#1tZz4e=}2S`w1|0%#6b7 zL1lC|xH0u(F{3(zGKzg|s`zIl5QMR$)sX>@EVApJxIRwrxVUlAI0l4j-`XAund5Q> zswedZA1F31BgHIL!KJ2uOSx7q?J4t_j)563>~h^5MHQ2pu_6LD+eSoy{ddCrTT-O= zA)PQYApO(8tc!nyp8Yfa^z*G}y#nWo{bSjZN}XvZr7TYJM_beTILSdJf7VAN?R+$@ z9v*qIkang@fL%*I>=A7!HX)E{{z3x?M%8ZwNO+-pB=P&}p!jh$^-A)2(YeTj|{dbQHP{ zlD-=7O!{Rd~e9!$|jZ4 zyYIs0;7*=ZE_waid}vEG2#aO_XL06FlD3`8OeSz_-bxPLe#WyX6NqrHk4;DD zCn$8w=Wctr0q%4uH5P48!c`W%t>*tqP%hPa<~5!$L4xu0`dA|lmS!Gz1ry#bZL-|e z8(b^=5_HoLh4yokTLaiSoXLHS6fKB9gqo@$T~R}3 z;^SH-p43!m>sgWSsX+$BS5F4o&+Byw8CLt+7C(Ls+R9XNrEQz67!MaSDk$7MjwJd* zV@a5~EMAg;Qp8rWgo6r)FU~$kFPF)#ZJ{_?uqLJY%3wd3$Z=$fKkln?6VCEyHJUpr zp-FNErLEo8eN5^|CxAKxsPF<2>WFdtZnT<~9ToSR<> zf2~m=QH@*(4lzKNK*w z%sU{!F)i$>4}Ef!9e$#iKzgIoTm#l0vo}388VwvPNBq))oPiA=eYW_KGR%7^%b8JF zLg#qzPg+M@zBk7E?i)MYTJGALQGfAWd7C(qwPPrr zvzEk|%g1r^h5H;^fK`B4;2W|EBj)>tkav6oE|ZGs+;xZGK)gw@>u*PRx5E9^W5)!b9RG88w&1+9kI%F{v6d6BHOERtz^ z&dA}s{_Ep_K96cpXi9JY5LUbziprVzr0v<32^#a|T`e9rSOc|-ENIe{1u(-ip5p-`R1?sg3N#)xoS zzv~TI)sV>1aozpWzCJQvp!FKT-1PnFbKv3a|J$Fx4*ym+uT$M( RAG^_O0K?sfR(FV*`A;b-DTx38 literal 0 HcmV?d00001 diff --git a/data/skins7/marking/cammo1.png b/data/skins7/marking/cammo1.png new file mode 100644 index 0000000000000000000000000000000000000000..adbfb55662f4c32bdda4b3e6667edf7a6335bf62 GIT binary patch literal 4195 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 literal 0 HcmV?d00001 diff --git a/data/skins7/marking/cammo2.png b/data/skins7/marking/cammo2.png new file mode 100644 index 0000000000000000000000000000000000000000..e194a0367ff2ad749a589cffaa253c0e99114b72 GIT binary patch literal 4770 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 O0U9bg$`y*XVgCyvJuzMY literal 0 HcmV?d00001 diff --git a/data/skins7/marking/cammostripes.png b/data/skins7/marking/cammostripes.png new file mode 100644 index 0000000000000000000000000000000000000000..c0519778efdb1f5683073fba978475d3f1780852 GIT binary patch literal 1461 zcmd6n{Xf$Q0LQ<3LKbSCSx8%$kQQwUQ;UsDnGAUdyRbYoN8LQtCe>>y>05-?7O^O` zLh}$?`$qK2oI2Mv#a(KZyf$Z2$gb|Xf1=m@@cz7i`27AB>SqT8BB04o_FIPAt%1FmX*_^D4&qzO$ew?-pRx2*DtHJ;7sZk zBKyeH@L!Am9NJioD1S=mG9@GvD}3psBkXf)Ai-&L64n|bdGd1N0d+Spm;*bbhTA+u zZ7lyE9t}W9)5L9)_gOzyY)hi^@K7=3*w7c$N-G8N$+bOgY@+TqYd6XPSms4RU&%+0 zdF$+S2px4UP5ETVA7UgSmkgE=-Qe9V0njY8lVYv&*ZFaRMVkgg7&j`>_~L6&x7c38 z)vl#q7+@(}re)apSy|{u;4mQzyaj$d(eycTwB10c!O-&~>!L;!aijZu=;v1X zL)frW;&J@Ub;cZuZgxP}oG_}6YNsWW{D_8!pKXH2*ViNz3lx^`fVTDomuX66d?a|e zQtE2>DY|2GzaQk(3X4f(=^vNI$qo_2_KJSvck$FWGVKu#NMFQ>4Lk7X(_mH@E6z#Y zx`W#J)vfR)_r@RSShUPc$YklGp*2wXFb2Q67u^!UY*{AlAISy%aw|I8u^!XP*q^L% zSyfN#j^Dg%o<((l`}Z4%3wo!k&xh0)i8njhPI%Pkb4HO-|CkAcj_Q@DC_Uxrf1pl+O1xRvlldOGo+aA0;0M7b+g{Jw)3X0o^1H~=xtI0% z_KwcJ0S{(jKo4Kf55X+NnP#q?h`eO8PBt6(tU6t?+W|@I|5O@xb%?AKK}}k<)btl8 z$gWSG``m;LK{xH+D@;4oZz!H+= zI2O0?T+UUiDz_~H_;7;&r<*l(_FFON@ozZ>=sUxyx3(vh{0mfSg5m%G literal 0 HcmV?d00001 diff --git a/data/skins7/marking/coonfluff.png b/data/skins7/marking/coonfluff.png new file mode 100644 index 0000000000000000000000000000000000000000..fa59bbb88f7de69bca3d817c6534aa64ae630835 GIT binary patch literal 2014 zcmcIlX;_kp7XCgF6`aH^A$>H4NKK2>vZ!2hPlmLh#IZ0PEj1N2m(mPP9WjlpMj8iK zZliv=&Wx#v$#pApNi&yRDbz9oT%KGq>@v^%y8rL}an5_5=bZEJU07aZw{1v$Bme;0 zJlvg6DUkVhsi`P>yWNEz08oAC;p|9B;7SVYv;Ew52yV=Ls_HX3$Rci#BI3i5S!%2< zLaMI1Bhsq_?!DwzoMu*9oORdi?yf9%(}TL+S!z94>?!>oJ)BD#PIvGQE?vx(Njl>P zOaFyvs9cM)%Sudq)A_!6Eh2u)FF5(tJ$_>3OwvqmK0cm5wCuO5ES&Y=e2pq&01#vYK#nU+>&RHm&;qx-veY#A_5mX{7_9t&u(Nn7 zqR8S;3$8x+y8-MY0>I*aF(?}q_a#L{bIp`UtF{$ap0ds}OHDPlY-@npnS}Q*i|qfZ zlt?5u1AEN{X=5&#t8V_SGesd^UH)ikIk(W_;-w3hC(m@(p&jolt(Vs>sKgJi4|SM1 zUbzU`nZIyrKRF~@qtR$fzVlJe!?2Do`)K&OIZO^vN6q#OcwvnM10YR$$K4nP7*LU^ z*>NY)3)G{4C>UFblM-`_D7567_gwpHZzAEir{<@4-?!DDxy4Gw&ogAkYz{yYiyIOFyVwtE>l{gfL-^pf9dj{<74xD;9~k z8mIXazBW{ZQh@=L^>mA_uP3NQI=N4*t-Xd1XIs`N$@c9tlq4O|PU!aC^wiqW8h}ne zfL2{*w}(k&*_Pknm+B!yT$=JUynLmPPNU_PnkH*})%WS3=Ka_gyn+_A?p$2k&vxa_ zz-H(UG+SXOxoqX<m-ixU?g|IHL<@58{vSJm6Yr#D*HT7$ATbQXVc z(fb`boX6qZP)YLre86t^qOo9pKDc0Nf7bIKgyP$6b)VME&i0I+O+iPk@zx(h)i499 z{eUFSFx~1F8Vo(ObyaHvy_m^kXm}032b^cOj|PD|p^rhu%j|fD^4}i$1hipRY=u?OW;0(lP$3@~KQW;orcW3>ZigpH0fd-piajA} z1V)OOu6)j>ett<-V(DsaAQm}_pC@l#(@@1w?Bk2)INqA7wIyOh149Sdj8dTNTg>WH~aMOJHj5 zi<5zx!uA1B8J{2}_bY{J5W|PGtjse6*CK)!U^ClUU>}R4&^U6$f%#}IQN5MOgAV&vjRZOq z_iNg)pAM5g4Mj9;ItfEo%Y65z{!MIyN&5pBVZm}_gC6A!D&a|Q$#SoMQ4R{spo{U1 zO-&Vo*f7Z`6HWPy7gu~o`JA8W7kncqSG8)J!$w1_R#PQ3TD~3_o^REKe(|7&bA-|s zFLDypr?P*uejZTG^^@%&Dj2EoE^CZ_;n+5c`sJP{{Lpwe}~>TFh{=Ho0nV^smN`> M!-eczecV6wKQ=9}5C8xG literal 0 HcmV?d00001 diff --git a/data/skins7/marking/donny.png b/data/skins7/marking/donny.png new file mode 100644 index 0000000000000000000000000000000000000000..69197bcb10c424daee0a6f5912667abb8ce280ba GIT binary patch literal 834 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVCM96aSW-L^Y-S(Y~e(i_K(Zg zc8j)TSzBm4QV}iOxFAoH8Unpj?ugSoLRBa`~Ba8wtHX4x<{=QJ7pU6onh5<#unp-0ycqj42L8g zESNdc86NT|%o$XI+gHA0{FNYbgZm5vd*j~&Dh7->tM`58{lTZhoZk?B!25yhht=ml zf4l0j`s%9cwz!!LdJM$}-u=IGoc)1V!EB)u7bK4{d}Ey6u=zm6^lv-4?l9*uehU3+H&Luyv+Hm1<>8~USCJE+sjMIPbdB^)eYs0nYrMp+XWb|SD-k5B< z_CJdSbIr=kzr5K+48{kT6VlHX*D(17zmt+$zVbYy9#i*~`VR}wZ19-5vT#-f+YHus zS8QFRpEv&8)&9ceGSiG@bF!>=?)}ZUXL-!>aE(8){^%Vqw&G`nrjq~CCT3iG~yQTHC~nmk3|T0DD$^MQ3{_v072Y`B&7 z+aWWE;q!r=uRcpXO|V~mZ$9(<-!|v@9vG;$DPR2^vGuLq(;(9ivxE-lJe~N4>4QK8 zOU>Hi1^aLC&0+Bil=C`!v0*>^EP(?W1yerV{10rLifr>mdKI;Vst09j5?;s5{u literal 0 HcmV?d00001 diff --git a/data/skins7/marking/downdony.png b/data/skins7/marking/downdony.png new file mode 100644 index 0000000000000000000000000000000000000000..e489e21923950f9131841633c54699684738f7be GIT binary patch literal 999 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU|#3x;uumf=j~np6=}%~$3LF` z(;2vqmz%q5<6=!NR}t>Ow51+G+?zH?q^2h(rk`d?br#uXmNi90Y|2p=oi@eIhELz0 zF;P1f(_Ngu_j{S?-I>+ztIz-ZYgbxzKl;1f^Oc`#?|-*^zNfTYM^A56jP-Nd##!sx z1lBROWHUUJa@aAj%-go_IddA=Kicez-dfwg*rJ*-Ilgi4j=Z$n-Od&TjF0oAt<^sH zFOJ*vjqxY@@j@o`>2m4w=O1L!i>->8@$;uq%%)>A;va8YYUq})#rWo!;D8imZ!!et+JP+ZC+wxgYjyo2tpaAxHe%W6n3mM>b^l z-v2Gx{!#Cq(HvEuI46eHZ_Qjbh>JgV|I@JlnZ@#*o;(ZEd~yXNUSDB2)cZ2-08`vy z`L^u8?bGG@CH($BOI)64YgoPhL6J?iO7{sv)&i#WkDaxj*NWLkN*)b7T-xw7``_m8 zjDGWz?W@=9F*o@AM@yUko9T{>t1;iCPaaI4(|v%sZMOaM*YVnmBer}>h(FQ&$ntRO z;)@c=3?JC!=6_cStK{cDUoG_7^cusr2f}x3-goL)w%jhvx+fgDe&amWImc>$B-|4| z=)U7R2fy88UWU4(f7{OAH7>sKKXOk)bwLi_@B0GDe>o!F^6g2ESeoCOTyf;XEBUR4 z`k3V0P_m^rR992!XGf5yn;%iile-E0TUzzm+Q KelF{r5}E)I0kFyd literal 0 HcmV?d00001 diff --git a/data/skins7/marking/duodonny.png b/data/skins7/marking/duodonny.png new file mode 100644 index 0000000000000000000000000000000000000000..7d3b0e94266b3deac553b1f4f5242f27a2941555 GIT binary patch literal 1014 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVBYKL;uumf=j~m~43R*FV;}!Z zoo`T+JF>u6gk__rL*PcCpp3=Sll%`nDq?sf(_q1_ zaE`H~c%YckGSy6`MLZT9JmT9GALRBPzrj$#IQQsVy@JihlCQ)ns@EGWW4Ob_H@)U9 zvtpb00riAMDZhg@@m~nK6E^kDc82VR+!o<|>z>>wZ&>W`>3>BfCqn}32aX#vKU$n` z*n1%5*yAg%^WHJ4H98+iXiMf1JAUHm$@Usn1_QP?j8(=KW%(a(BumOz&$8LeI>nkH zhV`1>2MhVe-~)SE&#z#7YRS+r^+4eRlZS5Wqz)eteZcl%>#@&S5|#W5uH-#XdAP35 zc*DdGUl<%)}--U^|_zS89)4m(pF+zabPehB8O32L$@JbABqVAYiAOCNY` z71aAJdq6F1KWmRjynEyK(s|(~mHY|T1#KVx++#Ee|51FOL(zuq*U7R%%~dw{m`#}7 z8#W*4lal>xd4c_o+L4LUm0Sth1$-8cKV1F9)EZtNNPUp%mV6~tj!A>zd1Lp1Nmji- zxpy$cD1E%ou}scE`eFT(eIAeP6=Dh}?alPDY{)xglw19zvPW>*Ys>Mb!Ycv#+*bYzr_MZQcz$bsU2N3B8~YposucN8@M4H=SbpHuruF-ip9LSd ze4FEg?cz!ubsfd>sX22G{Mz*XH_scUxKxw=S)PgwVuvQ>2ygcPBGCK!*SO2WW^7*~u^D{d*e6Fv$ z!yFOE=P-v^;5ow)ONPRs#+zSM7r|lf(zdhJtvG0ie z=IbjvZpuRU0(-(n8oXTg6Asl<|-)_ z&2QCu-IyQw^Y^6w#{W#XWo<&nU-Gw=R1@;*>9%4`kb_gMZ0>SXblA$*eS>_(+O|?NW@FPe##@B^7|ofMH}1}g`5gQp z^g)g9>$_Zg_;c1~+BR%HV0&Px)b*HcLVXO|1K*!xuMnHTzU}IUYm*Q3U)lYcF^_#& zR$TE-FOCFX=hyc*YglR+Z8o2|K9ePmVIQO2;(rd><|bR-9q`&AbmI$8fBClJ1BMBF zKeld;WyyP$Q_djW*xultnYX;Wn8Bo+<;7OM8>Wk{OND%6@Umc?B9N$e^Zz#?rWxw8 zeOJ#+-luVDRSSbygYC}t6AAhs;@>HH?cfWT$triw_DIJZhGoV4jHxn=TI-+7CA5@1 zms=1N#Gu~zy@=nC$-Uw6&h`~kO1K(Co+Lh)dZ*dsiSUC;E{pHsUfc}F%$v?PF4uhX zZG#YV!^iFfv5KZM@qfMqXE7Mq37%o>XB02uzvS4VTqx9Lgcc2jD(UTmFL66_`@9lpGG`Y%kH%m3Djsbn_Oy_6lZjbCqa_*GLc@I&#l&gpB{7_=@gnQby($=1!x z>$&IrpnGbztFC$F@-Haje#v#4=jFP2YJd6mxc(~nRDO8<)g6VL3vSmg5r|W{TWY%?bSPfvHdW5>G{|@_O||k@FjEq*`Bq}opN>u z`-Ul7moVr}mb0F|RH~tL%S)>U(*ukh#SD*R8Z6iq&XLD_SI_Y%*8TkYXMcbt4uhww KpUXO@geCyd*8RZ% literal 0 HcmV?d00001 diff --git a/data/skins7/marking/hipbel.png b/data/skins7/marking/hipbel.png new file mode 100644 index 0000000000000000000000000000000000000000..703dd4eb6c672a7ae93886392972d70d8f7d57b3 GIT binary patch literal 1263 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 XyM1k1Q)zNP_>_Q5^l@u(3C;KmSDrhn literal 0 HcmV?d00001 diff --git a/data/skins7/marking/lowpaint.png b/data/skins7/marking/lowpaint.png new file mode 100644 index 0000000000000000000000000000000000000000..eeb5bd83a7400e285843d6dee2c7999f6dccb52f GIT binary patch literal 1934 zcmdT__g9k%7X7{ugES$5P*xzYl(1o>3@Qi#L7IRP#n7T4kU zL17F<3EdEfCW1)O1Q5t4L;=A~m>^9toBa>=*ZtwW_rp8)o%8Ow_uTAr?#^=3|Bwa% zK+ff~gNJxh{#yqT;@)QyI068Yo-Ph{#DuEPRdzSjz11ka=1ViWF_4n_qq#xEk2J>YcM{ZJVPtRFs6&sdnN-Qcnps+=gt`r5-=L6C(TusBI(txWW z^@{nlU3UA6iN<)gRY70Ek?zOEch}c@d_^Lxbz)fW%r^hqJyD|Y%HSX==btLCV}Uyt ziNO0uz+dOVbQIs#kQdW2ucIODfNY<1mElOuA_kxtwOaWi zdR#l`_RhJonQ|xg8h{i;50Vcl8dEo}UBRJiNN11U+jXrc-emCYwy^{-WjLDk<`DSI z^&GDFjA}-_%zp*KauXdRq)zNELrv%iC`c^j&? zYI7L=+{%KfjC9<1&lXl)MqN{E3O5S2j%)}4U34FD)_U~JC7^2xVd@tw+0l-II;I+$LVeTFR>I|RBm3LpBe^}^1bh5 zJKLk4*b%ja7#SHM;?Q1RUR8RQ|FTmsF*SY8(l(;KapDO@!cJiPhryAH?hr?a$)9S}OYTPP1+ zWspMhOq;s%b?lSRCauSh*~)_-qEEPNpC^yrOPv94Q&k!QRjUE^&3Wk4+dm~_uGFBR z$yzs;F3w{{Ouu*kmf`j4io;@Oyl8)-(hPM+?-1JVak0jGIi5ozWwcIV7P}lkc z9szQ-n0cYS<5~AtuCutm%xmTF>@%n=F~}*2Q}|T4q~vQhh?m*K55L0g7H;_3o|0M+ z{3?pg#PbP+|!w)m#V@_y8$y>f4M$EPQQ7|mIHfO z(s>P#(n~P0w%)HY{ph%ejs1HpgRzw1hx6(DueM3$Z4*}` zAy(=bKR>a0Tk-nmr{#!Qoxw5=&0z1ipwTqLM7EtA(f%%>I&(Sk#*OuAZkMZ#g}>Ap zA1R4Ub1+=@4NQNtT=9+*~XU@JJA62ZCBW6zfy_EVxlc2R|d3MiIC%T{)zLPAKv%#!~6T^&Gc|TrwB*F0RT`W;GH~wkoj-q zU_ZLcCTIWvU={=?d#~ThKHj-psqU@Wo4j0JQqCitY|Sq!CGj#`JcMQ_bgN%}X8V4U zUY~4WN=iB6tkY)Skv|Y)#GB5}H^Xmel?IpG3XUje-7=Dwj`t@k&)Lumk~X-wu#HmE z+-B-)K?(LsJ66iUey}XlAjmMnO-?g;{tm!$8ZaymDpLbn8OhMx<>t#&M7YDuU;l5z zC|H4**uHlp^#b>JGY|+KU{0|pYIj>paH_?!HL!s5^fyImd$=ZTiENWMV-H$WNky}~ zMV93FDyB;?UlrhiWNrCA z%AjCpX=MDJw_b$81$+D!T=UL}8M~EoHxcFyS~0t_Ar>465(9i;Y)(2w|apluG_Y!E8a({fstiRBoi*_h>Ji*qm^i7|~zcyNj)} zkFPkT_xA|2c-%2ajJIyjpiCOL6~xx_O;OjzAJKMDq*30t3>QCf7hUX9>Ye$}N2j*H zrs@KWP%7b>vyX|s9fR(AjLb)cZxgK@eHPn~UN`wUaKWFh+BHbBA{Zl^)MF5+6FFNWwXKTt~5t(0;a!w$OAa_AVqxe_y z$H82XO`S0Lh+zM_iy?CXWED%jQJNY6%_r92bze)tlX6`%1^8NR&nFn zD@buM;*6icZoH%`KW((o{!)61bk&k}I6T@$l>VH4DinWO>BBe%6bA~Vr24nAu-+rl z7{g~&z6P1jqW`4ipF5<@?%b5nzP>rs+XM;&oeJ7lMlx{IcmUUL28f70im91wwL_x< z23w#+My|%P=u4_qFZfXq1qkEbCS~0=xDZ{9$gZ@C0&)2XT^RIE!xeUo50M0kC4Uyp z3N$&6(LEex0Z^{@NW6Vxp@#m_t-dw{NQ=S4ygiJj!N@$e7vM5dnX#yJsk!oXn#SuZE zHT7=Sn)I9_a?%JlCJeEV^8|8rw`Eiyo ob=AQFu7cugC4lTR|7(j5rhs7!R8@W+b)1ZQ`rItOyfKMY6CqyPW_ literal 0 HcmV?d00001 diff --git a/data/skins7/marking/mice.png b/data/skins7/marking/mice.png new file mode 100644 index 0000000000000000000000000000000000000000..1607b00f306408d811f495a334e3c7d8ffdfa987 GIT binary patch literal 1012 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVBYQN;uumf=k4wD-M)zo$3DK7 z==rFel_sKk%I&PZgN})m>&+QHnwc(UWO*QRt zks-;vbLj^2tOU*r+;3R6F-teTTP5G{a@G9u1HumsKk)px`ed)~)v%QzN%+p89EdL)il%1GmrlcAu}y z%)Wo&Mey>(AX~<1jOHBm7qy!M9~gX4{x%_Mse}W2fv|-@^@;dA=JtmCjqO*Ij&5%d zJXQW#F-NM?c2U^MxeV-_$8%@$GHg%kKcT&pVHfB9GopWg?#r0mk}tPBng68n((Mh2 zr|N+kWcqBC)`$2tm>yu+p!IFSoU{hs1J7?bZ(35s#53vK+4c>k2H!etf;ba2zBRZ? zblEEOhumXIYnZyx{Yu6(7QeM3ehqq;cK;MGD?chXQMHx9?5JEo`=xw;#fGDHX9C$? zC-I+{jdhGPrs0R_-ucttqRDstm_;09^gDxy2p>{ctfp9 z$!EpA!V99*3q&km84TXo6PqaR8xhc+kR~Yw#DE1A_0*CVs{g-;h7?$#9m*pXG zi}j7}Ewky+YF(yj?%&mlBLEHk$stD zP4?{DgcLE2ZhPXH|J(no=Q-E)J?A=a&UKyT`}v&n(8kJ?3n~f)0Duc+W_*=lasT%q ztPK58&%GZ2IGa(%2KJG}O;QwAa9pBCzgZ(UHxtnzomrG+d{>r_T?z&I=AnEg1;t*> zg_N=>tFtc&GdZxndo*rEvMTj-^3@?}lk~nAS>9)&YqkNp z=}JJx@_*}EJsmq;l~8Q{41QlqDCMO*6!DjvFIHu6iFJD$dg%m#s<3N{encD!)EMGXC1s!j33aes-O!Dpbp1@A_@-$6F z_IWkgTiBwDG%89;N@{CFyb6JLxN{T(s^hs6y#lbTBnF zuXD_j`2`$cIY?Rr*@Hf;uisAMk*o|23p*>eN5^W;<@w75o}Hfs6tHr1&0*+gAYN*b zet3Aemt~27jBvY*MxC60+mhvfM1MwWDBf(@Pa)|o znHGhH55^23+v00xO%Sw56|_MtVf>&Yk>l2*LKiD|fU6p%(zQ9utV3(m0B~bE!xE^% zuc@P>Q%@$Zw|jL!VIEywUEcij&ngpnXu=4t-%)Ov7BT|^10_wtWOANgP*BEe@+TWp z?d8=u2ap?>sn0~!Z=#&Iv9V#DmXy-v;q6f^VkDsxzA?;y?ctWoY=Se34M_3qLz;7( z0J5*LtW3uXK6e^vE3z?cyTg_Vj}>|D1ONGS*#wko6ux!!_NyE3Fn!*#@oO}uZkX^i zmV}QUTm-sh`SQ;;SOSTZD^hafeAdsE-6LaTkF})T3rBve`KVrLkv7PWQ>9B_EGJBP zZ-7=#4>V^tpa6woCeC_=V;gzQN*edC`Q|;t7LKUhjnfKq6}=sj65pL*^b0N+eAr`P z)g$H&`J)sMI5;JA?~x=nY6q@=itS8CddwRHrrVXQ;5vKm1{pmND-N`_k;ZdH_uY-@(&VO#_cisZW;JjM z=nH*&H+~b;3T|m$z&XjCa;-uez+Vk-9m|0ak0PYgw4ENkQ%MnU)E%G}0fAkx{u^Oo zVNXkZNeQmPhT!?B^e4U2}9iIF@>bR2I?Y<78!L zO^BJ-W0~%%A$R9^+P1I~C)8Tj0`<;B5?~V$EE>#+^^LnYDy^lZg}Ah| zl-m-kY8hoTrW(FEQw-bO4Ij5(4!H82l_>fI>jD5{C6<2eky{?Vi;aw0&DICSSwluN zLcG28f~x(^uFPFx_tHy{LXd3P)!y%Qgf<7dKl&GOhVvO}KS zxps{Q+@HyBOQP(Cl6Y}`t3BCIo-jAicF)!Y&gXDgV3p15;#i5ixLmK!)M3?4Fz)WZ ztF2B>PPaWBta<9hx`{r_O(@_B3WYM&6xZA77s~F2m~ry8 zQbA;Js69==pqW3^qk`Il8Xb|&Hii>km-wRooGE%>jdz=RQNSMnI2<@Nzovh{i&7q1 zQ!+9)ak?v!k%fyDr`A^T^70b#2(+;juO@Sgm#wx*Z=H^T95b&`W%Iv{=~051G(yCV zH}64aJUDh}_3&d#msS#N^B~6K5NeZ4irKt7WSSG5uXYkt%slIdnfz=m$qaI7yZ79{ z$ZCSvx0Cz_*h+Tm0sX|e*b?*jFoMw0n#jvoo=LytF3!ulH<_b{X3Dux z$@A<@t*pvMELeRFXK`=1@RKW@Q)V)wMZP)weyv>E=6wp6-mr>rNAutKIFRTTPaG`f zupIMhIatkoxv+4vIX`1l4N(F?%hF?3{O9p2Qi<-bn;q=zYS8^pFZl6DXk$v*GB%ma z_AFnSz*-*#N-X6GQe;#J5#izQ!Z)2{t@Dl!o5Fq`U%P&N;+E_gu>VKOXzd#*SC#@M zHxqSwQchlshE2YOp=rh$&*kp4*W?*zXXiglw=yn=>+0(-LYGIrQ>-8ATMk?rwFQFHYxE0rcXV{jS5;NjZNIm(zB@NJXCG_0$mAasS@IYVK%eNVh-IL-rkIGEPBM@m0Za$v zrfg08Ax+h;cjLiqk*hDfLl@m{-i)PxV3X9fadw`S`2L%l>htN6H?IbJ0c0(~+v`Ij zpP6|n^2IKJpy$AFRbPKCUQu(383tz?i=)Z?MME$r0pJDS6 zpHS9O=DRw#C7hd5vDf$-oo$n`%eHKKzGX}{k{+L%-K(E0R08T)s0tbgPP$D&a zHw0R>h}xZ(`bvv%ZgE*1UR<&HH;M@L8a zIGUl=)fmSh-Ecv@1_uBLprp+CoaNw)lZbW5%+Br_v(}HjA(DXg6&`E@$!^x(qNH#hz#gd?(nAE{uvz&^p-mH~^|qQUCS0$%_tSGcz;%DP61X z=m(PzJCarJ7MM*|-J?!&E@+0b2w!yZ(FtDa6e+@neYO8;bO-@38tb+gVdZJAmv)2M z=N;~4h->}k9YqxPYzboP=&)Ai;U0a0b5GC*N2f000@B^u51EWymU_Wk?Twz!kyQw5dP)mtT&EJ+<)i9BoPae7Vo0kh>{>0Y7w z;*Yt{F1kY7|InI;;Q8~e6y&SmhGIJGFUzp0z&_XLPP6zF&;mVDntqVT8_E!pcG&uO z0vnIIlw4U(I4r{MtC3ezay~dVWCtQ8VfxEdJADGKFZQ8q(mpd$Jf+K6RWuC=CorHo zkH(O_=BB1tbt?S|+*9f#z-}Xu*wJAK<=}Xs v+IRiL&Ka8$RFGk`pZ`DX`Ty@I7(4;xft?Q=XNFW5*9?F{S{YXwqT~Mqd6OL` literal 0 HcmV?d00001 diff --git a/data/skins7/marking/mixture2.png b/data/skins7/marking/mixture2.png new file mode 100644 index 0000000000000000000000000000000000000000..b0741ff06ae913ea4d43e3dbe39d6978c438badb GIT binary patch literal 3362 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 literal 0 HcmV?d00001 diff --git a/data/skins7/marking/monkey.png b/data/skins7/marking/monkey.png new file mode 100644 index 0000000000000000000000000000000000000000..c4cb6fe52cbe54a0f074e9a62ab1b4365017b08d GIT binary patch literal 1919 zcmb`I={uW=8piV`5lf_Mrx_)&rk3W=T9Y8PBqI}sRF9?hbu3M7C$vLswXY>(m|CW> zlpa(^Eu*|+iM3%cZ7CY6=qQR&it2IBzcA;+{oL1m-OtD8dVU!WbX%0zIWY(Xf}&Ea z98aD7Ux^@2XCKLX6ao>pp;}ooSU+vt3rTmjRv1iJZ~T$f=hmsMWtY*GlvX`$?p2W#q^#to(;ADcuW9vWb}@piY;4p;i#c@+S8j0C%mA{XrB-5GGR)-% zlU1TYB>#wDZrrtQ8puxwI5FEAGcgzsrWC@jT%7ko;+K374!MdJCIOJQB|wqy-wjhT zZWi!!Y)cCyUgz`t4Uq07QTbg1gNUvaErHX7T?=^O)I2A81mD|B-sjFeFvjch2Sw)e z;{~lt(=!X;0wsAIq9=SPU$jLMz`TTf(mxBmMc<3VTSzmBOkJkvMBnb-Ec-I(2Cg%@ z0_!?~06+qEAA3DPyZJ23XmT&4rXEoOCy3Fqb=CZpn9BOjbuw4W5V=Cm&=8#co8%yu ztROVn#ZIGs8a4+I=$``hY!h#>1AUM;tHcm%X9%b{IX8+;x$q`Dw_k2j*53$(2$Z}c zLNO+v;3cSIMYo}0t;YZlvIW!*$~mf<1J@vBzQ}jkEoY#bs;cYtMkI>j^KU0T9%d84 zAa3vBfSe;fLG^K}?xWP7C^K>FlOaa^!Z~FwO&hoQF$z;O4EY9p1rEI=6ZEBUzQHvu z{`%luu;?WgiWkC#;Iy)U6&TVdOJoSj9s3>Er@~Zg7VTKPpP>pG3(`ukBNH$Jc{}H| z2Q!|u^ZtyEp7Hot|0bXT^?c5ZPogRQP+jpWdmaRq^_Cp`&XZ(av?A{KcAI07<|K0X z@_U=jmA$WbL+P!a#qoyIl?l*h((0$yyK2-i->|~GWNM)fy^w42W{;;yZNX9tKhk$X z?Z4c(%_EdK=?X`3{J-I?%f|JG9&1NBztpA{iZ^V}2A-~Nrx1*4dK%Qny&pUw(3-Yq zJM#YN3FLtP8u}c6?F_69+9b=Z()?g`xH}J4CG9)~Sc!Kp5;n3?V8?;tuTfMTn7eRS z0p_!ZU)Abr{IU!`bqG3{sL1aq6|o!jc?Jy#q}+nL99=8s3IA@I6&eCGZgZ>ceEx#e zV$t|GlbRkO)^NpFO>^R1Yb76*z3@9%<+Bxtpv)XP0ryAP?Xw$pN>d-zdsW{kAW9ZX zHt{XXoqy`|e{PDavB%t#oQ0r<{#1}Jhdqg%mdXsHxVX0$9vc-$oEFmgX2E*x&aSdd zT{|Rxp*BA^#Sml+MwJy@H`?oUo_hiJSaG7*lF?r6zMcLaSNnf!r8Pz~M5<5|OC_5j zO&68~t{N{)FQiN$f?{ZGqYatH?VRD(T(PQ$L@@k+bxr36P?~RcT~B7utA^Ij3C08R zId?9B`Qk6oi%t|Z_t9z8g9eq9OBS51C(bu6EEQDKe&Ycc)I_nD*kACKPf}8e zvF%2r>0k+nl;?P~E~p`(<++k~h%KW1sW3`cpGr!%@#%I&E4@dyuu zhfw^U<}k7^5uI4ZbX$p*F+ROSbKeb=JAZgzSZ(NhhdMaO_#&$}pFhyN`!3GDUkv*m zneNY~Qld6;ACe6nZ=;>*c)l(6=O{Vxbu2ceEKd=$tj#4`7&_^3Bb^TXEDjY-Ia}4~ zl6<=_Ps_n0AXjn<5}9w)qs)4}+iIh-=&mO?$hz-pd=mGg;qo)@!Q6}a{&PL7=aC$AYdM5xA=bSwD-_-xi7@47l=nHA z7fv5{yL?9dBNO5iZbkR=DU>C)EQfABsM3@go1NKh(K3R-i&ct0O3F$9fAHgP0O$)a XQ^-l;4oebF!3v>T)2$lGUP=D}P_9!v literal 0 HcmV?d00001 diff --git a/data/skins7/marking/panda1.png b/data/skins7/marking/panda1.png new file mode 100644 index 0000000000000000000000000000000000000000..0517a8935dd860c8b3f6a3181b39518c63e3e2d9 GIT binary patch literal 2189 zcmbtWYdq5p1O0C@W0MeO^OS3oJL!g3kudjin?;6KHTN!VJ!s2ZbI&zWYL7~)&1J6T zZ-$g|saF$b$V!__kxLU#@5lG+^L{wzd^o@J?fib{WVpH5NlT%n005B2+1s4{t>pg# z4*ShrmI1E;0PcaavGRzkn7Wk`MZ=lL)2TYyaCD<* ziwz=4Zg&xzE|>gM@g9D64@f&?U2o|lC-q9R zQuFTSqfE7Eh#59&BfHzzYS}vjH^G1PXK!ytguz@?tBkE2{6N$p zUisr9nc}8Udiy>r2Pock`~I(RFp^Q!^57@)>`!s>Uj03ZPKCDd~U@qum+v-qnS2?c8JZOIg8kdCAHAA%b$V^>X=40vp*sR;JYgw6WS23OVyCI+HQ#-g)C!JN4&1qBv&f}tg4EWYHz z6DhDJvgYd=8IQgj3&$s83F#s?o;O%06)-8jc~>0ckfBAE<;8+ST2l*MIq~6eKRs%00^|7ZR&F}xK>XR$Q{IZj;Gwhg<7ZU@{Yh~FZePu>L$VQzGzmuvNeK7zaE=Qw5#Tu#RfKI|9Q&8Hd~$mq$#zFoQ-s+zw0fz zb&BqpgjD1~Q9U~oSJlgs3n1FB1ey?m77b8z7v{6YiCM&81!^8tTQGKOT;O`Ut{sMv zpzc2IGr7uA^`dp&D_HC7fvAZOzBYb`G?WbN-m2~L`fOcK`;_S8bI{h_Fd#TFVQ|Qh z@Ah?>GrZ!!+S3z9>Jt%Xs`B)Y`&4H#9maTp=Ja|4}!1jh}P=U0DpMiA#lur z8;NzBJ+WQ{)>Y4|3r)k%AcsUdr&~ZQ!3o1tz$4sj27lxYLOF!*NwQ0Q#lw{n3G#9N zR;8~gu8oD3rJ-#mcExI~Rok)zpvfD)tXAWv_^1m`haBm!=Hv#|Cs)Ty{HL{fj9}k} ztrCw)OfgzEzSeGcu#k`9kNl`W-)Y&gZt|vR;87{?cvp<0#gbox@u>KKj)lP-$8s25 zu(Ai|>f!VZBjaFpUj~!cKp19b&dnE^emsA*v2su!j1b~I6mr<(-3f(1w=*6yNLBL|maN=8+kfO7MJBmD9;`>;%< z0+P^WGMR&%Nq_rppkVbpLeexlzpW+G64?QI2;bXSBR@sB%<0( zN8fuy4#p$LZos!vJQ*)cLkyWZt3MRqaZ;lX%0RY3m#Iww`OX3CWyR>q6Ef=?>S0mw z(D^<14VC;CPxPwO@{EOwZt}f%V#L;h7QD_?_Fh;B^-ZOZeANsq`fd|u$M{kZ>d7P~ zbF?F7gGBIKtoO$-l|AD%VW2OAy7j&K6j9;O#F-n;C(n27o{CZ5&gDQ$bB|`$w7#s9 zI8)jp)keaEjWtk{>EYaHu>2vnk$Yp_2^K4T;6xY%XqPI7W-C$Sfw8U*w4lTNoH27T zO18M^tcBR`sv~^R0s+=zl6+NS|Dbo357 z-)5DUko|sHE^GOzl9nwu+RkRAo23{KQ*pJ|{Bz?amA^!O60%TBN&TMvbEOiDFEmX6 q3*DdSPnL)LKkNC=Hury1TL>dcu3raP68ZaP0JxJbHczdI*Z&6l_!Axg literal 0 HcmV?d00001 diff --git a/data/skins7/marking/panda2.png b/data/skins7/marking/panda2.png new file mode 100644 index 0000000000000000000000000000000000000000..c4aaa615aa63f88463f3e15196d70565cf6ee026 GIT binary patch literal 1769 zcmcIl`&Uy3623PfHxMB}2+shU7+@m>1W8?j784R6fe;8!MTGFM5|tQOn&4vL#sDJn zx`IWm6a;K>SwWO4Euyy~m?gRpd9H#)QO~gpB7wFvYT5MkU)cR&X1+7uoH=J6XXa$A zD8h>1LI423O2FsEnJoOfm_sH#7?Pm{0O+cK$4Mx>u++4tikwJhk6L29TaPnrPCkd8 zyNH>25dluRypvv3S%BB@OY4P3!my`@alw7CHk?C~F*1*zwrS|FwM8n5*myDyf^<5t z`A;`@e6hq-#3WDlPyX^%`?jdTBIS=w#XtQlv!Ll`E`6*3M5(F3LLV@k2Uaa$obKcN zYc@$-9nl~^IPd>X6x}0I_d_5Z{tZo$#7Dj+Vu|1l?9!#~XpRo&L`)^7T3mn@_pKPa zQ5U_lo^^0*9}SI0Cs71Vv+q`7qEwBlU)d2KE`CHSjlK7u3Y)vPZ0>f&?r5Two}-oC z?iVN}<{3%m7dvYTHY{m7-9nulR*Kh*2Q7wzKPsfkcgZ)6-E5TJ!l&ZiC#<{^qn&8L zfKZ8lYx9;mHm7Cgf5XQxQ`mwj>;dfjnc@)p-DZo8p^qoc;ss)}KJ&BOU|hm0u3p$a zca;?6Gt@gx&#kYG69(E0+GcfQjXq_8=Y(+L`Z^mBuPSPA=-kS1;Nb1{^8*4-VbT`8)VD;_K+u=Q0WXs!JjB{;?f( z=Q#E`B>e%yUfy@4!fh+7mWHBaKQqOvhUtw~0RaCcUqc6OV){vUo)Cpv+7Wm=}23rE*_>NJp@1l4jC zPp00@rj`n~rblJ<=O@wMkw&p0U8@^!02$V6vs&b>Gn#i~d&obLyU7c!O`F{eHeadW=)Vc!4}=jaz;e-`F*>+>hW~+%9$RCG9%mIqy6&E8PzM z9QJ+50vsQi95Ya!@;dx)j#}j0$V>io?8C!pZR7#lzFl&hzx$RoF<Zy-e z_LdpTh9*i!G`v0g#~XX_9mPDa(-T6Hv|ravkMD~f9~E6CvS_^MXX@K7EkcTa?fSl3 zz3T*))AJKEs)W`=0A59)33x`u)OhPL+4Jh)-cK;6eZpn4QzjpZN55nE9vkxoG>V=T z^p_s&YuqMVRbt77Dh0?kj%L-h&7FEHaHZ7!&?#WB@Ht9TXV>mI=`Grm#)d`ufNaY) z|7PzbCO9Gt+b**_<;Fj`UsKFYRPX#Ywlsnr^8l&|zwz0ILqLt<#o>kP49PQY-XYlI z9d!Y!M?9bGIo2yY9gRk5t}QFY3Zrs>sRjfPZG0%c?oD!Y!=oi$c}F_W0cJkr@4s$Q zm(7rGk(1^A2ILw&az44!GZ?vE`PgonkX>|kk5?||1U6_?Ogx> literal 0 HcmV?d00001 diff --git a/data/skins7/marking/purelove.png b/data/skins7/marking/purelove.png new file mode 100644 index 0000000000000000000000000000000000000000..447553526debc90f18e847e564b0b5a92cc047a3 GIT binary patch literal 2838 zcmb`J`9IVP7stP|VaPJp$=)E0LgCuY3}c%s%TNlHluRjWH5%KPbP>X3P}XFPv6Qit zWkN%_jGMhKMikf5jHK*6o`2zaJwKfD{^6V-KIeR1uk$)duFm$N!t%lZ0Ejv|;E4yM z{x^gK4sy5ErG5Yqo^r(FJRgCsk6{;Vt{ul{kpJjkK$pd_2^TR>1}cJ@}`ErnW!xr=(mRvFfx3&3~;# zuS_2^H+Qd#{XV$5vJ&ONv3DVRS~><8OpQ(%t?zF$DAm(Tr%|--3vD=0)`_$Ez_DI9 zE9`#)8Z^ItSNMKcaWz8Q<9#@I;ma4KynM;FC7r1G(EsEk+-aSY>Rv#2csOth2Dnyv zn9QXZSl4-bINXqOHHxY0KbndcE|$9Y0Zr>M0;L|Btc6$Vc_%O2O-W1p*^>SD5jW-H zi!N)k-3g|*7F{paT9PEysQNw;lD6qWp!mrc%~Q|@IhLwfu%jfd2BzOv7)iuSc!Quu z`}6aq;Av=Ehv#hvnvkD1mCBw0TV!(3oVxRMSyL5KdXj3qK!kje%Dgfe2k!p+I&8V>C!n;K}yd|!s@8{_)(jpwnPt9( z?%(`0q1ZzJK&?L7xBtYox*=LEGT$D8Tol-M(~`~>zHg1#)Ts}Mit=H0^vH)w;c6ZO z!&e?U9i5XAzAvdi(p1m+l|{5gOoH=?K;^5OfJP)EyTI*>)gHSr+5hjf=g znhIG8Fsq4CxkS|lNmfr!eCVWKLyIS#vb~3&MDE8t(-rp^F zDDL55sZZ$yS#xn9u=~Xy|G-xO=haWywusi`_`@oG6M{f){tctH&L=BUsQ1?}SgHnB zAu}_xIj>M!ZqCLD27Ebk>51zQp(>JpIodt@OtZZO=as}}lvR;lUOrOupk)UDL_|bz z4_vYdWjS$o!XYA?H}@>7JeJ~!RUUi`afT7<`K4#bPXT<2-!h2JX49lOkhHn9?NN8h zjB)A=y38v7?UlhEo^hk%I|UV$wDz?lY-Io}-uzV{ru)Mc4p@~?wUb4h8T0Ba&9cVx zEz&&DwpAV_(-8I7i_D%e2$l=afv`H`{v62Q`IpWO#B&O zcz77N2?J7hK%>#4>rFvZ6u(#3QVou0GdL5rE? zKDtFFdIio9SKXY+j%ahQOu+OaR__}^LQSQS(b*WOcmw>93Ddyd#ud6P};?!%-JV$ z-v6`7U69%bn-pzy4+tZ>gUw=KJ`;7qcL-MmXeGY85J54+50=oC>)}g|Ea%4yvO!&P zz5WGbjEi_{V9zk7)yH|RPRL571sdBxl5lcQ|8UB*2;64QZQmJW!G`qa0vRp>L9lP1 z!?sKr8!DEZNEDs0r&Hjp0$9bYOf-nTxOb|J6?-VCwQ3;LHe_w1AWpbA&1K)7>e)Ef zJR1NmXxsj2=$o0iBAM~3m(4DOIvte`f;=RjtFYbON_03;uf-bKRKkOJh>YG7{Tq6q za5~}hc~&x0a0x{_!WGBLV)l9QsCpTFm1eQHxCp9q;qFuWv_fc`n90S!_CX<|f4K`u zeV20&tN0>K3=YI;(Fty^M%_TQT}9Sk<53~U5e8O=umC7E7QNs1v1@U0v1DxfV3;Cm zH_HYa-NaKORBgX0SVc-^7(;k9Q|B;^DV_S?(Yv`b(y6@dI*799**aGtCY;27+T@p( z7$q~a_Zy|esJ=9dqMtV~-4;C4G2774aM!DMc|ZcOWpF!md$7P&mhjk^k!NdE?QBNMs<2rpdQ?dDZlf z-Zl53TFu;Ep#oQEdjDsdbf=T{M)+SJL;11uuw7U_Zm`;4qrf?BxVX9Va___K^A+IO zUS8(N4{Y_dd1Hw9t+Q#aaDOcm-?2e5Jg5iZdlvtop!Tgh;kcfjo{O*SSEX+9>Tr#b zY27UO+qAgFsGEiWb_S+zZmxg_79RASQ<{EO`qDe&k#of`jGQJHF-bDLF^X;g^Li1Fy|}snL%GMS^`$mKD`RpIwPK{97C5W z4|xrf0*B+;GjiostoZ<_{Tueez7Fc8k1pn9Vx;)vN_cU-{KH2bQ`K_ED23$T%ZN!n z&+U+S20Y>no_6fh3aswRi^(0Ej({sd{)((y5?Xl!6axbTy-yXM_g{yWjmXq|UT3Q3 z4g6>|o0WPXNE&N>HWSvGFPq`L*2~!$ctBhlN6KTW;!)J2-Bknc_K^`p?a|fUNb@sj z`a8{gZLuav@neGeGC19wVRv_T&&0k9FI~Dw>`<;Q*MC^*&g%6XS)_}#i_nQja+OJn zK}5ooHtO1k3Ln_)D6#6+9W?#q<4VV8YnK2wZ2A{q^I>AX8VbWI+7>menGh-)LDdID zL`1w@w{U7wH>l65KT}v7v*OEq7`$e|i)4A*mQMWwk?x#lFbhu;4rAVrG7}vAeZPq< z!dbE8Um9M^fmF}M;@D#5g&!8YKT6wIUnmGH&6r7mSR6Y$yCx8so6Pzy6VlHTkE=Kp zoYm9PS?EwBS`#ex*bt;SJ?{SEEAJL#V?0h=yX-gSUDNM=WyDFlyd2Evdq0M+Um{+- z!o~&QqxmN|>$~Avo|-7Q}j*XB=#$uqhm)8w1z6&IndiQab*sK3FYd5i)XM)Qhj-$yR{Z^6j1K z*#6JovOUnZjqA_hYYp4U;&`MEO8ckso9P5id7kdhGEkb;VT12KeCL1srXi@wL{zwEqAWHz@P~ literal 0 HcmV?d00001 diff --git a/data/skins7/marking/saddo.png b/data/skins7/marking/saddo.png new file mode 100644 index 0000000000000000000000000000000000000000..0540a0c8a792c588bed11de1b6c7280a4a343b64 GIT binary patch literal 1983 zcmb`I|2NZ%AIIO@Y|EJW8eV!>r8w)dPPGY6(sS)< z9`W*n4K6htZVtzIr@0?)8G=mo^Kd9ejBJ(X^AW{xmtv}reJRv~yiKTQQP#=XA2|33 ze^PEOR?cdbN53Q-|D0jx*)#L8v0<`m;{8P9u&v3R8?@wpbV~7ksE5FSiLq38yKz$^ zfj2VXK`N-^4cMyde-=936j|><@a&>Ch~|$c`h|2`(F_;S3yZoLExT~uxJ6trm%=;3 zV<_H7S3<>R($~)&gPA~8@lWW4AB!K(DbTA9-TWPO3V^4M*J;Z0MX>?OGVqQDlpK~gd z)nAwZ%WDJvsgi&DoMWE4pk5eL=bzjPaG*ITu($lYW)S=lTiafj+lL+ETJVY00(Gz1Dt z=Y>UR2OyCG{6H>J2O6ZuFARpivj<;tvbHgHS^E%0Lt@2g0h^0ky{oGx+cV*^zv4Q=dHPdar=JuK8w-ebPYBokQ48EbIQ#p93 zQvV^~(VVNYyIe|+qu5(6y=DnyB*5-v1a6s}Cs@G&tuP)o3hUntJO&Fc-&!cK;npWU za7W@0DH~HVPG$WYc(3;!oc&NVlBB437dY0haU3olq))DI*woPJq)2F)NKTttgv%_e zNp%eOS1okEQB)*|o*?O4bJV|x?FPl^dmx+UW3dS~M4P;`PckzJQCehVnZS_R?&%AQ zeEQaRjM9I`y!8*Qi^CODr$g4Jt^jZFfQveOkm`etxU2g0Arae%P!nTwd`)x>l4wV_pn&1Bp zv|U(1`1J1lE!S}kj6Fv23~tjczPO{Wm=?;vgjBkN4Em!-ee(J`E)HBjh^}~%OFiZ= zEC{gy-!{qG_-#$P*}1oMVk2Jx9uH0+xSLH${Yuu6M)N!wnxgK$W1GV5iULnQ5Pf%$ z_`WJ=Du27V{&VO)rGQd_OkpP1!>%<8T2=wnQJMA8momZ5=-@h;^OA9da_-FnYK{6 zQWH8*@AOFYha645(p5UPckV(K?01Cjm@WD@mhlw(N${O~)LWSz_i=k`t3m4SwvmJM z7*9#XX{G0`<)C&_n`T*~VYnr8RnLID^`W!yO(|>W*LEP6{Xy&*Y!UEECI3L9lAb|< zIbh0BwYsf<<+=@R3+c+SRj*fjMf92IHl-kk3E^MQut;3%@9*}b8QtAR)w%EXTinEC zVK?$Ng5qZ`I`ZOkX?P+95k8+@n->lebMvbRvF86@CjB?OeMLdmxL&~Z4J9ps83-b6 KCpP;tbN&H{R(K2m literal 0 HcmV?d00001 diff --git a/data/skins7/marking/setisu.png b/data/skins7/marking/setisu.png new file mode 100644 index 0000000000000000000000000000000000000000..6b4196ac909d591e9a6d09ec6d73333793a9802d GIT binary patch literal 2189 zcmcgu`8(9>8~$L7eMey-dKYmX4v|K72l=|*Xn*%_;(tLW>pij!u=Jyal z&wp|OlO;TaeDKFPr&irFzxWxNo(83X#=+^peuzQZnYEh07o~EmaF3e3pDD3S+oHB9K?NJW|QB!AlY}L3?vsB)dxrwC(CM$xeJiE zo9XK`!ZD&shZesW&yQu^jQtYixfxl>3>bE>y;sE!ZmT+N%DvJl_M*PsW_8 z-?S_z0Vy;QJ3!M}r4Cpa(suG1Sq@B9THh=iwcrzrdRUibs70&BuS;}rj?6f8&`zbv zafIiGOP8bKjO9t3cYqDOwho3QD0|6dQ6+4S)skUoNAnKVPfT4R&S}ML|Z8I-mx5U zv4W8tOM*UhYuW$Df}09G>Zz znSkG?U%+7b4%#Kn%09Utb2{(Vjni6V&mzfv4k-?`L4Sx`L-L?O& z@cYH<3-B;eDQoc+q(e@FGOtLqL@PrTZ>zyofd_~MMQ$fj+$5UMW$PG-c+6a-Ss1SR zH&tQ0a{-9bnDHzltY}frW2C)G!njHWCQT#90b!`Z!N9qxz%kMb7BN#sKEo{~$L0+x z)A@wsF*#><_kgn$wC1av&cZ7c#^~s6|JOOhTdC+mO?}Qrjx8k!`OkMO)#X$xw>*oXQZG4+)LvbCLJtZUfzUQRY|Bg-+kLOh)`(UOQ=h zGWkweNNxRolue~ewo7qY?Z__3JELz{PEA=tmWYT${054#sCNs{o?sX|j+*i_`KZ;d ze>_V>%wUL*M#bg$3!bg*bA&~}90)hK8kS$0Ju!_U&s?o0R z+2b7Z5$cJD$;d=xE)QZrphF7=HFBFMrtVn1k;+HMqczXzrub>JUWZRsU_qPy zwb%C6o0~1mTauHLS=&}cD-k9Tb#?Vr%Of%(QD@orIoefirRKd=oXVHga?-2*cdbFPuk$Je(m ze{XJdbo3}})YPxl>%BeZ{j1`;F29+Zj1Q);o)9FVZv7kBhZcxZEa1E2eSjkH0kLK-`YLx+w0t4;+>1%CqkoafxNgHoQtn^y@a3Xn3Z=2qwZUAK*4hf12&}nk`37ZnvU%};1A4#czjJ`6 zhN&s-~B4M9-k0XCcNtU0q#w)Y|BDy0!V> zO8B%}3iaKi0M>^=hlb)}t(ER%?U}64H+56&WD+`z!an9#LKZzSn3%RUo|eS=(oL~t z6%~5-w!hDj{Qsy8oOm>~R^u6sQ&`oeH%nYbSrtpBS+LAXaa|?RwN`xk2kNgMvex1x zA$qnlT0d+>!tyqrF1mf))@P~$G)cT!=vFEPb%ywnZ{J+2In5$8qVX~W9-msCVX1X{ z*T3Q8rv?Qxz*tFhC(acFRE8}J)&0bR2S@JF*bZbjW;T_)i4S<|zZ_M)_ll9d*Fm%2 zh7OJM%mS0Q?|@OPoOR5~QX(&>>i>Wr`MXL?{FJ(b+Te-X_~RWxi9qH9NqXQ!@NM}Z y{tc&zRM=a8S@Y6ER}UlpGc*17PyWAyZ$ayONhPIwMqRu$1F$)VM3h*1Jo*Puwglb) literal 0 HcmV?d00001 diff --git a/data/skins7/marking/sidemarks.png b/data/skins7/marking/sidemarks.png new file mode 100644 index 0000000000000000000000000000000000000000..c0ab94463928436ea5ed7b47008667baad334736 GIT binary patch literal 1168 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrUbipZ0Q84g|Q|p!mw_3gh zcQ~a)wd_COvczep?!IzEpL;uvi_cr$-&tp;FVHq~=W|Qr^VRcypP5@+u;lW~gk8^4 z{SOpbG8FPT%wZOI&Txe8%$tll9(McoykXeJyq{q`^Wu}mnYp|l?G5gEA3t6_GIhhwGs_#ofBrwozGLz4jll=bAFw{~_=$b&*(**DDjz5q@a;Lh z*5B^H>9uO6kZyP zH>k7EyLjfVvyE*p^Y4b`Gya}z-{GF2_n6CI{*K4{H$RvW-f8`EGwbm=r;;96C!`)U zTrUuKSHmJLUNXF4>J;0b4BidXx%w@q$^zK}_b316>c1K8#?T)owf(wm8SmD*{P@7k zJM)Z~D%M=up&K6-7m|7Bw|4LCU^j+rp*~9;m30iKg+Vk^#H@ldi4VciEX4R{GLW-z z!&gRQCJBFwXB&R3{>mtO;Lank^z&%>3yf!U?YG(`xG~g+NNs=0wJ@rF)8B=ccG%wa z3vZal?QuRt>ico8$KR%GR7hbgn7ApMsgBi*;W}5J_&+|I znB<_pDY@s~Fvv6PUjAcF_W}1Kp8uP6{x@%VeL($g{pW^W7oOw;tL*x1W6HDkrsnZP zy1w}{^9|3&d;e>b{{DZPAp6neqteFDnTz-egDcm4oYU~S^f6@l$ zWfQ9#%^T((k+naPHbFO1_!M7)_=b5uI`jW&MJx(T`u}+Lfw)I$X0BxnV&4;e&6|Cm xFdX5%JuO@N3}Z(z!y}mn3wDKb^k)9%|0=muc1f~_39zJL@O1TaS?83{1OUw`1&9Cu literal 0 HcmV?d00001 diff --git a/data/skins7/marking/singu.png b/data/skins7/marking/singu.png new file mode 100644 index 0000000000000000000000000000000000000000..56e6c432a02e7f96450127aa6750391f14c7e967 GIT binary patch literal 1837 zcmb`I`#;l*AICqR&6bg7iW$qv<&cg`bh@}-7ju{q)tu5WT}~yJP+9q`_#)SYCL7BA zxSsKKImtdnnahI{Iwq9llCTMrN$vPPzW>7c;r)6&9&84S0$;#v7IpszhO(HwSv7}zmlaH3c)O) zlb7?4qDc{=B_NHyM zhZh!l`$Qun;;+wchtB3n$E4icZwIVW^~n2I&8r46>f9G{)HF;Xai_h7J{0Z_OtXP* zGcYR%vA6Tz2As2HJNM@5FYVcnWan!EPX>$dLNrJ08nwId(fRx3hA_H#MV9VAVMvdC zPw)klj@ctmqE}zFt&n?GOUzOcE5FNW9EF16-};5quqGH$L5#%H6_zh*y(h$?*+)~$ z?pS()9*n*(i_8I`($IQfOCO@4W^Kh>x4JTChz89qCNuQoT(gV&5OE6)8RousEftk{ zgYH04{uUiqt*-Rm^0!cAE`U?v4LWxF7=GH+tHUhl$j0qW_uqfmrd?GJ(9>^^4kQ^) z+@hO=YQC@w9LHAm;`o`9R{SJRa*LqFFq@JByaT=z%WM^iz?zcf2J{26aTxGN`0IXk z4Mopw9%f}2d_2lC#S#4!^Z_`3DOv?{D4G#(=&wJn{dubfwNaiT0 zH*C^>gV}!%AM6PlKp61T98wJ#gB()5Z3RgLD1ZN3V2u*hQ@@)CYQSv!NDrr(`4ZNr zo_24&2g+P*c$(X zW(#1)!A7c=m_+M<3eC`O2p zcH7EL@xHi15|V&Bj*OnEns=fHyp8j=1@e1ooza5R?K_^g1@w|$%bkCf%FL27`)R}a^0?U7KY)H?ajk&HA}&Umwtdo4;i`7B;|=RK*6~-r zz^E|=yybO8AHgm1s_oZDTnE3ER|R%DK9 zP)9Q{))VT7A?u6l#zE3Bfz6-u?G?4G1V}M0Y`1PH4w&jNd3MFNV3vAIOT@K%xF1&Ehe3o6h_mnJ4rMkR5l$fHF+i(jXxjzO_>kaS>%{H z0>i|8qy19kbuEPX2PMc0vh7TsV1t8Y5yY4%c zB+?)+_*y4Bh}FIQEc4+nSsg}DziDkE4$#p;1*%Qp(%FyYmbyw*IvRD#U&t31ovMX9 z5&R~@1Lz}jC&lEcV@(IPerE7Dj*b&?`3Y-WPrtM2LE}OHKiPjZk#>eUqGoFm$x$zZ zMw%B7%JanUBycUEC*37~sU_rymq;rXObn9~Vys!r>b&gB3^8+nH%jal{}Sq;!1oyyq)NBX(E`^>E!;gK?uz~!nsN-LY6vK!rhD?J*wgFR6{PyGq-QK- zY7}}@P^7dc&ytV2q(4wXHx49%|H%Xj_E=q_*dNLm+s>iL+Hm}S-q)7qq4}$Rvy%;} zvZi)&wUOWG}z1&NlqK Qu~mZrnM84}airh=2ebN0RsaA1 literal 0 HcmV?d00001 diff --git a/data/skins7/marking/stripe.png b/data/skins7/marking/stripe.png new file mode 100644 index 0000000000000000000000000000000000000000..5fb8d2693276a1cf4bb861e3afa10d5b1b26a4f7 GIT binary patch literal 1191 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV9E1zaSW-L^LDOpM_8&v+xnju zU3^%B3ze0e#8z&}G?eIApeJ&0Dr@G0O`-2@?G!&$%aVJ{b(aTguHrv!r7rt`nJ$Zr zodv}Fq;9pG+ftF9Ah1w!-)Ecm<@28Jd+_1yyWeNN$33^T|2((2N=;2opz`I~y-bVr zgcK~Pi2iO3KQP1YL2iQW2F3_>9dBsLA8;6O#IQNlCQf9DV12WY=a1ClK6Vj7w#I5V*>ziD8|qFq)N|Eve6c8tXWz%P zuk20EoNe3}xc|7_Tz~yR^n=A-cY7~<+uTt2z~$(_WYOKy3*Ry^)P%469{(U@>APbc z3I`+~WVYVhd|)!y-d-WchN$SIwKJCoDseFUYc}MQDSv0O@NGI{@E*=ToM#GO?e^Mv z<4V<{GYtNX(tWkhWq12@Dlr^TJ-)8HFLs%W(1GlLIHvG9yUui~91uNqaHi?@mBqI* zom3dz8s%lG4*C2S@Mid6$@jtLnt?l0NqW2VMV1|@bL^S^B;I-GWx`muG)8LMqx8dX zv|NNAsD9{9{1*#mGvu&)GHqetG~iZo_?{B($aH4T@dNMOZeH!sIN-eaj^g4wt6W4L z=;!3!lQ2E8#?OWE*v6l0=dol6iZ!b;MzyfaP!w8_msYCcaQDnB7tsfG-bSm1LmRkx zpZ79empb=`YY|t0%#VEs_b{{U;o>OF3|_w>4~bZ1)ns)xS1A_4e6)hI;coak0)%{m)Tl&OHuSMq=W;Yb`KC@*E3wme8<$6VO!AspT#SQBp zo>`}`h~abNTDG0Ld4PThY|uSm(|m_9q0=!hREcZDWsU9pH}a;$_GU1BVY24=y!Oc| zHxouXhG~+|@|o`$oGDkI#c(b91yjQ2!gg(;1=iMgI!Y8@)IM0|d|vsK>%w{&V8od) zSH!N1<*IpfCDm_=WCH8cqvwj}{;FfTr+i@ck;P{i=07z5!SFul97mLYi)4bF0neVN zcdDgkd!Jk9GK2Bk1N$EVdY0=~U%0Iy$ns-PQu*t8^$TT;zZ&&5^@~2;n|}A$^DURB za2#g{UbE;8-#4c09;>#7!;??<1JkmLP=Xb+h+r9WxAhq*);^}@DdKLezYZR1^zqV=4Wzk_h@)RMf8VV>*ujc U$4~i00t+ApPgg&ebxsLQ0CkJ}u>b%7 literal 0 HcmV?d00001 diff --git a/data/skins7/marking/striped.png b/data/skins7/marking/striped.png new file mode 100644 index 0000000000000000000000000000000000000000..c5ccaf7c59a757d9b040539511761fee4b0fa99a GIT binary patch literal 3352 zcmb7{`8O2&7so%dWEk6gW#6@UY*}hVgfVu)VC?&vHA@CrG9y$(shE%`*`|!C3>iyP z3h|I^L$W-{lgRqmm+9;I3%=+3!@c+1bMCqKocqJ;zVA((jTtXe90>s6wJ>&NfF6wQUm zmUe+!`s-=$d86Sq-vHSQWhfgxJ;06C`;J$%ku<~^3L07|eY-E4esrAoSgLt=RrLHQ zb!MW(KyoDPVPRwQ@{Vm#(8aa^k4g4^P|NbGH(N<>wl1bGFivFE-71rjwWC>r8%+Yh zj|wL~P=?6<{lC)zLjw$IetEeQZ7tdPwjlz9k9Vl^sCv9ctqkgU-qe1Mm|IwASEuw* zH-C@708^dEgJr@AVg4Mf8hewP3T)d}Lw8im>gpmr=bNq`>@tRqo~5I~!ti$O^MWI? zA<3Hb6To8T*|S-|$ymMC_J-<)B+`<1e6R2c(FK98JzmxvjAAx6HZ;`LQS1I_Je28x zdmwj13gla!RidtuPzKz()F;E8zd5h1A$<<^)e{4e?#Zt-g`Xl2}zn8gxJ{oeGkw1Lw`fr z@O`oE3ma@rnv>E6)cXqV*R5X+&c}T35*%LsiCWQ{R$1Ym)?|8Em-x=}J>kJfM7?)U z`0L#f`Y?u=6H=qKM41(?3m*3&#ucO8>&`;ufS;NjExyQxZh{n3(RbR{)f zrKVx_4 zu{x)CK_F5fO##rjwE6ep!Q^B%VN8Zgu7w;Y67}spmwP9>{In8k2NzhBU29c_xIZi3k$84d(u&@1%c-Z0v{*pMPvvO zBcU=-ZyfCu@Lm&sq?aT--axKnv#+uU6U`KUoQg0#GiPRI=FHu1A=9K#DZ}UHe9Ax# zTCDlY+o|4RiGxp48#nOUfUM?iW0BR#(~j9dVttQHzpOlvGtr&RV@|dv(c6mu`peff zUZc{%424_>=%0cV&#%~;AObSzHy;TxYIU|=_;=>=eMoXp$`7ils!HKHF899raG2tA zpn~gNPciw-E_<$?zqWeW{bavo)!^pey~U+=En582=M&)DKq23f3xxFt4namOG=mzOTbY6xRdKc%CT#4zB}1jBK&fJ;uE=sK*U>eW&m%Ag{{c))Hnv0h?j z?y~rWSykuQX=Y1bPG?6)@>KbZwtr>Flf)wx1sv0@Ta(AhJ$liPQJlirCbf{Axw&e! znLpA+^8=1Aco{1G32xyIK2R*h5$3UVg>RQ8nh6CNOmRtWH(tPr^pBM~;yA~N zJmL?xV5iRor`(GWNGmrNmj{aZmR>STUmEnNZktRM=ZDALYonA_x+}y3!ZT@CV^%-7TsHyQO_%Vg^Liv~!Q{!g1EM zaHcS2@PjRy_B7Qq_&(44($XVD17$KcG_>2fz_LOBj96%qY)Kb|7L!P+bt-qzn=__D z>+9<(WeB3$_;>BxwWkjKNL_&It$GHfN$~@QbQw-z`>sw7T0~E+q-_7M z_Un@t)bwHeTt~1?9-E28uGgtIIDn2cvypAX zg4NUMim;XK)tOJLDtLmY7R0Bd0!CE(L(R3Q6{pI&{m<#bTc<&Gc6OU)CYE!SZNQU- zHPg(&ZeFs8=;h&)K0ivM#_H7Wm*ACzI^BarJIZNlu%vHSu3ZHtJMxKlWz;J|Lc2?X##5i>U z$s$BJs@>@gN=eB^VXtUdhzfFp6XnFsL~aeK&YGtubS(qibnKV79F_79+v3VECpuZu zf7d2OU5gkm@Y}ohF+hNS<$k?9zw8d36>(y`A%6=Va9YfpM@SPq5r9+Ud1$Jj%a3SE zxNc_$@tI%{+pqIWU?y^8@-0ooz#uv$B?EO#B!gE@)8sMtoauq{>^~r~Wwqnz+b>;R z=Iz&tReIJc;uk)mt;Zf=N-k$WodrRHSNjNgIxid%$%$$5aPm4jIx_Nik2r%mUcS5K zi+cu%nsP5Y3ZN>gs!A}~al;eoxQQ|Wq=_6yKZMyRX>}Sg)epO1YpV>qV^cM5J-ALq zS^}4gKf(0)jxtw^cp`CcYdNfTU~YbXZOLb;yX&;EoZ~YHEr>sAc~WooqbzZ9(kt?- zoUn*U(!8Y0VsqNnCcGd}2p!!$PgDgC4i0W_CJ#%d9@2b8TkTv$L^oxlwC2a(VZK2E z41LP!3HR>Z>;AC^6+|BK{?kXagRev<4L=IeuJqmG{HTsQ`=6oey9j=11sQ$>PC+nP zix%PD8Fqx?1jP`HOpSnL#oZ9s5&z*c(8R>V2UUav0`&6RW2&so1CV{ zP~k#gN!C@T?aHebScqS=a9CNZi`@kC;C0!;7)V zBy0YvICvAW7lZy(gAa0b3cYk`X|S))rs4!XKE5%Q7jw3F;yOF8(bmy72jYc2@Pw4< zLvc_reTdR-gz3dsV|`SrYAGTMD~WOfv3tK$581~H!o+1{oV~>0QNKnt#+pf38AQ4e zF}18sif6hmEAEhq0RhH8n8CTydP*>Ph);fE>HNYp`fPl$@P7ff&`F*w)aSBb#gVNSYNLJ*Rdvzh< z;lJ`So3sx_&03K$i|~6+yD$FEbBE_uZ{Hf0bh?XVNP+4$^f$t^Syv;$3gXhkSypL9 zRt*V+-)1=Z|Ufwc`LcTI)&d4Lz&y@yL)?q1clom zR9ss6sxVHvv~}ej_nF+8WRdNXzLd1kWX{NA)sHSgs)+ZPYBi7Ksf>GO62!5wvCXkD z*ZlPUQ`4-SWHt;70lJV9WrMQU;dpN_rLS@`wH-TGxb< zGcQ>PmUDj&|L33305>?bOMhkaw^jC;`b>hRCnNTwZVZj&OYt0NBu$m?Ji8u+);DIs zh8#;?)W-pr!F*+BB+-cAva7!$LRsKqP;tfMRhCpW;6XDqJvmPu77kXSDy- ztL%=A1(w;?@>Jc1&Q2oIVa6yM(vN9pHiGWim=SbuOGuHsROeXecmw3mu?D?E8TLHT z^>X0_(}m$x3etVXC>gjo$=_3KqUxGxW~l5X8!bt6bX*Tckn-$YBxL;NAdf2hN)4vI zD)^j~_PK^eqXpNGE7Vi~&Rt#)%_BRiKAwj<8KnZ>N!CZBD3n=1TuDe%s6GiH!l90I z6>tYr_N0IV2yhlzz+3gsac*pF`4`FO&ZtC6zle}%SNB6)P=T1Qr_z%}?o_|{zn+7C bi+@8FBUFb=*3wQNemua!#KxG4^-23LwdN1Q literal 0 HcmV?d00001 diff --git a/data/skins7/marking/stripes.png b/data/skins7/marking/stripes.png new file mode 100644 index 0000000000000000000000000000000000000000..77bd4dd0e09bf577998b25322ca9a08a98661a94 GIT binary patch literal 2059 zcmcgtYdDna8XkvX941swl^l{4MmY_|LQUl~#v%sathA=gU>WUP4j~psrgl!b%rqDV zg|ucqi$3H~>gzxl#%^RpDy^J{*bdp+hCTNFz5lKs&*8qF`+DE!x_>=2T!(*#fsehnS%E+AVJAqxGJbO}6t${}qOb zE`u$G#yEQwn>{l(6+nU@YUrK`k8p<7j5Zl%b&x&%ivzq_?pMNdS?<&-JFEQbCU#Ei z>r!sX%D+c_>K5dT1R_a=%6O= zP&V0iFvfA|@Y3#u#0}pZf2mH5fBAB8wUHBh>$x^c9J6F#jP+pF3?>{8Qrck{@*5^y*4 z(mw3uKB$*>peOU%2V!!COXL@y<1$>IZtHq)3c3VgiVK>~<8z7#p#z-xl z7GEs{p?t(A)W$xDxA z`nc0POKc&IG$Aig6ekD2xJ_D;33yiOTpk{ZbMw5<^RW_Q~nsYf_qY z_CfQFE@(V{lM~RJoArSd718W*~exxdsF)^eey-dKy#(iq8KDx} zStey7AjQP0<#dmN)v)~+^u6@bXw1gU}*r&bhoft?Zx>7Fo zVEFAX*40MB;M=&5T&zA1d|YeuOn#yJXHQ!}yLWd!;`*P<+CYRtPUqy!Lye$!gj7fR z>vMZ(feNV521RXhiVW=R%aFj4Q1~4o8_qxL2WcDaHYKeH;~yWXsoE zw>|#eY;FlFMSdIM#}5$_K*^AP(L;aeY?b?CGU7&F@sq^ra^nFE4QV zrOmL*Ot5ZNI61zl&IV(uV6TjJP$r)< zMFy}WprJ1hPFZ!t*Y4ODr2-%r{Y3`$|4egR!S&qH=ala}SDDEpyw3Wb`2$DJ{13Bt B-;@9V literal 0 HcmV?d00001 diff --git a/data/skins7/marking/stripes2.png b/data/skins7/marking/stripes2.png new file mode 100644 index 0000000000000000000000000000000000000000..adf0120d167f647264782b207f4521ef0796e21e GIT binary patch literal 1335 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 literal 0 HcmV?d00001 diff --git a/data/skins7/marking/tiger1.png b/data/skins7/marking/tiger1.png new file mode 100644 index 0000000000000000000000000000000000000000..83303a0e03adead785dcef70e7b9b54804e4259a GIT binary patch literal 3599 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 K(Ju`#@&5xJJcqjg literal 0 HcmV?d00001 diff --git a/data/skins7/marking/tiger2.png b/data/skins7/marking/tiger2.png new file mode 100644 index 0000000000000000000000000000000000000000..aed2bffe1955e5e230381a76c64eabaf85b8b2d7 GIT binary patch literal 3373 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 literal 0 HcmV?d00001 diff --git a/data/skins7/marking/toptri.png b/data/skins7/marking/toptri.png new file mode 100644 index 0000000000000000000000000000000000000000..25549ae3d8962a28b594b58c8a2568583051506f GIT binary patch literal 1069 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 R;Qf}S0gAIXxsgP@^cUPel9>Pi literal 0 HcmV?d00001 diff --git a/data/skins7/marking/tricircular.png b/data/skins7/marking/tricircular.png new file mode 100644 index 0000000000000000000000000000000000000000..18df6037911e156f0f511bf4e74b9b936c6d2d26 GIT binary patch literal 2174 zcmcJR={wX58^?dMGWJ5ojEpp55M!(qB5dz7+tJm+tCo)_QybHDk#yYK7zG94YPB*idd002nZSf4z*OWJ=U zirk&8$9;PMK$K#0()_nS@)q*Llh0Amoj01QbCDd~yTz3`Oy+fs$Frxc?X^a;yA85x zia{a~&tJ1Bcl|*r2W7Oy#I;mC32yK8a=xqE#ag7Jc zXB!SKiwvwM@{M6w`Y1h{Hl#4{Ntp}13!ew(Xl#%14HZe($`Ab8so28u47OAL(PFfw z!InEc61>NwlN1D=9{&$v8pq%T+UaD>>WG ztc13nV$}FC&yOwM>^}HF>~4kQb^qtXmi^r>ZdJ}Dw8@X#Vy+_*ddmSK!SP-OLeTEDvi zCqmzpssZ_~kH7#(9)9PdC%c8weJjc!rQOD3A5cCS7qSNQO>E1+x$cK-B+h?$2KYvsW_#Uj6P(>c-(?~_?%=m;l7#Fcc_lIPYR>A9vx7BX=9k#F6JRmMi4z)7n9Hd zF>vC3tD)x}@}MUOQ0@(Z1?W%TfHJvj(#d{NQZ9()QbunezNA5^7rctux#HKoUjgAL znKoKeY4QaT$YHK1}+-Tk3c%* zx^b3fU(xvbBbeC64;-Kaq8mKclFyd3PMb0a*YC_O9jIglZP+cmCUok8GHcJ)%x1%N z%pejX*N19)&7KhGz8Z3N=j25c{ zEH8n&0CReiPgfs^ujqoi$M4>zs{dqu#IAgbzbgl+*DNI_Fet9n4q7<0ocwJx92$sU zdm`bQZKAqTt6h|-*cF8NGvKeZa78E;mI=+g7&yKqix%4o02Ro_{!8Avu$FJINXU}P zjI_{p1R9*=(Ka_*rptTRfLyiYijzft_y&7=$PTb z$Ufvjl!A?J6eiEo51ZCju}QM}jtOBu0a~22CqJg-RabecoCC_K7XC#Jb?%lV{u1GADV8TuqObSy`iBY^4NhZZqmu(G3NrIV9O785KRZGUl&$xEI zI$;oTCNeo43}?)xP?VwQn~$=VPVF_a=Qn>&!gK7A5hP)>IIr4UWGkX8h^>kL;IF|U zuip3lRpr+cRfFmP`c*4aL#i4TSi?;&T~CW%6(1Jm?t9T@c%`8;K%ApLJFiF=)C12B zJbWh`m2uIK^u;(kfkUVk6U5?A_uQ^>O=8J*@OzKSoD1tgXw z4wdEw#)GrXFhc7EfiM0^57lE27+l)3lyg-@??{RSD)g&VsgjPUkI5>vE_#Bl5heB` zir`%NpNBq802MYlwdk9cM90>uY&WTe71YF77zI2X+fb?f%i?%y_M357bLQKjhQ0%# z5}R~yyX!EI^e}6}&SK`zJuy`)kY$$njSgaVK|rp8-sna=EAoej?N)1{>A}jY`s{%B zrS{B^cZ0w?PGR#9W%52RPIJGm%H)qt%rfQqA05y$u9;nIxsXd0Ds4c2*F z_@am*Og|$mIkeiSOE1hSFSN)tH4^jD=M`T)*%~WJ`Z5tg;wCS!mkcrWR;>QIxBlcq zHBNwvclR;Q2J-;IKcPzP{nFAA#bLe7jhrSCc0cc66_?{PrNq6}0=ig_*xPNTrd4U_ zGiQ&@!Q& UUm&DQcY6h3Lvc7+ZQ+yn4~n$e_5c6? literal 0 HcmV?d00001 diff --git a/data/skins7/marking/tripledon.png b/data/skins7/marking/tripledon.png new file mode 100644 index 0000000000000000000000000000000000000000..5b899d5d1c28c27c1291eb94301095f3a0f66a6c GIT binary patch literal 1206 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV5#+VaSW-L^Y)H!M`)_d@sIB_ zmM(OPh?H`?%$@7%DcF;7@%FM%eW53dCM~&kaaFIixZ~Eo+@^hBo=M<+W8Av4TnzqH3 zA$vNrz;lKpmJEe_4s&S4ypx*Czd((jiJ#G4^>qKq>&qLsPyV0Ju#RmV^E&-&FR!~d zF!T2A4HTaFZ|07r2dod&7XSIps?>D(K@Zn)o|dtl$BetVuAmB%OB_b@ly$a$~O$8Ps~e*aIYibWM?-kbKW zHZHoy+Ot{m2Hzb%AEwtG&4)4*xGE;!$UPCjeZcL>KJE>b7SJ%b{yq5McjnU#$p^L{ zi09q&Ea-vTgRlo&5AL$XZ8odrPvH6?62Z&U@V;|%`SDH-KZf+Kr*CF_-G8KIoyN4O zM>i=&-gof%rhBwXXvdt6-0Z*hNqdVbq$~J8$Qe%LE}i#jKg;dL>t1%-jEgMTelS$* zdy;(G@1J*y!Oscq3=H~AyLWsM%8&iVbl&uXRqu&J@d>-cD`a2R2j1MFq zxxMS>9Oip$acLhJOXoQ;IG^tL{=r=Rb?K44O_z^!E_sNcHc+e-AEc)TMsYN~mHj_)@)iKA+cmj=tzH!TIJhQpy%x7weZTm8h7p!|P^b zEmwl&2gAbkb)jipsg{O6AFFNZ(eIq>ULW?QX6wsf1_ARV8CAPeS3~zRlnXo;wOX&w z-sr5}arV{EZR|$v|2wkT_Rgqz5zoLN&v?7zZ^iU80_L4HH@-QDEy~~Ba3%LZw9dQ3 z^Fx1fRj~Y6_O?+cM1DJibMTSIYxAZZ-za~>g$5(Xo@cUdG?%J5 zd+KW?_XI1J9EEq6t;0WO|8>42x@F=#f4e7}Sv_K%=UCq1yc6>9bDjH}yl>3*;Xn4Q zJ!ROiQ1t-wfz3zQJx_Tw7ykeN literal 0 HcmV?d00001 diff --git a/data/skins7/marking/tritri.png b/data/skins7/marking/tritri.png new file mode 100644 index 0000000000000000000000000000000000000000..28d90a60acb7068340aea1621e2337351aba9d6c GIT binary patch literal 1932 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 T$3#(J0mIgTe~DWM4fGu84p literal 0 HcmV?d00001 diff --git a/data/skins7/marking/twincross.png b/data/skins7/marking/twincross.png new file mode 100644 index 0000000000000000000000000000000000000000..9a47093c9f9c7780212750bf8563ac85ff8a8be1 GIT binary patch literal 2978 zcmb`J=R4br8^-fZBq@=o+N3BN)TTyITh%;P2R&44Bq3U@Jxb6}BQ-lxu@BXjQc>EY zh^kdYQpaBDq2;tz9XsgH?|(Qip6B!Gd2!#@bKTdS=ImsHjJm z!H#x^$@M-E2tly5GIx)nEf?O+et7X@w`p?*gYV;cGe@YWWRkU}tvPRtc#^yDmbQ4s zz!g@8(m|pd^QSR!RQwm7s1x4LVxKwH?3m`v}K0}=>>dtmVR2_6pC98@IqU!z8q zRLbacTf~WKYLnxi<~EA7dzwLf3@_1T zZ_*Y-P#@5sFXFJ+shx*zL6-SqF{I!pwbQk*2-$)Ua8+hAYNy7!xl%4GrNk~1WO8wD z_TErV=dJwf&ChZ~>PPFmhH|z~%l?h!JWdIAf~a@KtpV!KpfDLv8<6bKvL&p$4IY)& z8GVx{KA8PVnQ$5)!7#mrSokvF5RJY|bb$@P^jTv@#ECV%j8+7`*C3GHNa*zh zb-+porx}ooGg656V*f7O&2Fll+$sz7p~Gz{s! #ERG{sko;^*r7;lu)&)7lGXO{ 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 Vomjs|7nsNyJYD@<);T3K0RYL#E#?3K literal 0 HcmV?d00001 diff --git a/data/skins7/marking/warpaint.png b/data/skins7/marking/warpaint.png new file mode 100644 index 0000000000000000000000000000000000000000..e403b5adfdcc03e7bd629393a22196551bb38298 GIT binary patch literal 2077 zcmcgt`#;nBAAiq=4P%YtmR!C?mP<*ki->&{R^cqyeNkGfb22dwI~qQuV`Y)7l+1mV z+|HM=Ifz^)6lygYqGe}a-z6bTH)rRM_&ny1EUT7^-EdT)8 z$J>*ns?gto)KKMax5z;NG-rG~Jp$-ObDY5o8xNWFbvQSs_UsdA4jiF&RgV@WyA#v` zW8KyFIFusmVk^=-1HbF}ztX>&DS1hEXF^S?#l3NnERT+? zX1-pfk)@kCrFIkE7IJkW;%vDivk4>7c&FJVeM0H;bEV{3!g+M<@<%5wyZdv`FGR6Z zj*lIfKA~2xwk2P0#twi_$?b53{`z{pq%_P?PjQZou}Ca2g$mg;Q}<`&FRZ4-AY|Ur#YWKcNUyEHt2RiH&>o>DLh#8B6Rk(63UE^)#mlMo>Zr~O{X!PKMXx$^l~=g_(|b6FOo2Xw!2(3=cYm$ z7(4rdYyWmT5tmjX7|~}s{b>!-DzA0K%ZGZR_OBtIDCgE+ZC(*yJnc_xzpA4qC}l3q z%e8Hn1k>%*qBb~7_Z(eqxaF&ki;pp-cq!bb18(M!BX@BZ2J?~zbF~S`R87mA)#gZ8 zj^P%BZS>9or_PJx26-$m;{-O{0|~=fSF)d9k3(6Ne>)e#$J>~Gpq4J5oHB+}So4Fu zxeP;%z1ZoORXk2IfckC{Wn)Bc2Capqa(V}LZoZ|SY$6Q6E_h(vaq3pq0qn3UgVVeA zxF1Fpe`1Ts+EksRyS8?QxJ--+I}a~rMlxgg*%9?wG4=e`DU*qna>`_k(PYom=2h3? z2Mt-BS3q!mYN7SlS)(&&!wnSbRtPmN6dmlM*IQCu?I(|Tg7iharVH|{zfP$pL})Wh zp|JZ}T1vsJC)D?^yP;ia8XwEEX?~`;e0I8G`93ipr1nVHCNc= zEdSw}`Fyr;_nFlI$A4Rt2(A4TH@?Je4Pq1;mu$0HeJ6UN{VD?jCX6zJ2|6*(N|0&O?&%9XkCA+1D-QRlo` zR>gzhgZ?#OkCQR~gnMLno{69d&|LM%Tb3Zq0fLUS?Q6_^I}OP1A1f%a8N}F33w` z$DF|s`TW^T%>Bj#7GTPR@`kwa{i!XnSb2N8i5GI}cd?E7YY%paRZpSQ83*5HHq8N< zp*b0oR32Q-roFen=gn+(y64*eu3%7BwL@pvl=2(N8+W#*$(iZtajN_(MtWF?kG~oZ zBXY5yzoklE*MI)@{gyZx-C4+9eCx5`M_yLgjwqHBE7=`D8P#D-d3qe2VUwIyKSiN4 zCwp+Kb*ACyL{}H#XB0GPEtN)%LLS@0+SN7j+tCvw407Zmw~;0g=`1wFvX8$Oz4dRubK&<3jcxxvpE%uX$it`y@(N zn6#Llk-Fp5r~T=}3IK%PbKJkSf(qpyy8fOj1_4%x*MlHSq}ETB_9}b7hK6>j8Qf%) zy2GGw)G1h4Qi55aRM(jsP}Z-hWeDNSX-9ZFFaDooPbIDLgU^p=2M%xWi&Dux@Hyn` JSw}dT@fT>kw5b39 literal 0 HcmV?d00001 diff --git a/data/skins7/marking/warstripes.png b/data/skins7/marking/warstripes.png new file mode 100644 index 0000000000000000000000000000000000000000..bed937b0808d852c84f55d24b9da8c337a491a17 GIT binary patch literal 1098 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU=j3maSW-L^Y-TXY>{%AV;{?f ziZ~QSxwS6Ihkq1Xa>Q9%%QB;TMZnS|Z~oRGftEQ{CmjE`K6~VPq5IpVZxfV66&<_3 zDLqmYe|>e$)NL0U7HUL3Uf%wm<+0h#95d^>^v^c>Gfw}Tb^gqr&Bim|c5?WH=f7jr z(PvjU$JkNK@JOb?V(2h~IUeXPSl|2otLXc?SIm+bY7fLckTKw_;n3Ok(X#t!wR-pY zTi(@*p;7)WPv$KY-o|*m!TEs32GJj)KV&NQ96VpgzJu!x`?H432W*->`^|sc{ORhl zU5{C?P)li@tBY-1rJ&$*9mI(JA~HPA2qL%6HGTgQ?@PQd)BL?Zw`yz z=wGdOlRYBUQbaJ=x9r`d`^N7dJWOia>LYr4O7;FQtA>9aKa7;pqHZh{6r8S{w1KIZ zUu^T1q-8gb{o^_lJCk9X%aQGZf!T%XdTvJ|SfAy^9P7Fn6{x+f()-=c;y9Ll>^+R} zpZ~A^dG(L?u2cVau32dILgVHBmV;vP_WUs{`ac9o{E>BQGz1+p_Dj=xTC5L{LNLGi4G)2yAL!YLL%CVaO$ z!0=&0iB`rlwt`7tN^knMHOPBxUp{*#YZ>EtulX6Qmi0VUY(48g&aysGxa<7`=UG23 zGNorps$V|X{->O&x?%SLc5R#Q4PUbF+>xHU;`279{fxWAE8fYyESdOL;mJQsi-!D$ z*0$sS8Tc;$WtYEXCZm>nxrlp7?)hKhGprdr#C=cuv-&sszi9jYVr{j{t!2wk%SkV1 z65qPhpV7Oj;q?K>OMSD>S^H(WHO4p0K5#tc-|O~QEDRf%e{lTJ{H~z-drC>-kMwtr o3!)e!*g|)(ZRnv_PI+f9*AWtTT;$U*S>FVdQ&MBb@0P29=k^lez literal 0 HcmV?d00001 diff --git a/data/skins7/marking/whisker.png b/data/skins7/marking/whisker.png new file mode 100644 index 0000000000000000000000000000000000000000..a96b1c9208e28d6471b5655be68364ef439ad3ea GIT binary patch literal 924 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU{3aQaSW-L^Y*4~c5tFh+ehtC zD+`f$?Vu@PbFUoK;VUb8sn(So{=BZS>4Er#qf5J;_cvtBeVfMGe(C7h>5G~(woK(y z3bN%pFP!8TWaHWQeChYU%{-PL=KOJ9XXEK7X8) zVZJ@iFze6!=cZ!k_pT2yZCF_$eMII0)3rwV13`VCb&eLVJAQnIbirSP^UDQ#RXk74 zdzdeLK&n9T-hGC347(+-?PFdReEb}9zT2FaG9Ro;bA)G{-0)4|gU}9{_xqXB8s`e8 zpYpw0*6{m)>jJ@i-v@qerR^_jKc3w2E&kWsWGlP0XrNg)Cc3%=J$TT_ZT12E2bnjUpFFWDV5o@srjQxRdw?rw>wAVU19pXT zj2)|s8Lo5O|8?5TZ^qJTuOtrGJbZpd>w|2BUg5N-7KgLuWbQi>*%ov>o^k%meO%KS zmN)pbo-Y*o;j!b$?Q2YG-1CxEN^dhRd$E`0wnEGw&09=&_3c`2YW-Y)Kkn5I=A4T= zH(e`Z`w{R#W_jcHmC3ii{F@$_3Dm$K_A;i+w?VY4{$pqVPN8&#b6X+~$tbUU%*UX3 z=k@{NL*-3IzD(u~?FZgZN|%g?W!^C7%17k`MNjCO0Kc<~cNEHQNPG1inClokUHx3v IIVCg!08q+_VE_OC literal 0 HcmV?d00001 diff --git a/data/skins7/marking/wildpaint.png b/data/skins7/marking/wildpaint.png new file mode 100644 index 0000000000000000000000000000000000000000..ccad83538ad8df512058731c4660f33d67c44abe GIT binary patch literal 3137 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 literal 0 HcmV?d00001 diff --git a/data/skins7/marking/yinyang.png b/data/skins7/marking/yinyang.png new file mode 100644 index 0000000000000000000000000000000000000000..caf6a6e87806435560871a3a641b4b203e401d54 GIT binary patch literal 1897 zcmcIl`#;kQ1O9F{=DIZpX~{K&m`hkgTi?-?kjXe9A*9|+CHH)d7UeQ`6QYLHVG{9* z+Hp!l^}3bVVQv|RSV&ZFj`Kgf?+?%Od_K?f`8>~$PrAG75ky zl=L?L;c%uG0A%pa6tZVb>8H|DX(vzagr0NyJrR}~mX6$C66UG{Wydh>l~%b9XOZNb zyd1XoP|65MKb$~qxSQ-x;TF{&4!Mb*Lb6;y`J-tHS4`^6C54u*=&g|`+fSE6c!@so zWt&@Q7N@^?{afgJoA^&(c+K=)J|AHD|Jtj>H&jz55%i%;F2KiPpvN4R9|Sjig>cW3 zt=Rp44kT_keEXu7jtj80CMDmiq|=FDgeu?KG3=RZ_B5i_qO0W_+hF{91r;sZpgKGlwN6gk_naa38T?ud#H}7>j zeBKN;gS4d6n>@Bu`7uQi_9ZrdrVP!nmn4Im#{6OaZBz2cOj(Q@v4P;&@v_xkc@Zp^ z^5TXWtGPfis#4Rm_lYkm7i)XO!-lnH+tzH%10iiQ0VnZhS<`~xFD!}RSft*6l-E2H zJB+~FwdSy?O-pbLrblO|=A1k&x+R!aQPwJtuo$w3{U~TyOI`2B>67fF_<8)jui|)c5iy_F@i7~@boqj% zvOX!%6>!V*Wx6G|Xw)CNzA?fYKEp?Zz4p?3E()`?z43aI4kRiK`+JE+5z(zqAtkZq z_mS$Lfi9Y611m}%EXcl-{Fv~h8RIN`3%dwBa}1Gx>d`dFQc$rB{JCt1H;&?FS=|Qi zL5_ldt&q8^3+9krPSS}@+h1d@bjQlgJy=Z&#c(?MOyC)$_rhT*Wtx8wT4KR4OoQD;p}Nxz_HH8X7$ca z)Cp*kDpo5~(^P7wZyLtk`ROqeDmR-5rD2dCm_M2~0uNlW2{W7fw!nU?IqfdKLVolk zyAR(U5s+D3X(@qpED4+GdMxuV!}C+uH=}Un+a95|g#@>C`=F*m(HO zG@+(!Y7h4O=XC%zX@_XDOZcK~9Fa{+S{~m!5gTkB>2NA+oG|Ads@MD%!K4r}2!sSgQtybQHb6S*7I@9!@bwj?Gpb`w$rL~Rr! zT%xnJ0>_Vj{#M7Aes%EK2xJ0}H9)|8{eLgX(qsC1-)fJ=8I1{iC{>uwz`n5H69)4G z^MJ%e;*d$7BYAWQ1y2srt!}wrqYfbhc;Ke$gx^e&b)72*mz)h{%RXKG>JcagcSZy3 zI+2hz^z^13>7A*J$axcYO!^*Ly`-`IW;w?l5k~fJF7dglwZ{is@$0<4qZ0#I6!Of7 zsO&2{NMg`78cO4_bw^ni-`Oqs^@SR!R0DoYuuFT@B1vvakD(@sA4#U9S|BKtP_e%u zkqvJ}*-=Pk=t)_iaXx78iagH97qLe=D_?`UzfuwQ?ls&&SEL5^PUx)^Y5I!6;_U%l zK@4ZMsop(JDMhJqRr>;r)YXg3Z}Ke3G1Xfa^(b+7RLTPuet%K02L4}rtGMX&<~&k$ zTxOUwOy`MKEv+a@O0?)0sG!rGml}cv^p4;)eaOZKZuo;X*ujL(o9Eq2jzei!id7md(Lx5tQl9e;}wsNbmysoWGK8#J7QOq zmW)~UoB6g5!7&_>T_+rmURHhSu3S-aM(}&DhT!wV{&RPg?;l$81FOsJ1tIT(s5Q$q zgb=N_3TI(|^|MN+c65l)10(hV)hIKA%c)E!ScXwalxo0C>i_Og-X7xnIGh{t+!)=y OP{7&Il~R8=fcZC6?@t>5 literal 0 HcmV?d00001 diff --git a/data/skins7/monkey.json b/data/skins7/monkey.json new file mode 100644 index 000000000..224fbdd10 --- /dev/null +++ b/data/skins7/monkey.json @@ -0,0 +1,42 @@ +{"skin": { + "body": { + "filename": "monkey", + "custom_colors": "true", + "hue": 21, + "sat": 175, + "lgt": 196 + }, + "marking": { + "filename": "monkey", + "custom_colors": "true", + "hue": 22, + "sat": 132, + "lgt": 50, + "alp": 255 + }, + "decoration": { + "filename": "hair", + "custom_colors": "true", + "hue": 35, + "sat": 230, + "lgt": 155 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 25, + "sat": 82, + "lgt": 144 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 19, + "sat": 113, + "lgt": 175 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/paintgre.json b/data/skins7/paintgre.json new file mode 100644 index 000000000..d534c2eb7 --- /dev/null +++ b/data/skins7/paintgre.json @@ -0,0 +1,35 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 36, + "sat": 154, + "lgt": 106 + }, + "marking": { + "filename": "lowpaint", + "custom_colors": "true", + "hue": 52, + "sat": 255, + "lgt": 255, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 39, + "sat": 171, + "lgt": 139 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 32, + "sat": 132, + "lgt": 59 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/pandabear.json b/data/skins7/pandabear.json new file mode 100644 index 000000000..11caa1029 --- /dev/null +++ b/data/skins7/pandabear.json @@ -0,0 +1,42 @@ +{"skin": { + "body": { + "filename": "bear", + "custom_colors": "true", + "hue": 150, + "sat": 16, + "lgt": 78 + }, + "marking": { + "filename": "panda1", + "custom_colors": "true", + "hue": 158, + "sat": 42, + "lgt": 233, + "alp": 255 + }, + "decoration": { + "filename": "hair", + "custom_colors": "false" + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 27, + "sat": 0, + "lgt": 158 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 28, + "sat": 0, + "lgt": 62 + }, + "eyes": { + "filename": "standard", + "custom_colors": "true", + "hue": 0, + "sat": 160, + "lgt": 255 + }} +} diff --git a/data/skins7/panther.json b/data/skins7/panther.json new file mode 100644 index 000000000..844a28608 --- /dev/null +++ b/data/skins7/panther.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "kitty", + "custom_colors": "true", + "hue": 165, + "sat": 0, + "lgt": 0 + }, + "marking": { + "filename": "wildpaint", + "custom_colors": "true", + "hue": 0, + "sat": 255, + "lgt": 255, + "alp": 43 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 27, + "sat": 0, + "lgt": 16 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 28, + "sat": 0, + "lgt": 54 + }, + "eyes": { + "filename": "negative", + "custom_colors": "true", + "hue": 32, + "sat": 255, + "lgt": 59 + }} +} diff --git a/data/skins7/pento.json b/data/skins7/pento.json new file mode 100644 index 000000000..20b7921e8 --- /dev/null +++ b/data/skins7/pento.json @@ -0,0 +1,42 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 158, + "sat": 178, + "lgt": 123 + }, + "marking": { + "filename": "triplate", + "custom_colors": "true", + "hue": 0, + "sat": 255, + "lgt": 255, + "alp": 209 + }, + "decoration": { + "filename": "unipento", + "custom_colors": "true", + "hue": 158, + "sat": 178, + "lgt": 123 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 184 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 152, + "sat": 137, + "lgt": 157 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/piggy.json b/data/skins7/piggy.json new file mode 100644 index 000000000..c1deb4cf6 --- /dev/null +++ b/data/skins7/piggy.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "piglet", + "custom_colors": "true", + "hue": 251, + "sat": 220, + "lgt": 180 + }, + "marking": { + "filename": "hipbel", + "custom_colors": "true", + "hue": 3, + "sat": 101, + "lgt": 112, + "alp": 171 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 249, + "sat": 189, + "lgt": 147 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 252, + "sat": 180, + "lgt": 143 + }, + "eyes": { + "filename": "colorable", + "custom_colors": "true", + "hue": 0, + "sat": 255, + "lgt": 128 + }} +} diff --git a/data/skins7/pinky.json b/data/skins7/pinky.json new file mode 100644 index 000000000..c6a68e805 --- /dev/null +++ b/data/skins7/pinky.json @@ -0,0 +1,35 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 242, + "sat": 201, + "lgt": 187 + }, + "marking": { + "filename": "whisker", + "custom_colors": "true", + "hue": 243, + "sat": 198, + "lgt": 214, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 229, + "sat": 137, + "lgt": 218 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 229, + "sat": 137, + "lgt": 218 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/raccoon.json b/data/skins7/raccoon.json new file mode 100644 index 000000000..44a82f4da --- /dev/null +++ b/data/skins7/raccoon.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "raccoon", + "custom_colors": "true", + "hue": 16, + "sat": 133, + "lgt": 121 + }, + "marking": { + "filename": "coonfluff", + "custom_colors": "true", + "hue": 17, + "sat": 110, + "lgt": 54, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 16, + "sat": 133, + "lgt": 121 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 17, + "sat": 129, + "lgt": 38 + }, + "eyes": { + "filename": "standard", + "custom_colors": "true", + "hue": 23, + "sat": 196, + "lgt": 45 + }} +} diff --git a/data/skins7/redbopp.json b/data/skins7/redbopp.json new file mode 100644 index 000000000..db74aca51 --- /dev/null +++ b/data/skins7/redbopp.json @@ -0,0 +1,42 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 246, + "sat": 216, + "lgt": 108 + }, + "marking": { + "filename": "donny", + "custom_colors": "true", + "hue": 2, + "sat": 217, + "lgt": 202, + "alp": 255 + }, + "decoration": { + "filename": "unibop", + "custom_colors": "true", + "hue": 246, + "sat": 216, + "lgt": 108 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 246, + "sat": 216, + "lgt": 108 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 116, + "sat": 85, + "lgt": 233 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/redstripe.json b/data/skins7/redstripe.json new file mode 100644 index 000000000..1950ddaf6 --- /dev/null +++ b/data/skins7/redstripe.json @@ -0,0 +1,31 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 248, + "sat": 214, + "lgt": 123 + }, + "marking": { + "filename": "stripe", + "custom_colors": "false" + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 184 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 149, + "sat": 4, + "lgt": 71 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/saddo.json b/data/skins7/saddo.json new file mode 100644 index 000000000..e3acbdde7 --- /dev/null +++ b/data/skins7/saddo.json @@ -0,0 +1,35 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 109, + "sat": 109, + "lgt": 127 + }, + "marking": { + "filename": "saddo", + "custom_colors": "true", + "hue": 108, + "sat": 54, + "lgt": 68, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 55, + "sat": 141, + "lgt": 170 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 88, + "sat": 97, + "lgt": 119 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/setisu.json b/data/skins7/setisu.json new file mode 100644 index 000000000..b841f4aec --- /dev/null +++ b/data/skins7/setisu.json @@ -0,0 +1,42 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 12, + "sat": 255, + "lgt": 52 + }, + "marking": { + "filename": "setisu", + "custom_colors": "true", + "hue": 34, + "sat": 255, + "lgt": 198, + "alp": 178 + }, + "decoration": { + "filename": "hair", + "custom_colors": "true", + "hue": 25, + "sat": 70, + "lgt": 41 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 30, + "sat": 147, + "lgt": 63 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 25, + "sat": 154, + "lgt": 76 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/snowti.json b/data/skins7/snowti.json new file mode 100644 index 000000000..ca8636042 --- /dev/null +++ b/data/skins7/snowti.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "kitty", + "custom_colors": "true", + "hue": 23, + "sat": 0, + "lgt": 255 + }, + "marking": { + "filename": "tiger2", + "custom_colors": "true", + "hue": 28, + "sat": 107, + "lgt": 0, + "alp": 204 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 22, + "sat": 0, + "lgt": 173 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 22, + "sat": 0, + "lgt": 192 + }, + "eyes": { + "filename": "colorable", + "custom_colors": "true", + "hue": 28, + "sat": 147, + "lgt": 42 + }} +} diff --git a/data/skins7/spiky.json b/data/skins7/spiky.json new file mode 100644 index 000000000..8b7b9c3f0 --- /dev/null +++ b/data/skins7/spiky.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "spiky", + "custom_colors": "true", + "hue": 28, + "sat": 0, + "lgt": 255 + }, + "marking": { + "filename": "warstripes", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 0, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 27, + "sat": 0, + "lgt": 255 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 28, + "sat": 135, + "lgt": 255 + }, + "eyes": { + "filename": "colorable", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 28 + }} +} diff --git a/data/skins7/swardy.json b/data/skins7/swardy.json new file mode 100644 index 000000000..4c1e31902 --- /dev/null +++ b/data/skins7/swardy.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "spiky", + "custom_colors": "true", + "hue": 75, + "sat": 171, + "lgt": 32 + }, + "marking": { + "filename": "duodonny", + "custom_colors": "true", + "hue": 85, + "sat": 52, + "lgt": 189, + "alp": 96 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 85, + "sat": 112, + "lgt": 0 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 85, + "sat": 87, + "lgt": 156 + }, + "eyes": { + "filename": "standard", + "custom_colors": "true", + "hue": 28, + "sat": 178, + "lgt": 94 + }} +} diff --git a/data/skins7/tiger.json b/data/skins7/tiger.json new file mode 100644 index 000000000..c759e5a9f --- /dev/null +++ b/data/skins7/tiger.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "kitty", + "custom_colors": "true", + "hue": 22, + "sat": 210, + "lgt": 107 + }, + "marking": { + "filename": "tiger1", + "custom_colors": "true", + "hue": 19, + "sat": 255, + "lgt": 219, + "alp": 220 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 22, + "sat": 180, + "lgt": 99 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 22, + "sat": 210, + "lgt": 114 + }, + "eyes": { + "filename": "colorable", + "custom_colors": "true", + "hue": 28, + "sat": 255, + "lgt": 0 + }} +} diff --git a/data/skins7/tooxy.json b/data/skins7/tooxy.json new file mode 100644 index 000000000..1a8a0b54e --- /dev/null +++ b/data/skins7/tooxy.json @@ -0,0 +1,42 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 72, + "sat": 214, + "lgt": 123 + }, + "marking": { + "filename": "wildpaint", + "custom_colors": "true", + "hue": 65, + "sat": 0, + "lgt": 128, + "alp": 218 + }, + "decoration": { + "filename": "unimelo", + "custom_colors": "true", + "hue": 244, + "sat": 0, + "lgt": 140 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 184 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 149, + "sat": 4, + "lgt": 71 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/toptri.json b/data/skins7/toptri.json new file mode 100644 index 000000000..e74314f55 --- /dev/null +++ b/data/skins7/toptri.json @@ -0,0 +1,31 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 93, + "sat": 95, + "lgt": 163 + }, + "marking": { + "filename": "toptri", + "custom_colors": "false" + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 55, + "sat": 141, + "lgt": 170 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 88, + "sat": 97, + "lgt": 119 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/twinbop.json b/data/skins7/twinbop.json new file mode 100644 index 000000000..98d461921 --- /dev/null +++ b/data/skins7/twinbop.json @@ -0,0 +1,42 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 233, + "sat": 158, + "lgt": 183 + }, + "marking": { + "filename": "duodonny", + "custom_colors": "true", + "hue": 231, + "sat": 146, + "lgt": 218, + "alp": 255 + }, + "decoration": { + "filename": "twinbopp", + "custom_colors": "true", + "hue": 233, + "sat": 158, + "lgt": 183 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 233, + "sat": 158, + "lgt": 183 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 0, + "sat": 146, + "lgt": 224 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/twintri.json b/data/skins7/twintri.json new file mode 100644 index 000000000..440b9d318 --- /dev/null +++ b/data/skins7/twintri.json @@ -0,0 +1,35 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 52, + "sat": 156, + "lgt": 124 + }, + "marking": { + "filename": "twintri", + "custom_colors": "true", + "hue": 40, + "sat": 222, + "lgt": 227, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 185 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 147, + "sat": 4, + "lgt": 72 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/warmouse.json b/data/skins7/warmouse.json new file mode 100644 index 000000000..ecc15c354 --- /dev/null +++ b/data/skins7/warmouse.json @@ -0,0 +1,38 @@ +{"skin": { + "body": { + "filename": "mouse", + "custom_colors": "true", + "hue": 28, + "sat": 0, + "lgt": 213 + }, + "marking": { + "filename": "mice", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 255, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 27, + "sat": 0, + "lgt": 255 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 28, + "sat": 135, + "lgt": 255 + }, + "eyes": { + "filename": "negative", + "custom_colors": "true", + "hue": 0, + "sat": 200, + "lgt": 28 + }} +} diff --git a/data/skins7/warpaint.json b/data/skins7/warpaint.json new file mode 100644 index 000000000..b4543ef14 --- /dev/null +++ b/data/skins7/warpaint.json @@ -0,0 +1,31 @@ +{"skin": { + "body": { + "filename": "standard", + "custom_colors": "true", + "hue": 29, + "sat": 173, + "lgt": 87 + }, + "marking": { + "filename": "warpaint", + "custom_colors": "false" + }, + "hands": { + "filename": "standard", + "custom_colors": "true", + "hue": 11, + "sat": 115, + "lgt": 1 + }, + "feet": { + "filename": "standard", + "custom_colors": "true", + "hue": 29, + "sat": 173, + "lgt": 87 + }, + "eyes": { + "filename": "standard", + "custom_colors": "false" + }} +} diff --git a/data/skins7/x_ninja.json b/data/skins7/x_ninja.json new file mode 100644 index 000000000..595a729f5 --- /dev/null +++ b/data/skins7/x_ninja.json @@ -0,0 +1,29 @@ +{"skin": { + "body": { + "filename": "x_ninja", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 0 + }, + "marking": { + "filename": "uppy", + "custom_colors": "true", + "hue": 0, + "sat": 0, + "lgt": 64, + "alp": 255 + }, + "hands": { + "filename": "standard", + "custom_colors": "false" + }, + "feet": { + "filename": "standard", + "custom_colors": "false" + }, + "eyes": { + "filename": "x_ninja", + "custom_colors": "false" + }} +} diff --git a/data/skins7/xmas_hat.png b/data/skins7/xmas_hat.png new file mode 100644 index 0000000000000000000000000000000000000000..28c0b022f3fe42d359631a5baf26b35137971c7e GIT binary patch literal 17887 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 0) + { + new_cursor = str_utf8_forward(src, cursor); + if(new_cursor >= dst_size) // reserve 1 byte for the null termination + break; + else + cursor = new_cursor; + --num; + } + + str_copy(dst, src, cursor < dst_size ? cursor + 1 : dst_size); +} + void str_utf8_stats(const char *str, size_t max_size, size_t max_count, size_t *size, size_t *count) { const char *cursor = str; diff --git a/src/base/system.h b/src/base/system.h index 260b6abf7..438432707 100644 --- a/src/base/system.h +++ b/src/base/system.h @@ -2395,6 +2395,22 @@ int str_utf8_encode(char *ptr, int chr); */ int str_utf8_check(const char *str); +/* + Function: str_utf8_copy_num + Copies a number of utf8 characters from one string to another. + + Parameters: + dst - Pointer to a buffer that shall receive the string. + src - String to be copied. + dst_size - Size of the buffer dst. + num - maximum number of utf8 characters to be copied. + + Remarks: + - The strings are treated as zero-terminated strings. + - Garantees that dst string will contain zero-termination. +*/ +void str_utf8_copy_num(char *dst, const char *src, int dst_size, int num); + /* Function: str_utf8_stats Determines the byte size and utf8 character count of a utf8 string. diff --git a/src/game/client/components/skins7.cpp b/src/game/client/components/skins7.cpp index 183ce886a..2b19f52ad 100644 --- a/src/game/client/components/skins7.cpp +++ b/src/game/client/components/skins7.cpp @@ -25,6 +25,8 @@ char *CSkins7::ms_apSkinVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{0}} int *CSkins7::ms_apUCCVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{0}}; int *CSkins7::ms_apColorVariables[NUM_DUMMIES][protocol7::NUM_SKINPARTS] = {{0}}; +#define SKINS_DIR "skins7" + // TODO: uncomment // const float MIN_EYE_BODY_COLOR_DIST = 80.f; // between body and eyes (LAB color space) @@ -48,7 +50,7 @@ int CSkins7::SkinPartScan(const char *pName, int IsDir, int DirType, void *pUser return 0; char aFilename[IO_MAX_PATH_LENGTH]; - str_format(aFilename, sizeof(aFilename), "skins/%s/%s", CSkins7::ms_apSkinPartNames[pSelf->m_ScanningPart], pName); + str_format(aFilename, sizeof(aFilename), SKINS_DIR "/%s/%s", CSkins7::ms_apSkinPartNames[pSelf->m_ScanningPart], pName); CImageInfo Info; if(!pSelf->Graphics()->LoadPng(Info, aFilename, DirType)) { @@ -118,7 +120,7 @@ int CSkins7::SkinScan(const char *pName, int IsDir, int DirType, void *pUser) // read file data into buffer char aFilename[IO_MAX_PATH_LENGTH]; - str_format(aFilename, sizeof(aFilename), "skins/%s", pName); + str_format(aFilename, sizeof(aFilename), SKINS_DIR "/%s", pName); void *pFileData; unsigned JsonFileSize; if(!pSelf->Storage()->ReadFile(aFilename, IStorage::TYPE_ALL, &pFileData, &JsonFileSize)) @@ -276,7 +278,7 @@ void CSkins7::OnInit() // load skin parts char aBuf[64]; - str_format(aBuf, sizeof(aBuf), "skins/%s", ms_apSkinPartNames[Part]); + str_format(aBuf, sizeof(aBuf), SKINS_DIR "/%s", ms_apSkinPartNames[Part]); m_ScanningPart = Part; Storage()->ListDirectory(IStorage::TYPE_ALL, aBuf, SkinPartScan, this); @@ -313,7 +315,7 @@ void CSkins7::OnInit() // load skins m_vSkins.clear(); - Storage()->ListDirectory(IStorage::TYPE_ALL, "skins", SkinScan, this); + Storage()->ListDirectory(IStorage::TYPE_ALL, SKINS_DIR, SkinScan, this); GameClient()->m_Menus.RenderLoading(Localize("Loading DDNet Client"), Localize("Loading skin files"), 0); // add dummy skin @@ -322,7 +324,7 @@ void CSkins7::OnInit() { // add xmas hat - const char *pFileName = "skins/xmas_hat.png"; + const char *pFileName = SKINS_DIR "/xmas_hat.png"; CImageInfo Info; if(!Graphics()->LoadPng(Info, pFileName, IStorage::TYPE_ALL) || Info.m_Width != 128 || Info.m_Height != 512) { @@ -342,7 +344,7 @@ void CSkins7::OnInit() { // add bot decoration - const char *pFileName = "skins/bot.png"; + const char *pFileName = SKINS_DIR "/bot.png"; CImageInfo Info; if(!Graphics()->LoadPng(Info, pFileName, IStorage::TYPE_ALL) || Info.m_Width != 384 || Info.m_Height != 160) { diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index 5cc75cb63..c872c0a3c 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -2334,6 +2334,12 @@ void CGameClient::CClientData::Reset() m_Country = -1; m_aSkinName[0] = '\0'; m_SkinColor = 0; + for(int i = 0; i < protocol7::NUM_SKINPARTS; ++i) + { + m_aaSkinPartNames[i][0] = '\0'; + m_aUseCustomColors[i] = 0; + m_aSkinPartColors[i] = 0; + } m_Team = 0; m_Emoticon = 0; m_EmoticonStartFraction = 0; diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 6a2732bb9..2ec09e4c5 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -365,6 +365,12 @@ public: int m_Country; char m_aSkinName[24]; int m_SkinColor; + + // 0.7 Skin + char m_aaSkinPartNames[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_LENGTH]; + int m_aUseCustomColors[protocol7::NUM_SKINPARTS]; + int m_aSkinPartColors[protocol7::NUM_SKINPARTS]; + int m_Team; int m_Emoticon; float m_EmoticonStartFraction; @@ -485,6 +491,8 @@ public: void OnInit() override; void OnConsoleInit() override; void OnStateChange(int NewState, int OldState) override; + template + void ApplySkin7InfoFromGameMsg(const T *pMsg, int ClientId); void *TranslateGameMsg(int *pMsgId, CUnpacker *pUnpacker, int Conn); void OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dummy) override; void InvalidateSnapshot() override; diff --git a/src/game/client/render.cpp b/src/game/client/render.cpp index ed2974a7a..8f6246b72 100644 --- a/src/game/client/render.cpp +++ b/src/game/client/render.cpp @@ -325,7 +325,7 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn SelectSprite7(client_data7::SPRITE_TEE_BOT_FOREGROUND, 0, 0, 0); Item = BotItem; Graphics()->QuadsDraw(&Item, 1); - Graphics()->SetColor(pInfo->m_Sixup.m_BotColor.r, pInfo->m_Sixup.m_BotColor.g, pInfo->m_Sixup.m_BotColor.b, pInfo->m_Sixup.m_BotColor.a); + Graphics()->SetColor(pInfo->m_Sixup.m_BotColor); SelectSprite7(client_data7::SPRITE_TEE_BOT_GLOW, 0, 0, 0); Item = BotItem; Graphics()->QuadsDraw(&Item, 1); @@ -338,7 +338,7 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_DECORATION]); Graphics()->QuadsBegin(); Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); - Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION].r, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION].g, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION].b, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION].a); + Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION]); SelectSprite7(OutLine ? client_data7::SPRITE_TEE_DECORATION_OUTLINE : client_data7::SPRITE_TEE_DECORATION, 0, 0, 0); Item = BodyItem; Graphics()->QuadsDraw(&Item, 1); @@ -356,7 +356,7 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn } else { - Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY].r, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY].g, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY].b, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY].a); + Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY]); SelectSprite7(client_data7::SPRITE_TEE_BODY, 0, 0, 0); } Item = BodyItem; @@ -369,8 +369,8 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_MARKING]); Graphics()->QuadsBegin(); Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); - Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].r * pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].a, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].g * pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].a, - pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].b * pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].a, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING].a); + ColorRGBA MarkingColor = pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING]; + Graphics()->SetColor(MarkingColor.r * MarkingColor.a, MarkingColor.g * MarkingColor.a, MarkingColor.b * MarkingColor.a, MarkingColor.a); SelectSprite7(client_data7::SPRITE_TEE_MARKING, 0, 0, 0); Item = BodyItem; Graphics()->QuadsDraw(&Item, 1); @@ -399,11 +399,11 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); if(IsBot) { - Graphics()->SetColor(pInfo->m_Sixup.m_BotColor.r, pInfo->m_Sixup.m_BotColor.g, pInfo->m_Sixup.m_BotColor.b, pInfo->m_Sixup.m_BotColor.a); + Graphics()->SetColor(pInfo->m_Sixup.m_BotColor); Emote = EMOTE_SURPRISE; } else - Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES].r, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES].g, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES].b, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES].a); + Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES]); if(Pass == 1) { switch(Emote) @@ -433,33 +433,32 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn } Graphics()->QuadsEnd(); - // TODO: ddnet has own hats // draw xmas hat - // if(!OutLine && pInfo->m_HatTexture.IsValid()) - // { - // Graphics()->TextureSet(pInfo->m_HatTexture); - // Graphics()->QuadsBegin(); - // Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle*pi * 2); - // Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); - // int Flag = Direction.x < 0.0f ? SPRITE_FLAG_FLIP_X : 0; - // switch(pInfo->m_HatSpriteIndex) - // { - // case 0: - // SelectSprite7(SPRITE_TEE_HATS_TOP1, Flag, 0, 0); - // break; - // case 1: - // SelectSprite7(SPRITE_TEE_HATS_TOP2, Flag, 0, 0); - // break; - // case 2: - // SelectSprite7(SPRITE_TEE_HATS_SIDE1, Flag, 0, 0); - // break; - // case 3: - // SelectSprite7(SPRITE_TEE_HATS_SIDE2, Flag, 0, 0); - // } - // Item = BodyItem; - // Graphics()->QuadsDraw(&Item, 1); - // Graphics()->QuadsEnd(); - // } + if(!OutLine && pInfo->m_Sixup.m_HatTexture.IsValid()) + { + Graphics()->TextureSet(pInfo->m_Sixup.m_HatTexture); + Graphics()->QuadsBegin(); + Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + int Flag = Direction.x < 0.0f ? SPRITE_FLAG_FLIP_X : 0; + switch(pInfo->m_Sixup.m_HatSpriteIndex) + { + case 0: + SelectSprite7(client_data7::SPRITE_TEE_HATS_TOP1, Flag, 0, 0); + break; + case 1: + SelectSprite7(client_data7::SPRITE_TEE_HATS_TOP2, Flag, 0, 0); + break; + case 2: + SelectSprite7(client_data7::SPRITE_TEE_HATS_SIDE1, Flag, 0, 0); + break; + case 3: + SelectSprite7(client_data7::SPRITE_TEE_HATS_SIDE2, Flag, 0, 0); + } + Item = BodyItem; + Graphics()->QuadsDraw(&Item, 1); + Graphics()->QuadsEnd(); + } } // draw feet diff --git a/src/game/client/render.h b/src/game/client/render.h index ed5a31a9f..53e8216c7 100644 --- a/src/game/client/render.h +++ b/src/game/client/render.h @@ -81,6 +81,12 @@ public: for(auto &Texture : m_aTextures) Texture = IGraphics::CTextureHandle(); m_BotTexture = IGraphics::CTextureHandle(); + for(ColorRGBA &PartColor : m_aColors) + { + PartColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + m_HatSpriteIndex = 0; + m_BotColor = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); } bool Valid() const { @@ -91,9 +97,11 @@ public: } IGraphics::CTextureHandle m_aTextures[protocol7::NUM_SKINPARTS]; - vec4 m_aColors[protocol7::NUM_SKINPARTS]; + ColorRGBA m_aColors[protocol7::NUM_SKINPARTS]; + IGraphics::CTextureHandle m_HatTexture; IGraphics::CTextureHandle m_BotTexture; - vec4 m_BotColor; + int m_HatSpriteIndex; + ColorRGBA m_BotColor; }; CSixup m_Sixup; diff --git a/src/game/client/sixup_translate_game.cpp b/src/game/client/sixup_translate_game.cpp index bd918b33d..a93ef0730 100644 --- a/src/game/client/sixup_translate_game.cpp +++ b/src/game/client/sixup_translate_game.cpp @@ -74,6 +74,52 @@ void CGameClient::DoTeamChangeMessage7(const char *pName, int ClientId, int Team m_Chat.AddLine(-1, 0, aBuf); } +template +void CGameClient::ApplySkin7InfoFromGameMsg(const T *pMsg, int ClientId) +{ + CClientData *pClient = &m_aClients[ClientId]; + char *apSkinPartsPtr[protocol7::NUM_SKINPARTS]; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + str_utf8_copy_num(pClient->m_aaSkinPartNames[Part], pMsg->m_apSkinPartNames[Part], sizeof(pClient->m_aaSkinPartNames[Part]), protocol7::MAX_SKIN_LENGTH); + apSkinPartsPtr[Part] = pClient->m_aaSkinPartNames[Part]; + pClient->m_aUseCustomColors[Part] = pMsg->m_aUseCustomColors[Part]; + pClient->m_aSkinPartColors[Part] = pMsg->m_aSkinPartColors[Part]; + } + m_Skins7.ValidateSkinParts(apSkinPartsPtr, pClient->m_aUseCustomColors, pClient->m_aSkinPartColors, m_pClient->m_TranslationContext.m_GameFlags); + + if(time_season() == SEASON_XMAS) + { + pClient->m_SkinInfo.m_Sixup.m_HatTexture = m_Skins7.m_XmasHatTexture; + pClient->m_SkinInfo.m_Sixup.m_HatSpriteIndex = ClientId % CSkins7::HAT_NUM; + } + else + pClient->m_SkinInfo.m_Sixup.m_HatTexture.Invalidate(); + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + int Id = m_Skins7.FindSkinPart(Part, pClient->m_aaSkinPartNames[Part], false); + const CSkins7::CSkinPart *pSkinPart = m_Skins7.GetSkinPart(Part, Id); + if(pClient->m_aUseCustomColors[Part]) + { + pClient->m_SkinInfo.m_Sixup.m_aTextures[Part] = pSkinPart->m_ColorTexture; + pClient->m_SkinInfo.m_Sixup.m_aColors[Part] = m_Skins7.GetColor(pClient->m_aSkinPartColors[Part], Part == protocol7::SKINPART_MARKING); + } + else + { + pClient->m_SkinInfo.m_Sixup.m_aTextures[Part] = pSkinPart->m_OrgTexture; + pClient->m_SkinInfo.m_Sixup.m_aColors[Part] = ColorRGBA(1.0f, 1.0f, 1.0f, 1.0f); + } + if(pClient->m_SkinInfo.m_Sixup.m_HatTexture.IsValid()) + { + if(Part == protocol7::SKINPART_BODY && str_comp(pClient->m_aaSkinPartNames[Part], "standard")) + pClient->m_SkinInfo.m_Sixup.m_HatSpriteIndex = CSkins7::HAT_OFFSET_SIDE + (ClientId % CSkins7::HAT_NUM); + if(Part == protocol7::SKINPART_DECORATION && !str_comp(pClient->m_aaSkinPartNames[Part], "twinbopp")) + pClient->m_SkinInfo.m_Sixup.m_HatSpriteIndex = CSkins7::HAT_OFFSET_SIDE + (ClientId % CSkins7::HAT_NUM); + } + } +} + void *CGameClient::TranslateGameMsg(int *pMsgId, CUnpacker *pUnpacker, int Conn) { if(!m_pClient->IsSixup()) @@ -228,22 +274,7 @@ void *CGameClient::TranslateGameMsg(int *pMsgId, CUnpacker *pUnpacker, int Conn) CTranslationContext::CClientData &Client = m_pClient->m_TranslationContext.m_aClients[pMsg7->m_ClientId]; Client.m_Active = true; - CClientData *pClient = &m_aClients[pMsg7->m_ClientId]; - for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) - { - int Id = m_Skins7.FindSkinPart(p, pMsg7->m_apSkinPartNames[p], false); - const CSkins7::CSkinPart *pSkinPart = m_Skins7.GetSkinPart(p, Id); - if(pMsg7->m_aUseCustomColors[p]) - { - pClient->m_SkinInfo.m_Sixup.m_aTextures[p] = pSkinPart->m_ColorTexture; - pClient->m_SkinInfo.m_Sixup.m_aColors[p] = m_Skins7.GetColor(pMsg7->m_aSkinPartColors[p], p == protocol7::SKINPART_MARKING); - } - else - { - pClient->m_SkinInfo.m_Sixup.m_aTextures[p] = pSkinPart->m_OrgTexture; - pClient->m_SkinInfo.m_Sixup.m_aColors[p] = vec4(1.0f, 1.0f, 1.0f, 1.0f); - } - } + ApplySkin7InfoFromGameMsg(pMsg7, pMsg7->m_ClientId); // skin will be moved to the 0.6 snap by the translation context // and we drop the game message return nullptr; @@ -445,23 +476,7 @@ void *CGameClient::TranslateGameMsg(int *pMsgId, CUnpacker *pUnpacker, int Conn) str_copy(Client.m_aName, pMsg7->m_pName); str_copy(Client.m_aClan, pMsg7->m_pClan); Client.m_Country = pMsg7->m_Country; - CClientData *pClient = &m_aClients[pMsg7->m_ClientId]; - for(int p = 0; p < protocol7::NUM_SKINPARTS; p++) - { - int Id = m_Skins7.FindSkinPart(p, pMsg7->m_apSkinPartNames[p], false); - const CSkins7::CSkinPart *pSkinPart = m_Skins7.GetSkinPart(p, Id); - if(pMsg7->m_aUseCustomColors[p]) - { - pClient->m_SkinInfo.m_Sixup.m_aTextures[p] = pSkinPart->m_ColorTexture; - pClient->m_SkinInfo.m_Sixup.m_aColors[p] = m_Skins7.GetColor(pMsg7->m_aSkinPartColors[p], p == protocol7::SKINPART_MARKING); - } - else - { - pClient->m_SkinInfo.m_Sixup.m_aTextures[p] = pSkinPart->m_OrgTexture; - pClient->m_SkinInfo.m_Sixup.m_aColors[p] = vec4(1.0f, 1.0f, 1.0f, 1.0f); - } - } - + ApplySkin7InfoFromGameMsg(pMsg7, pMsg7->m_ClientId); if(m_pClient->m_TranslationContext.m_aLocalClientId[Conn] == -1) return nullptr; if(pMsg7->m_Silent || pMsg7->m_Local) From 093a78464a7b780f176b934e70da32c32569911d Mon Sep 17 00:00:00 2001 From: ChillerDragon Date: Sat, 30 Dec 2023 09:15:38 +0100 Subject: [PATCH 3/8] Add custom ddnet 0.7 skin greensward https://github.com/teeworlds/teeworlds/pull/2152 --- CMakeLists.txt | 2 ++ data/skins7/body/greensward.png | Bin 0 -> 9888 bytes data/skins7/greensward.json | 29 +++++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 data/skins7/body/greensward.png create mode 100644 data/skins7/greensward.json diff --git a/CMakeLists.txt b/CMakeLists.txt index adfd29853..10afbcc61 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1709,6 +1709,7 @@ set(EXPECTED_DATA skins7/body/dog.png skins7/body/force.png skins7/body/fox.png + skins7/body/greensward.png skins7/body/hippo.png skins7/body/kitty.png skins7/body/koala.png @@ -1741,6 +1742,7 @@ set(EXPECTED_DATA skins7/feet/standard.png skins7/force.json skins7/fox.json + skins7/greensward.json skins7/greycoon.json skins7/greyfox.json skins7/hands/standard.png diff --git a/data/skins7/body/greensward.png b/data/skins7/body/greensward.png new file mode 100644 index 0000000000000000000000000000000000000000..87dbd52a84c8e9f2b27a4c1705fb20bb95729948 GIT binary patch literal 9888 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 Date: Sat, 30 Mar 2024 13:32:50 +0800 Subject: [PATCH 4/8] Resend 0.7 skin change if it got filtered by the server --- src/game/client/gameclient.cpp | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index c872c0a3c..ba4aa2113 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -2445,6 +2445,33 @@ void CGameClient::SendSkinChange7(bool Dummy) bool CGameClient::GotWantedSkin7(bool Dummy) { + // validate the wanted skinparts before comparison + // because the skin parts we compare against are also validated + // otherwise it tries to resend the skin info when the eyes are set to "negative" + // in team based modes + char aSkinParts[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_ARRAY_SIZE]; + char *apSkinPartsPtr[protocol7::NUM_SKINPARTS]; + int aUCCVars[protocol7::NUM_SKINPARTS]; + int aColorVars[protocol7::NUM_SKINPARTS]; + for(int SkinPart = 0; SkinPart < protocol7::NUM_SKINPARTS; SkinPart++) + { + str_copy(aSkinParts[SkinPart], CSkins7::ms_apSkinVariables[(int)Dummy][SkinPart], protocol7::MAX_SKIN_ARRAY_SIZE); + apSkinPartsPtr[SkinPart] = aSkinParts[SkinPart]; + aUCCVars[SkinPart] = *CSkins7::ms_apUCCVariables[(int)Dummy][SkinPart]; + aColorVars[SkinPart] = *CSkins7::ms_apColorVariables[(int)Dummy][SkinPart]; + } + m_Skins7.ValidateSkinParts(apSkinPartsPtr, aUCCVars, aColorVars, m_pClient->m_TranslationContext.m_GameFlags); + + for(int SkinPart = 0; SkinPart < protocol7::NUM_SKINPARTS; SkinPart++) + { + if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_aaSkinPartNames[SkinPart], apSkinPartsPtr[SkinPart])) + return false; + if(m_aClients[m_aLocalIds[(int)Dummy]].m_aUseCustomColors[SkinPart] != aUCCVars[SkinPart]) + return false; + if(m_aClients[m_aLocalIds[(int)Dummy]].m_aSkinPartColors[SkinPart] != aColorVars[SkinPart]) + return false; + } + // TODO: add name change ddnet extension to 0.7 protocol // if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_aName, Dummy ? Client()->DummyName() : Client()->PlayerName())) // return false; From 8903f4971645c24756e7a0a917512a7b76fe25de Mon Sep 17 00:00:00 2001 From: ChillerDragon Date: Wed, 3 Apr 2024 16:00:16 +0800 Subject: [PATCH 5/8] Render skins in 0.7 demos --- CMakeLists.txt | 1 + src/engine/client.h | 8 +- src/engine/client/client.cpp | 87 ++- src/engine/server/server.cpp | 45 +- src/engine/shared/demo.cpp | 2 + src/engine/shared/demo.h | 2 +- .../shared/sixup_translate_snapshot.cpp | 524 +--------------- src/engine/shared/snapshot.h | 12 +- src/engine/shared/translation_context.h | 4 + src/game/client/gameclient.cpp | 13 +- src/game/client/gameclient.h | 21 +- src/game/client/sixup_translate_game.cpp | 39 +- src/game/client/sixup_translate_snapshot.cpp | 564 ++++++++++++++++++ 13 files changed, 747 insertions(+), 575 deletions(-) create mode 100644 src/game/client/sixup_translate_snapshot.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 10afbcc61..7c85dd345 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2427,6 +2427,7 @@ if(CLIENT) render.h render_map.cpp sixup_translate_game.cpp + sixup_translate_snapshot.cpp skin.h ui.cpp ui.h diff --git a/src/engine/client.h b/src/engine/client.h index 69eafbdef..8c0f501ac 100644 --- a/src/engine/client.h +++ b/src/engine/client.h @@ -14,7 +14,6 @@ #include #include -#include #include #include @@ -374,6 +373,7 @@ public: virtual const char *GetItemName(int Type) const = 0; virtual const char *Version() const = 0; virtual const char *NetVersion() const = 0; + virtual const char *NetVersion7() const = 0; virtual int DDNetVersion() const = 0; virtual const char *DDNetVersionStr() const = 0; @@ -388,9 +388,13 @@ public: virtual protocol7::CNetObjHandler *GetNetObjHandler7() = 0; virtual int ClientVersion7() const = 0; + + virtual void ApplySkin7InfoFromSnapObj(const protocol7::CNetObj_De_ClientInfo *pObj, int ClientId) = 0; + virtual int OnDemoRecSnap7(class CSnapshot *pFrom, class CSnapshot *pTo, int Conn) = 0; + virtual int TranslateSnap(class CSnapshot *pSnapDstSix, class CSnapshot *pSnapSrcSeven, int Conn, bool Dummy) = 0; }; -void SnapshotRemoveExtraProjectileInfo(CSnapshot *pSnap); +void SnapshotRemoveExtraProjectileInfo(class CSnapshot *pSnap); extern IGameClient *CreateGameClient(); #endif diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index b637fa2b1..860f42e77 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -1988,15 +1988,7 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) unsigned char aTmpTransSnapBuffer[CSnapshot::MAX_SIZE]; CSnapshot *pTmpTransSnapBuffer = (CSnapshot *)aTmpTransSnapBuffer; mem_copy(pTmpTransSnapBuffer, pTmpBuffer3, CSnapshot::MAX_SIZE); - AltSnapSize = pTmpTransSnapBuffer->TranslateSevenToSix( - pAltSnapBuffer, - m_TranslationContext, - LocalTime(), - CClient::GameTick(g_Config.m_ClDummy), - Conn, - Dummy, - GameClient()->GetNetObjHandler7(), - GameClient()->GetNetObjHandler()); + AltSnapSize = GameClient()->TranslateSnap(pAltSnapBuffer, pTmpTransSnapBuffer, Conn, Dummy); } else { @@ -2017,13 +2009,26 @@ void CClient::ProcessServerPacket(CNetChunk *pPacket, int Conn, bool Dummy) // for antiping: if the projectile netobjects from the server contains extra data, this is removed and the original content restored before recording demo SnapshotRemoveExtraProjectileInfo(pTmpBuffer3); + unsigned char aSnapSeven[CSnapshot::MAX_SIZE]; + CSnapshot *pSnapSeven = (CSnapshot *)aSnapSeven; + int DemoSnapSize = SnapSize; + if(IsSixup()) + { + DemoSnapSize = GameClient()->OnDemoRecSnap7(pTmpBuffer3, pSnapSeven, Conn); + if(DemoSnapSize < 0) + { + dbg_msg("sixup", "demo snapshot failed. error=%d", DemoSnapSize); + return; + } + } + // add snapshot to demo for(auto &DemoRecorder : m_aDemoRecorder) { if(DemoRecorder.IsRecording()) { // write snapshot - DemoRecorder.RecordSnapshot(GameTick, pTmpBuffer3, SnapSize); + DemoRecorder.RecordSnapshot(GameTick, IsSixup() ? pSnapSeven : pTmpBuffer3, DemoSnapSize); } } } @@ -2524,25 +2529,24 @@ void CClient::OnDemoPlayerSnapshot(void *pData, int Size) // create a verified and unpacked snapshot unsigned char aAltSnapBuffer[CSnapshot::MAX_SIZE]; CSnapshot *pAltSnapBuffer = (CSnapshot *)aAltSnapBuffer; - const int AltSnapSize = UnpackAndValidateSnapshot((CSnapshot *)pData, pAltSnapBuffer); - if(AltSnapSize < 0) - { - dbg_msg("client", "unpack snapshot and validate failed. error=%d", AltSnapSize); - return; - } + int AltSnapSize; - unsigned char aTmpTranslateBuffer[CSnapshot::MAX_SIZE]; - CSnapshot *pTmpTranslateBuffer = nullptr; - int TranslatedSize = 0; if(IsSixup()) { - pTmpTranslateBuffer = (CSnapshot *)aTmpTranslateBuffer; - TranslatedSize = ((CSnapshot *)pData)->TranslateSevenToSix(pTmpTranslateBuffer, m_TranslationContext, LocalTime(), GameTick(g_Config.m_ClDummy), CONN_MAIN, false, GameClient()->GetNetObjHandler7(), GameClient()->GetNetObjHandler()); - if(TranslatedSize < 0) + AltSnapSize = GameClient()->TranslateSnap(pAltSnapBuffer, (CSnapshot *)pData, CONN_MAIN, false); + if(AltSnapSize < 0) { - dbg_msg("sixup", "failed to translate snapshot. error=%d", TranslatedSize); - pTmpTranslateBuffer = nullptr; - TranslatedSize = 0; + dbg_msg("sixup", "failed to translate snapshot. error=%d", AltSnapSize); + return; + } + } + else + { + AltSnapSize = UnpackAndValidateSnapshot((CSnapshot *)pData, pAltSnapBuffer); + if(AltSnapSize < 0) + { + dbg_msg("client", "unpack snapshot and validate failed. error=%d", AltSnapSize); + return; } } @@ -2551,9 +2555,6 @@ void CClient::OnDemoPlayerSnapshot(void *pData, int Size) mem_copy(m_aapSnapshots[0][SNAP_CURRENT]->m_pSnap, pData, Size); mem_copy(m_aapSnapshots[0][SNAP_CURRENT]->m_pAltSnap, pAltSnapBuffer, AltSnapSize); - if(pTmpTranslateBuffer && TranslatedSize > 0) - mem_copy(m_aapSnapshots[0][SNAP_CURRENT]->m_pAltSnap, pTmpTranslateBuffer, TranslatedSize); - GameClient()->OnNewSnapshot(); } @@ -3916,7 +3917,20 @@ void CClient::DemoRecorder_Start(const char *pFilename, bool WithTimestamp, int str_format(aFilename, sizeof(aFilename), "demos/%s.demo", pFilename); } - m_aDemoRecorder[Recorder].Start(Storage(), m_pConsole, aFilename, GameClient()->NetVersion(), m_aCurrentMap, m_pMap->Sha256(), m_pMap->Crc(), "client", m_pMap->MapSize(), 0, m_pMap->File()); + m_aDemoRecorder[Recorder].Start( + Storage(), + m_pConsole, + aFilename, + IsSixup() ? GameClient()->NetVersion7() : GameClient()->NetVersion(), + m_aCurrentMap, + m_pMap->Sha256(), + m_pMap->Crc(), + "client", + m_pMap->MapSize(), + 0, + m_pMap->File(), + nullptr, + nullptr); } } @@ -4880,7 +4894,20 @@ void CClient::RaceRecord_Start(const char *pFilename) if(State() != IClient::STATE_ONLINE) m_pConsole->Print(IConsole::OUTPUT_LEVEL_STANDARD, "demorec/record", "client is not online"); else - m_aDemoRecorder[RECORDER_RACE].Start(Storage(), m_pConsole, pFilename, GameClient()->NetVersion(), m_aCurrentMap, m_pMap->Sha256(), m_pMap->Crc(), "client", m_pMap->MapSize(), 0, m_pMap->File()); + m_aDemoRecorder[RECORDER_RACE].Start( + Storage(), + m_pConsole, + pFilename, + IsSixup() ? GameClient()->NetVersion7() : GameClient()->NetVersion(), + m_aCurrentMap, + m_pMap->Sha256(), + m_pMap->Crc(), + "client", + m_pMap->MapSize(), + 0, + m_pMap->File(), + nullptr, + nullptr); } void CClient::RaceRecord_Stop() diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index bbaa93406..58db0157e 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -3375,7 +3375,20 @@ void CServer::DemoRecorder_HandleAutoStart() str_timestamp(aTimestamp, sizeof(aTimestamp)); char aFilename[IO_MAX_PATH_LENGTH]; str_format(aFilename, sizeof(aFilename), "demos/auto/server/%s_%s.demo", m_aCurrentMap, aTimestamp); - m_aDemoRecorder[RECORDER_AUTO].Start(Storage(), m_pConsole, aFilename, GameServer()->NetVersion(), m_aCurrentMap, m_aCurrentMapSha256[MAP_TYPE_SIX], m_aCurrentMapCrc[MAP_TYPE_SIX], "server", m_aCurrentMapSize[MAP_TYPE_SIX], m_apCurrentMapData[MAP_TYPE_SIX]); + m_aDemoRecorder[RECORDER_AUTO].Start( + Storage(), + m_pConsole, + aFilename, + GameServer()->NetVersion(), + m_aCurrentMap, + m_aCurrentMapSha256[MAP_TYPE_SIX], + m_aCurrentMapCrc[MAP_TYPE_SIX], + "server", + m_aCurrentMapSize[MAP_TYPE_SIX], + m_apCurrentMapData[MAP_TYPE_SIX], + nullptr, + nullptr, + nullptr); if(Config()->m_SvAutoDemoMax) { @@ -3402,7 +3415,20 @@ void CServer::StartRecord(int ClientId) { char aFilename[IO_MAX_PATH_LENGTH]; str_format(aFilename, sizeof(aFilename), "demos/%s_%d_%d_tmp.demo", m_aCurrentMap, m_NetServer.Address().port, ClientId); - m_aDemoRecorder[ClientId].Start(Storage(), Console(), aFilename, GameServer()->NetVersion(), m_aCurrentMap, m_aCurrentMapSha256[MAP_TYPE_SIX], m_aCurrentMapCrc[MAP_TYPE_SIX], "server", m_aCurrentMapSize[MAP_TYPE_SIX], m_apCurrentMapData[MAP_TYPE_SIX]); + m_aDemoRecorder[ClientId].Start( + Storage(), + Console(), + aFilename, + GameServer()->NetVersion(), + m_aCurrentMap, + m_aCurrentMapSha256[MAP_TYPE_SIX], + m_aCurrentMapCrc[MAP_TYPE_SIX], + "server", + m_aCurrentMapSize[MAP_TYPE_SIX], + m_apCurrentMapData[MAP_TYPE_SIX], + nullptr, + nullptr, + nullptr); } } @@ -3451,7 +3477,20 @@ void CServer::ConRecord(IConsole::IResult *pResult, void *pUser) str_timestamp(aTimestamp, sizeof(aTimestamp)); str_format(aFilename, sizeof(aFilename), "demos/demo_%s.demo", aTimestamp); } - pServer->m_aDemoRecorder[RECORDER_MANUAL].Start(pServer->Storage(), pServer->Console(), aFilename, pServer->GameServer()->NetVersion(), pServer->m_aCurrentMap, pServer->m_aCurrentMapSha256[MAP_TYPE_SIX], pServer->m_aCurrentMapCrc[MAP_TYPE_SIX], "server", pServer->m_aCurrentMapSize[MAP_TYPE_SIX], pServer->m_apCurrentMapData[MAP_TYPE_SIX]); + pServer->m_aDemoRecorder[RECORDER_MANUAL].Start( + pServer->Storage(), + pServer->Console(), + aFilename, + pServer->GameServer()->NetVersion(), + pServer->m_aCurrentMap, + pServer->m_aCurrentMapSha256[MAP_TYPE_SIX], + pServer->m_aCurrentMapCrc[MAP_TYPE_SIX], + "server", + pServer->m_aCurrentMapSize[MAP_TYPE_SIX], + pServer->m_apCurrentMapData[MAP_TYPE_SIX], + nullptr, + nullptr, + nullptr); } void CServer::ConStopRecord(IConsole::IResult *pResult, void *pUser) diff --git a/src/engine/shared/demo.cpp b/src/engine/shared/demo.cpp index 21678de73..34ddbc3c3 100644 --- a/src/engine/shared/demo.cpp +++ b/src/engine/shared/demo.cpp @@ -316,6 +316,8 @@ void CDemoRecorder::RecordSnapshot(int Tick, const void *pData, int Size) // create delta char aDeltaData[CSnapshot::MAX_SIZE + sizeof(int)]; + m_pSnapshotDelta->SetStaticsize(protocol7::NETEVENTTYPE_SOUNDWORLD, true); + m_pSnapshotDelta->SetStaticsize(protocol7::NETEVENTTYPE_DAMAGE, true); const int DeltaSize = m_pSnapshotDelta->CreateDelta((CSnapshot *)m_aLastSnapshotData, (CSnapshot *)pData, &aDeltaData); if(DeltaSize) { diff --git a/src/engine/shared/demo.h b/src/engine/shared/demo.h index 5792429f1..3cb83d184 100644 --- a/src/engine/shared/demo.h +++ b/src/engine/shared/demo.h @@ -45,7 +45,7 @@ public: CDemoRecorder() {} ~CDemoRecorder() override; - int Start(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, const char *pNetversion, const char *pMap, const SHA256_DIGEST &Sha256, unsigned MapCrc, const char *pType, unsigned MapSize, unsigned char *pMapData, IOHANDLE MapFile = nullptr, DEMOFUNC_FILTER pfnFilter = nullptr, void *pUser = nullptr); + int Start(class IStorage *pStorage, class IConsole *pConsole, const char *pFilename, const char *pNetversion, const char *pMap, const SHA256_DIGEST &Sha256, unsigned MapCrc, const char *pType, unsigned MapSize, unsigned char *pMapData, IOHANDLE MapFile, DEMOFUNC_FILTER pfnFilter, void *pUser); int Stop(IDemoRecorder::EStopMode Mode, const char *pTargetFilename = "") override; void AddDemoMarker(); diff --git a/src/engine/shared/sixup_translate_snapshot.cpp b/src/engine/shared/sixup_translate_snapshot.cpp index d90925d1c..f693b45c8 100644 --- a/src/engine/shared/sixup_translate_snapshot.cpp +++ b/src/engine/shared/sixup_translate_snapshot.cpp @@ -1,516 +1,26 @@ -#include "compression.h" -#include "snapshot.h" -#include "uuid_manager.h" - -#include -#include - -#include #include -#include -#include +#include "snapshot.h" -#include -#include -#include - -#include - -int CSnapshot::TranslateSevenToSix( - CSnapshot *pSixSnapDest, - CTranslationContext &TranslationContext, - float LocalTime, - int GameTick, - int Conn, - bool Dummy, - protocol7::CNetObjHandler *pNetObjHandler, - CNetObjHandler *pNetObjHandler6) +void CSnapshotBuilder::Init7(const CSnapshot *pSnapshot) { - CSnapshotBuilder Builder; - Builder.Init(); + // the method is called Init7 because it is only used for 0.7 support + // but the snap we are building is a 0.6 snap + m_Sixup = false; - for(auto &PlayerInfosRace : TranslationContext.m_apPlayerInfosRace) - PlayerInfosRace = nullptr; - - int SpectatorId = -3; - - for(int i = 0; i < m_NumItems; i++) + if(pSnapshot->m_DataSize + sizeof(CSnapshot) + pSnapshot->m_NumItems * sizeof(int) * 2 > CSnapshot::MAX_SIZE || pSnapshot->m_NumItems > CSnapshot::MAX_ITEMS) { - const CSnapshotItem *pItem7 = (CSnapshotItem *)GetItem(i); - const int Size = GetItemSize(i); - const int ItemType = GetItemType(i); - - // ddnet ex snap items - if((ItemType > __NETOBJTYPE_UUID_HELPER && ItemType < OFFSET_NETMSGTYPE_UUID) || pItem7->Type() == NETOBJTYPE_EX) - { - CUnpacker Unpacker; - Unpacker.Reset(pItem7->Data(), Size); - - void *pRawObj = pNetObjHandler6->SecureUnpackObj(ItemType, &Unpacker); - if(!pRawObj) - { - if(ItemType != UUID_UNKNOWN) - dbg_msg("sixup", "dropped weird ddnet ex object '%s' (%d), failed on '%s'", pNetObjHandler6->GetObjName(ItemType), ItemType, pNetObjHandler6->FailedObjOn()); - continue; - } - const int ItemSize = pNetObjHandler6->GetUnpackedObjSize(ItemType); - - void *pObj = Builder.NewItem(pItem7->Type(), pItem7->Id(), ItemSize); - if(!pObj) - return -17; - - mem_copy(pObj, pRawObj, ItemSize); - continue; - } - - if(pNetObjHandler->ValidateObj(pItem7->Type(), pItem7->Data(), Size) != 0) - { - if(pItem7->Type() > 0 && pItem7->Type() < OFFSET_UUID_TYPE) - { - dbg_msg( - "sixup", - "invalidated index=%d type=%d (%s) size=%d id=%d", - i, - pItem7->Type(), - pNetObjHandler->GetObjName(pItem7->Type()), - Size, - pItem7->Id()); - } - InvalidateItem(i); - } - - if(pItem7->Type() == protocol7::NETOBJTYPE_PLAYERINFORACE) - { - const protocol7::CNetObj_PlayerInfoRace *pInfo = (const protocol7::CNetObj_PlayerInfoRace *)pItem7->Data(); - int ClientId = pItem7->Id(); - if(ClientId < MAX_CLIENTS && TranslationContext.m_aClients[ClientId].m_Active) - { - TranslationContext.m_apPlayerInfosRace[ClientId] = pInfo; - } - } - else if(pItem7->Type() == protocol7::NETOBJTYPE_SPECTATORINFO) - { - const protocol7::CNetObj_SpectatorInfo *pSpec7 = (const protocol7::CNetObj_SpectatorInfo *)pItem7->Data(); - SpectatorId = pSpec7->m_SpectatorId; - if(pSpec7->m_SpecMode == protocol7::SPEC_FREEVIEW) - SpectatorId = SPEC_FREEVIEW; - } + // key and offset per item + dbg_assert(m_DataSize + sizeof(CSnapshot) + m_NumItems * sizeof(int) * 2 < CSnapshot::MAX_SIZE, "too much data"); + dbg_assert(m_NumItems < CSnapshot::MAX_ITEMS, "too many items"); + dbg_msg("sixup", "demo recording failed on invalid snapshot"); + m_DataSize = 0; + m_NumItems = 0; + return; } - // hack to put game info in the snap - // even though in 0.7 we get it as a game message - // this message has to be on the top - // the client looks at the items in order and needs the gameinfo at the top - // otherwise it will not render skins with team colors - if(TranslationContext.m_ShouldSendGameInfo) - { - void *pObj = Builder.NewItem(NETOBJTYPE_GAMEINFO, 0, sizeof(CNetObj_GameInfo)); - if(!pObj) - return -1; - - int GameStateFlagsSix = 0; - if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_GAMEOVER) - GameStateFlagsSix |= GAMESTATEFLAG_GAMEOVER; - if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_SUDDENDEATH) - GameStateFlagsSix |= GAMESTATEFLAG_SUDDENDEATH; - if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_PAUSED) - GameStateFlagsSix |= GAMESTATEFLAG_PAUSED; - - /* - This is a 0.7 only flag that we just ignore for now - - GAMESTATEFLAG_ROUNDOVER - */ - - CNetObj_GameInfo Info6 = {}; - Info6.m_GameFlags = TranslationContext.m_GameFlags; - Info6.m_GameStateFlags = GameStateFlagsSix; - Info6.m_RoundStartTick = TranslationContext.m_GameStartTick7; - Info6.m_WarmupTimer = 0; // removed in 0.7 - if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_WARMUP) - Info6.m_WarmupTimer = TranslationContext.m_GameStateEndTick7 - GameTick; - if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_STARTCOUNTDOWN) - Info6.m_WarmupTimer = TranslationContext.m_GameStateEndTick7 - GameTick; - - // hack to port 0.7 race timer to ddnet warmup gametimer hack - int TimerClientId = clamp(TranslationContext.m_aLocalClientId[Conn], 0, (int)MAX_CLIENTS); - if(SpectatorId >= 0) - TimerClientId = SpectatorId; - const protocol7::CNetObj_PlayerInfoRace *pRaceInfo = TranslationContext.m_apPlayerInfosRace[TimerClientId]; - if(pRaceInfo) - { - Info6.m_WarmupTimer = -pRaceInfo->m_RaceStartTick; - Info6.m_GameStateFlags |= GAMESTATEFLAG_RACETIME; - } - - Info6.m_ScoreLimit = TranslationContext.m_ScoreLimit; - Info6.m_TimeLimit = TranslationContext.m_TimeLimit; - Info6.m_RoundNum = TranslationContext.m_MatchNum; - Info6.m_RoundCurrent = TranslationContext.m_MatchCurrent; - - mem_copy(pObj, &Info6, sizeof(CNetObj_GameInfo)); - } - - for(int i = 0; i < MAX_CLIENTS; i++) - { - const CTranslationContext::CClientData &Client = TranslationContext.m_aClients[i]; - if(!Client.m_Active) - continue; - - void *pObj = Builder.NewItem(NETOBJTYPE_CLIENTINFO, i, sizeof(CNetObj_ClientInfo)); - if(!pObj) - return -2; - - CNetObj_ClientInfo Info6 = {}; - StrToInts(&Info6.m_Name0, 4, Client.m_aName); - StrToInts(&Info6.m_Clan0, 3, Client.m_aClan); - Info6.m_Country = Client.m_Country; - StrToInts(&Info6.m_Skin0, 6, Client.m_aSkinName); - Info6.m_UseCustomColor = Client.m_UseCustomColor; - Info6.m_ColorBody = Client.m_ColorBody; - Info6.m_ColorFeet = Client.m_ColorFeet; - mem_copy(pObj, &Info6, sizeof(CNetObj_ClientInfo)); - } - - bool NewGameData = false; - - for(int i = 0; i < m_NumItems; i++) - { - const CSnapshotItem *pItem7 = GetItem(i); - const int Size = GetItemSize(i); - // the first few items are a full match - // no translation needed - if(pItem7->Type() == protocol7::NETOBJTYPE_PROJECTILE || - pItem7->Type() == protocol7::NETOBJTYPE_LASER || - pItem7->Type() == protocol7::NETOBJTYPE_FLAG) - { - void *pObj = Builder.NewItem(pItem7->Type(), pItem7->Id(), Size); - if(!pObj) - return -4; - - mem_copy(pObj, pItem7->Data(), Size); - } - else if(pItem7->Type() == protocol7::NETOBJTYPE_PICKUP) - { - void *pObj = Builder.NewItem(NETOBJTYPE_PICKUP, pItem7->Id(), sizeof(CNetObj_Pickup)); - if(!pObj) - return -5; - - const protocol7::CNetObj_Pickup *pPickup7 = (const protocol7::CNetObj_Pickup *)pItem7->Data(); - CNetObj_Pickup Pickup6 = {}; - Pickup6.m_X = pPickup7->m_X; - Pickup6.m_Y = pPickup7->m_Y; - PickupType_SevenToSix(pPickup7->m_Type, Pickup6.m_Type, Pickup6.m_Subtype); - - mem_copy(pObj, &Pickup6, sizeof(CNetObj_Pickup)); - } - else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATA) - { - const protocol7::CNetObj_GameData *pGameData = (const protocol7::CNetObj_GameData *)pItem7->Data(); - TranslationContext.m_GameStateFlags7 = pGameData->m_GameStateFlags; - TranslationContext.m_GameStartTick7 = pGameData->m_GameStartTick; - TranslationContext.m_GameStateEndTick7 = pGameData->m_GameStateEndTick; - } - else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATATEAM) - { - // 0.7 added GameDataTeam and GameDataFlag - // both items merged together have all fields of the 0.6 GameData - // so if we get either one of them we store the details in the translation context - // and build one GameData snap item after this loop - const protocol7::CNetObj_GameDataTeam *pTeam7 = (const protocol7::CNetObj_GameDataTeam *)pItem7->Data(); - - TranslationContext.m_TeamscoreRed = pTeam7->m_TeamscoreRed; - TranslationContext.m_TeamscoreBlue = pTeam7->m_TeamscoreBlue; - NewGameData = true; - } - else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATAFLAG) - { - const protocol7::CNetObj_GameDataFlag *pFlag7 = (const protocol7::CNetObj_GameDataFlag *)pItem7->Data(); - - TranslationContext.m_FlagCarrierRed = pFlag7->m_FlagCarrierRed; - TranslationContext.m_FlagCarrierBlue = pFlag7->m_FlagCarrierBlue; - NewGameData = true; - - // used for blinking the flags in the hud - // but that already works the 0.6 way - // and is covered by the NETOBJTYPE_GAMEDATA translation - // pFlag7->m_FlagDropTickRed; - // pFlag7->m_FlagDropTickBlue; - } - else if(pItem7->Type() == protocol7::NETOBJTYPE_CHARACTER) - { - void *pObj = Builder.NewItem(NETOBJTYPE_CHARACTER, pItem7->Id(), sizeof(CNetObj_Character)); - if(!pObj) - return -6; - - const protocol7::CNetObj_Character *pChar7 = (const protocol7::CNetObj_Character *)pItem7->Data(); - - CNetObj_Character Char6 = {}; - // character core is unchanged - mem_copy(&Char6, pItem7->Data(), sizeof(CNetObj_CharacterCore)); - - Char6.m_PlayerFlags = 0; - if(pItem7->Id() >= 0 && pItem7->Id() < MAX_CLIENTS) - Char6.m_PlayerFlags = PlayerFlags_SevenToSix(TranslationContext.m_aClients[pItem7->Id()].m_PlayerFlags7); - Char6.m_Health = pChar7->m_Health; - Char6.m_Armor = pChar7->m_Armor; - Char6.m_AmmoCount = pChar7->m_AmmoCount; - Char6.m_Weapon = pChar7->m_Weapon; - Char6.m_Emote = pChar7->m_Emote; - Char6.m_AttackTick = pChar7->m_AttackTick; - - if(TranslationContext.m_aLocalClientId[Conn] != pItem7->Id()) - { - if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_GROUND_JUMP) - { - void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); - if(!pEvent) - return -7; - - CNetEvent_SoundWorld Sound = {}; - Sound.m_X = pChar7->m_X; - Sound.m_Y = pChar7->m_Y; - Sound.m_SoundId = SOUND_PLAYER_JUMP; - mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); - } - if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_ATTACH_PLAYER) - { - void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); - if(!pEvent) - return -7; - - CNetEvent_SoundWorld Sound = {}; - Sound.m_X = pChar7->m_X; - Sound.m_Y = pChar7->m_Y; - Sound.m_SoundId = SOUND_HOOK_ATTACH_PLAYER; - mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); - } - if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_ATTACH_GROUND) - { - void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); - if(!pEvent) - return -7; - - CNetEvent_SoundWorld Sound = {}; - Sound.m_X = pChar7->m_X; - Sound.m_Y = pChar7->m_Y; - Sound.m_SoundId = SOUND_HOOK_ATTACH_GROUND; - mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); - } - if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_HIT_NOHOOK) - { - void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); - if(!pEvent) - return -7; - - CNetEvent_SoundWorld Sound = {}; - Sound.m_X = pChar7->m_X; - Sound.m_Y = pChar7->m_Y; - Sound.m_SoundId = SOUND_HOOK_NOATTACH; - mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); - } - } - - mem_copy(pObj, &Char6, sizeof(CNetObj_Character)); - } - else if(pItem7->Type() == protocol7::NETOBJTYPE_PLAYERINFO) - { - void *pObj = Builder.NewItem(NETOBJTYPE_PLAYERINFO, pItem7->Id(), sizeof(CNetObj_PlayerInfo)); - if(!pObj) - return -8; - - const protocol7::CNetObj_PlayerInfo *pInfo7 = (const protocol7::CNetObj_PlayerInfo *)pItem7->Data(); - CNetObj_PlayerInfo Info6 = {}; - Info6.m_Local = TranslationContext.m_aLocalClientId[Conn] == pItem7->Id(); - Info6.m_ClientId = pItem7->Id(); - Info6.m_Team = 0; - if(pItem7->Id() >= 0 && pItem7->Id() < MAX_CLIENTS) - { - Info6.m_Team = TranslationContext.m_aClients[pItem7->Id()].m_Team; - TranslationContext.m_aClients[pItem7->Id()].m_PlayerFlags7 = pInfo7->m_PlayerFlags; - } - Info6.m_Score = pInfo7->m_Score; - Info6.m_Latency = pInfo7->m_Latency; - mem_copy(pObj, &Info6, sizeof(CNetObj_PlayerInfo)); - } - else if(pItem7->Type() == protocol7::NETOBJTYPE_SPECTATORINFO) - { - void *pObj = Builder.NewItem(NETOBJTYPE_SPECTATORINFO, pItem7->Id(), sizeof(CNetObj_SpectatorInfo)); - if(!pObj) - return -9; - - const protocol7::CNetObj_SpectatorInfo *pSpec7 = (const protocol7::CNetObj_SpectatorInfo *)pItem7->Data(); - CNetObj_SpectatorInfo Spec6 = {}; - Spec6.m_SpectatorId = pSpec7->m_SpectatorId; - if(pSpec7->m_SpecMode == protocol7::SPEC_FREEVIEW) - Spec6.m_SpectatorId = SPEC_FREEVIEW; - Spec6.m_X = pSpec7->m_X; - Spec6.m_Y = pSpec7->m_Y; - mem_copy(pObj, &Spec6, sizeof(CNetObj_SpectatorInfo)); - } - else if(pItem7->Type() == protocol7::NETEVENTTYPE_EXPLOSION) - { - void *pEvent = Builder.NewItem(NETEVENTTYPE_EXPLOSION, pItem7->Id(), sizeof(CNetEvent_Explosion)); - if(!pEvent) - return -10; - - const protocol7::CNetEvent_Explosion *pExplosion7 = (const protocol7::CNetEvent_Explosion *)pItem7->Data(); - CNetEvent_Explosion Explosion6 = {}; - Explosion6.m_X = pExplosion7->m_X; - Explosion6.m_Y = pExplosion7->m_Y; - mem_copy(pEvent, &Explosion6, sizeof(CNetEvent_Explosion)); - } - else if(pItem7->Type() == protocol7::NETEVENTTYPE_SPAWN) - { - void *pEvent = Builder.NewItem(NETEVENTTYPE_SPAWN, pItem7->Id(), sizeof(CNetEvent_Spawn)); - if(!pEvent) - return -11; - - const protocol7::CNetEvent_Spawn *pSpawn7 = (const protocol7::CNetEvent_Spawn *)pItem7->Data(); - CNetEvent_Spawn Spawn6 = {}; - Spawn6.m_X = pSpawn7->m_X; - Spawn6.m_Y = pSpawn7->m_Y; - mem_copy(pEvent, &Spawn6, sizeof(CNetEvent_Spawn)); - } - else if(pItem7->Type() == protocol7::NETEVENTTYPE_HAMMERHIT) - { - void *pEvent = Builder.NewItem(NETEVENTTYPE_HAMMERHIT, pItem7->Id(), sizeof(CNetEvent_HammerHit)); - if(!pEvent) - return -12; - - const protocol7::CNetEvent_HammerHit *pHammerHit7 = (const protocol7::CNetEvent_HammerHit *)pItem7->Data(); - CNetEvent_HammerHit HammerHit6 = {}; - HammerHit6.m_X = pHammerHit7->m_X; - HammerHit6.m_Y = pHammerHit7->m_Y; - mem_copy(pEvent, &HammerHit6, sizeof(CNetEvent_HammerHit)); - } - else if(pItem7->Type() == protocol7::NETEVENTTYPE_DEATH) - { - void *pEvent = Builder.NewItem(NETEVENTTYPE_DEATH, pItem7->Id(), sizeof(CNetEvent_Death)); - if(!pEvent) - return -13; - - const protocol7::CNetEvent_Death *pDeath7 = (const protocol7::CNetEvent_Death *)pItem7->Data(); - CNetEvent_Death Death6 = {}; - Death6.m_X = pDeath7->m_X; - Death6.m_Y = pDeath7->m_Y; - Death6.m_ClientId = pDeath7->m_ClientId; - mem_copy(pEvent, &Death6, sizeof(CNetEvent_Death)); - } - else if(pItem7->Type() == protocol7::NETEVENTTYPE_SOUNDWORLD) - { - void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); - if(!pEvent) - return -14; - - const protocol7::CNetEvent_SoundWorld *pSoundWorld7 = (const protocol7::CNetEvent_SoundWorld *)pItem7->Data(); - CNetEvent_SoundWorld SoundWorld6 = {}; - SoundWorld6.m_X = pSoundWorld7->m_X; - SoundWorld6.m_Y = pSoundWorld7->m_Y; - SoundWorld6.m_SoundId = pSoundWorld7->m_SoundId; - mem_copy(pEvent, &SoundWorld6, sizeof(CNetEvent_SoundWorld)); - } - else if(pItem7->Type() == protocol7::NETEVENTTYPE_DAMAGE) - { - // 0.7 introduced amount for damage indicators - // so for one 0.7 item we might create multiple 0.6 ones - const protocol7::CNetEvent_Damage *pDmg7 = (const protocol7::CNetEvent_Damage *)pItem7->Data(); - - int Amount = pDmg7->m_HealthAmount + pDmg7->m_ArmorAmount; - if(Amount < 1) - continue; - - int ClientId = pDmg7->m_ClientId; - TranslationContext.m_aDamageTaken[ClientId]++; - - float Angle; - // create healthmod indicator - if(LocalTime < TranslationContext.m_aDamageTakenTick[ClientId] + 0.5f) - { - // make sure that the damage indicators don't group together - Angle = TranslationContext.m_aDamageTaken[ClientId] * 0.25f; - } - else - { - TranslationContext.m_aDamageTaken[ClientId] = 0; - Angle = 0; - } - - TranslationContext.m_aDamageTakenTick[ClientId] = LocalTime; - - float a = 3 * pi / 2 + Angle; - float s = a - pi / 3; - float e = a + pi / 3; - for(int k = 0; k < Amount; k++) - { - // pItem7->Id() is reused that is technically wrong - // but the client implementation does not look at the ids - // and renders the damage indicators just fine - void *pEvent = Builder.NewItem(NETEVENTTYPE_DAMAGEIND, pItem7->Id(), sizeof(CNetEvent_DamageInd)); - if(!pEvent) - return -16; - - CNetEvent_DamageInd Dmg6 = {}; - Dmg6.m_X = pDmg7->m_X; - Dmg6.m_Y = pDmg7->m_Y; - float f = mix(s, e, float(k + 1) / float(Amount + 2)); - Dmg6.m_Angle = (int)(f * 256.0f); - mem_copy(pEvent, &Dmg6, sizeof(CNetEvent_DamageInd)); - } - } - else if(pItem7->Type() == protocol7::NETOBJTYPE_DE_CLIENTINFO) - { - const protocol7::CNetObj_De_ClientInfo *pInfo = (const protocol7::CNetObj_De_ClientInfo *)pItem7->Data(); - - const int ClientId = pItem7->Id(); - - if(ClientId < 0 || ClientId >= MAX_CLIENTS) - { - dbg_msg("sixup", "De_ClientInfo got invalid ClientId: %d", ClientId); - return -17; - } - - if(pInfo->m_Local) - { - TranslationContext.m_aLocalClientId[Conn] = ClientId; - } - CTranslationContext::CClientData &Client = TranslationContext.m_aClients[ClientId]; - Client.m_Active = true; - Client.m_Team = pInfo->m_Team; - IntsToStr(pInfo->m_aName, 4, Client.m_aName, std::size(Client.m_aName)); - IntsToStr(pInfo->m_aClan, 3, Client.m_aClan, std::size(Client.m_aClan)); - Client.m_Country = pInfo->m_Country; - - // TODO: get this method into scope - // ApplySkin7InfoFromGameMsg(pInfo, ClientId); - } - else if(pItem7->Type() == protocol7::NETOBJTYPE_DE_GAMEINFO) - { - const protocol7::CNetObj_De_GameInfo *pInfo = (const protocol7::CNetObj_De_GameInfo *)pItem7->Data(); - - TranslationContext.m_GameFlags = pInfo->m_GameFlags; - TranslationContext.m_ScoreLimit = pInfo->m_ScoreLimit; - TranslationContext.m_TimeLimit = pInfo->m_TimeLimit; - TranslationContext.m_MatchNum = pInfo->m_MatchNum; - TranslationContext.m_MatchCurrent = pInfo->m_MatchCurrent; - TranslationContext.m_ShouldSendGameInfo = true; - } - } - - if(NewGameData) - { - void *pObj = Builder.NewItem(NETOBJTYPE_GAMEDATA, 0, sizeof(CNetObj_GameData)); - if(!pObj) - return -17; - - CNetObj_GameData GameData = {}; - GameData.m_TeamscoreRed = TranslationContext.m_TeamscoreRed; - GameData.m_TeamscoreBlue = TranslationContext.m_TeamscoreBlue; - GameData.m_FlagCarrierRed = TranslationContext.m_FlagCarrierRed; - GameData.m_FlagCarrierBlue = TranslationContext.m_FlagCarrierBlue; - mem_copy(pObj, &GameData, sizeof(CNetObj_GameData)); - } - - return Builder.Finish(pSixSnapDest); + m_DataSize = pSnapshot->m_DataSize; + m_NumItems = pSnapshot->m_NumItems; + mem_copy(m_aOffsets, pSnapshot->Offsets(), sizeof(int) * m_NumItems); + mem_copy(m_aData, pSnapshot->DataStart(), m_DataSize); } diff --git a/src/engine/shared/snapshot.h b/src/engine/shared/snapshot.h index 4060b9938..24aeaafa9 100644 --- a/src/engine/shared/snapshot.h +++ b/src/engine/shared/snapshot.h @@ -63,15 +63,6 @@ public: unsigned Crc() const; void DebugDump() const; - int TranslateSevenToSix( - CSnapshot *pSixSnapDest, - class CTranslationContext &TranslationContext, - float LocalTime, - int GameTick, - int Conn, - bool Dummy, - protocol7::CNetObjHandler *pNetObjHandler, - CNetObjHandler *pNetObjHandler6); bool IsValid(size_t ActualSize) const; static const CSnapshot *EmptySnapshot() { return &ms_EmptySnapshot; } @@ -171,12 +162,13 @@ class CSnapshotBuilder int GetExtendedItemTypeIndex(int TypeId); int GetTypeFromIndex(int Index) const; - bool m_Sixup; + bool m_Sixup = false; public: CSnapshotBuilder(); void Init(bool Sixup = false); + void Init7(const CSnapshot *pSnapshot); void *NewItem(int Type, int Id, int Size); diff --git a/src/engine/shared/translation_context.h b/src/engine/shared/translation_context.h index 60cae63c9..53d9cdbb9 100644 --- a/src/engine/shared/translation_context.h +++ b/src/engine/shared/translation_context.h @@ -1,6 +1,7 @@ #ifndef ENGINE_SHARED_TRANSLATION_CONTEXT_H #define ENGINE_SHARED_TRANSLATION_CONTEXT_H +#include #include #include @@ -98,6 +99,9 @@ public: bool m_ShouldSendGameInfo; int m_GameStateFlags7; + // 0.7 game flags + // use in combination with protocol7::GAMEFLAG_* + // for example protocol7::GAMEFLAG_TEAMS int m_GameFlags; int m_ScoreLimit; int m_TimeLimit; diff --git a/src/game/client/gameclient.cpp b/src/game/client/gameclient.cpp index ba4aa2113..881ef4f44 100644 --- a/src/game/client/gameclient.cpp +++ b/src/game/client/gameclient.cpp @@ -81,6 +81,7 @@ using namespace std::chrono_literals; const char *CGameClient::Version() const { return GAME_VERSION; } const char *CGameClient::NetVersion() const { return GAME_NETVERSION; } +const char *CGameClient::NetVersion7() const { return GAME_NETVERSION7; } int CGameClient::DDNetVersion() const { return DDNET_VERSION_NUMBER; } const char *CGameClient::DDNetVersionStr() const { return m_aDDNetVersionStr; } int CGameClient::ClientVersion7() const { return CLIENT_VERSION7; } @@ -2336,9 +2337,9 @@ void CGameClient::CClientData::Reset() m_SkinColor = 0; for(int i = 0; i < protocol7::NUM_SKINPARTS; ++i) { - m_aaSkinPartNames[i][0] = '\0'; - m_aUseCustomColors[i] = 0; - m_aSkinPartColors[i] = 0; + m_Sixup.m_aaSkinPartNames[i][0] = '\0'; + m_Sixup.m_aUseCustomColors[i] = 0; + m_Sixup.m_aSkinPartColors[i] = 0; } m_Team = 0; m_Emoticon = 0; @@ -2464,11 +2465,11 @@ bool CGameClient::GotWantedSkin7(bool Dummy) for(int SkinPart = 0; SkinPart < protocol7::NUM_SKINPARTS; SkinPart++) { - if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_aaSkinPartNames[SkinPart], apSkinPartsPtr[SkinPart])) + if(str_comp(m_aClients[m_aLocalIds[(int)Dummy]].m_Sixup.m_aaSkinPartNames[SkinPart], apSkinPartsPtr[SkinPart])) return false; - if(m_aClients[m_aLocalIds[(int)Dummy]].m_aUseCustomColors[SkinPart] != aUCCVars[SkinPart]) + if(m_aClients[m_aLocalIds[(int)Dummy]].m_Sixup.m_aUseCustomColors[SkinPart] != aUCCVars[SkinPart]) return false; - if(m_aClients[m_aLocalIds[(int)Dummy]].m_aSkinPartColors[SkinPart] != aColorVars[SkinPart]) + if(m_aClients[m_aLocalIds[(int)Dummy]].m_Sixup.m_aSkinPartColors[SkinPart] != aColorVars[SkinPart]) return false; } diff --git a/src/game/client/gameclient.h b/src/game/client/gameclient.h index 2ec09e4c5..60aa7f4e0 100644 --- a/src/game/client/gameclient.h +++ b/src/game/client/gameclient.h @@ -365,12 +365,6 @@ public: int m_Country; char m_aSkinName[24]; int m_SkinColor; - - // 0.7 Skin - char m_aaSkinPartNames[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_LENGTH]; - int m_aUseCustomColors[protocol7::NUM_SKINPARTS]; - int m_aSkinPartColors[protocol7::NUM_SKINPARTS]; - int m_Team; int m_Emoticon; float m_EmoticonStartFraction; @@ -433,6 +427,17 @@ public: void UpdateRenderInfo(bool IsTeamPlay); void Reset(); + + class CSixup + { + public: + char m_aaSkinPartNames[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_LENGTH]; + int m_aUseCustomColors[protocol7::NUM_SKINPARTS]; + int m_aSkinPartColors[protocol7::NUM_SKINPARTS]; + }; + + // 0.7 Skin + CSixup m_Sixup; }; CClientData m_aClients[MAX_CLIENTS]; @@ -493,7 +498,10 @@ public: void OnStateChange(int NewState, int OldState) override; template void ApplySkin7InfoFromGameMsg(const T *pMsg, int ClientId); + void ApplySkin7InfoFromSnapObj(const protocol7::CNetObj_De_ClientInfo *pObj, int ClientId) override; + int OnDemoRecSnap7(class CSnapshot *pFrom, class CSnapshot *pTo, int Conn) override; void *TranslateGameMsg(int *pMsgId, CUnpacker *pUnpacker, int Conn); + int TranslateSnap(CSnapshot *pSnapDstSix, CSnapshot *pSnapSrcSeven, int Conn, bool Dummy) override; void OnMessage(int MsgId, CUnpacker *pUnpacker, int Conn, bool Dummy) override; void InvalidateSnapshot() override; void OnNewSnapshot() override; @@ -522,6 +530,7 @@ public: const char *GetItemName(int Type) const override; const char *Version() const override; const char *NetVersion() const override; + const char *NetVersion7() const override; int DDNetVersion() const override; const char *DDNetVersionStr() const override; virtual int ClientVersion7() const override; diff --git a/src/game/client/sixup_translate_game.cpp b/src/game/client/sixup_translate_game.cpp index a93ef0730..c11100ca3 100644 --- a/src/game/client/sixup_translate_game.cpp +++ b/src/game/client/sixup_translate_game.cpp @@ -1,3 +1,7 @@ +#include +#include +#include +#include #include #include @@ -81,12 +85,12 @@ void CGameClient::ApplySkin7InfoFromGameMsg(const T *pMsg, int ClientId) char *apSkinPartsPtr[protocol7::NUM_SKINPARTS]; for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) { - str_utf8_copy_num(pClient->m_aaSkinPartNames[Part], pMsg->m_apSkinPartNames[Part], sizeof(pClient->m_aaSkinPartNames[Part]), protocol7::MAX_SKIN_LENGTH); - apSkinPartsPtr[Part] = pClient->m_aaSkinPartNames[Part]; - pClient->m_aUseCustomColors[Part] = pMsg->m_aUseCustomColors[Part]; - pClient->m_aSkinPartColors[Part] = pMsg->m_aSkinPartColors[Part]; + str_utf8_copy_num(pClient->m_Sixup.m_aaSkinPartNames[Part], pMsg->m_apSkinPartNames[Part], sizeof(pClient->m_Sixup.m_aaSkinPartNames[Part]), protocol7::MAX_SKIN_LENGTH); + apSkinPartsPtr[Part] = pClient->m_Sixup.m_aaSkinPartNames[Part]; + pClient->m_Sixup.m_aUseCustomColors[Part] = pMsg->m_aUseCustomColors[Part]; + pClient->m_Sixup.m_aSkinPartColors[Part] = pMsg->m_aSkinPartColors[Part]; } - m_Skins7.ValidateSkinParts(apSkinPartsPtr, pClient->m_aUseCustomColors, pClient->m_aSkinPartColors, m_pClient->m_TranslationContext.m_GameFlags); + m_Skins7.ValidateSkinParts(apSkinPartsPtr, pClient->m_Sixup.m_aUseCustomColors, pClient->m_Sixup.m_aSkinPartColors, m_pClient->m_TranslationContext.m_GameFlags); if(time_season() == SEASON_XMAS) { @@ -98,12 +102,12 @@ void CGameClient::ApplySkin7InfoFromGameMsg(const T *pMsg, int ClientId) for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) { - int Id = m_Skins7.FindSkinPart(Part, pClient->m_aaSkinPartNames[Part], false); + int Id = m_Skins7.FindSkinPart(Part, pClient->m_Sixup.m_aaSkinPartNames[Part], false); const CSkins7::CSkinPart *pSkinPart = m_Skins7.GetSkinPart(Part, Id); - if(pClient->m_aUseCustomColors[Part]) + if(pClient->m_Sixup.m_aUseCustomColors[Part]) { pClient->m_SkinInfo.m_Sixup.m_aTextures[Part] = pSkinPart->m_ColorTexture; - pClient->m_SkinInfo.m_Sixup.m_aColors[Part] = m_Skins7.GetColor(pClient->m_aSkinPartColors[Part], Part == protocol7::SKINPART_MARKING); + pClient->m_SkinInfo.m_Sixup.m_aColors[Part] = m_Skins7.GetColor(pMsg->m_aSkinPartColors[Part], Part == protocol7::SKINPART_MARKING); } else { @@ -112,14 +116,29 @@ void CGameClient::ApplySkin7InfoFromGameMsg(const T *pMsg, int ClientId) } if(pClient->m_SkinInfo.m_Sixup.m_HatTexture.IsValid()) { - if(Part == protocol7::SKINPART_BODY && str_comp(pClient->m_aaSkinPartNames[Part], "standard")) + if(Part == protocol7::SKINPART_BODY && str_comp(pClient->m_Sixup.m_aaSkinPartNames[Part], "standard")) pClient->m_SkinInfo.m_Sixup.m_HatSpriteIndex = CSkins7::HAT_OFFSET_SIDE + (ClientId % CSkins7::HAT_NUM); - if(Part == protocol7::SKINPART_DECORATION && !str_comp(pClient->m_aaSkinPartNames[Part], "twinbopp")) + if(Part == protocol7::SKINPART_DECORATION && !str_comp(pClient->m_Sixup.m_aaSkinPartNames[Part], "twinbopp")) pClient->m_SkinInfo.m_Sixup.m_HatSpriteIndex = CSkins7::HAT_OFFSET_SIDE + (ClientId % CSkins7::HAT_NUM); } } } +void CGameClient::ApplySkin7InfoFromSnapObj(const protocol7::CNetObj_De_ClientInfo *pObj, int ClientId) +{ + char aSkinPartNames[protocol7::NUM_SKINPARTS][protocol7::MAX_SKIN_ARRAY_SIZE]; + protocol7::CNetMsg_Sv_SkinChange Msg; + Msg.m_ClientId = ClientId; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + IntsToStr(pObj->m_aaSkinPartNames[Part], 6, aSkinPartNames[Part], std::size(aSkinPartNames[Part])); + Msg.m_apSkinPartNames[Part] = aSkinPartNames[Part]; + Msg.m_aUseCustomColors[Part] = pObj->m_aUseCustomColors[Part]; + Msg.m_aSkinPartColors[Part] = pObj->m_aSkinPartColors[Part]; + } + ApplySkin7InfoFromGameMsg(&Msg, ClientId); +} + void *CGameClient::TranslateGameMsg(int *pMsgId, CUnpacker *pUnpacker, int Conn) { if(!m_pClient->IsSixup()) diff --git a/src/game/client/sixup_translate_snapshot.cpp b/src/game/client/sixup_translate_snapshot.cpp new file mode 100644 index 000000000..1cbb03f18 --- /dev/null +++ b/src/game/client/sixup_translate_snapshot.cpp @@ -0,0 +1,564 @@ +#include +#include +#include +#include +#include + +int CGameClient::TranslateSnap(CSnapshot *pSnapDstSix, CSnapshot *pSnapSrcSeven, int Conn, bool Dummy) +{ + CSnapshotBuilder Builder; + Builder.Init(); + + float LocalTime = Client()->LocalTime(); + int GameTick = Client()->GameTick(g_Config.m_ClDummy); + CTranslationContext &TranslationContext = Client()->m_TranslationContext; + + for(auto &PlayerInfosRace : TranslationContext.m_apPlayerInfosRace) + PlayerInfosRace = nullptr; + + int SpectatorId = -3; + + for(int i = 0; i < pSnapSrcSeven->NumItems(); i++) + { + const CSnapshotItem *pItem7 = (CSnapshotItem *)pSnapSrcSeven->GetItem(i); + const int Size = pSnapSrcSeven->GetItemSize(i); + const int ItemType = pSnapSrcSeven->GetItemType(i); + + // ddnet ex snap items + if((ItemType > __NETOBJTYPE_UUID_HELPER && ItemType < OFFSET_NETMSGTYPE_UUID) || pItem7->Type() == NETOBJTYPE_EX) + { + CUnpacker Unpacker; + Unpacker.Reset(pItem7->Data(), Size); + + void *pRawObj = GetNetObjHandler()->SecureUnpackObj(ItemType, &Unpacker); + if(!pRawObj) + { + if(ItemType != UUID_UNKNOWN) + dbg_msg("sixup", "dropped weird ddnet ex object '%s' (%d), failed on '%s'", GetNetObjHandler()->GetObjName(ItemType), ItemType, GetNetObjHandler()->FailedObjOn()); + continue; + } + const int ItemSize = GetNetObjHandler()->GetUnpackedObjSize(ItemType); + + void *pObj = Builder.NewItem(pItem7->Type(), pItem7->Id(), ItemSize); + if(!pObj) + return -17; + + mem_copy(pObj, pRawObj, ItemSize); + continue; + } + + if(GetNetObjHandler7()->ValidateObj(pItem7->Type(), pItem7->Data(), Size) != 0) + { + if(pItem7->Type() > 0 && pItem7->Type() < CSnapshot::OFFSET_UUID_TYPE) + { + dbg_msg( + "sixup", + "invalidated index=%d type=%d (%s) size=%d id=%d", + i, + pItem7->Type(), + GetNetObjHandler7()->GetObjName(pItem7->Type()), + Size, + pItem7->Id()); + } + pSnapSrcSeven->InvalidateItem(i); + } + + if(pItem7->Type() == protocol7::NETOBJTYPE_PLAYERINFORACE) + { + const protocol7::CNetObj_PlayerInfoRace *pInfo = (const protocol7::CNetObj_PlayerInfoRace *)pItem7->Data(); + int ClientId = pItem7->Id(); + if(ClientId < MAX_CLIENTS && TranslationContext.m_aClients[ClientId].m_Active) + { + TranslationContext.m_apPlayerInfosRace[ClientId] = pInfo; + } + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_SPECTATORINFO) + { + const protocol7::CNetObj_SpectatorInfo *pSpec7 = (const protocol7::CNetObj_SpectatorInfo *)pItem7->Data(); + SpectatorId = pSpec7->m_SpectatorId; + if(pSpec7->m_SpecMode == protocol7::SPEC_FREEVIEW) + SpectatorId = SPEC_FREEVIEW; + } + } + + // hack to put game info in the snap + // even though in 0.7 we get it as a game message + // this message has to be on the top + // the client looks at the items in order and needs the gameinfo at the top + // otherwise it will not render skins with team colors + if(TranslationContext.m_ShouldSendGameInfo) + { + void *pObj = Builder.NewItem(NETOBJTYPE_GAMEINFO, 0, sizeof(CNetObj_GameInfo)); + if(!pObj) + return -1; + + int GameStateFlagsSix = 0; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_GAMEOVER) + GameStateFlagsSix |= GAMESTATEFLAG_GAMEOVER; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_SUDDENDEATH) + GameStateFlagsSix |= GAMESTATEFLAG_SUDDENDEATH; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_PAUSED) + GameStateFlagsSix |= GAMESTATEFLAG_PAUSED; + + /* + This is a 0.7 only flag that we just ignore for now + + GAMESTATEFLAG_ROUNDOVER + */ + + CNetObj_GameInfo Info6 = {}; + Info6.m_GameFlags = TranslationContext.m_GameFlags; + Info6.m_GameStateFlags = GameStateFlagsSix; + Info6.m_RoundStartTick = TranslationContext.m_GameStartTick7; + Info6.m_WarmupTimer = 0; // removed in 0.7 + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_WARMUP) + Info6.m_WarmupTimer = TranslationContext.m_GameStateEndTick7 - GameTick; + if(TranslationContext.m_GameStateFlags7 & protocol7::GAMESTATEFLAG_STARTCOUNTDOWN) + Info6.m_WarmupTimer = TranslationContext.m_GameStateEndTick7 - GameTick; + + // hack to port 0.7 race timer to ddnet warmup gametimer hack + int TimerClientId = clamp(TranslationContext.m_aLocalClientId[Conn], 0, (int)MAX_CLIENTS); + if(SpectatorId >= 0) + TimerClientId = SpectatorId; + const protocol7::CNetObj_PlayerInfoRace *pRaceInfo = TranslationContext.m_apPlayerInfosRace[TimerClientId]; + if(pRaceInfo) + { + Info6.m_WarmupTimer = -pRaceInfo->m_RaceStartTick; + Info6.m_GameStateFlags |= GAMESTATEFLAG_RACETIME; + } + + Info6.m_ScoreLimit = TranslationContext.m_ScoreLimit; + Info6.m_TimeLimit = TranslationContext.m_TimeLimit; + Info6.m_RoundNum = TranslationContext.m_MatchNum; + Info6.m_RoundCurrent = TranslationContext.m_MatchCurrent; + + mem_copy(pObj, &Info6, sizeof(CNetObj_GameInfo)); + } + + for(int i = 0; i < MAX_CLIENTS; i++) + { + const CTranslationContext::CClientData &Client = TranslationContext.m_aClients[i]; + if(!Client.m_Active) + continue; + + void *pObj = Builder.NewItem(NETOBJTYPE_CLIENTINFO, i, sizeof(CNetObj_ClientInfo)); + if(!pObj) + return -2; + + CNetObj_ClientInfo Info6 = {}; + StrToInts(&Info6.m_Name0, 4, Client.m_aName); + StrToInts(&Info6.m_Clan0, 3, Client.m_aClan); + Info6.m_Country = Client.m_Country; + StrToInts(&Info6.m_Skin0, 6, Client.m_aSkinName); + Info6.m_UseCustomColor = Client.m_UseCustomColor; + Info6.m_ColorBody = Client.m_ColorBody; + Info6.m_ColorFeet = Client.m_ColorFeet; + mem_copy(pObj, &Info6, sizeof(CNetObj_ClientInfo)); + } + + bool NewGameData = false; + + for(int i = 0; i < pSnapSrcSeven->NumItems(); i++) + { + const CSnapshotItem *pItem7 = pSnapSrcSeven->GetItem(i); + const int Size = pSnapSrcSeven->GetItemSize(i); + // the first few items are a full match + // no translation needed + if(pItem7->Type() == protocol7::NETOBJTYPE_PROJECTILE || + pItem7->Type() == protocol7::NETOBJTYPE_LASER || + pItem7->Type() == protocol7::NETOBJTYPE_FLAG) + { + void *pObj = Builder.NewItem(pItem7->Type(), pItem7->Id(), Size); + if(!pObj) + return -4; + + mem_copy(pObj, pItem7->Data(), Size); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_PICKUP) + { + void *pObj = Builder.NewItem(NETOBJTYPE_PICKUP, pItem7->Id(), sizeof(CNetObj_Pickup)); + if(!pObj) + return -5; + + const protocol7::CNetObj_Pickup *pPickup7 = (const protocol7::CNetObj_Pickup *)pItem7->Data(); + CNetObj_Pickup Pickup6 = {}; + Pickup6.m_X = pPickup7->m_X; + Pickup6.m_Y = pPickup7->m_Y; + PickupType_SevenToSix(pPickup7->m_Type, Pickup6.m_Type, Pickup6.m_Subtype); + + mem_copy(pObj, &Pickup6, sizeof(CNetObj_Pickup)); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATA) + { + const protocol7::CNetObj_GameData *pGameData = (const protocol7::CNetObj_GameData *)pItem7->Data(); + TranslationContext.m_GameStateFlags7 = pGameData->m_GameStateFlags; + TranslationContext.m_GameStartTick7 = pGameData->m_GameStartTick; + TranslationContext.m_GameStateEndTick7 = pGameData->m_GameStateEndTick; + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATATEAM) + { + // 0.7 added GameDataTeam and GameDataFlag + // both items merged together have all fields of the 0.6 GameData + // so if we get either one of them we store the details in the translation context + // and build one GameData snap item after this loop + const protocol7::CNetObj_GameDataTeam *pTeam7 = (const protocol7::CNetObj_GameDataTeam *)pItem7->Data(); + + TranslationContext.m_TeamscoreRed = pTeam7->m_TeamscoreRed; + TranslationContext.m_TeamscoreBlue = pTeam7->m_TeamscoreBlue; + NewGameData = true; + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_GAMEDATAFLAG) + { + const protocol7::CNetObj_GameDataFlag *pFlag7 = (const protocol7::CNetObj_GameDataFlag *)pItem7->Data(); + + TranslationContext.m_FlagCarrierRed = pFlag7->m_FlagCarrierRed; + TranslationContext.m_FlagCarrierBlue = pFlag7->m_FlagCarrierBlue; + NewGameData = true; + + // used for blinking the flags in the hud + // but that already works the 0.6 way + // and is covered by the NETOBJTYPE_GAMEDATA translation + // pFlag7->m_FlagDropTickRed; + // pFlag7->m_FlagDropTickBlue; + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_CHARACTER) + { + void *pObj = Builder.NewItem(NETOBJTYPE_CHARACTER, pItem7->Id(), sizeof(CNetObj_Character)); + if(!pObj) + return -6; + + const protocol7::CNetObj_Character *pChar7 = (const protocol7::CNetObj_Character *)pItem7->Data(); + + CNetObj_Character Char6 = {}; + // character core is unchanged + mem_copy(&Char6, pItem7->Data(), sizeof(CNetObj_CharacterCore)); + + Char6.m_PlayerFlags = 0; + if(pItem7->Id() >= 0 && pItem7->Id() < MAX_CLIENTS) + Char6.m_PlayerFlags = PlayerFlags_SevenToSix(TranslationContext.m_aClients[pItem7->Id()].m_PlayerFlags7); + Char6.m_Health = pChar7->m_Health; + Char6.m_Armor = pChar7->m_Armor; + Char6.m_AmmoCount = pChar7->m_AmmoCount; + Char6.m_Weapon = pChar7->m_Weapon; + Char6.m_Emote = pChar7->m_Emote; + Char6.m_AttackTick = pChar7->m_AttackTick; + + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_ATTACH_PLAYER) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_HOOK_ATTACH_PLAYER; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + + if(TranslationContext.m_aLocalClientId[Conn] != pItem7->Id()) + { + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_GROUND_JUMP) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_PLAYER_JUMP; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_ATTACH_GROUND) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_HOOK_ATTACH_GROUND; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + if(pChar7->m_TriggeredEvents & protocol7::COREEVENTFLAG_HOOK_HIT_NOHOOK) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -7; + + CNetEvent_SoundWorld Sound = {}; + Sound.m_X = pChar7->m_X; + Sound.m_Y = pChar7->m_Y; + Sound.m_SoundId = SOUND_HOOK_NOATTACH; + mem_copy(pEvent, &Sound, sizeof(CNetEvent_SoundWorld)); + } + } + + mem_copy(pObj, &Char6, sizeof(CNetObj_Character)); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_PLAYERINFO) + { + void *pObj = Builder.NewItem(NETOBJTYPE_PLAYERINFO, pItem7->Id(), sizeof(CNetObj_PlayerInfo)); + if(!pObj) + return -8; + + const protocol7::CNetObj_PlayerInfo *pInfo7 = (const protocol7::CNetObj_PlayerInfo *)pItem7->Data(); + CNetObj_PlayerInfo Info6 = {}; + Info6.m_Local = TranslationContext.m_aLocalClientId[Conn] == pItem7->Id(); + Info6.m_ClientId = pItem7->Id(); + Info6.m_Team = 0; + if(pItem7->Id() >= 0 && pItem7->Id() < MAX_CLIENTS) + { + Info6.m_Team = TranslationContext.m_aClients[pItem7->Id()].m_Team; + TranslationContext.m_aClients[pItem7->Id()].m_PlayerFlags7 = pInfo7->m_PlayerFlags; + } + Info6.m_Score = pInfo7->m_Score; + Info6.m_Latency = pInfo7->m_Latency; + mem_copy(pObj, &Info6, sizeof(CNetObj_PlayerInfo)); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_SPECTATORINFO) + { + void *pObj = Builder.NewItem(NETOBJTYPE_SPECTATORINFO, pItem7->Id(), sizeof(CNetObj_SpectatorInfo)); + if(!pObj) + return -9; + + const protocol7::CNetObj_SpectatorInfo *pSpec7 = (const protocol7::CNetObj_SpectatorInfo *)pItem7->Data(); + CNetObj_SpectatorInfo Spec6 = {}; + Spec6.m_SpectatorId = pSpec7->m_SpectatorId; + if(pSpec7->m_SpecMode == protocol7::SPEC_FREEVIEW) + Spec6.m_SpectatorId = SPEC_FREEVIEW; + Spec6.m_X = pSpec7->m_X; + Spec6.m_Y = pSpec7->m_Y; + mem_copy(pObj, &Spec6, sizeof(CNetObj_SpectatorInfo)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_EXPLOSION) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_EXPLOSION, pItem7->Id(), sizeof(CNetEvent_Explosion)); + if(!pEvent) + return -10; + + const protocol7::CNetEvent_Explosion *pExplosion7 = (const protocol7::CNetEvent_Explosion *)pItem7->Data(); + CNetEvent_Explosion Explosion6 = {}; + Explosion6.m_X = pExplosion7->m_X; + Explosion6.m_Y = pExplosion7->m_Y; + mem_copy(pEvent, &Explosion6, sizeof(CNetEvent_Explosion)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_SPAWN) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SPAWN, pItem7->Id(), sizeof(CNetEvent_Spawn)); + if(!pEvent) + return -11; + + const protocol7::CNetEvent_Spawn *pSpawn7 = (const protocol7::CNetEvent_Spawn *)pItem7->Data(); + CNetEvent_Spawn Spawn6 = {}; + Spawn6.m_X = pSpawn7->m_X; + Spawn6.m_Y = pSpawn7->m_Y; + mem_copy(pEvent, &Spawn6, sizeof(CNetEvent_Spawn)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_HAMMERHIT) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_HAMMERHIT, pItem7->Id(), sizeof(CNetEvent_HammerHit)); + if(!pEvent) + return -12; + + const protocol7::CNetEvent_HammerHit *pHammerHit7 = (const protocol7::CNetEvent_HammerHit *)pItem7->Data(); + CNetEvent_HammerHit HammerHit6 = {}; + HammerHit6.m_X = pHammerHit7->m_X; + HammerHit6.m_Y = pHammerHit7->m_Y; + mem_copy(pEvent, &HammerHit6, sizeof(CNetEvent_HammerHit)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_DEATH) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_DEATH, pItem7->Id(), sizeof(CNetEvent_Death)); + if(!pEvent) + return -13; + + const protocol7::CNetEvent_Death *pDeath7 = (const protocol7::CNetEvent_Death *)pItem7->Data(); + CNetEvent_Death Death6 = {}; + Death6.m_X = pDeath7->m_X; + Death6.m_Y = pDeath7->m_Y; + Death6.m_ClientId = pDeath7->m_ClientId; + mem_copy(pEvent, &Death6, sizeof(CNetEvent_Death)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_SOUNDWORLD) + { + void *pEvent = Builder.NewItem(NETEVENTTYPE_SOUNDWORLD, pItem7->Id(), sizeof(CNetEvent_SoundWorld)); + if(!pEvent) + return -14; + + const protocol7::CNetEvent_SoundWorld *pSoundWorld7 = (const protocol7::CNetEvent_SoundWorld *)pItem7->Data(); + CNetEvent_SoundWorld SoundWorld6 = {}; + SoundWorld6.m_X = pSoundWorld7->m_X; + SoundWorld6.m_Y = pSoundWorld7->m_Y; + SoundWorld6.m_SoundId = pSoundWorld7->m_SoundId; + mem_copy(pEvent, &SoundWorld6, sizeof(CNetEvent_SoundWorld)); + } + else if(pItem7->Type() == protocol7::NETEVENTTYPE_DAMAGE) + { + // 0.7 introduced amount for damage indicators + // so for one 0.7 item we might create multiple 0.6 ones + const protocol7::CNetEvent_Damage *pDmg7 = (const protocol7::CNetEvent_Damage *)pItem7->Data(); + + int Amount = pDmg7->m_HealthAmount + pDmg7->m_ArmorAmount; + if(Amount < 1) + continue; + + int ClientId = pDmg7->m_ClientId; + TranslationContext.m_aDamageTaken[ClientId]++; + + float Angle; + // create healthmod indicator + if(LocalTime < TranslationContext.m_aDamageTakenTick[ClientId] + 0.5f) + { + // make sure that the damage indicators don't group together + Angle = TranslationContext.m_aDamageTaken[ClientId] * 0.25f; + } + else + { + TranslationContext.m_aDamageTaken[ClientId] = 0; + Angle = 0; + } + + TranslationContext.m_aDamageTakenTick[ClientId] = LocalTime; + + float a = 3 * pi / 2 + Angle; + float s = a - pi / 3; + float e = a + pi / 3; + for(int k = 0; k < Amount; k++) + { + // pItem7->Id() is reused that is technically wrong + // but the client implementation does not look at the ids + // and renders the damage indicators just fine + void *pEvent = Builder.NewItem(NETEVENTTYPE_DAMAGEIND, pItem7->Id(), sizeof(CNetEvent_DamageInd)); + if(!pEvent) + return -16; + + CNetEvent_DamageInd Dmg6 = {}; + Dmg6.m_X = pDmg7->m_X; + Dmg6.m_Y = pDmg7->m_Y; + float f = mix(s, e, float(k + 1) / float(Amount + 2)); + Dmg6.m_Angle = (int)(f * 256.0f); + mem_copy(pEvent, &Dmg6, sizeof(CNetEvent_DamageInd)); + } + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_DE_CLIENTINFO) + { + const protocol7::CNetObj_De_ClientInfo *pInfo = (const protocol7::CNetObj_De_ClientInfo *)pItem7->Data(); + + const int ClientId = pItem7->Id(); + + if(ClientId < 0 || ClientId >= MAX_CLIENTS) + { + dbg_msg("sixup", "De_ClientInfo got invalid ClientId: %d", ClientId); + return -17; + } + + if(pInfo->m_Local) + { + TranslationContext.m_aLocalClientId[Conn] = ClientId; + } + CTranslationContext::CClientData &Client = TranslationContext.m_aClients[ClientId]; + Client.m_Active = true; + Client.m_Team = pInfo->m_Team; + IntsToStr(pInfo->m_aName, 4, Client.m_aName, std::size(Client.m_aName)); + IntsToStr(pInfo->m_aClan, 3, Client.m_aClan, std::size(Client.m_aClan)); + Client.m_Country = pInfo->m_Country; + + ApplySkin7InfoFromSnapObj(pInfo, ClientId); + } + else if(pItem7->Type() == protocol7::NETOBJTYPE_DE_GAMEINFO) + { + const protocol7::CNetObj_De_GameInfo *pInfo = (const protocol7::CNetObj_De_GameInfo *)pItem7->Data(); + + TranslationContext.m_GameFlags = pInfo->m_GameFlags; + TranslationContext.m_ScoreLimit = pInfo->m_ScoreLimit; + TranslationContext.m_TimeLimit = pInfo->m_TimeLimit; + TranslationContext.m_MatchNum = pInfo->m_MatchNum; + TranslationContext.m_MatchCurrent = pInfo->m_MatchCurrent; + TranslationContext.m_ShouldSendGameInfo = true; + } + } + + if(NewGameData) + { + void *pObj = Builder.NewItem(NETOBJTYPE_GAMEDATA, 0, sizeof(CNetObj_GameData)); + if(!pObj) + return -17; + + CNetObj_GameData GameData = {}; + GameData.m_TeamscoreRed = TranslationContext.m_TeamscoreRed; + GameData.m_TeamscoreBlue = TranslationContext.m_TeamscoreBlue; + GameData.m_FlagCarrierRed = TranslationContext.m_FlagCarrierRed; + GameData.m_FlagCarrierBlue = TranslationContext.m_FlagCarrierBlue; + mem_copy(pObj, &GameData, sizeof(CNetObj_GameData)); + } + + return Builder.Finish(pSnapDstSix); +} + +int CGameClient::OnDemoRecSnap7(CSnapshot *pFrom, CSnapshot *pTo, int Conn) +{ + CSnapshotBuilder Builder; + Builder.Init7(pFrom); + + // add client info + for(int i = 0; i < MAX_CLIENTS; i++) + { + if(!m_aClients[i].m_Active) + continue; + + void *pItem = Builder.NewItem(protocol7::NETOBJTYPE_DE_CLIENTINFO, i, sizeof(protocol7::CNetObj_De_ClientInfo)); + if(!pItem) + return -1; + + CTranslationContext::CClientData &ClientData = Client()->m_TranslationContext.m_aClients[i]; + + protocol7::CNetObj_De_ClientInfo ClientInfoObj; + ClientInfoObj.m_Local = i == Client()->m_TranslationContext.m_aLocalClientId[Conn]; + ClientInfoObj.m_Team = ClientData.m_Team; + StrToInts(ClientInfoObj.m_aName, 4, m_aClients[i].m_aName); + StrToInts(ClientInfoObj.m_aClan, 3, m_aClients[i].m_aClan); + ClientInfoObj.m_Country = ClientData.m_Country; + + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + StrToInts(ClientInfoObj.m_aaSkinPartNames[Part], 6, m_aClients[i].m_Sixup.m_aaSkinPartNames[Part]); + ClientInfoObj.m_aUseCustomColors[Part] = m_aClients[i].m_Sixup.m_aUseCustomColors[Part]; + ClientInfoObj.m_aSkinPartColors[Part] = m_aClients[i].m_Sixup.m_aSkinPartColors[Part]; + } + + mem_copy(pItem, &ClientInfoObj, sizeof(protocol7::CNetObj_De_ClientInfo)); + } + + // add tuning + CTuningParams StandardTuning; + if(mem_comp(&StandardTuning, &m_aTuning[Conn], sizeof(CTuningParams)) != 0) + { + void *pItem = Builder.NewItem(protocol7::NETOBJTYPE_DE_TUNEPARAMS, 0, sizeof(protocol7::CNetObj_De_TuneParams)); + if(!pItem) + return -2; + + protocol7::CNetObj_De_TuneParams TuneParams; + mem_copy(&TuneParams.m_aTuneParams, &m_aTuning[Conn], sizeof(TuneParams)); + mem_copy(pItem, &TuneParams, sizeof(protocol7::CNetObj_De_TuneParams)); + } + + // add game info + void *pItem = Builder.NewItem(protocol7::NETOBJTYPE_DE_GAMEINFO, 0, sizeof(protocol7::CNetObj_De_GameInfo)); + if(!pItem) + return -3; + + protocol7::CNetObj_De_GameInfo GameInfo; + + GameInfo.m_GameFlags = Client()->m_TranslationContext.m_GameFlags; + GameInfo.m_ScoreLimit = Client()->m_TranslationContext.m_ScoreLimit; + GameInfo.m_TimeLimit = Client()->m_TranslationContext.m_TimeLimit; + GameInfo.m_MatchNum = Client()->m_TranslationContext.m_MatchNum; + GameInfo.m_MatchCurrent = Client()->m_TranslationContext.m_MatchCurrent; + + mem_copy(pItem, &GameInfo, sizeof(protocol7::CNetObj_De_GameInfo)); + + return Builder.Finish(pTo); +} From 399075c3397e89db19d3271779081855e7acf59a Mon Sep 17 00:00:00 2001 From: ChillerDragon Date: Thu, 25 Apr 2024 09:04:58 +0800 Subject: [PATCH 6/8] Display 0.7 skins in chat --- src/game/client/components/chat.cpp | 41 +++++++++++++++++++++++++++++ src/game/client/components/chat.h | 15 +++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/game/client/components/chat.cpp b/src/game/client/components/chat.cpp index d4a25f2c4..b94339dfa 100644 --- a/src/game/client/components/chat.cpp +++ b/src/game/client/components/chat.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -816,6 +817,38 @@ void CChat::AddLine(int ClientId, int Team, const char *pLine) pCurrentLine->m_RenderSkinMetrics = LineAuthor.m_RenderInfo.m_SkinMetrics; pCurrentLine->m_HasRenderTee = true; + + // 0.7 + if(Client()->IsSixup()) + { + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + const char *pPartName = LineAuthor.m_Sixup.m_aaSkinPartNames[Part]; + int Id = m_pClient->m_Skins7.FindSkinPart(Part, pPartName, false); + const CSkins7::CSkinPart *pSkinPart = m_pClient->m_Skins7.GetSkinPart(Part, Id); + if(LineAuthor.m_Sixup.m_aUseCustomColors[Part]) + { + pCurrentLine->m_Sixup.m_aTextures[Part] = pSkinPart->m_ColorTexture; + pCurrentLine->m_Sixup.m_aColors[Part] = m_pClient->m_Skins7.GetColor( + LineAuthor.m_Sixup.m_aSkinPartColors[Part], + Part == protocol7::SKINPART_MARKING); + } + else + { + pCurrentLine->m_Sixup.m_aTextures[Part] = pSkinPart->m_OrgTexture; + pCurrentLine->m_Sixup.m_aColors[Part] = vec4(1.0f, 1.0f, 1.0f, 1.0f); + } + + if(LineAuthor.m_SkinInfo.m_Sixup.m_HatTexture.IsValid()) + { + if(Part == protocol7::SKINPART_BODY && str_comp(pPartName, "standard")) + pCurrentLine->m_Sixup.m_HatSpriteIndex = CSkins7::HAT_OFFSET_SIDE + (ClientId % CSkins7::HAT_NUM); + if(Part == protocol7::SKINPART_DECORATION && str_comp(pPartName, "twinbopp")) + pCurrentLine->m_Sixup.m_HatSpriteIndex = CSkins7::HAT_OFFSET_SIDE + (ClientId % CSkins7::HAT_NUM); + pCurrentLine->m_Sixup.m_HatTexture = LineAuthor.m_SkinInfo.m_Sixup.m_HatTexture; + } + } + } } } } @@ -1270,6 +1303,14 @@ void CChat::OnRender() RenderInfo.m_ColorFeet = Line.m_ColorFeet; RenderInfo.m_Size = TeeSize; + for(int Part = 0; Part < protocol7::NUM_SKINPARTS; Part++) + { + RenderInfo.m_Sixup.m_aColors[Part] = Line.m_Sixup.m_aColors[Part]; + RenderInfo.m_Sixup.m_aTextures[Part] = Line.m_Sixup.m_aTextures[Part]; + RenderInfo.m_Sixup.m_HatSpriteIndex = Line.m_Sixup.m_HatSpriteIndex; + RenderInfo.m_Sixup.m_HatTexture = Line.m_Sixup.m_HatTexture; + } + float RowHeight = FontSize() + RealMsgPaddingY; float OffsetTeeY = TeeSize / 2.0f; float FullHeightMinusTee = RowHeight - TeeSize; diff --git a/src/game/client/components/chat.h b/src/game/client/components/chat.h index c22af4eab..28a8007ee 100644 --- a/src/game/client/components/chat.h +++ b/src/game/client/components/chat.h @@ -12,6 +12,7 @@ #include #include #include +#include class CChat : public CComponent { @@ -54,6 +55,20 @@ class CChat : public CComponent float m_TextYOffset; int m_TimesRepeated; + + class CSixup + { + public: + IGraphics::CTextureHandle m_aTextures[protocol7::NUM_SKINPARTS]; + IGraphics::CTextureHandle m_HatTexture; + IGraphics::CTextureHandle m_BotTexture; + int m_HatSpriteIndex; + ColorRGBA m_BotColor; + ColorRGBA m_aColors[protocol7::NUM_SKINPARTS]; + }; + + // 0.7 Skin + CSixup m_Sixup; }; bool m_PrevScoreBoardShowed; From 53b01862d4e5a3882cf7633cfbec24d87f3fc5bf Mon Sep 17 00:00:00 2001 From: ChillerDragon Date: Mon, 6 May 2024 08:44:27 +0800 Subject: [PATCH 7/8] Remove netversion from demo editor init --- src/engine/client/client.cpp | 2 +- src/engine/client/demoedit.cpp | 2 +- src/engine/shared/demo.cpp | 3 +-- src/engine/shared/demo.h | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/engine/client/client.cpp b/src/engine/client/client.cpp index 860f42e77..39f9c8677 100644 --- a/src/engine/client/client.cpp +++ b/src/engine/client/client.cpp @@ -2910,7 +2910,7 @@ void CClient::InitInterfaces() m_pNotifications = Kernel()->RequestInterface(); m_pStorage = Kernel()->RequestInterface(); - m_DemoEditor.Init(m_pGameClient->NetVersion(), &m_SnapshotDelta, m_pConsole, m_pStorage); + m_DemoEditor.Init(&m_SnapshotDelta, m_pConsole, m_pStorage); m_ServerBrowser.SetBaseInfo(&m_aNetClient[CONN_CONTACT], m_pGameClient->NetVersion()); diff --git a/src/engine/client/demoedit.cpp b/src/engine/client/demoedit.cpp index faae70300..410331e8a 100644 --- a/src/engine/client/demoedit.cpp +++ b/src/engine/client/demoedit.cpp @@ -14,7 +14,7 @@ CDemoEdit::CDemoEdit(const char *pNetVersion, class CSnapshotDelta *pSnapshotDel m_EndTick = EndTick; // Init the demoeditor - m_DemoEditor.Init(pNetVersion, &m_SnapshotDelta, NULL, pStorage); + m_DemoEditor.Init(&m_SnapshotDelta, NULL, pStorage); } void CDemoEdit::Run() diff --git a/src/engine/shared/demo.cpp b/src/engine/shared/demo.cpp index 34ddbc3c3..4a3c67a5f 100644 --- a/src/engine/shared/demo.cpp +++ b/src/engine/shared/demo.cpp @@ -1219,9 +1219,8 @@ public: } }; -void CDemoEditor::Init(const char *pNetVersion, class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage) +void CDemoEditor::Init(class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage) { - m_pNetVersion = pNetVersion; m_pSnapshotDelta = pSnapshotDelta; m_pConsole = pConsole; m_pStorage = pStorage; diff --git a/src/engine/shared/demo.h b/src/engine/shared/demo.h index 3cb83d184..32777b424 100644 --- a/src/engine/shared/demo.h +++ b/src/engine/shared/demo.h @@ -190,10 +190,9 @@ class CDemoEditor : public IDemoEditor IConsole *m_pConsole; IStorage *m_pStorage; class CSnapshotDelta *m_pSnapshotDelta; - const char *m_pNetVersion; public: - virtual void Init(const char *pNetVersion, class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage); + virtual void Init(class CSnapshotDelta *pSnapshotDelta, class IConsole *pConsole, class IStorage *pStorage); bool Slice(const char *pDemo, const char *pDst, int StartTick, int EndTick, DEMOFUNC_FILTER pfnFilter, void *pUser) override; }; From e0bc98e79a5f9eb04c6e5e8d833d90cd46d264e8 Mon Sep 17 00:00:00 2001 From: ChillerDragon Date: Mon, 22 Jul 2024 10:13:40 +0800 Subject: [PATCH 8/8] Add alpha support for 0.7 skins --- src/game/client/render.cpp | 42 ++++++++++++++++++++++++-------------- src/game/client/render.h | 2 +- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/game/client/render.cpp b/src/game/client/render.cpp index 8f6246b72..ad33d9e76 100644 --- a/src/game/client/render.cpp +++ b/src/game/client/render.cpp @@ -273,7 +273,7 @@ void CRenderTools::GetRenderTeeOffsetToRenderedTee(const CAnimState *pAnim, cons void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha) const { if(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_BODY].IsValid()) - RenderTee7(pAnim, pInfo, Emote, Dir, Pos); + RenderTee7(pAnim, pInfo, Emote, Dir, Pos, Alpha); else RenderTee6(pAnim, pInfo, Emote, Dir, Pos, Alpha); @@ -281,7 +281,7 @@ void CRenderTools::RenderTee(const CAnimState *pAnim, const CTeeRenderInfo *pInf Graphics()->QuadsSetRotation(0); } -void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos) const +void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha) const { vec2 Direction = Dir; vec2 Position = Pos; @@ -309,7 +309,7 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn { Graphics()->TextureSet(pInfo->m_Sixup.m_BotTexture); Graphics()->QuadsBegin(); - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); SelectSprite7(client_data7::SPRITE_TEE_BOT_BACKGROUND, 0, 0, 0); Item = BotItem; Graphics()->QuadsDraw(&Item, 1); @@ -321,11 +321,13 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn { Graphics()->TextureSet(pInfo->m_Sixup.m_BotTexture); Graphics()->QuadsBegin(); - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); SelectSprite7(client_data7::SPRITE_TEE_BOT_FOREGROUND, 0, 0, 0); Item = BotItem; Graphics()->QuadsDraw(&Item, 1); - Graphics()->SetColor(pInfo->m_Sixup.m_BotColor); + ColorRGBA Color = pInfo->m_Sixup.m_BotColor; + Color.a = Alpha; + Graphics()->SetColor(Color); SelectSprite7(client_data7::SPRITE_TEE_BOT_GLOW, 0, 0, 0); Item = BotItem; Graphics()->QuadsDraw(&Item, 1); @@ -338,7 +340,9 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_DECORATION]); Graphics()->QuadsBegin(); Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); - Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION]); + ColorRGBA Color = pInfo->m_Sixup.m_aColors[protocol7::SKINPART_DECORATION]; + Color.a = Alpha; + Graphics()->SetColor(Color); SelectSprite7(OutLine ? client_data7::SPRITE_TEE_DECORATION_OUTLINE : client_data7::SPRITE_TEE_DECORATION, 0, 0, 0); Item = BodyItem; Graphics()->QuadsDraw(&Item, 1); @@ -351,12 +355,14 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); if(OutLine) { - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); SelectSprite7(client_data7::SPRITE_TEE_BODY_OUTLINE, 0, 0, 0); } else { - Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY]); + ColorRGBA Color = pInfo->m_Sixup.m_aColors[protocol7::SKINPART_BODY]; + Color.a = Alpha; + Graphics()->SetColor(Color); SelectSprite7(client_data7::SPRITE_TEE_BODY, 0, 0, 0); } Item = BodyItem; @@ -370,7 +376,7 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn Graphics()->QuadsBegin(); Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); ColorRGBA MarkingColor = pInfo->m_Sixup.m_aColors[protocol7::SKINPART_MARKING]; - Graphics()->SetColor(MarkingColor.r * MarkingColor.a, MarkingColor.g * MarkingColor.a, MarkingColor.b * MarkingColor.a, MarkingColor.a); + Graphics()->SetColor(MarkingColor.r * MarkingColor.a, MarkingColor.g * MarkingColor.a, MarkingColor.b * MarkingColor.a, MarkingColor.a * Alpha); SelectSprite7(client_data7::SPRITE_TEE_MARKING, 0, 0, 0); Item = BodyItem; Graphics()->QuadsDraw(&Item, 1); @@ -383,7 +389,7 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn Graphics()->TextureSet(pInfo->m_Sixup.m_aTextures[protocol7::SKINPART_BODY]); Graphics()->QuadsBegin(); Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); for(int t = 0; t < 2; t++) { SelectSprite7(t == 0 ? client_data7::SPRITE_TEE_BODY_SHADOW : client_data7::SPRITE_TEE_BODY_UPPER_OUTLINE, 0, 0, 0); @@ -399,11 +405,17 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); if(IsBot) { - Graphics()->SetColor(pInfo->m_Sixup.m_BotColor); + ColorRGBA Color = pInfo->m_Sixup.m_BotColor; + Color.a = Alpha; + Graphics()->SetColor(Color); Emote = EMOTE_SURPRISE; } else - Graphics()->SetColor(pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES]); + { + ColorRGBA Color = pInfo->m_Sixup.m_aColors[protocol7::SKINPART_EYES]; + Color.a = Alpha; + Graphics()->SetColor(Color); + } if(Pass == 1) { switch(Emote) @@ -439,7 +451,7 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn Graphics()->TextureSet(pInfo->m_Sixup.m_HatTexture); Graphics()->QuadsBegin(); Graphics()->QuadsSetRotation(pAnim->GetBody()->m_Angle * pi * 2); - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); int Flag = Direction.x < 0.0f ? SPRITE_FLAG_FLIP_X : 0; switch(pInfo->m_Sixup.m_HatSpriteIndex) { @@ -473,7 +485,7 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn if(OutLine) { - Graphics()->SetColor(1.0f, 1.0f, 1.0f, 1.0f); + Graphics()->SetColor(1.0f, 1.0f, 1.0f, Alpha); SelectSprite7(client_data7::SPRITE_TEE_FOOT_OUTLINE, 0, 0, 0); } else @@ -486,7 +498,7 @@ void CRenderTools::RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pIn pInfo->m_Sixup.m_aColors[protocol7::SKINPART_FEET].r * ColorScale, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_FEET].g * ColorScale, pInfo->m_Sixup.m_aColors[protocol7::SKINPART_FEET].b * ColorScale, - pInfo->m_Sixup.m_aColors[protocol7::SKINPART_FEET].a); + pInfo->m_Sixup.m_aColors[protocol7::SKINPART_FEET].a * Alpha); SelectSprite7(client_data7::SPRITE_TEE_FOOT, 0, 0, 0); } diff --git a/src/game/client/render.h b/src/game/client/render.h index 53e8216c7..ede6a6e7d 100644 --- a/src/game/client/render.h +++ b/src/game/client/render.h @@ -168,7 +168,7 @@ class CRenderTools static void GetRenderTeeFeetScale(float BaseSize, float &FeetScaleWidth, float &FeetScaleHeight); void RenderTee6(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha = 1.0f) const; - void RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos) const; + void RenderTee7(const CAnimState *pAnim, const CTeeRenderInfo *pInfo, int Emote, vec2 Dir, vec2 Pos, float Alpha = 1.0f) const; public: class IGraphics *Graphics() const { return m_pGraphics; }