Skip to content

feat(live): defibrillate live mapping - #1027

Open
Coldensjo wants to merge 3 commits into
Open-Tibia-Tools:masterfrom
Coldensjo:master
Open

feat(live): defibrillate live mapping#1027
Coldensjo wants to merge 3 commits into
Open-Tibia-Tools:masterfrom
Coldensjo:master

Conversation

@Coldensjo

Copy link
Copy Markdown
  • Updated LiveClient to include allowCopy flag, controlling copy permissions.
  • Implemented checks in EditorManager to prevent copying if permissions are not granted.
  • Added sendChatMessage functionality to LiveClient and LiveServer for chat message broadcasting.
  • Adjusted UI interactions in LiveLogTab to utilize the new chat message sending mechanism.
  • Updated LiveDialogs to set copy permissions during server connection setup.
  • Incremented __LIVE_NET_VERSION__ to 6 to reflect these changes.
  • Enabled live mapping in menubar.xml

- Updated `LiveClient` to include `allowCopy` flag, controlling copy permissions.
- Implemented checks in `EditorManager` to prevent copying if permissions are not granted.
- Added `sendChatMessage` functionality to `LiveClient` and `LiveServer` for chat message broadcasting.
- Adjusted UI interactions in `LiveLogTab` to utilize the new chat message sending mechanism.
- Updated `LiveDialogs` to set copy permissions during server connection setup.
- Incremented `__LIVE_NET_VERSION__` to 6 to reflect these changes.
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the live mapping feature by adding granular control over copy permissions and implementing a functional chat system. These changes improve security and communication during collaborative mapping sessions while ensuring network compatibility through a version update.

Highlights

  • Copy Permissions: Introduced an allowCopy flag to LiveClient and LiveServer to manage and enforce copy permissions during live mapping sessions.
  • Chat Functionality: Implemented sendChatMessage across the live networking stack, enabling chat message broadcasting and updating the UI to trigger sends on text entry.
  • Live Mapping Activation: Enabled the Live mapping menu in the application interface and incremented the network version to 6.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Enabled Live menu with Host Server, Join Server, and Leave Server options.
    • Added configurable "allow copy & paste between maps" setting when hosting a server.
    • Copy and paste operations now respect server-configured permissions.
    • Chat message input now requires pressing Enter to send, improving user experience.

Walkthrough

This PR extends live collaborative mapping with copy-paste permission control and refactors chat messaging through a polymorphic interface. The server can now restrict copy operations per session, which the client receives during handshake and the editor enforces. Chat messaging is unified via a virtual method in the socket base class, and the live menu is activated in the UI.

Changes

Live Collaboration Enhancements: Copy Permissions and Chat Messaging

Layer / File(s) Summary
Copy-paste permission system
source/live/live_server.h, source/live/live_server.cpp, source/live/live_client.h, source/live/live_client.cpp, source/live/live_peer.cpp, source/editor/managers/editor_manager.cpp, source/ui/live_dialogs.cpp
LiveServer adds allowCopy state with getter/setter; client receives the flag during parseHello and stores it. Editor's DoCut and DoCopy check the client's isAllowCopy() and abort if copying is disallowed. Protocol includes the flag in the server handshake. Host dialog sets the server's permission from a checkbox.
Polymorphic chat messaging interface
source/live/live_socket.h, source/live/live_server.cpp, source/live/live_client.cpp, source/live/live_tab.cpp
LiveSocket declares virtual sendChatMessage with a no-op default. LiveServer and LiveClient override it to route messages appropriately. LiveLogTab::OnChat now uses the polymorphic socket->sendChatMessage, and only triggers on Enter keypress instead of every text change.
Infrastructure and configuration updates
data/menubar.xml, source/app/definitions.h, source/live/live_server.cpp
Live menu is uncommented and activated. Network protocol version __LIVE_NET_VERSION__ is incremented from 5 to 6. Server assigns a unique id to each LivePeer during client acceptance, and updateClientList() guards against null log pointers.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'defibrillate live mapping' is vague and non-descriptive, using idiomatic language that doesn't clearly convey what was actually changed in the changeset. Use a more descriptive title that clearly states the main changes, such as 'feat(live): add copy permissions and chat message functionality' or 'feat(live): implement live mapping permissions and chat features'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description is directly related to the changeset, providing a detailed list of all major modifications across multiple files including allowCopy flag, permission checks, chat functionality, and version increment.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Infer (1.2.0)
source/live/live_client.cpp

source/live/live_client.cpp:18:10: fatal error: 'app/main.h' file not found
18 | #include "app/main.h"
| ^~~~~~~~~~~~
1 error generated.
Error: the following clang command did not run successfully:
/opt/infer-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/bin/clang-18
@/tmp/coderabbit-infer/22cec185459bdb0ead4f1089d7795b8051c0b193-627ce140de794bd3/tmp/clang_command_.tmp.b78563.txt
++Contents of '/tmp/coderabbit-infer/22cec185459bdb0ead4f1089d7795b8051c0b193-627ce140de794bd3/tmp/clang_command_.tmp.b78563.txt':
"-cc1" "-load"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
"-add-plugin" "BiniouASTExporter" "-plugin-arg-BiniouASTExporter" "-"
"-plugin-arg-BiniouASTExporter" "PREPEND_CURRENT_DIR=1"
"-plugin-arg-BiniouASTExporter" "MAX_STRING_SIZE=65535" "-cc1" "-triple"
"x86_64-unknown-linux-gnu" "-emit-obj" "-mrelax-all" "-disable-free"
"

... [truncated 1062 characters] ...

install/lib/clang/18/include"
"-internal-isystem" "/usr/local/include" "-internal-isystem"
"/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/include"
"-internal-externc-isystem" "/usr/include/x86_64-linux-gnu"
"-internal-externc-isystem" "/include" "-internal-externc-isystem"
"/usr/include" "-Wno-ignored-optimization-argument" "-Wno-everything"
"-fdeprecated-macro" "-ferror-limit" "19" "-fgnuc-version=4.2.1"
"-fskip-odr-check-in-gmf" "-fcxx-exceptions" "-fexceptions"
"-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o"
"/tmp/coderabbit-infer/627ce140de794bd3/file.o" "-x" "c++"
"source/live/live_client.cpp" "-O0" "-fno-builtin" "-include"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../lib/clang_wrappers/global_defines.h"
"-Wno-everything"

source/live/live_peer.cpp

source/live/live_peer.cpp:18:10: fatal error: 'app/main.h' file not found
18 | #include "app/main.h"
| ^~~~~~~~~~~~
1 error generated.
Error: the following clang command did not run successfully:
/opt/infer-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/bin/clang-18
@/tmp/coderabbit-infer/22cec185459bdb0ead4f1089d7795b8051c0b193-09211856c725d621/tmp/clang_command_.tmp.cfafd5.txt
++Contents of '/tmp/coderabbit-infer/22cec185459bdb0ead4f1089d7795b8051c0b193-09211856c725d621/tmp/clang_command_.tmp.cfafd5.txt':
"-cc1" "-load"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
"-add-plugin" "BiniouASTExporter" "-plugin-arg-BiniouASTExporter" "-"
"-plugin-arg-BiniouASTExporter" "PREPEND_CURRENT_DIR=1"
"-plugin-arg-BiniouASTExporter" "MAX_STRING_SIZE=65535" "-cc1" "-triple"
"x86_64-unknown-linux-gnu" "-emit-obj" "-mrelax-all" "-disable-free"
"-c

... [truncated 1056 characters] ...

g/install/lib/clang/18/include"
"-internal-isystem" "/usr/local/include" "-internal-isystem"
"/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/include"
"-internal-externc-isystem" "/usr/include/x86_64-linux-gnu"
"-internal-externc-isystem" "/include" "-internal-externc-isystem"
"/usr/include" "-Wno-ignored-optimization-argument" "-Wno-everything"
"-fdeprecated-macro" "-ferror-limit" "19" "-fgnuc-version=4.2.1"
"-fskip-odr-check-in-gmf" "-fcxx-exceptions" "-fexceptions"
"-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o"
"/tmp/coderabbit-infer/09211856c725d621/file.o" "-x" "c++"
"source/live/live_peer.cpp" "-O0" "-fno-builtin" "-include"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../lib/clang_wrappers/global_defines.h"
"-Wno-everything"

source/editor/managers/editor_manager.cpp

source/editor/managers/editor_manager.cpp:5:10: fatal error: 'app/main.h' file not found
5 | #include "app/main.h"
| ^~~~~~~~~~~~
1 error generated.
Error: the following clang command did not run successfully:
/opt/infer-linux-x86_64-v1.2.0/lib/infer/facebook-clang-plugins/clang/install/bin/clang-18
@/tmp/coderabbit-infer/22cec185459bdb0ead4f1089d7795b8051c0b193-c9a73824f826cdf9/tmp/clang_command_.tmp.5fc335.txt
++Contents of '/tmp/coderabbit-infer/22cec185459bdb0ead4f1089d7795b8051c0b193-c9a73824f826cdf9/tmp/clang_command_.tmp.5fc335.txt':
"-cc1" "-load"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../../facebook-clang-plugins/libtooling/build/FacebookClangPlugin.dylib"
"-add-plugin" "BiniouASTExporter" "-plugin-arg-BiniouASTExporter" "-"
"-plugin-arg-BiniouASTExporter" "PREPEND_CURRENT_DIR=1"
"-plugin-arg-BiniouASTExporter" "MAX_STRING_SIZE=65535" "-cc1" "-triple"
"x86_64-unknown-linux-gnu" "-emit-obj" "-mrelax-all" "-disabl

... [truncated 1096 characters] ...

18/include"
"-internal-isystem" "/usr/local/include" "-internal-isystem"
"/usr/lib/gcc/x86_64-linux-gnu/12/../../../../x86_64-linux-gnu/include"
"-internal-externc-isystem" "/usr/include/x86_64-linux-gnu"
"-internal-externc-isystem" "/include" "-internal-externc-isystem"
"/usr/include" "-Wno-ignored-optimization-argument" "-Wno-everything"
"-fdeprecated-macro" "-ferror-limit" "19" "-fgnuc-version=4.2.1"
"-fskip-odr-check-in-gmf" "-fcxx-exceptions" "-fexceptions"
"-D__GCC_HAVE_DWARF2_CFI_ASM=1" "-o"
"/tmp/coderabbit-infer/c9a73824f826cdf9/file.o" "-x" "c++"
"source/editor/managers/editor_manager.cpp" "-O0" "-fno-builtin"
"-include"
"/opt/infer-linux-x86_64-v1.2.0/lib/infer/infer/bin/../lib/clang_wrappers/global_defines.h"
"-Wno-everything"

  • 3 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enables the 'Live' mapping menu, increments the network version, and implements a feature to restrict copying/cutting during live sessions. It also updates the chat system to send messages on Enter and fixes a potential crash in client list updates. Feedback highlights several improvement opportunities to align with the C++20/23 style guide, such as using default member initializers in headers, using const auto* for observation, and using auto with proper string trimming. Additionally, a thread-safety issue was identified regarding the static local id variable used for generating peer IDs in LiveServer::acceptClient().

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

} else {
auto peer = std::make_unique<LivePeer>(this, std::move(*socket));
peer->log = log;
peer->id = id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a static local variable id inside acceptClient() to generate peer IDs is a design issue. It is not thread-safe if acceptClient is called from multiple threads, and it shares the ID counter across all LiveServer instances (violating modularity). Consider refactoring id to be a member variable of LiveServer (e.g., uint32_t nextPeerId = 0;).

Comment thread source/live/live_tab.cpp
Comment on lines +187 to +192
wxString text = input->GetValue().Trim();
if (text.empty()) {
return;
}
socket->sendChatMessage(text);
input->Clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use auto for the local variable type since the type of input->GetValue() is obvious. Additionally, Trim() only trims trailing whitespace by default. To trim both leading and trailing whitespace, chain Trim(true).Trim(false). This also avoids calling Trim() on a temporary object directly.

	auto text = input->GetValue();
	text.Trim(true).Trim(false);
	if (text.empty()) {
		return;
	}
	socket->sendChatMessage(text);
	input->Clear();
References
  1. Apply incremental improvements: auto where type is obvious. (link)

Comment on lines +378 to +381
LiveClient* liveClient = editor->live_manager.GetClient();
if (liveClient && !liveClient->isAllowCopy()) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Use const auto* to declare liveClient since it is only used for observation and is not mutated. This aligns with the style guide's recommendation to use auto where the type is obvious and to enforce const-correctness.

	const auto* liveClient = editor->live_manager.GetClient();
	if (liveClient && !liveClient->isAllowCopy()) {
		return;
	}
References
  1. Apply incremental improvements: auto where type is obvious. (link)

Comment on lines +398 to +401
LiveClient* liveClient = editor->live_manager.GetClient();
if (liveClient && !liveClient->isAllowCopy()) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Use const auto* to declare liveClient for const-correctness and to adhere to the style guide's recommendation of using auto where appropriate.

	const auto* liveClient = editor->live_manager.GetClient();
	if (liveClient && !liveClient->isAllowCopy()) {
		return;
	}
References
  1. Apply incremental improvements: auto where type is obvious. (link)

Comment thread source/live/live_client.h

std::unique_ptr<Editor> editor;

bool allowCopy;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

In modern C++20/23, prefer initializing member variables directly in the class definition (header) rather than in the constructor initializer list. This prevents uninitialized member bugs and keeps the constructor clean.

Suggested change
bool allowCopy;
bool allowCopy = false;
References
  1. Every new and modified file MUST use C++20/23 features. Prefer default member initializers in class definitions. (link)

LiveSocket(),
readMessage(), queryNodeList(), currentOperation(),
resolver(nullptr), socket(nullptr), editor(nullptr), stopped(false) {
resolver(nullptr), socket(nullptr), editor(nullptr), allowCopy(false), stopped(false) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Since allowCopy is now initialized directly in the header, we can omit it from the constructor initializer list.

	resolver(nullptr), socket(nullptr), editor(nullptr), stopped(false) {

Comment thread source/live/live_server.h

uint32_t clientIds;
uint16_t port;
bool allowCopy;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

In modern C++20/23, prefer initializing member variables directly in the class definition (header) rather than in the constructor initializer list.

Suggested change
bool allowCopy;
bool allowCopy = false;
References
  1. Every new and modified file MUST use C++20/23 features. Prefer default member initializers in class definitions. (link)

LiveSocket(),
clients(), acceptor(nullptr), socket(nullptr), editor(&editor),
clientIds(0), port(0), stopped(false) {
clientIds(0), port(0), allowCopy(false), stopped(false) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Since allowCopy is now initialized directly in the header, we can omit it from the constructor initializer list.

	clientIds(0), port(0), stopped(false) {

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
source/live/live_server.cpp (1)

88-110: ⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Race condition on static id counter.

The static id variable at line 89 is incremented at line 110 (clients.emplace(id++, ...)) without synchronization. Since acceptClient() schedules async callbacks that can execute concurrently when multiple clients connect simultaneously, two accept operations could read and increment the same id value, assigning duplicate IDs to different peers.

Consider using std::atomic<uint32_t> or protecting the increment with clientMutex.

🔒 Proposed fix using atomic
 void LiveServer::acceptClient() {
-	static uint32_t id = 0;
+	static std::atomic<uint32_t> id{0};
 	if (stopped) {
 		return;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/live/live_server.cpp` around lines 88 - 110, The static uint32_t id in
LiveServer::acceptClient is incremented without synchronization causing a race
when multiple async accepts complete; fix by making the counter atomic or
protecting increments with clientMutex: replace the static id with
std::atomic<uint32_t> id (or a member std::atomic<uint32_t> idCounter) and use
id.fetch_add(1) (or increment under the clientMutex before calling
clients.emplace), ensuring LiveServer::acceptClient and the
clients.emplace(id..., ...) use the synchronized/atomic value to avoid duplicate
IDs.
source/editor/managers/editor_manager.cpp (2)

368-386: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

No user feedback when copy is denied.

When a client attempts to cut without allowCopy permission (lines 379-381), the operation silently returns without any notification to the user. Consider adding a status message or visual feedback to inform users why the operation didn't execute.

The enforcement logic itself is correct.

💬 Proposed feedback addition
 	LiveClient* liveClient = editor->live_manager.GetClient();
 	if (liveClient && !liveClient->isAllowCopy()) {
+		g_status.SetStatusText("Copy operations are not permitted on this live session.");
 		return;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/editor/managers/editor_manager.cpp` around lines 368 - 386, The DoCut
path silently returns when LiveClient::isAllowCopy() denies the operation; add
user feedback before returning so users know why the cut failed. In
EditorManager::DoCut, when liveClient exists and !liveClient->isAllowCopy(),
call the GUI status/notification API (e.g. g_gui.ShowStatus or
g_gui.SetStatusMessage) with a short message like "Cut denied: copy permission
required" (or use your app's standard localization key), then return; keep the
existing early-return logic and do not perform editor->copybuffer.cut or view
refresh in this branch.

388-406: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

No user feedback when copy is denied.

Same concern as DoCut: when copy is blocked (lines 399-401), users receive no feedback explaining why their action had no effect.

💬 Proposed feedback addition
 	LiveClient* liveClient = editor->live_manager.GetClient();
 	if (liveClient && !liveClient->isAllowCopy()) {
+		g_status.SetStatusText("Copy operations are not permitted on this live session.");
 		return;
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/editor/managers/editor_manager.cpp` around lines 388 - 406, The DoCopy
method silently returns when a LiveClient forbids copying
(LiveClient::isAllowCopy()), so add user feedback before the early return: when
liveClient exists and isAllowCopy() is false, call the GUI feedback mechanism
(e.g., display a status/message or dialog via g_gui — matching how DoCut reports
denials) to inform the user the copy was blocked, then return; keep
g_gui.RefreshView() and g_gui.root->UpdateMenubar() behavior unchanged for
successful copies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@source/live/live_server.cpp`:
- Around line 148-153: LiveServer::broadcastChat currently dereferences log
without null-check which can race with LiveSocket (log starts as nullptr) and
createLogWindow (which sets log) since bind/async networking (triggered in
LiveDialogs::ShowHostDialog) may call LivePeer::parseChatMessage ->
LiveServer::broadcastChat before log is set; fix by adding a guard around all
uses of log inside LiveServer::broadcastChat (e.g., if (log) { log->Chat(...); }
) or otherwise ensure createLogWindow runs before bind starts accepting packets;
update references in broadcastChat so every call to log->... is protected.

In `@source/live/live_tab.cpp`:
- Around line 183-193: LiveLogTab::OnChat uses wxString::Trim() which only trims
the right side by default, so leading whitespace can be sent; update the code
around the input->GetValue().Trim() call to trim both leading and trailing
whitespace (e.g., call text.Trim(false); text.Trim(true); or use an equivalent
two-sided/Strip method) before checking text.empty(), referencing
LiveLogTab::OnChat and the local variable text.

---

Outside diff comments:
In `@source/editor/managers/editor_manager.cpp`:
- Around line 368-386: The DoCut path silently returns when
LiveClient::isAllowCopy() denies the operation; add user feedback before
returning so users know why the cut failed. In EditorManager::DoCut, when
liveClient exists and !liveClient->isAllowCopy(), call the GUI
status/notification API (e.g. g_gui.ShowStatus or g_gui.SetStatusMessage) with a
short message like "Cut denied: copy permission required" (or use your app's
standard localization key), then return; keep the existing early-return logic
and do not perform editor->copybuffer.cut or view refresh in this branch.
- Around line 388-406: The DoCopy method silently returns when a LiveClient
forbids copying (LiveClient::isAllowCopy()), so add user feedback before the
early return: when liveClient exists and isAllowCopy() is false, call the GUI
feedback mechanism (e.g., display a status/message or dialog via g_gui —
matching how DoCut reports denials) to inform the user the copy was blocked,
then return; keep g_gui.RefreshView() and g_gui.root->UpdateMenubar() behavior
unchanged for successful copies.

In `@source/live/live_server.cpp`:
- Around line 88-110: The static uint32_t id in LiveServer::acceptClient is
incremented without synchronization causing a race when multiple async accepts
complete; fix by making the counter atomic or protecting increments with
clientMutex: replace the static id with std::atomic<uint32_t> id (or a member
std::atomic<uint32_t> idCounter) and use id.fetch_add(1) (or increment under the
clientMutex before calling clients.emplace), ensuring LiveServer::acceptClient
and the clients.emplace(id..., ...) use the synchronized/atomic value to avoid
duplicate IDs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b2319d09-213e-40a5-91f6-d513da15950c

📥 Commits

Reviewing files that changed from the base of the PR and between 9baf1f1 and 22cec18.

📒 Files selected for processing (11)
  • data/menubar.xml
  • source/app/definitions.h
  • source/editor/managers/editor_manager.cpp
  • source/live/live_client.cpp
  • source/live/live_client.h
  • source/live/live_peer.cpp
  • source/live/live_server.cpp
  • source/live/live_server.h
  • source/live/live_socket.h
  • source/live/live_tab.cpp
  • source/ui/live_dialogs.cpp
💤 Files with no reviewable changes (1)
  • data/menubar.xml

Comment on lines 148 to 153
void LiveServer::updateClientList() const {
std::lock_guard<std::mutex> lock(clientMutex);
log->UpdateClientList(clients);
if (log) {
log->UpdateClientList(clients);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find all log pointer dereferences to verify null-safety

rg -n 'log->' --type=cpp -g 'live_*.cpp' -C2

Repository: karolak6612/remeres-map-editor-redux

Length of output: 7950


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect live_server.cpp around the broadcastChat() implementation and any log lifecycle points.
echo "=== live_server.cpp: broadcastChat + nearby ==="
rg -n "broadcastChat\\(" source/live/live_server.cpp
echo
rg -n "void LiveServer::updateClientList\\(" source/live/live_server.cpp
echo
rg -n "\\blog\\s*=\\s*nullptr|\\blog\\s*=\\s*newd\\s+LiveLogTab|log = nullptr|log\\s*=" source/live/live_server.cpp
echo
# Print a small surrounding window around those areas.
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("source/live/live_server.cpp")
txt = p.read_text(errors="ignore").splitlines()
need = set()
for i,line in enumerate(txt, start=1):
    if "broadcastChat(" in line or "updateClientList(" in line or "log = nullptr" in line or "newd LiveLogTab" in line or "log->Chat" in line:
        need.add(i)
# include a bit of context around found lines
ctx = set()
for i in sorted(need):
    for j in range(max(1,i-8), min(len(txt), i+8)+1):
        ctx.add(j)
for j in sorted(ctx):
    print(f"{j:4d}: {txt[j-1]}")
PY

# 2) Find all call sites of broadcastChat() across the codebase.
echo
echo "=== Call sites of broadcastChat() ==="
rg -n "broadcastChat\\(" -S --type=cpp source/live

Repository: karolak6612/remeres-map-editor-redux

Length of output: 3590


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== live_server.h: find log member ==="
rg -n "class LiveServer|\\blog\\b" source/live/live_server.h -n
echo
rg -n "LiveServer::(LiveServer\\(|~LiveServer\\(|shutdown\\(|stop\\(|close\\(|Destroy\\(|createLogWindow\\(|sendChatMessage\\(|broadcastChat\\())" -S source/live/live_server.cpp
echo
echo "=== live_server.cpp: around broadcastChat caller(s) and log lifecycle ==="
python3 - <<'PY'
import pathlib
p = pathlib.Path("source/live/live_server.cpp")
lines = p.read_text(errors="ignore").splitlines()
targets = [
    ("shutdown-ish", "log = nullptr"),
    ("peer log assignment", "peer->log = log"),
    ("createLogWindow", "createLogWindow"),
    ("sendChatMessage", "sendChatMessage"),
    ("broadcastChat", "broadcastChat"),
]
def find_lines(needle):
    idx=[]
    for i,l in enumerate(lines, start=1):
        if needle in l:
            idx.append(i)
    return idx
for name, needle in targets:
    idx=find_lines(needle)
    if not idx: 
        print(f"-- {name}: no matches for {needle}")
        continue
    for i in idx[:3]:
        print(f"\n-- {name} (line {i})")
        for j in range(max(1,i-10), min(len(lines), i+10)+1):
            print(f"{j:4d}: {lines[j-1]}")
PY

echo
echo "=== live_peer.cpp: around server->broadcastChat(...) ==="
rg -n "server->broadcastChat" -C6 source/live/live_peer.cpp
echo
echo "=== search for createLogWindow() call sites ==="
rg -n "createLogWindow\\(" source/live -S
echo

Repository: karolak6612/remeres-map-editor-redux

Length of output: 489


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== live_server.h: locate log declaration ==="
rg -n "\\blog\\b" source/live/live_server.h
echo

echo "=== live_server.cpp: locate log lifecycle assignments ==="
rg -n "log\\s*=\\s*nullptr|log\\s*=\\s*newd\\s+LiveLogTab|\\blog->" source/live/live_server.cpp -C2

echo
echo "=== live_server.cpp: show constructor/destructor and shutdown-ish regions ==="
rg -n "LiveServer::(LiveServer|~LiveServer|shutdown|stop|close|Destroy|createLogWindow)" source/live/live_server.cpp
echo

# Print around createLogWindow
echo
echo "=== live_server.cpp: full createLogWindow() context ==="
rg -n "LiveLogTab\\* LiveServer::createLogWindow" source/live/live_server.cpp
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("source/live/live_server.cpp")
lines=p.read_text(errors="ignore").splitlines()
# find line with signature
sig=None
for i,l in enumerate(lines, start=1):
    if "LiveLogTab* LiveServer::createLogWindow" in l:
        sig=i;break
assert sig
start=max(1,sig-5)
end=min(len(lines), sig+120)
for j in range(start, end+1):
    print(f"{j:4d}: {lines[j-1]}")
PY

echo
echo "=== live_server.cpp: broadcastChat() context ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("source/live/live_server.cpp")
lines=p.read_text(errors="ignore").splitlines()
sig=None
for i,l in enumerate(lines, start=1):
    if "void LiveServer::broadcastChat" in l:
        sig=i;break
assert sig
start=max(1,sig-20)
end=min(len(lines), sig+60)
for j in range(start, end+1):
    print(f"{j:4d}: {lines[j-1]}")
PY

echo
echo "=== call sites: createLogWindow() ==="
rg -n "createLogWindow\\(" source/live -S

Repository: karolak6612/remeres-map-editor-redux

Length of output: 126


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== live_server.h: show around class LiveServer definition ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("source/live/live_server.h")
txt=p.read_text(errors="ignore").splitlines()
for i,l in enumerate(txt, start=1):
    if "class LiveServer" in l:
        start=max(1,i-20); end=min(len(txt), i+120)
        for j in range(start,end+1):
            print(f"{j:4d}: {txt[j-1]}")
        break
PY

echo
echo "=== live_server.h: find LiveLogTab/log-related identifiers ==="
rg -n "LiveLogTab|\\blog\\b|\\blog\\s*[;=]|\\blog\\s*\\w+|UpdateClientList|Chat\\(" source/live/live_server.h || true

echo
echo "=== live_server.cpp: find log member/type declarations/uses ==="
rg -n "LiveLogTab\\*|LiveLogTab\\s+\\w+|\\blog\\b\\s*[;=]|\\blog\\s*\\.|\\blog->" source/live/live_server.cpp -S || true

echo
echo "=== live_server.cpp: show all assignments to log ==="
rg -n "log\\s*=" source/live/live_server.cpp -S

Repository: karolak6612/remeres-map-editor-redux

Length of output: 3470


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== live_socket.h: find log declaration (likely in base class) ==="
rg -n "\\blog\\b|LiveLogTab" source/live/live_socket.h source/live/live_socket.cpp || true

echo
echo "=== live_server.cpp: find LiveServer constructor/destructor/close() and any log-related behavior ==="
rg -n "LiveServer::LiveServer\\(|LiveServer::~LiveServer\\(|void LiveServer::close\\(|void LiveServer::acceptClient\\(|void LiveServer::removeClient\\(" source/live/live_server.cpp

python3 - <<'PY'
import pathlib, re
p=pathlib.Path("source/live/live_server.cpp")
lines=p.read_text(errors="ignore").splitlines()
def print_region(start_pat, window=120):
    for i,l in enumerate(lines, start=1):
        if re.search(start_pat, l):
            s=max(1,i-10); e=min(len(lines), i+window)
            print(f"\n--- region around line {i} matching {start_pat} ---")
            for j in range(s,e+1):
                print(f"{j:4d}: {lines[j-1]}")
            break

print_region(r"LiveServer::LiveServer\\(")
print_region(r"~LiveServer\\(")
print_region(r"void LiveServer::close\\(")
PY

echo
echo "=== call sites: createLogWindow() ==="
rg -n "createLogWindow\\(" source/live -S
echo
echo "=== call sites: sendChatMessage() ==="
rg -n "sendChatMessage\\(" source/live -S
echo
echo "=== call sites: broadcastChat() ==="
rg -n "broadcastChat\\(" source/live -S

echo
echo "=== live_peer.cpp: inspect how chat packets reach server->broadcastChat ==="
rg -n "PACKET_SERVER_TALK|Chat\\(|broadcastChat\\(" source/live/live_peer.cpp -C2

Repository: karolak6612/remeres-map-editor-redux

Length of output: 2118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== live_socket.h around log ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("source/live/live_socket.h")
lines=p.read_text(errors="ignore").splitlines()
for i,l in enumerate(lines, start=1):
    if "LiveLogTab* log" in l or "LiveLogTab* log;" in l or "LiveLogTab* log" in l:
        start=max(1,i-15); end=min(len(lines), i+15)
        for j in range(start,end+1):
            print(f"{j:4d}: {lines[j-1]}")
        break
PY

echo
echo "=== live_socket.cpp around log init and any null checks ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("source/live/live_socket.cpp")
lines=p.read_text(errors="ignore").splitlines()
# show around constructor init list line containing "log(nullptr)"
for i,l in enumerate(lines, start=1):
    if "log(nullptr)" in l:
        start=max(1,i-20); end=min(len(lines), i+20)
        for j in range(start,end+1):
            print(f"{j:4d}: {lines[j-1]}")
        break
PY

echo
echo "=== live_server.cpp: show constructor/destructor/close/accept/remove regions (small) ==="
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("source/live/live_server.cpp")
lines=p.read_text(errors="ignore").splitlines()
def show_sig(sig):
    for i,l in enumerate(lines, start=1):
        if sig in l:
            start=max(1,i-10); end=min(len(lines), i+120)
            print(f"\n--- {sig} near line {i} ---")
            for j in range(start,end+1):
                print(f"{j:4d}: {lines[j-1]}")
            return
    print(f"missing {sig}")
show_sig("LiveServer::LiveServer")
show_sig("LiveServer::~LiveServer")
show_sig("void LiveServer::close")
show_sig("void LiveServer::acceptClient")
show_sig("void LiveServer::removeClient")
PY

echo
echo "=== createLogWindow() call sites ==="
rg -n "createLogWindow\\(" source -S

echo
echo "=== live_server.cpp: broadcastChat() implementation + surrounding ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("source/live/live_server.cpp")
lines=p.read_text(errors="ignore").splitlines()
for i,l in enumerate(lines, start=1):
    if "void LiveServer::broadcastChat" in l:
        start=max(1,i-15); end=min(len(lines), i+60)
        for j in range(start,end+1):
            print(f"{j:4d}: {lines[j-1]}")
        break
PY

echo
echo "=== live_server.cpp: sendChatMessage() context ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("source/live/live_server.cpp")
lines=p.read_text(errors="ignore").splitlines()
for i,l in enumerate(lines, start=1):
    if "void LiveServer::sendChatMessage" in l:
        start=max(1,i-10); end=min(len(lines), i+40)
        for j in range(start,end+1):
            print(f"{j:4d}: {lines[j-1]}")
        break
PY

echo
echo "=== live_peer.cpp: locate PACKET_SERVER_TALK -> broadcastChat chain ==="
rg -n "PACKET_SERVER_TALK|broadcastChat\\(" source/live/live_peer.cpp -C4

Repository: karolak6612/remeres-map-editor-redux

Length of output: 25410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== live_dialogs.cpp: context around LiveServer::createLogWindow call ==="
rg -n "createLogWindow\\(" source/ui/live_dialogs.cpp -n
python3 - <<'PY'
import pathlib
p=pathlib.Path("source/ui/live_dialogs.cpp")
lines=p.read_text(errors="ignore").splitlines()
targets=[]
for i,l in enumerate(lines, start=1):
    if "createLogWindow" in l:
        targets.append(i)
for i in targets:
    print(f"\n--- around createLogWindow at line {i} ---")
    for j in range(max(1,i-30), min(len(lines), i+30)+1):
        print(f"{j:4d}: {lines[j-1]}")
PY

echo
echo "=== live_dialogs.cpp: search for LiveServer bind/start ordering ==="
rg -n "bind\\(|acceptClient\\(|sendChatMessage\\(|PACKET_SERVER_TALK|broadcastChat|createLogWindow\\(" source/ui/live_dialogs.cpp -S

echo
echo "=== live_server.cpp: any guard against stopped/acceptor during chat handling? ==="
rg -n "parseChatMessage|stopped|receiveHeader|PACKET_SERVER_TALK" source/live -S

Repository: karolak6612/remeres-map-editor-redux

Length of output: 5970


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== live_packets.h: find TALK packet IDs ==="
rg -n "TALK" source/live/live_packets.h -n

echo
echo "=== live_peer.cpp: find parseChatMessage call site and switch cases ==="
rg -n "parseChatMessage\\(|PACKET_.*TALK|PACKET_.*CHAT" source/live/live_peer.cpp -n -S -C3

echo
echo "=== live_peer.cpp: show receiveHeader() and message handling block (around packet switch) ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("source/live/live_peer.cpp")
lines=p.read_text(errors="ignore").splitlines()
# find the line containing "switch" near receiveHeader/message loop
for i,l in enumerate(lines, start=1):
    if "receiveHeader" in l and "void LivePeer::receiveHeader" in l:
        start=i-5
        break
else:
    start=1
# print around first 220 lines maybe
for j in range(start, min(len(lines), start+220)):
    print(f"{j:4d}: {lines[j-1]}")
PY

echo
echo "=== live_peer.cpp: show around PACKET_SERVER_TALK handling (if any) and chat send ==="
rg -n "PACKET_SERVER_TALK|sendChat|Chat\\(" source/live/live_peer.cpp -n -S -C4

Repository: karolak6612/remeres-map-editor-redux

Length of output: 8754


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== locate NetworkConnection implementation ==="
rg -n "class NetworkConnection|NetworkConnection::" source/net -S
echo

echo "=== inspect NetworkConnection::start() and threading/run-loop ==="
rg -n "NetworkConnection::start\\(|io_context\\.run\\(|run\\(\\)|get_service\\(|std::thread" source/net -S

# show relevant sections
python3 - <<'PY'
import pathlib, re
paths = [p for p in pathlib.Path("source").rglob("net_connection.*") if p.is_file()]
print("Found:", [str(x) for x in paths])
PY

Repository: karolak6612/remeres-map-editor-redux

Length of output: 1504


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib
p=pathlib.Path("source/net/net_connection.cpp")
lines=p.read_text(errors="ignore").splitlines()
# print around start()
for i,l in enumerate(lines, start=1):
    if "bool NetworkConnection::start()" in l:
        start=i;break
else:
    start=1
s=max(1,start-20); e=min(len(lines), start+120)
print("=== net_connection.cpp around start() ===")
for j in range(s,e+1):
    print(f"{j:4d}: {lines[j-1]}")
PY

echo
echo "=== net_connection.cpp around stop() ==="
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("source/net/net_connection.cpp")
lines=p.read_text(errors="ignore").splitlines()
for i,l in enumerate(lines, start=1):
    if "void NetworkConnection::stop()" in l:
        start=i;break
else:
    start=1
s=max(1,start-20); e=min(len(lines), start+120)
for j in range(s,e+1):
    print(f"{j:4d}: {lines[j-1]}")
PY

echo
echo "=== net_connection.cpp around get_service() ==="
python3 - <<'PY'
import pathlib
p=pathlib.Path("source/net/net_connection.cpp")
lines=p.read_text(errors="ignore").splitlines()
for i,l in enumerate(lines, start=1):
    if "boost::asio::io_context& NetworkConnection::get_service()" in l:
        start=i;break
else:
    start=1
s=max(1,start-10); e=min(len(lines), start+40)
for j in range(s,e+1):
    print(f"{j:4d}: {lines[j-1]}")
PY

Repository: karolak6612/remeres-map-editor-redux

Length of output: 3767


Unconditional log deref in LiveServer::broadcastChat() can crash when log == nullptr

  • LiveSocket initializes log to nullptr, and LiveServer::createLogWindow() (which assigns log) is called after LiveServer::bind() starts async networking in LiveDialogs::ShowHostDialog.
  • NetworkConnection runs the io_context on a background thread, so LivePeer::parseChatMessage() -> LiveServer::broadcastChat() can execute before log is set; broadcastChat() unconditionally does log->Chat(...) (~source/live/live_server.cpp:248-264).
  • Add a if (log) { ... } guard in broadcastChat() or restructure startup so log is created before accepting/processing packets.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/live/live_server.cpp` around lines 148 - 153,
LiveServer::broadcastChat currently dereferences log without null-check which
can race with LiveSocket (log starts as nullptr) and createLogWindow (which sets
log) since bind/async networking (triggered in LiveDialogs::ShowHostDialog) may
call LivePeer::parseChatMessage -> LiveServer::broadcastChat before log is set;
fix by adding a guard around all uses of log inside LiveServer::broadcastChat
(e.g., if (log) { log->Chat(...); } ) or otherwise ensure createLogWindow runs
before bind starts accepting packets; update references in broadcastChat so
every call to log->... is protected.

Comment thread source/live/live_tab.cpp
Comment on lines 183 to 193
void LiveLogTab::OnChat(wxCommandEvent& evt) {
if (!socket) {
return;
}
wxString text = input->GetValue().Trim();
if (text.empty()) {
return;
}
socket->sendChatMessage(text);
input->Clear();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

wxWidgets wxString Trim method behavior default parameter

💡 Result:

In wxWidgets, the wxString::Trim method accepts a single boolean parameter, fromRight, which determines whether whitespace is removed from the end or the beginning of the string [1][2]. The default value for this parameter is true [1][3]. When called without arguments, wxString::Trim defaults to removing whitespace from the right (end) of the string [1][4]. If you pass false to the method, it will remove whitespace from the left (beginning) of the string instead [1][2]. The method removes characters identified as whitespace, including spaces, tabs, form feeds, newlines, and carriage returns [1][3]. The method returns a reference to the modified string [1][5].

Citations:


Fix whitespace trimming in LiveLogTab::OnChat
wxString::Trim() called with no args defaults to trimming from the right only, so leading whitespace is preserved and can be sent (e.g., " hello"). Whitespace-only input will still be trimmed to empty and blocked by the existing text.empty() check.

Trim both leading and trailing whitespace
-	wxString text = input->GetValue().Trim();
+	wxString text = input->GetValue().Trim(true).Trim(false);
 	if (text.empty()) {
 		return;
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void LiveLogTab::OnChat(wxCommandEvent& evt) {
if (!socket) {
return;
}
wxString text = input->GetValue().Trim();
if (text.empty()) {
return;
}
socket->sendChatMessage(text);
input->Clear();
}
void LiveLogTab::OnChat(wxCommandEvent& evt) {
if (!socket) {
return;
}
wxString text = input->GetValue().Trim(true).Trim(false);
if (text.empty()) {
return;
}
socket->sendChatMessage(text);
input->Clear();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/live/live_tab.cpp` around lines 183 - 193, LiveLogTab::OnChat uses
wxString::Trim() which only trims the right side by default, so leading
whitespace can be sent; update the code around the input->GetValue().Trim() call
to trim both leading and trailing whitespace (e.g., call text.Trim(false);
text.Trim(true); or use an equivalent two-sided/Strip method) before checking
text.empty(), referencing LiveLogTab::OnChat and the local variable text.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant