summary refs log tree commit diff
path: root/src/Chat.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/Chat.c')
-rw-r--r--src/Chat.c311
1 files changed, 311 insertions, 0 deletions
diff --git a/src/Chat.c b/src/Chat.c
new file mode 100644
index 0000000..5f45d78
--- /dev/null
+++ b/src/Chat.c
@@ -0,0 +1,311 @@
+#include "Chat.h"
+#include "Commands.h"
+#include "String.h"
+#include "Stream.h"
+#include "Event.h"
+#include "Game.h"
+#include "Logger.h"
+#include "Server.h"
+#include "Funcs.h"
+#include "Utils.h"
+#include "Options.h"
+#include "Drawer2D.h"
+ 
+static char status[5][STRING_SIZE];
+static char bottom[3][STRING_SIZE];
+static char client[2][STRING_SIZE];
+static char announcement[STRING_SIZE];
+static char bigAnnouncement[STRING_SIZE];
+static char smallAnnouncement[STRING_SIZE];
+
+cc_string Chat_Status[5]       = { String_FromArray(status[0]), String_FromArray(status[1]), String_FromArray(status[2]),
+                                                                String_FromArray(status[3]), String_FromArray(status[4]) };
+cc_string Chat_BottomRight[3]  = { String_FromArray(bottom[0]), String_FromArray(bottom[1]), String_FromArray(bottom[2]) };
+cc_string Chat_ClientStatus[2] = { String_FromArray(client[0]), String_FromArray(client[1]) };
+
+cc_string Chat_Announcement = String_FromArray(announcement);
+cc_string Chat_BigAnnouncement = String_FromArray(bigAnnouncement);
+cc_string Chat_SmallAnnouncement = String_FromArray(smallAnnouncement);
+
+double Chat_AnnouncementReceived;
+double Chat_BigAnnouncementReceived;
+double Chat_SmallAnnouncementReceived;
+
+struct StringsBuffer Chat_Log, Chat_InputLog;
+cc_bool Chat_Logging;
+
+/*########################################################################################################################*
+*-------------------------------------------------------Chat logging------------------------------------------------------*
+*#########################################################################################################################*/
+double Chat_RecentLogTimes[CHATLOG_TIME_MASK + 1];
+
+static void ClearChatLogs(void) {
+	Mem_Set(Chat_RecentLogTimes, 0, sizeof(Chat_RecentLogTimes));
+	StringsBuffer_Clear(&Chat_Log);
+}
+
+static char      logNameBuffer[STRING_SIZE];
+static cc_string logName = String_FromArray(logNameBuffer);
+static char      logPathBuffer[FILENAME_SIZE];
+static cc_string logPath = String_FromArray(logPathBuffer);
+
+static struct Stream logStream;
+static int lastLogDay, lastLogMonth, lastLogYear;
+
+/* Resets log name to empty and resets last log date */
+static void ResetLogFile(void) {
+	logName.length = 0;
+	lastLogYear    = -123;
+}
+
+/* Closes handle to the chat log file */
+static void CloseLogFile(void) {
+	cc_result res;
+	if (!logStream.meta.file) return;
+
+	res = logStream.Close(&logStream);
+	if (res) { Logger_SysWarn2(res, "closing", &logPath); }
+}
+
+/* Whether the given character is an allowed in a log filename */
+static cc_bool AllowedLogNameChar(char c) {
+	return
+		c == '{' || c == '}' || c == '[' || c == ']' || c == '(' || c == ')' ||
+		(c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
+}
+
+void Chat_SetLogName(const cc_string* name) {
+	char c;
+	int i;
+	if (logName.length) return;
+
+	for (i = 0; i < name->length; i++) {
+		c = name->buffer[i];
+
+		if (AllowedLogNameChar(c)) {
+			String_Append(&logName, c);
+		} else if (c == '&') {
+			i++; /* skip over following color code */
+		}
+	}
+}
+
+void Chat_DisableLogging(void) {
+	Chat_Logging = false;
+	lastLogYear  = -321;
+	Chat_AddRaw("&cDisabling chat logging");
+	CloseLogFile();
+}
+
+static cc_bool CreateLogsDirectory(void) {
+	static const cc_string dir = String_FromConst("logs");
+	cc_result res;
+	/* Utils_EnsureDirectory cannot be used here because it causes a stack overflow  */
+	/* when running the game and an error occurs when trying to create the directory */
+	/* This happens because when running the game, Logger_WarnFunc is changed to log */
+	/* a message in chat instead of showing a dialog box, which causes the following */
+	/* functions to be called in a recursive loop: */
+	/*                                                                                         */
+	/* Utils_EnsureDirectory --> Logger_SysWarn2 --> Chat_Add --> AppendChatLog -> OpenChatLog */
+	/*  --> Utils_EnsureDirectory --> Logger_SysWarn2 --> Chat_Add --> AppendChatLog -> OpenChatLog */
+	/*       --> Utils_EnsureDirectory --> Logger_SysWarn2 --> Chat_Add --> AppendChatLog -> OpenChatLog */
+	/*            --> Utils_EnsureDirectory --> Logger_SysWarn2 --> Chat_Add --> AppendChatLog ... */
+	/* and so on, until eventually the stack overflows */
+	res = Directory_Create(&dir);
+	if (!res || res == ReturnCode_DirectoryExists) return true;
+
+	Chat_DisableLogging();
+	Logger_SysWarn2(res, "creating directory", &dir); 
+	return false;
+}
+
+static void OpenChatLog(struct DateTime* now) {
+	cc_result res;
+	int i;
+	if (Platform_ReadonlyFilesystem || !CreateLogsDirectory()) return;
+
+	/* Ensure multiple instances do not end up overwriting each other's log entries. */
+	for (i = 0; i < 20; i++) {
+		logPath.length = 0;
+		String_Format3(&logPath, "logs/%p4-%p2-%p2 ", &now->year, &now->month, &now->day);
+
+		if (i > 0) {
+			String_Format2(&logPath, "%s _%i.txt", &logName, &i);
+		} else {
+			String_Format1(&logPath, "%s.txt", &logName);
+		}
+
+		res = Stream_AppendFile(&logStream, &logPath);
+		if (res && res != ReturnCode_FileShareViolation) {
+			Chat_DisableLogging();
+			Logger_SysWarn2(res, "appending to", &logPath);
+			return;
+		}
+
+		if (res == ReturnCode_FileShareViolation) continue;
+		return;
+	}
+
+	Chat_DisableLogging();
+	Chat_Add1("&cFailed to open a chat log file after %i tries, giving up", &i);	
+}
+
+static void AppendChatLog(const cc_string* text) {
+	cc_string str; char strBuffer[DRAWER2D_MAX_TEXT_LENGTH];
+	struct DateTime now;
+	cc_result res;	
+
+	if (!logName.length || !Chat_Logging) return;
+	DateTime_CurrentLocal(&now);
+
+	if (now.day != lastLogDay || now.month != lastLogMonth || now.year != lastLogYear) {
+		CloseLogFile();
+		OpenChatLog(&now);
+	}
+
+	lastLogDay = now.day; lastLogMonth = now.month; lastLogYear = now.year;
+	if (!logStream.meta.file) return;
+
+	/* [HH:mm:ss] text */
+	String_InitArray(str, strBuffer);
+	String_Format3(&str, "[%p2:%p2:%p2] ", &now.hour, &now.minute, &now.second);
+	Drawer2D_WithoutColors(&str, text);
+
+	res = Stream_WriteLine(&logStream, &str);
+	if (!res) return;
+	Chat_DisableLogging();
+	Logger_SysWarn2(res, "writing to", &logPath);
+}
+
+void Chat_Add1(const char* format, const void* a1) {
+	Chat_Add4(format, a1, NULL, NULL, NULL);
+}
+void Chat_Add2(const char* format, const void* a1, const void* a2) {
+	Chat_Add4(format, a1, a2, NULL, NULL);
+}
+void Chat_Add3(const char* format, const void* a1, const void* a2, const void* a3) {
+	Chat_Add4(format, a1, a2, a3, NULL);
+}
+void Chat_Add4(const char* format, const void* a1, const void* a2, const void* a3, const void* a4) {
+	cc_string msg; char msgBuffer[STRING_SIZE * 2];
+	String_InitArray(msg, msgBuffer);
+
+	String_Format4(&msg, format, a1, a2, a3, a4);
+	Chat_AddOf(&msg, MSG_TYPE_NORMAL);
+}
+
+void Chat_AddRaw(const char* raw) {
+	cc_string str = String_FromReadonly(raw);
+	Chat_AddOf(&str, MSG_TYPE_NORMAL);
+}
+void Chat_Add(const cc_string* text) { Chat_AddOf(text, MSG_TYPE_NORMAL); }
+
+void Chat_AddOf(const cc_string* text, int msgType) {
+	cc_string str;
+	if (msgType == MSG_TYPE_NORMAL) {
+		/* Check for chat overflow (see issue #837) */
+		/* This happens because Offset/Length are packed into a single 32 bit value, */
+		/*  with 9 bits used for length. Hence if offset exceeds 2^23 (8388608), it */
+		/*  overflows and earlier chat messages start wrongly appearing instead */
+		if (Chat_Log.totalLength > 8388000) {
+			ClearChatLogs();
+			Chat_AddRaw("&cChat log cleared as it hit 8.3 million character limit");
+		}
+
+		/* StringsBuffer_Add will abort game if try to add string > 511 characters */
+		str        = *text; 
+		str.length = min(str.length, DRAWER2D_MAX_TEXT_LENGTH);
+
+		Chat_GetLogTime(Chat_Log.count) = Game.Time;
+		AppendChatLog(&str);
+		StringsBuffer_Add(&Chat_Log, &str);
+	} else if (msgType >= MSG_TYPE_STATUS_1 && msgType <= MSG_TYPE_STATUS_3) {
+		/* Status[0] is for texture pack downloading message */
+		/* Status[1] is for reduced performance mode message */
+		String_Copy(&Chat_Status[2 + (msgType - MSG_TYPE_STATUS_1)], text);
+	} else if (msgType >= MSG_TYPE_BOTTOMRIGHT_1 && msgType <= MSG_TYPE_BOTTOMRIGHT_3) {	
+		String_Copy(&Chat_BottomRight[msgType - MSG_TYPE_BOTTOMRIGHT_1], text);
+	} else if (msgType == MSG_TYPE_ANNOUNCEMENT) {
+		String_Copy(&Chat_Announcement, text);
+		Chat_AnnouncementReceived = Game.Time;
+	} else if (msgType == MSG_TYPE_BIGANNOUNCEMENT) {
+		String_Copy(&Chat_BigAnnouncement, text);
+		Chat_BigAnnouncementReceived = Game.Time;
+	} else if (msgType == MSG_TYPE_SMALLANNOUNCEMENT) {
+		String_Copy(&Chat_SmallAnnouncement, text);
+		Chat_SmallAnnouncementReceived = Game.Time;
+	} else if (msgType >= MSG_TYPE_CLIENTSTATUS_1 && msgType <= MSG_TYPE_CLIENTSTATUS_2) {
+		String_Copy(&Chat_ClientStatus[msgType - MSG_TYPE_CLIENTSTATUS_1], text);
+	} else if (msgType >= MSG_TYPE_EXTRASTATUS_1 && msgType <= MSG_TYPE_EXTRASTATUS_2) {
+		String_Copy(&Chat_Status[msgType - MSG_TYPE_EXTRASTATUS_1], text);
+	} 
+
+	Event_RaiseChat(&ChatEvents.ChatReceived, text, msgType);
+}
+
+
+/*########################################################################################################################*
+*-------------------------------------------------------Generic chat------------------------------------------------------*
+*#########################################################################################################################*/
+static void LogInputUsage(const cc_string* text) {
+	/* Simplify navigating through input history by not logging duplicate entries */
+	if (Chat_InputLog.count) {
+		int lastIndex  = Chat_InputLog.count - 1;
+		cc_string last = StringsBuffer_UNSAFE_Get(&Chat_InputLog, lastIndex);
+
+		if (String_Equals(text, &last)) return;
+	}
+	StringsBuffer_Add(&Chat_InputLog, text);
+}
+
+void Chat_Send(const cc_string* text, cc_bool logUsage) {
+	if (!text->length) return;
+	Event_RaiseChat(&ChatEvents.ChatSending, text, 0);
+	if (logUsage) LogInputUsage(text);
+
+	if (!Commands_Execute(text)) {
+		Server.SendChat(text);
+	}
+}
+
+static void OnInit(void) {
+#if defined CC_BUILD_MOBILE || defined CC_BUILD_WEB
+	/* Better to not log chat by default on mobile/web, */
+	/* since it's not easily visible to end users */
+	Chat_Logging = Options_GetBool(OPT_CHAT_LOGGING, false);
+#else
+	Chat_Logging = Options_GetBool(OPT_CHAT_LOGGING, true);
+#endif
+}
+
+static void ClearCPEMessages(void) {
+	Chat_AddOf(&String_Empty, MSG_TYPE_ANNOUNCEMENT);
+	Chat_AddOf(&String_Empty, MSG_TYPE_BIGANNOUNCEMENT);
+	Chat_AddOf(&String_Empty, MSG_TYPE_SMALLANNOUNCEMENT);
+	Chat_AddOf(&String_Empty, MSG_TYPE_STATUS_1);
+	Chat_AddOf(&String_Empty, MSG_TYPE_STATUS_2);
+	Chat_AddOf(&String_Empty, MSG_TYPE_STATUS_3);
+	Chat_AddOf(&String_Empty, MSG_TYPE_BOTTOMRIGHT_1);
+	Chat_AddOf(&String_Empty, MSG_TYPE_BOTTOMRIGHT_2);
+	Chat_AddOf(&String_Empty, MSG_TYPE_BOTTOMRIGHT_3);
+}
+
+static void OnReset(void) {
+	CloseLogFile();
+	ResetLogFile();
+	ClearCPEMessages();
+}
+
+static void OnFree(void) {
+	CloseLogFile();
+	ClearCPEMessages();
+
+	ClearChatLogs();
+	StringsBuffer_Clear(&Chat_InputLog);
+}
+
+struct IGameComponent Chat_Component = {
+	OnInit, /* Init  */
+	OnFree, /* Free  */
+	OnReset /* Reset */
+};