chore(release): cut edge over to main — the Rust screens and the app's words (v0.6.0) #59

Merged
whitlocktech merged 32 commits from edge into main 2026-10-23 02:09:14 -05:00
4 changed files with 62 additions and 1 deletions
Showing only changes of commit c41aff9b2e - Show all commits

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: