// Server-owned audit stream. The file contains every event; chat reads a bounded // recent-history window with explicit gap notices for clients that fall behind. class ERR_HQJournal { protected ref array m_Recent = {}; protected ref array m_Pending = {}; protected int m_Sequence; protected int m_Message; protected static int s_Part; protected static int s_Session; protected string m_Session; void ERR_HQJournal() { s_Session++; m_Session = string.Format("%1-%2", System.GetUnixTime(), s_Session); } protected float m_RetryAt; protected int m_Lost; string Path() { if (s_Part == 0) return "$logs:rereintroduced-hq.log"; return "$logs:rereintroduced-hq-" + s_Part.ToString() + ".log"; } int Message() { return ++m_Message; } int Sequence() { return m_Sequence; } void Write(string kind, string message, int messageId = 0) { if (!Replication.IsServer()) return; float seconds = GetGame().GetWorld().GetWorldTime() / 1000; string line = string.Format("#%1 t=%2s M%3 %4 %5", ++m_Sequence, Math.Round(seconds * 10) / 10.0, messageId, kind, message) + " [session=" + m_Session + "]"; if (m_Recent.Count() >= 300) m_Recent.Remove(0); m_Recent.Insert(line); if (m_Pending.Count() < 4096) m_Pending.Insert(line); else m_Lost++; } void Read(int after, out array lines, out int cursor, int limit = 8) { lines.Clear(); cursor = Math.Clamp(after, 0, m_Sequence); int first = m_Sequence - m_Recent.Count(); if (cursor < first) { lines.Insert(string.Format("HQ log: %1 older entries are outside chat history; see %2", first - cursor, Path())); cursor = first; } int characters; for (int i = cursor - first; i < m_Recent.Count() && lines.Count() < Math.Clamp(limit, 1, 40); i++) { // Bound each menu RPC without skipping unread records. Chat still // requests eight; the live menu can drain a busy journal faster. if (characters + m_Recent[i].Length() > 16000 && !lines.IsEmpty()) break; lines.Insert(m_Recent[i]); characters += m_Recent[i].Length(); cursor++; } } void Flush() { if (m_Pending.IsEmpty() || !Replication.IsServer()) return; float now = GetGame().GetWorld().GetWorldTime(); if (now < m_RetryAt) return; FileHandle file = FileIO.OpenFile(Path(), FileMode.APPEND); if (!file) { m_RetryAt = now + 30000; PrintFormat("Escapists AI [HQ log]: cannot open %1; retry in 30s, chat history remains available", Path(), level: LogLevel.ERROR); return; } foreach (string line : m_Pending) file.WriteLine(line); if (m_Lost > 0) file.WriteLine(string.Format("HQ log: %1 entries lost while file was unavailable and buffer full", m_Lost)); bool rotate = file.GetLength() >= 5242880; file.Close(); m_Pending.Clear(); m_Lost = 0; if (rotate) s_Part++; } }