diff --git a/CMakeLists.txt b/CMakeLists.txt index 395451b81..2def8e284 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -731,6 +731,8 @@ set_src(ENGINE_SHARED GLOB src/engine/shared config_variables.h console.cpp console.h + csv.cpp + csv.h datafile.cpp datafile.h demo.cpp @@ -1336,6 +1338,7 @@ if(GTEST_FOUND OR DOWNLOAD_GTEST) set_src(TESTS GLOB src/test aio.cpp color.cpp + csv.cpp datafile.cpp fs.cpp git_revision.cpp diff --git a/src/engine/shared/csv.cpp b/src/engine/shared/csv.cpp new file mode 100644 index 000000000..0fc90a879 --- /dev/null +++ b/src/engine/shared/csv.cpp @@ -0,0 +1,40 @@ +#include "csv.h" + +void CsvWrite(IOHANDLE File, int NumColumns, const char *const *ppColumns) +{ + for(int i = 0; i < NumColumns; i++) + { + if(i != 0) + { + io_write(File, ",", 1); + } + const char *pColumn = ppColumns[i]; + int ColumnLength = str_length(pColumn); + if(!str_find(pColumn, "\"") && !str_find(pColumn, ",")) + { + io_write(File, pColumn, ColumnLength); + continue; + } + + int Start = 0; + io_write(File, "\"", 1); + for(int j = 0; j < ColumnLength; j++) + { + if(pColumn[j] == '"') + { + if(Start != j) + { + io_write(File, pColumn + Start, j - Start); + } + Start = j + 1; + io_write(File, "\"\"", 2); + } + } + if(Start != ColumnLength) + { + io_write(File, pColumn + Start, ColumnLength - Start); + } + io_write(File, "\"", 1); + } + io_write_newline(File); +} diff --git a/src/engine/shared/csv.h b/src/engine/shared/csv.h new file mode 100644 index 000000000..aa5273382 --- /dev/null +++ b/src/engine/shared/csv.h @@ -0,0 +1,8 @@ +#ifndef ENGINE_SHARED_CSV_H +#define ENGINE_SHARED_CSV_H + +#include + +void CsvWrite(IOHANDLE File, int NumColumns, const char *const *pColumns); + +#endif // ENGINE_SHARED_CSV_H diff --git a/src/game/client/components/chat.cpp b/src/game/client/components/chat.cpp index 2e7eef3bd..bc0c98792 100644 --- a/src/game/client/components/chat.cpp +++ b/src/game/client/components/chat.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -543,6 +544,63 @@ bool CChat::LineShouldHighlight(const char *pLine, const char *pName) return false; } +#define SAVES_FILE "ddnet-saves.txt" +const char *SAVES_HEADER[] = { + "Time", + "Player", + "Map", + "Code", +}; + +void CChat::StoreSave(const char *pText) +{ + const char *pStart = str_find(pText, "Team successfully saved by "); + const char *pMid = str_find(pText, ". Use '/load "); + const char *pEnd = str_find(pText, "' to continue"); + + if(!pStart || !pMid || !pEnd || pMid < pStart || pEnd < pMid) + return; + + char aName[16]; + str_copy(aName, pStart + 27, minimum(static_cast(pMid - pStart - 26), sizeof(aName))); + + char aSaveCode[64]; + str_copy(aSaveCode, pMid + 13, minimum(static_cast(pEnd - pMid - 12), sizeof(aSaveCode))); + + char aTimestamp[20]; + str_timestamp_format(aTimestamp, sizeof(aTimestamp), FORMAT_SPACE); + + // TODO: Find a simple way to get the names of team members. This doesn't + // work since team is killed first, then save message gets sent: + /* + for(int i = 0; i < MAX_CLIENTS; i++) + { + const CNetObj_PlayerInfo *pInfo = GameClient()->m_Snap.m_paInfoByDDTeam[i]; + if(!pInfo) + continue; + pInfo->m_Team // All 0 + } + */ + + IOHANDLE File = Storage()->OpenFile(SAVES_FILE, IOFLAG_APPEND, IStorage::TYPE_SAVE); + if(!File) + return; + + const char *apColumns[4] = { + aTimestamp, + aName, + Client()->GetCurrentMap(), + aSaveCode, + }; + + if(io_tell(File) == 0) + { + CsvWrite(File, 4, SAVES_HEADER); + } + CsvWrite(File, 4, apColumns); + io_close(File); +} + void CChat::AddLine(int ClientID, int Team, const char *pLine) { if(*pLine == 0 || @@ -640,6 +698,9 @@ void CChat::AddLine(int ClientID, int Team, const char *pLine) { str_copy(m_aLines[m_CurrentLine].m_aName, "*** ", sizeof(m_aLines[m_CurrentLine].m_aName)); str_format(m_aLines[m_CurrentLine].m_aText, sizeof(m_aLines[m_CurrentLine].m_aText), "%s", pLine); + + if(Client()->State() != IClient::STATE_DEMOPLAYBACK) + StoreSave(m_aLines[m_CurrentLine].m_aText); } else { diff --git a/src/game/client/components/chat.h b/src/game/client/components/chat.h index daf004a00..13f397459 100644 --- a/src/game/client/components/chat.h +++ b/src/game/client/components/chat.h @@ -91,6 +91,7 @@ class CChat : public CComponent static void ConEcho(IConsole::IResult *pResult, void *pUserData); bool LineShouldHighlight(const char *pLine, const char *pName); + void StoreSave(const char *pText); public: CChat(); diff --git a/src/test/csv.cpp b/src/test/csv.cpp new file mode 100644 index 000000000..bd2e6cf1f --- /dev/null +++ b/src/test/csv.cpp @@ -0,0 +1,56 @@ +#include "test.h" +#include + +#include + +static void Expect(int NumColumns, const char *const *ppColumns, const char *pExpected) +{ + CTestInfo Info; + + IOHANDLE File = io_open(Info.m_aFilename, IOFLAG_WRITE); + ASSERT_TRUE(File); + CsvWrite(File, NumColumns, ppColumns); + io_close(File); + + char aBuf[1024]; + File = io_open(Info.m_aFilename, IOFLAG_READ); + ASSERT_TRUE(File); + int Read = io_read(File, aBuf, sizeof(aBuf)); + io_close(File); + fs_remove(Info.m_aFilename); + + ASSERT_TRUE(Read >= 1); + Read -= 1; + ASSERT_EQ(aBuf[Read], '\n'); + aBuf[Read] = 0; + + #if defined(CONF_FAMILY_WINDOWS) + ASSERT_TRUE(Read >= 1); + Read -= 1; + ASSERT_EQ(aBuf[Read], '\r'); + aBuf[Read] = 0; + #endif + + for(int i = 0; i < Read; i++) + { + EXPECT_NE(aBuf[i], 0); + } + EXPECT_STREQ(aBuf, pExpected); +} + +TEST(Csv, Simple) +{ + const char *apCols1[] = {"a", "b"}; Expect(2, apCols1, "a,b"); + const char *apCols2[] = {"こんにちは"}; Expect(1, apCols2, "こんにちは"); + const char *apCols3[] = {"я", "", "й"}; Expect(3, apCols3, "я,,й"); + const char *apCols4[] = {""}; Expect(1, apCols4, ""); + const char *apCols5[] = {0}; Expect(0, apCols5, ""); +} + +TEST(Csv, LetTheQuotingBegin) +{ + const char *apCols1[] = {"\""}; Expect(1, apCols1, "\"\"\"\""); + const char *apCols2[] = {","}; Expect(1, apCols2, "\",\""); + const char *apCols3[] = {",,", ",\"\"\""}; Expect(2, apCols3, "\",,\",\",\"\"\"\"\"\"\""); + const char *apCols4[] = {"\",", " "}; Expect(2, apCols4, "\"\"\",\", "); +}