Native mods: read and write the open chat box (native_chat_input, native_set_chat_input)

A client mod that converts text as it's typed (Emoji Type's :code: to emoji)
reads the chat box's text and cursor and writes the result back as an edit,
so completions refresh as for typing. Only what typing lets in reaches the
box. Stacked on native_open_chat.

Part of #10107

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Horizon agentandClaude Opus 5.5 authored and Ludovic Marie committed 2026-11-07 01:52:03 +01:00
1 parent ac8a09ef0a
commit 53d0ea1320
3 files changed
+59

No files matched your search

+42
View File
@@ -737,6 +737,32 @@ impl Game<'_> {
self.right = false;
self.capture(false);
}
/// The open chat box's text and its cursor, counted in characters; `None`
/// while the chat screen isn't open. API @API:chat_input@.
#[allow(dead_code)]
pub(super) fn native_chat_input(&self) -> Option<(String, usize)> {
let screen = self.ui.chat_screen.as_ref()?;
Some((screen.input.value.clone(), screen.input.cursor))
}
/// Replaces the open chat box's text and puts the cursor at `cursor`
/// (characters, clamped to the text), as an edit: completions refresh as
/// they do for typing. What typing can't put in the box isn't put there
/// either (`chat_box_text`), and the 256-character limit holds. An error
/// while the chat screen isn't open. API @API:chat_input@.
#[allow(dead_code)]
pub(super) fn native_set_chat_input(&mut self, text: &str, cursor: usize) -> Result<(), String> {
let m = Measure(&self.gui.atlas.font);
let c = Commands {
dispatcher: chat_tree(&self.remote),
registries: &self.ui.registries,
};
let Some(screen) = self.ui.chat_screen.as_mut() else {
return Err("the chat isn't open".into());
};
screen.suggest_command(&m, &c, &chat_box_text(text));
screen.input.move_cursor_to(&m, cursor, false);
Ok(())
}
pub(super) fn close_chat(&mut self, reason: ChatExit) {
// InBedChatScreen.onClose: leaving the screen means getting up; the
// screen goes once the player has woken (`tick_in_bed_chat`).
@@ -1679,6 +1705,14 @@ impl Game<'_> {
}
}
/// `yyyy-MM-dd_HH.mm.ss[_n].png` (Util.getFilenameFormattedDateTime, in local time).
/// What a mod may write into the chat box: the characters `ChatScreen`'s
/// typing lets in (`StringUtil.isAllowedChatCharacter`: no control
/// characters, no DEL, no section sign).
fn chat_box_text(text: &str) -> String {
text.chars()
.filter(|&ch| ch >= ' ' && ch != '\u{7f}' && ch != '§')
.collect()
}
pub fn screenshot_name(dir: &std::path::Path) -> String {
// Util.getFilenameFormattedDateTime: the local date and time.
let base = chrono::Local::now().format("%Y-%m-%d_%H.%M.%S").to_string();
@@ -1710,6 +1744,14 @@ const LINK_COPY: usize = 1;
mod tests {
use super::*;
/// A mod's text reaches the chat box as typing would leave it: emoji and
/// accents stay, control characters, DEL and the section sign go.
#[test]
fn a_mods_chat_text_keeps_what_typing_allows() {
assert_eq!(super::chat_box_text("hi :smile: → 😀 é"), "hi :smile: → 😀 é");
assert_eq!(super::chat_box_text("a\nb\tc\u{7f}d§6e"), "abcd6e");
}
/// `ChatScreen.shouldDiscardDraft`.
#[test]
fn draft_discard_follows_the_exit_reason() {
+7
View File
@@ -3241,6 +3241,13 @@ edits and sends it, or presses Escape, and it goes into the chat history then li
256-character limit and its character filter. It is an error with no player in a world, and while another screen
(the chat itself included) is open. Bundles need `"hooks": @API:open_chat@`.
While the chat is open, `native_chat_input()` (API @API:chat_input@, `crates/horizon-client/src/game/chat.rs`) gives its
text and cursor (counted in characters), `None` when it's closed, and `native_set_chat_input(text, cursor)` replaces
them, as an edit: completions refresh as they do for typing, so a mod that turns `:smile:` into an emoji as it's typed
runs from `native_screen_char_typed_pre` and writes the result back. The cursor is clamped to the text. Only what typing
lets in reaches the box (no control characters, DEL or section sign), within its 256-character limit. Writing is an
error while the chat isn't open. Bundles need `"hooks": @API:chat_input@`.
### Packets a client mod sends
API 409, `crates/horizon-client/src/game/native_net.rs`: `native_send_packet(packet)` sends one
@@ -0,0 +1,10 @@
{
"hooks": [],
"helpers": [
{"name": "chat_input", "target": "horizon_client::game::chat::Game<'_>::native_chat_input"},
{"name": "set_chat_input", "target": "horizon_client::game::chat::Game<'_>::native_set_chat_input"}
],
"rust_summary": "a client mod reads and writes the open chat box (`native_chat_input`, `native_set_chat_input`).",
"doc_summary": "`native_chat_input()` gives the open chat box's text and cursor, `native_set_chat_input(text, cursor)` replaces them as an edit (completions refresh); an error while the chat is closed. Bundles need \"hooks\": @API:chat_input@.",
"comments": ["crates/horizon-client/src/game/chat.rs"]
}