Jump to content

Recommended Posts

[Release] Persistent ChatCache System per Character (Client-Side)
📷 Demo
https://vimeo.com/1134884801

📌 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

1️⃣ You send 10 messages as "Player1".
2️⃣ You log out and close the client.
3️⃣ 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)
 

Spoiler


#You'll have to find to function that handles the response from the server that your name was changed
#In my case, in game.py
		def __ChangeNameSucces(self, varArg):
			try:
				vararray = varArg.split("|")
				oldName = vararray[0]
				newName = vararray[1]

				self.USER_DIR = os.path.expanduser("~")
				old_path = os.path.join(self.USER_DIR, "Documents", "YOUR_SERVER_NAME", "chat_cache", "%s.json" % oldName)
				new_path = os.path.join(self.USER_DIR, "Documents", "YOUR_SERVER_NAME", "chat_cache", "%s.json" % newName)

				if os.path.exists(old_path):
					os.rename(old_path, new_path)
				else:
					import chat
					chat.AppendChat(chat.CHAT_TYPE_INFO, "[DEBUG] ChatCache: no file found for %s" % oldName)
				
			except Exception as e:
				import dbg
				dbg.LogBox("ChatCache rename error: %s" % e)

 

Edited by pampules
  • Metin2 Dev 1
  • Scream 1
  • Love 1
Link to comment
https://metin2.dev/topic/34097-chat-cache-per-character/
Share on other sites

[Solved problem]
Problem: Could't spam UPPER arrow key + ENTER, index was moving to the element before last element.
 

Spoiler
#Modify
	def PushChatMessage(self, msg):
		if not msg:
			return

		if hasattr(self, "last_saved_message") and msg == self.last_saved_message:
			#Add this line
			self.chat_history_index += 1
			return

 

 

  • Metin2 Dev 1
Link to comment
https://metin2.dev/topic/34097-chat-cache-per-character/#findComment-172991
Share on other sites

Don't use any images from : imgur, turkmmop, freakgamers, inforge, hizliresim... Or your content will be deleted without notice...
Use : https://metin2.download/media/add/

Please use https://metin2.download/ when uploading files smaller than 100MB, otherwise the approval will take longer due to manual upload.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.