[Release] Persistent ChatCache System per Character (Client-Side)
Demo
[Hidden Content]
General Description
This system extends the standard chat functionality by allowing each character to automatically save and restore their last sent messages, even after:
- channel change,
- logout or relog,
- or full client restart.
Each character keeps its own independent chat history stored locally, containing only unique messages (duplicates are ignored automatically).
Technical Overview
The system is fully client-side, with no server or database dependency.
It stores the last 32 distinct messages sent by the player.
Automatic saving occurs after sending a message in chat
When reloading the game, the chat history is restored exactly as it was before.
Data Structure & Safety
Each character’s messages are saved in an encrypted JSON file named:
Documents/<your_server_name>/chat_cache/CharacterName.json
Security & Stability:
The content is encrypted using a lightweight XOR cipher.
File writes are handled safely through a temporary .tmp file and a backup .bak file, ensuring data integrity even if:
the player teleports while the cache is saving,
the client closes suddenly,
or the process is interrupted mid-save.
The save operation is atomic, meaning it completes entirely or not at all — never leaving a half-written or corrupted file.
Cross-Platform Storage
Default save directory:
Windows: C:\Users\<user>\Documents\Regalis2\chat_cache
Linux/macOS: ~/Documents/Regalis2/chat_cache
If the Documents folder cannot be accessed (permission issue), the system automatically falls back to a local folder (./chat_cache), so no errors occur.
Key Features
Character-specific history (not shared across characters).
No SQL or server interaction.
High performance — runs entirely in memory and writes on controlled intervals.
Crash-safe, robust, and auto-recovers from incomplete saves.
Automatically skips duplicate messages — only unique ones are saved.
Example Behavior
You send 10 messages as "Player1".
You log out and close the client.
You reopen the game and log in again as "Player1".
You can navigate through the same 10 messages using the ↑ and ↓ keys — exactly as before.
Additional Technical Notes
Saving is handled via: json.dumps() → xor_crypt() → .tmp → atomic rename.
Automatic restore from .bak if the main file is locked or interrupted.
Files are fully encrypted without any noticeable performance loss.
The system is easily extensible — timestamped messages or per-channel logging can be added later.
Code Implementation
uiChat.py
#Add after imports
def xor_crypt(data, key="ChatKey"):
key_len = len(key)
result = []
for i, c in enumerate(data):
result.append(chr(ord(c) ^ ord(key[i % key_len])))
return "".join(result)
#Find
self.lastSentenceStack = []
self.lastSentencePos = 0
#Replace with (Replace YOUR_SERVER_NAME with your server name)
self.chat_history = []
self.chat_history_index = -1
self.USER_DIR = os.path.expanduser("~")
self.chat_cache_dir = os.path.join(self.USER_DIR, "Documents", "YOUR_SERVER_NAME", "chat_cache")
self.chat_cache_dir = os.path.normpath(self.chat_cache_dir)
self.chat_cache_file = None
self.LoadChatCache()
#Find
def OpenChat(self):
#Add
self.chat_history_index = len(self.chat_history)
#Find
def OnIMEKeyDown(self, key):
#Replace all lines with
def OnIMEKeyDown(self, key):
# NEW PERSISTENT CHAT HISTORY
if app.VK_UP == key:
if hasattr(self, "chat_history") and self.chat_history:
self.chat_history_index = max(0, self.chat_history_index - 1)
self.SetText(self.chat_history[self.chat_history_index])
self.SetEndPosition()
return True
if app.VK_DOWN == key:
if hasattr(self, "chat_history") and self.chat_history:
self.chat_history_index = min(len(self.chat_history), self.chat_history_index + 1)
if self.chat_history_index < len(self.chat_history):
self.SetText(self.chat_history[self.chat_history_index])
else:
self.SetText("")
self.SetEndPosition()
return True
ui.EditLine.OnIMEKeyDown(self, key)
#Find __PrevLastSentenceStack, __NextLastSentenceStack, __PushLastSentenceStack
#Replace content with
pass
#Find
# LAST_SENTENCE_STACK
self.__PushLastSentenceStack(text)
# END_OF_LAST_SENTENCE_STACK
#Replace with
if hasattr(self, "PushChatMessage"):
self.PushChatMessage(text)
#Find
def OnIMEReturn(self):
.
.
.
return True
#Add
def LoadChatCache(self):
try:
try:
if not os.path.exists(self.chat_cache_dir):
os.makedirs(self.chat_cache_dir)
except Exception:
self.chat_cache_dir = os.path.join(os.getcwd(), "chat_cache")
if not os.path.exists(self.chat_cache_dir):
os.makedirs(self.chat_cache_dir)
playerName = player.GetName()
self.chat_cache_file = os.path.join(self.chat_cache_dir, "%s.json" % playerName)
backup_path = self.chat_cache_file + ".bak"
if os.path.exists(backup_path):
try:
if os.path.exists(self.chat_cache_file):
os.remove(self.chat_cache_file)
os.rename(backup_path, self.chat_cache_file)
except Exception as e:
import dbg
dbg.LogBox("ChatCache restore from .bak failed: %s" % e)
if os.path.exists(self.chat_cache_file):
f = open(self.chat_cache_file, "rb")
encrypted = f.read()
f.close()
decrypted = xor_crypt(unicode(encrypted, errors="ignore"))
if decrypted.strip():
try:
data = json.loads(decrypted)
if not isinstance(data.get("messages", []), list):
self.chat_history = []
return
self.chat_history = [str(x) for x in data.get("messages", [])]
return
except Exception as e:
import dbg
dbg.LogBox("ChatCache parse error: %s" % e)
except Exception as e:
import dbg
dbg.LogBox("ChatCache load error: %s" % e)
self.chat_history = []
def SaveChatCache(self):
try:
if not getattr(self, "chat_cache_file", None):
return
if not self.chat_history:
return
data = {
"messages": self.chat_history[-32:]
}
temp_path = self.chat_cache_file + ".tmp"
backup_path = self.chat_cache_file + ".bak"
f = open(temp_path, "wb")
content = json.dumps(data, ensure_ascii=False, indent=2)
encrypted = xor_crypt(content)
f.write(encrypted)
f.flush()
os.fsync(f.fileno())
f.close()
if os.path.exists(self.chat_cache_file):
os.rename(self.chat_cache_file, backup_path)
os.rename(temp_path, self.chat_cache_file)
if os.path.exists(backup_path):
os.remove(backup_path)
except Exception as e:
import dbg
dbg.LogBox("ChatCache save error: %s" % e)
def PushChatMessage(self, msg):
if not msg:
return
if hasattr(self, "last_saved_message") and msg == self.last_saved_message:
return
self.last_saved_message = msg
if msg in self.chat_history:
self.chat_history.remove(msg)
self.chat_history.append(msg)
if len(self.chat_history) > 32:
self.chat_history = self.chat_history[-32:]
self.chat_history_index = len(self.chat_history)
try:
self.SaveChatCache()
except Exception as e:
chat.AppendChat(chat.CHAT_TYPE_INFO, "Eroare la salvare chat: %s" % e)
game.py (BONUS! Rename .json file after changing character name)