RunicVTT Open Source Virtual Tabletop for TTRPG using P2P
Loading...
Searching...
No Matches
ChatManager Class Reference

#include <ChatManager.h>

Inheritance diagram for ChatManager:
Collaboration diagram for ChatManager:

Public Member Functions

 ChatManager (std::weak_ptr< NetworkManager > nm, std::shared_ptr< IdentityManager > identity_manager)
 
void setNetwork (std::weak_ptr< NetworkManager > nm)
 
void setActiveGameTable (uint64_t tableId, const std::string &gameTableName)
 
bool hasCurrent () const
 
bool saveCurrent ()
 
bool loadCurrent ()
 
void applyReady (const msg::ReadyMessage &m)
 
void pushMessageLocal (uint64_t groupId, const std::string &fromPeer, const std::string &username, const std::string &text, double ts, bool incoming)
 
void render ()
 
void writeGroupsToSnapshotGT (std::vector< unsigned char > &buf) const
 
void readGroupsFromSnapshotGT (const std::vector< unsigned char > &buf, size_t &off)
 
ChatGroupModelgetGroup (uint64_t id)
 
void replaceUsernameForUnique (const std::string &uniqueId, const std::string &newUsername)
 
void tryHandleSlashCommand (uint64_t threadId, const std::string &input)
 
std::set< std::string > resolvePeerIdsForParticipants (const std::set< std::string > &participantUids) const
 
bool isMeParticipantOf (const ChatGroupModel &g) const
 

Public Attributes

uint64_t currentTableId_ = 0
 
std::string currentTableName_
 

Static Public Attributes

static constexpr uint64_t generalGroupId_ = 1
 

Private Member Functions

ImU32 getUsernameColor (const std::string &name) const
 
void renderColoredUsername (const std::string &name) const
 
void renderPlainMessage (const std::string &text) const
 
std::filesystem::path chatFilePathFor (uint64_t tableId, const std::string &name) const
 
void ensureGeneral ()
 
void markGroupRead (uint64_t groupId)
 
void emitGroupCreate (const ChatGroupModel &g)
 
void emitGroupUpdate (const ChatGroupModel &g)
 
void emitGroupDelete (uint64_t groupId)
 
void emitChatMessageFrame (uint64_t groupId, const std::string &username, const std::string &text, uint64_t ts)
 
void emitGroupLeave (uint64_t groupId)
 
void renderEditGroupPopup ()
 
void renderLeftPanel (float width)
 
void renderRightPanel (float leftPanelWidth)
 
void renderCreateGroupPopup ()
 
void renderDeleteGroupPopup ()
 
void renderDicePopup ()
 
bool saveLog (std::vector< uint8_t > &buf) const
 
bool loadLog (const std::vector< uint8_t > &buf)
 
uint64_t makeGroupIdFromName (const std::string &name) const
 

Static Private Member Functions

static ChatMessageModel::Kind classifyMessage (const std::string &s)
 
static double nowSec ()
 
static ImVec4 HSVtoRGB (float h, float s, float v)
 

Private Attributes

std::shared_ptr< IdentityManageridentity_manager
 
std::unordered_map< uint64_t, ChatGroupModelgroups_
 
uint64_t activeGroupId_ = generalGroupId_
 
std::array< char, 512 > input_ {}
 
bool focusInput_ = false
 
bool followScroll_ = true
 
bool jumpToBottom_ = false
 
bool chatWindowFocused_ = false
 
bool openCreatePopup_ = false
 
bool openDeletePopup_ = false
 
bool openDicePopup_ = false
 
std::array< char, 128 > newGroupName_ {}
 
std::set< std::string > newGroupSel_
 
float leftWidth_ = 170.0f
 
bool openEditPopup_ = false
 
uint64_t editGroupId_ = 0
 
std::array< char, 128 > editGroupName_ {}
 
std::set< std::string > editGroupSel_
 
int diceN_ = 1
 
int diceSides_ = 20
 
int diceMod_ = 0
 
bool diceModPerDie_ = false
 
std::weak_ptr< NetworkManagernetwork_
 
std::unordered_map< std::string, ImU32 > nameColorCache_
 

Detailed Description

Definition at line 49 of file ChatManager.h.

Constructor & Destructor Documentation

◆ ChatManager()

ChatManager::ChatManager ( std::weak_ptr< NetworkManager > nm,
std::shared_ptr< IdentityManager > identity_manager )
explicit

Definition at line 40 of file ChatManager.cpp.

40 :
std::shared_ptr< IdentityManager > identity_manager
std::weak_ptr< NetworkManager > network_

Member Function Documentation

◆ applyReady()

void ChatManager::applyReady ( const msg::ReadyMessage & m)

Definition at line 243 of file ChatManager.cpp.

244{
245 using K = msg::DCType;
246
247 switch (m.kind)
248 {
249 case K::ChatGroupCreate:
250 {
251 if (!m.tableId || !m.threadId)
252 return;
253 if (*m.tableId != currentTableId_)
254 return;
255
257 g.id = *m.threadId;
258 g.name = m.name.value_or("Group");
259
260 // derive owner uniqueId (best-effort)
261 std::string ownerUid;
263 ownerUid = identity_manager->uniqueForPeer(m.fromPeerId).value_or("Player");
264 g.ownerUniqueId = ownerUid;
265
266 if (m.participants)
267 g.participants = *m.participants;
268
269 auto it = groups_.find(g.id);
270 if (it == groups_.end())
271 groups_.emplace(g.id, std::move(g));
272 else
273 {
274 it->second.name = g.name;
275 it->second.participants = std::move(g.participants);
276 // keep existing ownerUniqueId if you want
277 }
278
279 Logger::instance().log("chat", Logger::Level::Info, "RX GroupCreate id=" + std::to_string(*m.threadId) + " name=" + m.name.value_or("Group"));
280 break;
281 }
282
283 case K::ChatGroupUpdate:
284 {
285 if (!m.tableId || !m.threadId)
286 return;
287 if (*m.tableId != currentTableId_)
288 return;
289
290 auto* g = getGroup(*m.threadId);
291 if (!g)
292 {
293 ChatGroupModel stub;
294 stub.id = *m.threadId;
295 stub.name = m.name.value_or("Group");
296 if (m.participants)
297 stub.participants = *m.participants;
298
299 // best-effort owner
301 stub.ownerUniqueId = identity_manager->uniqueForPeer(m.fromPeerId).value_or("Player");
302
303 groups_.emplace(stub.id, std::move(stub));
304 }
305 else
306 {
307 if (m.name)
308 g->name = *m.name;
309 if (m.participants)
310 g->participants = *m.participants;
311 }
312 break;
313 }
314
315 case K::ChatGroupDelete:
316 {
317 if (!m.tableId || !m.threadId)
318 return;
319 if (*m.tableId != currentTableId_)
320 return;
321 if (*m.threadId == generalGroupId_)
322 return; // never delete General
323
324 if (auto* g = getGroup(*m.threadId))
325 {
326 // permission: only owner can delete
327 const std::string reqOwner =
328 identity_manager ? identity_manager->uniqueForPeer(m.fromPeerId).value_or("Player") : std::string{};
329 if (!g->ownerUniqueId.empty() && g->ownerUniqueId == reqOwner)
330 {
331 groups_.erase(*m.threadId);
332 if (activeGroupId_ == *m.threadId)
334 }
335 }
336 break;
337 }
338
339 case K::ChatMessage:
340 {
341 if (!m.tableId || !m.threadId || !m.ts || !m.name || !m.text)
342 return;
343 if (*m.tableId != currentTableId_)
344 return;
345
346 // map sender to uniqueId if possible (if ReadyMessage.userPeerId later carries it, use that)
347 std::string senderUid =
348 identity_manager ? identity_manager->uniqueForPeer(m.fromPeerId).value_or("Player") : std::string{};
349
350 // inline append to include senderUniqueId
352 msg.kind = classifyMessage(*m.text);
353 msg.senderUniqueId = senderUid;
354 msg.username = *m.name;
355 msg.content = *m.text;
356 msg.ts = (double)*m.ts;
357
358 auto* g = getGroup(*m.threadId);
359 if (!g)
360 {
361 ChatGroupModel stub;
362 stub.id = *m.threadId;
363 stub.name = "(pending?)";
364 groups_.emplace(stub.id, std::move(stub));
365 g = getGroup(*m.threadId);
366 }
367 g->messages.push_back(std::move(msg));
368
369 const bool isActive = (activeGroupId_ == *m.threadId);
370 if (!isActive || !chatWindowFocused_ || !followScroll_)
371 g->unread = std::min<uint32_t>(g->unread + 1, 999u);
372 break;
373 }
374
375 default:
376 break;
377 }
378}
ChatGroupModel * getGroup(uint64_t id)
bool chatWindowFocused_
bool followScroll_
uint64_t activeGroupId_
static ChatMessageModel::Kind classifyMessage(const std::string &s)
std::unordered_map< uint64_t, ChatGroupModel > groups_
uint64_t currentTableId_
Definition ChatManager.h:90
static constexpr uint64_t generalGroupId_
Definition ChatManager.h:89
static Logger & instance()
Definition Logger.h:39
Definition Message.h:28
DCType
Definition Message.h:31
Here is the call graph for this function:

