feat(rust): print the weapon name the module sends in the feed
All checks were successful
PR Checks / android-build (pull_request) Successful in 14m33s

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-23 01:38:01 -05:00
parent bb9cd9008d
commit c41aff9b2e
4 changed files with 62 additions and 1 deletions

View File

@@ -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,

View File

@@ -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",

View File

@@ -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

View File

@@ -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: