feat(rust): coin balances on the Rust account screen (PLAN_REDESIGNS §11.2)
All checks were successful
PR Checks / android-build (pull_request) Successful in 11m28s
All checks were successful
PR Checks / android-build (pull_request) Successful in 11m28s
The app half of the module's Economics (protocol 14): a Coins section on the Rust account screen, from GET /player/rust/economics — one card per server, each linked account's balance, "last known" when the server did not answer, and "Economics is not installed on this server" where it is absent (D199). A third independent read beside the links and the entitlements; a link re-reads it, and a 404 (a module older than protocol 14) leaves the section out rather than showing an error the player can do nothing about. A balance never reported shows a dash, not zero. testDebugUnitTest, lintDebug and assembleDebug pass locally; four new tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
package com.runicgateway.app.data.api
|
||||
|
||||
import com.runicgateway.app.data.api.dto.RustEconomicsDto
|
||||
import com.runicgateway.app.data.api.dto.RustLinkListDto
|
||||
import com.runicgateway.app.data.api.dto.RustLinkRequest
|
||||
import com.runicgateway.app.data.api.dto.RustLinkResultDto
|
||||
@@ -72,4 +73,13 @@ interface PlayerRustApi {
|
||||
*/
|
||||
@GET("api/v1/player/rust/permissions")
|
||||
suspend fun permissions(): RustPlayerPermissionsDto
|
||||
|
||||
/**
|
||||
* The caller's coin balances on every server, per linked account
|
||||
* (`docs/modules/rust/PLAN_REDESIGNS.md` §11.2.2). Live where a server
|
||||
* answers, the last balance it reported where it does not. A module older
|
||||
* than protocol 14 answers 404, and the screen then shows no coins at all.
|
||||
*/
|
||||
@GET("api/v1/player/rust/economics")
|
||||
suspend fun economics(): RustEconomicsDto
|
||||
}
|
||||
|
||||
@@ -125,3 +125,43 @@ data class RustReachDto(
|
||||
val name: String = "",
|
||||
val live: Boolean = false,
|
||||
)
|
||||
|
||||
/**
|
||||
* `GET /player/rust/economics` — the caller's coin balances
|
||||
* (`docs/modules/rust/PLAN_REDESIGNS.md` §11.2.2): every server, each linked
|
||||
* Steam account. Economics is optional on a server, so one without it is listed
|
||||
* with [RustEconomicsServerDto.economics] false rather than left out.
|
||||
*/
|
||||
@Serializable
|
||||
data class RustEconomicsDto(
|
||||
val servers: List<RustEconomicsServerDto> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* One server's balances. [startingKnown] false means the server's Economics
|
||||
* config could not be read, and [startingBalance] is Economics' default.
|
||||
*/
|
||||
@Serializable
|
||||
data class RustEconomicsServerDto(
|
||||
val serverId: String = "",
|
||||
val server: String = "",
|
||||
val economics: Boolean = false,
|
||||
val reason: String? = null,
|
||||
val startingBalance: Double? = null,
|
||||
val startingKnown: Boolean = true,
|
||||
val live: Boolean = false,
|
||||
val accounts: List<RustBalanceDto> = emptyList(),
|
||||
)
|
||||
|
||||
/**
|
||||
* One linked account's balance on one server. [source] is `live` (the server
|
||||
* answered just now), `last-known` (it did not, and this is the last balance it
|
||||
* reported), or null with [balance] null when neither knows.
|
||||
*/
|
||||
@Serializable
|
||||
data class RustBalanceDto(
|
||||
val steamId: String = "",
|
||||
val name: String? = null,
|
||||
val balance: Double? = null,
|
||||
val source: String? = null,
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.runicgateway.app.core.result.ApiResult
|
||||
import com.runicgateway.app.core.result.map
|
||||
import com.runicgateway.app.core.result.safeApiCall
|
||||
import com.runicgateway.app.data.api.PlayerRustApi
|
||||
import com.runicgateway.app.data.api.dto.RustEconomicsServerDto
|
||||
import com.runicgateway.app.data.api.dto.RustLinkDto
|
||||
import com.runicgateway.app.data.api.dto.RustLinkRequest
|
||||
import com.runicgateway.app.data.api.dto.RustLinkResultDto
|
||||
@@ -48,4 +49,8 @@ class PlayerRustRepository @Inject constructor(
|
||||
/** Ranks and grants the site holds for the caller, resolved per server. */
|
||||
suspend fun permissions(): ApiResult<RustPlayerPermissionsDto> =
|
||||
safeApiCall { api.permissions() }
|
||||
|
||||
/** The caller's coin balances per server and linked account (§11.2.2). */
|
||||
suspend fun economics(): ApiResult<List<RustEconomicsServerDto>> =
|
||||
safeApiCall { api.economics() }.map { it.servers }
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.runicgateway.app.R
|
||||
import com.runicgateway.app.data.api.dto.RustEconomicsServerDto
|
||||
import com.runicgateway.app.data.api.dto.RustLinkDto
|
||||
import com.runicgateway.app.data.api.dto.RustPlayerPermissionsDto
|
||||
import com.runicgateway.app.data.api.dto.RustReachDto
|
||||
@@ -106,6 +107,73 @@ fun RustAccountScreen(
|
||||
is UiState.Error -> ErrorView(held.kind, onRetry = viewModel::load)
|
||||
is UiState.Success -> Held(held.data)
|
||||
}
|
||||
|
||||
// Coins (PLAN_REDESIGNS §11.2.2). A 404 is a module older than protocol
|
||||
// 14, which has no balances to give: the section is left out rather than
|
||||
// shown as an error the player can do nothing about.
|
||||
val coins = state.coins
|
||||
if (!(coins is UiState.Error && coins.httpStatus == 404)) {
|
||||
Text(
|
||||
stringResource(R.string.rust_coins_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
|
||||
when (coins) {
|
||||
is UiState.Loading -> LoadingView()
|
||||
is UiState.Error -> ErrorView(coins.kind, onRetry = viewModel::load)
|
||||
is UiState.Success -> Coins(coins.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One card per server: each linked account's balance where the server runs
|
||||
* Economics, and the reason where it does not (D199) — a player asking where
|
||||
* their coins are is owed the answer, not an empty space.
|
||||
*/
|
||||
@Composable
|
||||
private fun Coins(servers: List<RustEconomicsServerDto>) {
|
||||
if (servers.none { it.accounts.isNotEmpty() }) {
|
||||
Text(
|
||||
stringResource(R.string.rust_coins_unlinked),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
servers.forEach { server ->
|
||||
ShardCard(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(server.server.ifBlank { server.serverId }, style = MaterialTheme.typography.titleSmall)
|
||||
|
||||
if (!server.economics) {
|
||||
Text(
|
||||
stringResource(R.string.rust_coins_missing),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
} else {
|
||||
server.accounts.forEach { account ->
|
||||
Text(
|
||||
stringResource(R.string.rust_coins_balance, playerLabel(account.name, account.steamId), coinAmount(account.balance)),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
if (account.source == "last-known") {
|
||||
Text(
|
||||
stringResource(R.string.rust_coins_last_known),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.runicgateway.app.R
|
||||
import com.runicgateway.app.core.result.ApiResult
|
||||
import com.runicgateway.app.data.api.dto.RustEconomicsServerDto
|
||||
import com.runicgateway.app.data.api.dto.RustLinkDto
|
||||
import com.runicgateway.app.data.api.dto.RustPlayerPermissionsDto
|
||||
import com.runicgateway.app.data.repository.PlayerRustRepository
|
||||
@@ -50,6 +51,8 @@ class RustAccountViewModel @Inject constructor(
|
||||
data class State(
|
||||
val links: UiState<List<RustLinkDto>> = UiState.Loading,
|
||||
val held: UiState<RustPlayerPermissionsDto> = UiState.Loading,
|
||||
/** Coin balances (PLAN_REDESIGNS §11.2.2), a third independent read. */
|
||||
val coins: UiState<List<RustEconomicsServerDto>> = UiState.Loading,
|
||||
val busy: Boolean = false,
|
||||
val feedback: Feedback? = null,
|
||||
/** The Steam id currently being released, so only its own row shows it. */
|
||||
@@ -64,7 +67,7 @@ class RustAccountViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun load() {
|
||||
_state.update { it.copy(links = UiState.Loading, held = UiState.Loading) }
|
||||
_state.update { it.copy(links = UiState.Loading, held = UiState.Loading, coins = UiState.Loading) }
|
||||
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(links = repository.links().toUiState()) }
|
||||
@@ -73,6 +76,15 @@ class RustAccountViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(held = repository.permissions().toUiState()) }
|
||||
}
|
||||
|
||||
reloadCoins()
|
||||
}
|
||||
|
||||
/** The balances follow the links: a new link is a new account to read. */
|
||||
private fun reloadCoins() {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(coins = repository.economics().toUiState()) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-read what the site holds without blanking the accounts above it. */
|
||||
@@ -146,6 +158,7 @@ class RustAccountViewModel @Inject constructor(
|
||||
_state.update { it.copy(links = repository.links().toUiState()) }
|
||||
}
|
||||
reloadHeld()
|
||||
reloadCoins()
|
||||
}
|
||||
|
||||
private fun linkErrorRes(status: Int): Int = when (status) {
|
||||
|
||||
@@ -159,6 +159,17 @@ fun prefabName(name: String?): String {
|
||||
return name.replace(Regex("[_.]+"), " ").trim()
|
||||
}
|
||||
|
||||
/**
|
||||
* 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",
|
||||
* which is a claim about somebody's balance that nobody made.
|
||||
*/
|
||||
fun coinAmount(value: Double?, locale: java.util.Locale = java.util.Locale.getDefault()): String {
|
||||
val n = value ?: return "—"
|
||||
if (!n.isFinite()) return "—"
|
||||
return java.text.NumberFormat.getNumberInstance(locale).apply { maximumFractionDigits = 2 }.format(n)
|
||||
}
|
||||
|
||||
/** A steam id, shortened for a table cell, without pretending it is a name. */
|
||||
fun shortSteamId(steamId: String?): String {
|
||||
val id = steamId.orEmpty()
|
||||
|
||||
@@ -697,6 +697,11 @@
|
||||
<string name="rust_held_unlinked">None of this reaches the game yet — link a Steam account above and the site pushes it across on its next sync.</string>
|
||||
<string name="rust_held_waiting_note">A server marked waiting has not confirmed it yet. One that is offline catches up when it comes back.</string>
|
||||
<string name="rust_held_no_servers">No servers are configured yet.</string>
|
||||
<string name="rust_coins_title">Coins</string>
|
||||
<string name="rust_coins_unlinked">Link a Steam account to see your balance.</string>
|
||||
<string name="rust_coins_missing">Economics is not installed on this server.</string>
|
||||
<string name="rust_coins_balance">%1$s · %2$s coins</string>
|
||||
<string name="rust_coins_last_known">Last known — the server did not answer just now.</string>
|
||||
<string name="rust_reach_live">%1$s · has it</string>
|
||||
<string name="rust_reach_waiting">%1$s · waiting</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user