◆ chatFilePathFor()

std::filesystem::path ChatManager::chatFilePathFor ( uint64_t tableId,
const std::string & name ) const
private

Definition at line 78 of file ChatManager.cpp.

79{
80 auto gametables_folder = PathManager::getGameTablesPath();
81 auto gametable_name_folder = gametables_folder / name;
82 return gametable_name_folder / std::filesystem::path(name + "_" + std::to_string(tableId) + "_chatgroups.runic");
83}
static fs::path getGameTablesPath()
Definition PathManager.h:76
Here is the call graph for this function:
Here is the caller graph for this function:

◆ classifyMessage()

ChatMessageModel::Kind ChatManager::classifyMessage ( const std::string & s)
staticprivate

Definition at line 30 of file ChatManager.cpp.

31{
32 if (ends_with_icase(s, ".png") || ends_with_icase(s, ".jpg") || ends_with_icase(s, ".jpeg"))
34 if (s.rfind("http://", 0) == 0 || s.rfind("https://", 0) == 0)
37}
static bool ends_with_icase(const std::string &a, const char *suf)
Here is the call graph for this function:
Here is the caller graph for this function:

◆ emitChatMessageFrame()

void ChatManager::emitChatMessageFrame ( uint64_t groupId,
const std::string & username,
const std::string & text,
uint64_t ts )
private

Definition at line 489 of file ChatManager.cpp.

490{
491 auto nm = network_.lock();
492 if (!nm || !hasCurrent())
493 return;
494
495 auto it = groups_.find(groupId);
496 if (it == groups_.end())
497 return;
498
499 const auto& parts = it->second.participants;
500 auto j = msg::makeChatMessage(currentTableId_, groupId, ts, username, text);
501 if (groupId == generalGroupId_)
502 {
503 nm->broadcastChatJson(j);
504 return;
505 }
506 auto targets = resolvePeerIdsForParticipants(parts);
507 if (!targets.empty())
508 nm->sendChatJsonTo(targets, j);
509}
std::set< std::string > resolvePeerIdsForParticipants(const std::set< std::string > &participantUids) const
bool hasCurrent() const
Definition ChatManager.h:59
Json makeChatMessage(uint64_t tableId, uint64_t groupId, uint64_t ts, const std::string &username, const std::string &text)
Definition Message.h:404
Here is the call graph for this function:
Here is the caller graph for this function:

◆ emitGroupCreate()

void ChatManager::emitGroupCreate ( const ChatGroupModel & g)
private

Definition at line 428 of file ChatManager.cpp.

429{
430 auto nm = network_.lock();
431 if (!nm || !hasCurrent())
432 return;
433
434 auto j = msg::makeChatGroupCreate(currentTableId_, g.id, g.name, g.participants);
435
436 if (g.id == generalGroupId_)
437 {
438 nm->broadcastChatJson(j);
439 return;
440 }
441
442 auto targets = resolvePeerIdsForParticipants(g.participants);
443 if (!targets.empty())
444 nm->sendChatJsonTo(targets, j);
445}
Json makeChatGroupCreate(uint64_t tableId, uint64_t groupId, const std::string &name, const std::set< std::string > &participants)
Definition Message.h:366
Here is the call graph for this function:
Here is the caller graph for this function:

◆ emitGroupDelete()

void ChatManager::emitGroupDelete ( uint64_t groupId)
private

Definition at line 466 of file ChatManager.cpp.

467{
468 auto nm = network_.lock();
469 if (!nm || !hasCurrent())
470 return;
471
472 if (groupId == generalGroupId_)
473 return;
474
475 auto it = groups_.find(groupId);
476 if (it == groups_.end())
477 return;
478
479 const auto& g = it->second;
480
481 auto j = msg::makeChatGroupDelete(currentTableId_, groupId);
482
483 auto targets = resolvePeerIdsForParticipants(g.participants);
484
485 if (!targets.empty())
486 nm->sendChatJsonTo(targets, j);
487}
Json makeChatGroupDelete(uint64_t tableId, uint64_t groupId)
Definition Message.h:396
Here is the call graph for this function:
Here is the caller graph for this function:

◆ emitGroupLeave()

void ChatManager::emitGroupLeave ( uint64_t groupId)
private

Definition at line 511 of file ChatManager.cpp.

512{
513 auto nm = network_.lock();
514 if (!nm || !hasCurrent())
515 return;
516
517 // General cannot be “left”
518 if (groupId == generalGroupId_)
519 return;
520
521 auto it = groups_.find(groupId);
522 if (it == groups_.end())
523 return;
524
525 auto& g = it->second;
526 if (!identity_manager)
527 return;
528
529 const std::string meUid = identity_manager->myUniqueId();
530
531 // Owner cannot leave their own group
532 if (!g.ownerUniqueId.empty() && g.ownerUniqueId == meUid)
533 return;
534
535 // Remove me locally
536 g.participants.erase(meUid);
537
538 // Broadcast updated participants (re-using ChatGroupUpdate)
539 auto j = msg::makeChatGroupUpdate(currentTableId_, g.id, g.name, g.participants);
540 auto targets = resolvePeerIdsForParticipants(g.participants);
541 if (!targets.empty())
542 nm->sendChatJsonTo(targets, j);
543
544 // UX: if I’m no longer in it, stop showing it as active
545 if (activeGroupId_ == groupId && g.participants.count(meUid) == 0)
547}
Json makeChatGroupUpdate(uint64_t tableId, uint64_t groupId, const std::string &name, const std::set< std::string > &participants)
Definition Message.h:381
Here is the call graph for this function:
Here is the caller graph for this function:

◆ emitGroupUpdate()

void ChatManager::emitGroupUpdate ( const ChatGroupModel & g)
private

Definition at line 447 of file ChatManager.cpp.

448{
449 auto nm = network_.lock();
450 if (!nm || !hasCurrent())
451 return;
452
453 auto j = msg::makeChatGroupUpdate(currentTableId_, g.id, g.name, g.participants);
454
455 if (g.id == generalGroupId_)
456 {
457 nm->broadcastChatJson(j);
458 return;
459 }
460
461 auto targets = resolvePeerIdsForParticipants(g.participants);
462 if (!targets.empty())
463 nm->sendChatJsonTo(targets, j);
464}
Here is the call graph for this function:
Here is the caller graph for this function:

◆ ensureGeneral()

void ChatManager::ensureGeneral ( )
private

Definition at line 65 of file ChatManager.cpp.

66{
67 if (groups_.find(generalGroupId_) == groups_.end())
68 {
71 g.name = "General";
72 g.ownerUniqueId.clear(); // nobody “owns” General
73 groups_.emplace(g.id, std::move(g));
74 }
75}
Here is the caller graph for this function:

◆ getGroup()

ChatGroupModel * ChatManager::getGroup ( uint64_t id)

Definition at line 238 of file ChatManager.cpp.

239{
240 auto it = groups_.find(id);
241 return it == groups_.end() ? nullptr : &it->second;
242}
Here is the caller graph for this function:

◆ getUsernameColor()

ImU32 ChatManager::getUsernameColor ( const std::string & name) const
private

Definition at line 684 of file ChatManager.cpp.

685{
686 // C++14-friendly: no init-statement in if
687 auto it = nameColorCache_.find(name);
688 if (it != nameColorCache_.end())
689 return it->second;
690
691 // hash -> hue
692 std::hash<std::string> H;
693 uint64_t hv = H(name) ^ 0x9E3779B97F4A7C15ull;
694 float hue = (float)((hv % 10000) / 10000.0f); // [0,1)
695
696 // avoid a narrow hue band if desired
697 if (hue > 0.12f && hue < 0.18f)
698 hue += 0.08f;
699 if (hue >= 1.0f)
700 hue -= 1.0f;
701
702 const float sat = 0.65f;
703 const float val = 0.90f;
704
705 ImVec4 rgb = HSVtoRGB(hue, sat, val);
706 ImU32 col = ImGui::GetColorU32(rgb);
707
708 nameColorCache_.emplace(name, col);
709 return col;
710}
static ImVec4 HSVtoRGB(float h, float s, float v)
std::unordered_map< std::string, ImU32 > nameColorCache_
Here is the call graph for this function:
Here is the caller graph for this function:

◆ hasCurrent()

bool ChatManager::hasCurrent ( ) const
inline

Definition at line 59 of file ChatManager.h.

60 {
61 return currentTableId_ != 0;
62 }
Here is the caller graph for this function:

◆ HSVtoRGB()

ImVec4 ChatManager::HSVtoRGB ( float h,
float s,
float v )
staticprivate

Definition at line 637 of file ChatManager.cpp.

638{
639 if (s <= 0.0f)
640 return ImVec4(v, v, v, 1.0f);
641 h = std::fmodf(h, 1.0f) * 6.0f; // [0,6)
642 int i = (int)h;
643 float f = h - (float)i;
644 float p = v * (1.0f - s);
645 float q = v * (1.0f - s * f);
646 float t = v * (1.0f - s * (1.0f - f));
647 float r = 0, g = 0, b = 0;
648 switch (i)
649 {
650 case 0:
651 r = v;
652 g = t;
653 b = p;
654 break;
655 case 1:
656 r = q;
657 g = v;
658 b = p;
659 break;
660 case 2:
661 r = p;
662 g = v;
663 b = t;
664 break;
665 case 3:
666 r = p;
667 g = q;
668 b = v;
669 break;
670 case 4:
671 r = t;
672 g = p;
673 b = v;
674 break;
675 default:
676 r = v;
677 g = p;
678 b = q;
679 break;
680 }
681 return ImVec4(r, g, b, 1.0f);
682}
Here is the caller graph for this function:

◆ isMeParticipantOf()

bool ChatManager::isMeParticipantOf ( const ChatGroupModel & g) const

Definition at line 230 of file ChatManager.cpp.

231{
232 if (!identity_manager)
233 return false;
234 const std::string meUid = identity_manager->myUniqueId();
235 return g.participants.count(meUid) > 0;
236}
Here is the caller graph for this function:

◆ loadCurrent()

bool ChatManager::loadCurrent ( )

Definition at line 190 of file ChatManager.cpp.

191{
192 if (!hasCurrent())
193 return false;
195 if (!std::filesystem::exists(p))
196 {
198 return false;
199 }
200
201 try
202 {
203 std::ifstream is(p, std::ios::binary);
204 is.seekg(0, std::ios::end);
205 auto sz = is.tellg();
206 is.seekg(0, std::ios::beg);
207 std::vector<uint8_t> buf((size_t)sz);
208 if (sz > 0)
209 is.read((char*)buf.data(), sz);
210 return loadLog(buf);
211 }
212 catch (...)
213 {
215 return false;
216 }
217}
std::filesystem::path chatFilePathFor(uint64_t tableId, const std::string &name) const
std::string currentTableName_
Definition ChatManager.h:91
void ensureGeneral()
bool loadLog(const std::vector< uint8_t > &buf)
Here is the call graph for this function:
Here is the caller graph for this function:

◆ loadLog()

bool ChatManager::loadLog ( const std::vector< uint8_t > & buf)
private

Definition at line 114 of file ChatManager.cpp.

115{
116 size_t off = 0;
117 auto magic = Serializer::deserializeString(buf, off);
118 if (magic != "RUNIC-CHAT-GROUPS")
119 return false;
120
121 int version = Serializer::deserializeInt(buf, off);
122
123 groups_.clear();
124 int count = Serializer::deserializeInt(buf, off);
125 for (int i = 0; i < count; ++i)
126 {
128 g.id = Serializer::deserializeUInt64(buf, off);
129 g.name = Serializer::deserializeString(buf, off);
130
131 if (version >= 2)
132 g.ownerUniqueId = Serializer::deserializeString(buf, off);
133 else
134 {
135 // v1 had ownerPeerId; read it and keep as ownerUniqueId (best-effort: it used to be peerId)
136 const std::string legacyOwner = Serializer::deserializeString(buf, off);
137 g.ownerUniqueId = legacyOwner; // you can map through IdentityManager if you persisted mapping
138 }
139
140 int pc = Serializer::deserializeInt(buf, off);
141 for (int k = 0; k < pc; ++k)
142 g.participants.insert(Serializer::deserializeString(buf, off));
143
144 int mc = Serializer::deserializeInt(buf, off);
145 for (int m = 0; m < mc; ++m)
146 {
149
150 if (version >= 2)
151 msg.senderUniqueId = Serializer::deserializeString(buf, off);
152 else
153 msg.senderUniqueId.clear();
154
155 msg.username = Serializer::deserializeString(buf, off);
156 msg.content = Serializer::deserializeString(buf, off);
157 msg.ts = (double)Serializer::deserializeUInt64(buf, off);
158 g.messages.push_back(std::move(msg));
159 }
160
161 g.unread = (uint32_t)Serializer::deserializeInt(buf, off);
162 groups_.emplace(g.id, std::move(g));
163 }
164
166 if (groups_.find(activeGroupId_) == groups_.end())
168 return true;
169}
static int deserializeInt(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:367
static std::string deserializeString(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:388
static uint64_t deserializeUInt64(const std::vector< unsigned char > &buffer, size_t &offset)
Definition Serializer.h:360
Here is the call graph for this function:
Here is the caller graph for this function:

◆ makeGroupIdFromName()

uint64_t ChatManager::makeGroupIdFromName ( const std::string & name) const
private

Definition at line 220 of file ChatManager.cpp.

221{
222 std::hash<std::string> H;
223 uint64_t id = H(name);
224 if (id == 0 || id == generalGroupId_)
225 id ^= 0x9E3779B97F4A7C15ull;
226 return id;
227}
Here is the caller graph for this function:

◆ markGroupRead()

void ChatManager::markGroupRead ( uint64_t groupId)
private

Definition at line 1627 of file ChatManager.cpp.

1628{
1629 if (auto* g = getGroup(groupId))
1630 g->unread = 0;
1631}
Here is the call graph for this function:
Here is the caller graph for this function:

◆ nowSec()

double ChatManager::nowSec ( )
staticprivate

Definition at line 16 of file ChatManager.cpp.

17{
18 return (double)std::time(nullptr);
19}
Here is the caller graph for this function:

◆ pushMessageLocal()

void ChatManager::pushMessageLocal ( uint64_t groupId,
const std::string & fromPeer,
const std::string & username,
const std::string & text,
double ts,
bool incoming )

Definition at line 381 of file ChatManager.cpp.

387{
389 msg.kind = classifyMessage(text);
390 msg.username = username;
391 msg.content = text;
392 msg.ts = ts;
393
394 if (incoming)
395 {
396 // map from transport peer -> stable uniqueId (best effort)
397 msg.senderUniqueId = identity_manager ? identity_manager->uniqueForPeer(fromPeer).value_or("Player") : std::string{};
398 }
399 else
400 {
401 // local user’s unique id
402 msg.senderUniqueId = identity_manager ? identity_manager->myUniqueId() : std::string{};
403 }
404
405 auto* g = getGroup(groupId);
406 if (!g)
407 {
408 ChatGroupModel stub;
409 stub.id = groupId;
410 stub.name = "(pending?)";
411 groups_.emplace(groupId, std::move(stub));
412 g = getGroup(groupId);
413 }
414
415 g->messages.push_back(std::move(msg));
416
417 const bool isActive = (activeGroupId_ == groupId);
418 if (!isActive || !chatWindowFocused_ || !followScroll_)
419 g->unread = std::min<uint32_t>(g->unread + 1, 999u);
420
422 "LOCAL append gid=" + std::to_string(groupId) + " user=" + username +
423 " text=\"" + text + "\"");
424}
Here is the call graph for this function:
Here is the caller graph for this function:

◆ readGroupsFromSnapshotGT()

void ChatManager::readGroupsFromSnapshotGT ( const std::vector< unsigned char > & buf,
size_t & off )

◆ render()

void ChatManager::render ( )

Definition at line 604 of file ChatManager.cpp.

605{
606 if (!hasCurrent())
607 return;
608
609 ImGui::Begin("ChatWindow");
610 chatWindowFocused_ = ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_RootWindow);
611
612 const float leftWmin = 170.0f;
613
614 const float fullW = ImGui::GetContentRegionAvail().x;
615 const float fullH = ImGui::GetContentRegionAvail().y;
616
617 ImGui::BeginChild("Left", ImVec2(leftWidth_, 0), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize);
619 ImGui::EndChild();
620
621 ImGui::SameLine();
622 ImGui::InvisibleButton("##splitter", ImVec2(6, fullH));
623 if (ImGui::IsItemActive())
624 {
625 leftWidth_ += ImGui::GetIO().MouseDelta.x;
626 leftWidth_ = std::clamp(leftWidth_, leftWmin, fullW - 240.0f);
627 }
628 ImGui::SameLine();
629
630 ImGui::BeginChild("Right", ImVec2(0, 0), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize);
632 ImGui::EndChild();
633
634 ImGui::End();
635}
void renderRightPanel(float leftPanelWidth)
float leftWidth_
void renderLeftPanel(float width)
Here is the call graph for this function:

◆ renderColoredUsername()

void ChatManager::renderColoredUsername ( const std::string & name) const
private

Definition at line 712 of file ChatManager.cpp.

713{
714 const ImU32 col = getUsernameColor(name);
715 ImGui::PushStyleColor(ImGuiCol_Text, col);
716 ImGui::TextUnformatted(name.c_str());
717 ImGui::PopStyleColor();
718}
ImU32 getUsernameColor(const std::string &name) const
Here is the call graph for this function:
Here is the caller graph for this function:

◆ renderCreateGroupPopup()

void ChatManager::renderCreateGroupPopup ( )
private

Definition at line 1109 of file ChatManager.cpp.

1110{
1111 if (ImGui::BeginPopupModal("CreateGroup", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1112 {
1113 ImGui::TextUnformatted("Group name (unique):");
1114 ImGui::InputText("##gname", newGroupName_.data(), (int)newGroupName_.size());
1116 // list peers
1117 ImGui::Separator();
1118 ImGui::TextUnformatted("Participants:");
1119 if (auto nm = network_.lock())
1120 {
1121 for (auto& [pid, link] : nm->getPeers())
1122 {
1123 // 🔸 map peerId -> uniqueId for selection storage
1124 std::string uid = identity_manager
1125 ? identity_manager->uniqueForPeer(pid).value_or(std::string{})
1126 : std::string{};
1127
1128 if (uid.empty())
1129 continue; // not bound yet; skip or show disabled if you prefer
1130
1131 bool checked = newGroupSel_.count(uid) > 0; // 🔸 store UNIQUE IDs
1132 const std::string dn = nm->displayNameForPeer(pid);
1133 std::string label = dn.empty() ? pid : (dn + " (" + pid + ")");
1134 if (ImGui::Checkbox(label.c_str(), &checked))
1135 {
1136 if (checked)
1137 newGroupSel_.insert(uid); // 🔸 insert UID
1138 else
1139 newGroupSel_.erase(uid); // 🔸 erase UID
1140 }
1141 }
1142 }
1143
1144 // ensure name unique
1145 bool nameTaken = false;
1146 std::string desired = newGroupName_.data();
1147 if (!desired.empty())
1148 {
1149 for (auto& [id, g] : groups_)
1150 if (g.name == desired)
1151 {
1152 nameTaken = true;
1153 break;
1154 }
1155 }
1156
1157 ImGui::Separator();
1158 ImGui::BeginDisabled(desired.empty() || nameTaken);
1159 if (ImGui::Button("Create"))
1160 {
1162 g.name = desired;
1163 g.id = makeGroupIdFromName(g.name);
1164 g.participants = newGroupSel_; // 🔸 UNIQUE IDs
1165
1166 if (identity_manager)
1167 {
1168 g.ownerUniqueId = identity_manager->myUniqueId();
1169 if (!g.ownerUniqueId.empty())
1170 g.participants.insert(g.ownerUniqueId); // 🔸 owner always included (solo OK)
1171 }
1172
1173 groups_.emplace(g.id, g);
1174 emitGroupCreate(g);
1175
1176 activeGroupId_ = g.id;
1177 markGroupRead(g.id);
1178 focusInput_ = true;
1179 followScroll_ = true;
1180
1181 newGroupSel_.clear();
1182 newGroupName_.fill('\0');
1183 ImGui::CloseCurrentPopup();
1184 }
1185 ImGui::EndDisabled();
1186
1187 if (nameTaken)
1188 ImGui::TextColored(ImVec4(1, 0.4f, 0.2f, 1), "A group with that name already exists.");
1189
1190 ImGui::SameLine();
1191 if (ImGui::Button("Cancel"))
1192 {
1193 newGroupSel_.clear();
1194 newGroupName_.fill('\0');
1195 ImGui::CloseCurrentPopup();
1196 }
1197 ImGui::EndPopup();
1198 }
1199}
uint64_t makeGroupIdFromName(const std::string &name) const
std::set< std::string > newGroupSel_
void emitGroupCreate(const ChatGroupModel &g)
std::array< char, 128 > newGroupName_
void markGroupRead(uint64_t groupId)
void TrackThisInput()
std::string name
Definition ChatManager.h:41
Here is the call graph for this function:
Here is the caller graph for this function:

◆ renderDeleteGroupPopup()

void ChatManager::renderDeleteGroupPopup ( )
private

Definition at line 1448 of file ChatManager.cpp.

1449{
1450 if (ImGui::BeginPopupModal("DeleteGroup", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1451 {
1452 ImGui::TextUnformatted("Manage groups you participate in (General cannot be deleted/left).");
1453 ImGui::Separator();
1454
1455 // Who am I and am I GM?
1456 std::string meUid = identity_manager ? identity_manager->myUniqueId() : "";
1457 bool iAmGM = false;
1458 if (auto nm = network_.lock())
1459 {
1460 // Use the accessor you actually have: getGMId() / gmId() / GMId() — pick the right one.
1461 // If your NetworkManager exposes a different name, change here only.
1462 const std::string gmUid = nm->getGMId(); // <-- adjust if needed in your codebase
1463 iAmGM = (!gmUid.empty() && !meUid.empty() && gmUid == meUid);
1464 }
1465
1466 for (auto it = groups_.begin(); it != groups_.end(); /* ++ inside */)
1467 {
1468 const uint64_t id = it->first;
1469 ChatGroupModel& g = it->second;
1470
1471 // Skip General
1472 if (id == generalGroupId_)
1473 {
1474 ++it;
1475 continue;
1476 }
1477
1478 // Only show groups where I’m currently a participant (so I can Leave/Delete)
1479 const bool iParticipate = isMeParticipantOf(g);
1480
1481 // Compute permissions
1482 const bool isOwner = (!g.ownerUniqueId.empty() && g.ownerUniqueId == meUid);
1483 const bool canDelete = isOwner;
1484 const bool canLeave = (iParticipate && !isOwner); // owners cannot leave by rule
1485
1486 ImGui::PushID((int)id);
1487
1488 // Row label
1489 ImGui::TextUnformatted(g.name.c_str());
1490 ImGui::SameLine();
1491
1492 // DELETE button
1493 ImGui::BeginDisabled(!canDelete);
1494 if (ImGui::Button("Delete"))
1495 {
1496 // Emit delete to participants
1497 emitGroupDelete(id);
1498
1499 // Remove locally
1500 if (activeGroupId_ == id)
1502 it = groups_.erase(it);
1503
1504 ImGui::PopID();
1505 continue; // skip ++it
1506 }
1507 ImGui::EndDisabled();
1508
1509 ImGui::SameLine();
1510
1511 // LEAVE button
1512 ImGui::BeginDisabled(!canLeave);
1513 if (ImGui::Button("Leave"))
1514 {
1515 emitGroupLeave(id);
1516
1517 // If I’m no longer a participant, optionally hide it locally right away
1518 if (!isMeParticipantOf(g))
1519 {
1520 if (activeGroupId_ == id)
1522 it = groups_.erase(it);
1523 ImGui::PopID();
1524 continue; // skip ++it
1525 }
1526 }
1527 ImGui::EndDisabled();
1528
1529 // Helper hints
1530 if (!iParticipate)
1531 {
1532 ImGui::SameLine();
1533 ImGui::TextDisabled("(not a participant)");
1534 }
1535 else if (isOwner)
1536 {
1537 ImGui::SameLine();
1538 ImGui::TextDisabled("(owner)");
1539 }
1540 else if (iAmGM)
1541 {
1542 ImGui::SameLine();
1543 ImGui::TextDisabled("(GM)");
1544 }
1545
1546 ImGui::PopID();
1547 ++it;
1548 }
1549
1550 ImGui::Separator();
1551 if (ImGui::Button("Close"))
1552 ImGui::CloseCurrentPopup();
1553 ImGui::EndPopup();
1554 }
1555}
void emitGroupLeave(uint64_t groupId)
bool isMeParticipantOf(const ChatGroupModel &g) const
void emitGroupDelete(uint64_t groupId)
Here is the call graph for this function:
Here is the caller graph for this function:

◆ renderDicePopup()

void ChatManager::renderDicePopup ( )
private

Definition at line 1557 of file ChatManager.cpp.

1558{
1559 if (ImGui::BeginPopup("DiceRoller"))
1560 {
1561 ImGui::TextUnformatted("Dice Roller");
1562 ImGui::Separator();
1563
1564 ImGui::InputInt("Number", &diceN_);
1566 ImGui::InputInt("Sides", &diceSides_);
1568 ImGui::InputInt("Modifier", &diceMod_);
1570 ImGui::Checkbox("Apply modifier per die", &diceModPerDie_);
1571
1572 if (ImGui::Button("d4"))
1573 diceSides_ = 4;
1574 ImGui::SameLine();
1575 if (ImGui::Button("d6"))
1576 diceSides_ = 6;
1577 ImGui::SameLine();
1578 if (ImGui::Button("d8"))
1579 diceSides_ = 8;
1580 ImGui::SameLine();
1581 if (ImGui::Button("d10"))
1582 diceSides_ = 10;
1583 ImGui::SameLine();
1584 if (ImGui::Button("d12"))
1585 diceSides_ = 12;
1586 ImGui::SameLine();
1587 if (ImGui::Button("d20"))
1588 diceSides_ = 20;
1589
1590 ImGui::Separator();
1591 if (ImGui::Button("Roll"))
1592 {
1594 //int N = std::max(1, diceN_);
1595 //int M = std::max(1, diceSides_);
1596 //int K = diceModPerDie_ ? (diceMod_ * N) : diceMod_;
1597 //std::string text = "/roll " + std::to_string(N) + "d" + std::to_string(M) +
1598 // (K > 0 ? "+" + std::to_string(K) : (K < 0 ? std::string("-") + std::to_string(-K) : ""));
1600 //auto nm = network_.lock();
1601 //std::string uname = nm ? nm->getMyUsername() : "me";
1602 //if (uname.empty())
1603 // uname = "me";
1604 // build slash command
1605 int mod = diceMod_;
1606 int N = std::max(1, diceN_);
1607 int M = std::max(1, diceSides_);
1608 // If per-die modifier, convert into +K where K = modifier * N for the final text.
1609 int K = diceModPerDie_ ? (mod * N) : mod;
1610 std::string cmd = "/roll " + std::to_string(N) + "d" + std::to_string(M);
1611 if (K > 0)
1612 cmd += "+" + std::to_string(K);
1613 else if (K < 0)
1614 cmd += "-" + std::to_string(-K);
1616
1617 ImGui::CloseCurrentPopup();
1618 }
1619 ImGui::SameLine();
1620 if (ImGui::Button("Cancel"))
1621 ImGui::CloseCurrentPopup();
1622
1623 ImGui::EndPopup();
1624 }
1625}
void tryHandleSlashCommand(uint64_t threadId, const std::string &input)
bool diceModPerDie_
Here is the call graph for this function:
Here is the caller graph for this function:

◆ renderEditGroupPopup()

void ChatManager::renderEditGroupPopup ( )
private

Definition at line 1298 of file ChatManager.cpp.

1299{
1300 if (ImGui::BeginPopupModal("EditGroup", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
1301 {
1303 if (!g)
1304 {
1305 ImGui::TextDisabled("Group not found.");
1306 if (ImGui::Button("Close"))
1307 ImGui::CloseCurrentPopup();
1308 ImGui::EndPopup();
1309 return;
1310 }
1311
1312 // Optional: only owner can edit
1313 auto nm = network_.lock();
1314 const std::string meUid = identity_manager ? identity_manager->myUniqueId() : "";
1315 const bool ownerOnly = true;
1316 const bool canEdit = !ownerOnly || (!g->ownerUniqueId.empty() && g->ownerUniqueId == meUid);
1317 if (!canEdit)
1318 ImGui::TextColored(ImVec4(1, 0.5f, 0.3f, 1), "Only the owner can edit this group.");
1319
1320 ImGui::Separator();
1321 ImGui::TextUnformatted("Group name:");
1322 ImGui::BeginDisabled(!canEdit);
1323 ImGui::InputText("##edit_gname", editGroupName_.data(), (int)editGroupName_.size());
1325 ImGui::EndDisabled();
1326
1327 // Participants list (pre-checked)
1328 ImGui::Separator();
1329 ImGui::TextUnformatted("Participants:");
1330 if (nm)
1331 {
1332 ImGui::BeginDisabled(!canEdit);
1333 for (auto& [pid, link] : nm->getPeers())
1334 {
1335 // 🔸 map peerId -> uniqueId for selection storage
1336 std::string uid = identity_manager
1337 ? identity_manager->uniqueForPeer(pid).value_or(std::string{})
1338 : std::string{};
1339
1340 if (uid.empty())
1341 continue;
1342
1343 bool checked = editGroupSel_.count(uid) > 0; // 🔸 use UID
1344 const std::string dn = nm->displayNameForPeer(pid);
1345 std::string label = dn.empty() ? pid : (dn + " (" + pid + ")");
1346 if (ImGui::Checkbox(label.c_str(), &checked))
1347 {
1348 if (checked)
1349 editGroupSel_.insert(uid); // 🔸 insert UID
1350 else
1351 editGroupSel_.erase(uid); // 🔸 erase UID
1352 }
1353 }
1354 ImGui::EndDisabled();
1355 }
1356
1357 // Validate (unique name unless unchanged)
1358 bool nameTaken = false;
1359 const std::string desired = editGroupName_.data();
1360 if (desired.empty() == false && desired != g->name)
1361 {
1362 for (auto& [id, gg] : groups_)
1363 if (gg.name == desired)
1364 {
1365 nameTaken = true;
1366 break;
1367 }
1368 }
1369 if (nameTaken)
1370 ImGui::TextColored(ImVec4(1, 0.4f, 0.2f, 1), "A group with that name already exists.");
1371
1372 ImGui::Separator();
1373 ImGui::BeginDisabled(!canEdit || desired.empty() || nameTaken);
1374 if (ImGui::Button("Update", ImVec2(120, 0)))
1375 {
1376 // Apply local changes
1377 g->name = desired;
1378 g->participants = editGroupSel_; // 🔸 UNIQUE IDs
1379
1380 // Broadcast update
1381 emitGroupUpdate(*g);
1382
1383 // nice UX
1384 activeGroupId_ = g->id;
1385 markGroupRead(g->id);
1386 focusInput_ = true;
1387 followScroll_ = true;
1388
1389 ImGui::CloseCurrentPopup();
1390 }
1391 ImGui::EndDisabled();
1392
1393 ImGui::SameLine();
1394 if (ImGui::Button("Close", ImVec2(120, 0)))
1395 ImGui::CloseCurrentPopup();
1396
1397 ImGui::EndPopup();
1398 }
1399}
std::array< char, 128 > editGroupName_
std::set< std::string > editGroupSel_
void emitGroupUpdate(const ChatGroupModel &g)
uint64_t editGroupId_
Here is the call graph for this function:
Here is the caller graph for this function:

◆ renderLeftPanel()

void ChatManager::renderLeftPanel ( float width)
private

Definition at line 803 of file ChatManager.cpp.

804{
805 // Top actions
806 if (ImGui::Button("New Group"))
807 openCreatePopup_ = true;
808 ImGui::SameLine();
809 if (ImGui::Button("Delete Group"))
810 openDeletePopup_ = true;
811
812 // Enable edit only when there is a non-General active group
813 bool canEdit = (activeGroupId_ != 0 && activeGroupId_ != generalGroupId_);
814 ImGui::BeginDisabled(!canEdit);
815 if (ImGui::Button("Edit Group"))
816 {
817 // seed edit state from the active group
818 if (auto* g = getGroup(activeGroupId_))
819 {
820 editGroupId_ = g->id;
821 editGroupSel_ = g->participants;
822 std::fill(editGroupName_.begin(), editGroupName_.end(), '\0');
823 std::snprintf(editGroupName_.data(), (int)editGroupName_.size(), "%s", g->name.c_str());
824 openEditPopup_ = true;
825 }
826 }
827
828 ImGui::EndDisabled();
829
830 ImGui::Separator();
831 ImGui::TextUnformatted("Chat Groups");
832 ImGui::Separator();
833
834 // Sorted by name; General at top
835 std::vector<uint64_t> order;
836 order.reserve(groups_.size());
837 for (auto& [id, _] : groups_)
838 order.push_back(id);
839 std::sort(order.begin(), order.end(), [&](uint64_t a, uint64_t b)
840 {
841 if (a == generalGroupId_) return true;
842 if (b == generalGroupId_) return false;
843 return groups_[a].name < groups_[b].name; });
844
845 for (auto id : order)
846 {
847 auto& g = groups_[id];
848 const bool selected = (activeGroupId_ == id);
849
850 ImGui::PushID((int)id);
851 if (ImGui::Selectable(g.name.c_str(), selected, ImGuiSelectableFlags_SpanAllColumns, ImVec2(0, 0)))
852 {
853 activeGroupId_ = id;
854 markGroupRead(id);
855 focusInput_ = true;
856 followScroll_ = true;
857 }
858
859 if (g.unread > 0)
860 {
861 ImVec2 min = ImGui::GetItemRectMin();
862 ImVec2 max = ImGui::GetItemRectMax();
863 ImVec2 size(max.x - min.x, max.y - min.y);
864
865 char buf[16];
866 if (g.unread < 100)
867 std::snprintf(buf, sizeof(buf), "%u", g.unread);
868 else
869 std::snprintf(buf, sizeof(buf), "99+");
870
871 ImVec2 txtSize = ImGui::CalcTextSize(buf);
872 const float padX = 6.f, padY = 3.f;
873 const float radius = (txtSize.y + padY * 2.f) * 0.5f;
874 const float marginRight = 8.f;
875 ImVec2 center(max.x - marginRight - radius, min.y + size.y * 0.5f);
876
877 auto* dl = ImGui::GetWindowDrawList();
878 ImU32 bg = ImGui::GetColorU32(ImVec4(0.10f, 0.70f, 0.30f, 1.0f));
879 ImU32 fg = ImGui::GetColorU32(ImVec4(1, 1, 1, 1));
880
881 ImVec2 badgeMin(center.x - std::max(radius, txtSize.x * 0.5f + padX), center.y - radius);
882 ImVec2 badgeMax(center.x + std::max(radius, txtSize.x * 0.5f + padX), center.y + radius);
883 dl->AddRectFilled(badgeMin, badgeMax, bg, radius);
884 ImVec2 textPos(center.x - txtSize.x * 0.5f, center.y - txtSize.y * 0.5f);
885 dl->AddText(textPos, fg, buf);
886 }
887 ImGui::PopID();
888 }
889
891 {
892 openCreatePopup_ = false;
893 ImGui::OpenPopup("CreateGroup");
894 }
896
898 {
899 openDeletePopup_ = false;
900 ImGui::OpenPopup("DeleteGroup");
901 }
903
904 if (openEditPopup_)
905 {
906 openEditPopup_ = false;
907 ImGui::OpenPopup("EditGroup");
908 }
910}
void renderEditGroupPopup()
void renderCreateGroupPopup()
bool openEditPopup_
bool openCreatePopup_
void renderDeleteGroupPopup()
bool openDeletePopup_
Here is the call graph for this function:
Here is the caller graph for this function:

◆ renderPlainMessage()

void ChatManager::renderPlainMessage ( const std::string & text) const
private

Definition at line 720 of file ChatManager.cpp.

721{
722 ImGui::TextWrapped("%s", text.c_str());
723}
Here is the caller graph for this function:

◆ renderRightPanel()

void ChatManager::renderRightPanel ( float leftPanelWidth)
private

Definition at line 912 of file ChatManager.cpp.

913{
914 auto* g = getGroup(activeGroupId_);
915 const float footerRowH = ImGui::GetFrameHeightWithSpacing() * 2.0f;
916
917 ImVec2 avail = ImGui::GetContentRegionAvail();
918 ImGui::BeginChild("Messages", ImVec2(0, avail.y - footerRowH), true, ImGuiWindowFlags_AlwaysVerticalScrollbar);
919 if (g)
920 {
921 if (ImGui::GetScrollY() < ImGui::GetScrollMaxY() - 1.0f)
922 followScroll_ = false;
923
924 for (auto& m : g->messages)
925 {
926 renderColoredUsername(m.username);
927
928 // separator
929 ImGui::SameLine(0.0f, 6.0f);
930 ImGui::TextUnformatted("-");
931 ImGui::SameLine(0.0f, 6.0f);
932
933 // message (wrapped). Put it in a child region so the wrap can use full width if you want,
934 // but simplest is to just call TextWrapped now:
935 // ensure we start from current cursor X (SameLine set it)
936 ImGui::PushTextWrapPos(0);
937 renderPlainMessage(m.content);
938 ImGui::PopTextWrapPos();
939 }
940 if (followScroll_)
941 ImGui::SetScrollHereY(1.0f);
942 if (jumpToBottom_)
943 {
944 ImGui::SetScrollHereY(1.0f);
945 jumpToBottom_ = false;
946 }
947 }
948 ImGui::EndChild();
949
950 // Footer input
951 ImGui::BeginChild("Footer", ImVec2(0, 0), false, ImGuiWindowFlags_AlwaysAutoResize);
952
953 if (focusInput_)
954 {
955 ImGui::SetKeyboardFocusHere();
956 focusInput_ = false;
957 }
958
959 ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AllowTabInput;
960 if (ImGui::InputText("##chat_input", input_.data(), (int)input_.size(), flags))
961 {
962 std::string text(input_.data());
963 if (!text.empty() && g)
964 {
965 const uint64_t ts = (uint64_t)nowSec();
966 auto nm = network_.lock();
967 auto uname = nm ? nm->getMyUsername() : std::string("me");
968 if (uname.empty())
969 uname = "me";
970
971 if (!text.empty() && text[0] == '/')
972 {
974 }
975 else
976 {
977 emitChatMessageFrame(g->id, uname, text, ts);
978 pushMessageLocal(g->id, "me", uname, text, (double)ts, false);
979 }
980
981 input_.fill('\0');
982 followScroll_ = true;
983 focusInput_ = true;
984 markGroupRead(g->id);
985 }
986 }
988 ImGui::SameLine();
989 if (ImGui::Button("Send") && g)
990 {
991 std::string text(input_.data());
992 if (!text.empty())
993 {
994 const uint64_t ts = (uint64_t)nowSec();
995 auto nm = network_.lock();
996 auto uname = nm ? nm->getMyUsername() : std::string("me");
997 if (uname.empty())
998 uname = "me";
999
1000 emitChatMessageFrame(g->id, uname, text, ts);
1001 pushMessageLocal(g->id, "me", uname, text, (double)ts, false);
1002
1003 input_.fill('\0');
1004 followScroll_ = true;
1005 markGroupRead(g->id);
1006 }
1007 }
1008
1009 ImGui::BeginDisabled(followScroll_);
1010 if (ImGui::Button("Go to bottom"))
1011 {
1012 jumpToBottom_ = true;
1013 }
1014 ImGui::EndDisabled();
1015 ImGui::SameLine();
1016 if (ImGui::Button("Roll Dice"))
1017 {
1018 openDicePopup_ = true;
1019 }
1020 if (openDicePopup_)
1021 {
1022 openDicePopup_ = false;
1023 ImGui::OpenPopup("DiceRoller");
1024 }
1026
1027 ImGui::EndChild();
1028}
void renderPlainMessage(const std::string &text) const
void pushMessageLocal(uint64_t groupId, const std::string &fromPeer, const std::string &username, const std::string &text, double ts, bool incoming)
std::array< char, 512 > input_
static double nowSec()
void renderDicePopup()
void emitChatMessageFrame(uint64_t groupId, const std::string &username, const std::string &text, uint64_t ts)
void renderColoredUsername(const std::string &name) const
bool jumpToBottom_
bool openDicePopup_
Here is the call graph for this function:
Here is the caller graph for this function:

◆ replaceUsernameForUnique()

void ChatManager::replaceUsernameForUnique ( const std::string & uniqueId,
const std::string & newUsername )

Definition at line 566 of file ChatManager.cpp.

568{
569 // update cached labels in message history for this author
570 for (auto& [gid, g] : groups_)
571 {
572 // optional: if you show "owner" anywhere:
573 if (g.ownerUniqueId == uniqueId)
574 {
575 // no change to g.name; but if you render "owned by X", use identity_ at draw time.
576 }
577
578 for (auto& m : g.messages)
579 {
580 if (m.senderUniqueId == uniqueId)
581 m.username = newUsername; // cached label shown in UI
582 }
583 }
584}

◆ resolvePeerIdsForParticipants()

std::set< std::string > ChatManager::resolvePeerIdsForParticipants ( const std::set< std::string > & participantUids) const

Definition at line 549 of file ChatManager.cpp.

550{
551 std::set<std::string> out;
552 auto nm = network_.lock();
553 if (!nm || !identity_manager)
554 return out;
555
556 for (const auto& uid : participantUids)
557 {
558 if (auto pid = identity_manager->peerForUnique(uid); pid && !pid->empty())
559 {
560 out.insert(*pid);
561 }
562 }
563 return out;
564}
Here is the caller graph for this function:

◆ saveCurrent()

bool ChatManager::saveCurrent ( )

Definition at line 171 of file ChatManager.cpp.

172{
173 if (!hasCurrent())
174 return false;
175 try
176 {
177 std::vector<uint8_t> buf;
178 saveLog(buf);
180 std::ofstream os(p, std::ios::binary | std::ios::trunc);
181 os.write((const char*)buf.data(), (std::streamsize)buf.size());
182 return true;
183 }
184 catch (...)
185 {
186 return false;
187 }
188}
bool saveLog(std::vector< uint8_t > &buf) const
Here is the call graph for this function:
Here is the caller graph for this function:

◆ saveLog()

bool ChatManager::saveLog ( std::vector< uint8_t > & buf) const
private

Definition at line 85 of file ChatManager.cpp.

86{
87 Serializer::serializeString(buf, "RUNIC-CHAT-GROUPS");
88 Serializer::serializeInt(buf, 2); // <— version 2
89
90 Serializer::serializeInt(buf, (int)groups_.size());
91 for (auto& [id, g] : groups_)
92 {
94 Serializer::serializeString(buf, g.name);
95 Serializer::serializeString(buf, g.ownerUniqueId); // <— changed
96
97 Serializer::serializeInt(buf, (int)g.participants.size());
98 for (auto& p : g.participants)
99 Serializer::serializeString(buf, p); // your set contents (uniqueIds recommended)
100
101 Serializer::serializeInt(buf, (int)g.messages.size());
102 for (auto& m : g.messages)
103 {
104 Serializer::serializeInt(buf, (int)m.kind);
105 Serializer::serializeString(buf, m.senderUniqueId); // <— NEW in v2
106 Serializer::serializeString(buf, m.username);
107 Serializer::serializeString(buf, m.content);
108 Serializer::serializeUInt64(buf, (uint64_t)m.ts);
109 }
110 Serializer::serializeInt(buf, (int)g.unread);
111 }
112 return true;
113}
static void serializeUInt64(std::vector< unsigned char > &buffer, uint64_t value)
Definition Serializer.h:331
static void serializeString(std::vector< unsigned char > &buffer, const std::string &str)
Definition Serializer.h:318
static void serializeInt(std::vector< unsigned char > &buffer, int value)
Definition Serializer.h:302
Here is the call graph for this function:
Here is the caller graph for this function:

◆ setActiveGameTable()

void ChatManager::setActiveGameTable ( uint64_t tableId,
const std::string & gameTableName )

Definition at line 48 of file ChatManager.cpp.

49{
50 if (currentTableId_ != 0)
52 currentTableId_ = tableId;
53 currentTableName_ = gameTableName;
54
55 groups_.clear();
57
58 loadCurrent(); // will merge/overwrite groups_ from disk
59
61 focusInput_ = true;
62 followScroll_ = true;
63}
bool saveCurrent()
bool loadCurrent()
Here is the call graph for this function:

◆ setNetwork()

void ChatManager::setNetwork ( std::weak_ptr< NetworkManager > nm)

Definition at line 42 of file ChatManager.cpp.

43{
44 network_ = std::move(nm);
45}

◆ tryHandleSlashCommand()

void ChatManager::tryHandleSlashCommand ( uint64_t threadId,
const std::string & input )

Definition at line 725 of file ChatManager.cpp.

726{
727 // /roll NdM(+K) e.g., /roll 4d6+2 or /roll 1d20-5
728 if (input.rfind("/roll", 0) != 0)
729 return;
730
731 auto nm = network_.lock();
732 if (!nm)
733 return;
734 auto uname = nm->getMyUsername();
735 if (uname.empty())
736 uname = "me";
737
738 auto parseInt = [](const std::string& s, size_t& i) -> int
739 {
740 int v = 0;
741 bool any = false;
742 while (i < s.size() && std::isdigit((unsigned char)s[i]))
743 {
744 any = true;
745 v = v * 10 + (s[i] - '0');
746 ++i;
747 }
748 return any ? v : -1;
749 };
750
751 size_t i = 5;
752 while (i < input.size() && std::isspace((unsigned char)input[i]))
753 ++i;
754
755 int N = parseInt(input, i);
756 if (i >= input.size() || input[i] != 'd' || N <= 0)
757 { /*invalid*/
758 return;
759 }
760 ++i;
761 int M = parseInt(input, i);
762 if (M <= 0)
763 return;
764
765 int K = 0;
766 if (i < input.size() && (input[i] == '+' || input[i] == '-'))
767 {
768 bool neg = (input[i] == '-');
769 ++i;
770 int Z = parseInt(input, i);
771 if (Z > 0)
772 K = neg ? -Z : Z;
773 }
774
775 std::mt19937 rng((uint32_t)std::random_device{}());
776 std::uniform_int_distribution<int> dist(1, std::max(1, M));
777
778 int total = 0;
779 std::string rolls;
780 for (int r = 0; r < N; ++r)
781 {
782 int die = dist(rng);
783 total += die;
784 if (!rolls.empty())
785 rolls += ", ";
786 rolls += std::to_string(die);
787 }
788
789 // This command outputs as a chat message from "me"
790 const int modApplied = K;
791 const int finalTotal = total + modApplied;
792
793 const std::string text = "rolled " + std::to_string(N) + "d" + std::to_string(M) +
794 (diceModPerDie_ ? " (per-die mod " : " (mod ") +
795 std::to_string(modApplied) + ")" +
796 " => [" + rolls + "] = " + std::to_string(finalTotal);
797
798 const uint64_t ts = (uint64_t)nowSec();
799 emitChatMessageFrame(threadId, uname, text, ts);
800 pushMessageLocal(threadId, "me", uname, text, (double)ts, /*incoming*/ false);
801}
constexpr std::string_view Z
Definition Message.h:260
Here is the call graph for this function:
Here is the caller graph for this function:

◆ writeGroupsToSnapshotGT()

void ChatManager::writeGroupsToSnapshotGT ( std::vector< unsigned char > & buf) const

Member Data Documentation

◆ activeGroupId_

uint64_t ChatManager::activeGroupId_ = generalGroupId_
private

Definition at line 106 of file ChatManager.h.

◆ chatWindowFocused_

bool ChatManager::chatWindowFocused_ = false
private

Definition at line 113 of file ChatManager.h.

◆ currentTableId_

uint64_t ChatManager::currentTableId_ = 0

Definition at line 90 of file ChatManager.h.

◆ currentTableName_

std::string ChatManager::currentTableName_

Definition at line 91 of file ChatManager.h.

◆ diceMod_

int ChatManager::diceMod_ = 0
private

Definition at line 133 of file ChatManager.h.

◆ diceModPerDie_

bool ChatManager::diceModPerDie_ = false
private

Definition at line 134 of file ChatManager.h.

◆ diceN_

int ChatManager::diceN_ = 1
private

Definition at line 131 of file ChatManager.h.

◆ diceSides_

int ChatManager::diceSides_ = 20
private

Definition at line 132 of file ChatManager.h.

◆ editGroupId_

uint64_t ChatManager::editGroupId_ = 0
private

Definition at line 126 of file ChatManager.h.

◆ editGroupName_

std::array<char, 128> ChatManager::editGroupName_ {}
private

Definition at line 127 of file ChatManager.h.

127{};

◆ editGroupSel_

std::set<std::string> ChatManager::editGroupSel_
private

Definition at line 128 of file ChatManager.h.

◆ focusInput_

bool ChatManager::focusInput_ = false
private

Definition at line 110 of file ChatManager.h.

◆ followScroll_

bool ChatManager::followScroll_ = true
private

Definition at line 111 of file ChatManager.h.

◆ generalGroupId_

constexpr uint64_t ChatManager::generalGroupId_ = 1
staticconstexpr

Definition at line 89 of file ChatManager.h.

◆ groups_

std::unordered_map<uint64_t, ChatGroupModel> ChatManager::groups_
private

Definition at line 105 of file ChatManager.h.

◆ identity_manager

std::shared_ptr<IdentityManager> ChatManager::identity_manager
private

Definition at line 102 of file ChatManager.h.

◆ input_

std::array<char, 512> ChatManager::input_ {}
private

Definition at line 109 of file ChatManager.h.

109{};

◆ jumpToBottom_

bool ChatManager::jumpToBottom_ = false
private

Definition at line 112 of file ChatManager.h.

◆ leftWidth_

float ChatManager::leftWidth_ = 170.0f
private

Definition at line 123 of file ChatManager.h.

◆ nameColorCache_

std::unordered_map<std::string, ImU32> ChatManager::nameColorCache_
mutableprivate

Definition at line 143 of file ChatManager.h.

◆ network_

std::weak_ptr<NetworkManager> ChatManager::network_
private

Definition at line 137 of file ChatManager.h.

◆ newGroupName_

std::array<char, 128> ChatManager::newGroupName_ {}
private

Definition at line 121 of file ChatManager.h.

121{};

◆ newGroupSel_

std::set<std::string> ChatManager::newGroupSel_
private

Definition at line 122 of file ChatManager.h.

◆ openCreatePopup_

bool ChatManager::openCreatePopup_ = false
private

Definition at line 116 of file ChatManager.h.

◆ openDeletePopup_

bool ChatManager::openDeletePopup_ = false
private

Definition at line 117 of file ChatManager.h.

◆ openDicePopup_

bool ChatManager::openDicePopup_ = false
private

Definition at line 118 of file ChatManager.h.

◆ openEditPopup_

bool ChatManager::openEditPopup_ = false
private

Definition at line 125 of file ChatManager.h.


The documentation for this class was generated from the following files: