From c41aff9b2e518be53575387a71ef807fe45e5662 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Fri, 23 Oct 2026 01:38:01 -0500 Subject: [PATCH] feat(rust): print the weapon name the module sends in the feed PLAN.md M20 step 4. Module-Rust#60 gives every event frame a weaponName: the game's own name for a ranged weapon, or the prefab name as words. The feed printed the prefab name as words itself, which read "with ak47u entity". weaponName() in RustFormat.kt is the Kotlin half of module-rust's format.js weapon(). It prints frame.weaponName. Against a module too old to send it, it drops the .entity / .weapon / .deployed ending before prefabName(), as the website does, so an older module's kill reads "with ak47u". Tests: a kill with weaponName, the same kill from an older module, and the fallback's edges (blank name, each ending, an ending only at the end, no weapon). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY --- .../com/runicgateway/app/ui/rust/RustFeed.kt | 2 +- .../runicgateway/app/ui/rust/RustFormat.kt | 17 +++++++++++ .../runicgateway/app/ui/rust/RustFeedTest.kt | 29 +++++++++++++++++++ .../app/ui/rust/RustFormatTest.kt | 15 ++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/runicgateway/app/ui/rust/RustFeed.kt b/app/src/main/java/com/runicgateway/app/ui/rust/RustFeed.kt index fdf2c63..354c093 100644 --- a/app/src/main/java/com/runicgateway/app/ui/rust/RustFeed.kt +++ b/app/src/main/java/com/runicgateway/app/ui/rust/RustFeed.kt @@ -161,7 +161,7 @@ fun describe(row: RustEventDto): FeedLine { */ private fun death(row: RustEventDto, name: String?): FeedLine { val where = listOfNotNull( - row.str("weapon")?.let { "with ${prefabName(it)}" }, + row.str("weapon")?.let { "with ${weaponName(it, row.str("weaponName"))}" }, row.num("distance")?.let { "${Math.round(it)}m" }, row.str("grid"), if (row.flag("sleeping")) "while sleeping" else null, diff --git a/app/src/main/java/com/runicgateway/app/ui/rust/RustFormat.kt b/app/src/main/java/com/runicgateway/app/ui/rust/RustFormat.kt index bd6b8ae..8afe24f 100644 --- a/app/src/main/java/com/runicgateway/app/ui/rust/RustFormat.kt +++ b/app/src/main/java/com/runicgateway/app/ui/rust/RustFormat.kt @@ -159,6 +159,23 @@ fun prefabName(name: String?): String { return name.replace(Regex("[_.]+"), " ").trim() } +/** The endings a held entity's prefab name carries that no player says. */ +private val WEAPON_ENDING = Regex("[._](entity|weapon|deployed)$") + +/** + * The weapon in a kill, the Kotlin half of `module-rust`'s `format.js weapon()`. + * The module sends `weaponName` (Module-Rust#60), which is the game's own name for a + * gun or the prefab name as words (`server/model/titles/weaponNames.js`), and the + * app prints that. A module too old to send it gets the same fallback here: the + * `.entity` / `.weapon` / `.deployed` ending is dropped, then [prefabName] turns the + * rest into words. So the app never prints "ak47u entity". + */ +fun weaponName(weapon: String?, weaponName: String?): String { + if (weapon.isNullOrBlank()) return "" + if (!weaponName.isNullOrBlank()) return weaponName + return prefabName(weapon.trim().replace(WEAPON_ENDING, "")) +} + /** * An Economics balance (`docs/modules/rust/PLAN_REDESIGNS.md` §11.2.2): grouped, * at most two places, and a dash for one the server never reported — not "0", diff --git a/app/src/test/java/com/runicgateway/app/ui/rust/RustFeedTest.kt b/app/src/test/java/com/runicgateway/app/ui/rust/RustFeedTest.kt index 35e79ed..be172c8 100644 --- a/app/src/test/java/com/runicgateway/app/ui/rust/RustFeedTest.kt +++ b/app/src/test/java/com/runicgateway/app/ui/rust/RustFeedTest.kt @@ -41,6 +41,35 @@ class RustFeedTest { assertTrue(line.detail.contains("42m")) } + @Test + fun `a kill prints the weapon name the module sends`() { + // Module-Rust#60: the module reads the game's own name off its table and + // sends it beside the prefab, which stays the counting key. + val line = describe( + row( + "player.death", + """{"name":"Bob","attackerType":"player","attackerName":"Alice", + "weapon":"ak47u.entity","weaponName":"Assault Rifle"}""", + ), + ) + + assertEquals("with Assault Rifle", line.detail) + } + + @Test + fun `an older module's kill drops the prefab ending rather than printing it`() { + // A module from before #60 sends no `weaponName`. The feed once said + // "with ak47u entity"; the ending is dropped the way the website does it. + val line = describe( + row( + "player.death", + """{"name":"Bob","attackerType":"player","attackerName":"Alice","weapon":"ak47u.entity"}""", + ), + ) + + assertEquals("with ak47u", line.detail) + } + @Test fun `a fall is a death by nobody, not a kill`() { // The failure this distinction exists to avoid: `HitInfo` is legitimately diff --git a/app/src/test/java/com/runicgateway/app/ui/rust/RustFormatTest.kt b/app/src/test/java/com/runicgateway/app/ui/rust/RustFormatTest.kt index 06e8650..0f2dc9e 100644 --- a/app/src/test/java/com/runicgateway/app/ui/rust/RustFormatTest.kt +++ b/app/src/test/java/com/runicgateway/app/ui/rust/RustFormatTest.kt @@ -117,6 +117,21 @@ class RustFormatTest { assertEquals("", prefabName(null)) } + @Test + fun `a weapon prints the module's name, or the prefab without its ending`() { + assertEquals("Assault Rifle", weaponName("ak47u.entity", "Assault Rifle")) + // An older module sends no name; neither does one that sends a blank. + assertEquals("ak47u", weaponName("ak47u.entity", null)) + assertEquals("ak47u", weaponName("ak47u.entity", "")) + assertEquals("salvaged sword", weaponName("salvaged_sword.weapon", null)) + assertEquals("autoturret", weaponName("autoturret.deployed", null)) + assertEquals("rifle ak", weaponName("rifle.ak", null)) + // The ending is dropped only at the end. + assertEquals("entity thing", weaponName("entity.thing", null)) + // No weapon, no name, even when a name came along. + assertEquals("", weaponName(null, "Assault Rifle")) + } + @Test fun `a nameless player shows a shortened id, not the word unknown`() { // A steam id is not a name and does not look like one, which is the point: