stop minting a new account on every login

if the client presents an id we don't know, create it under that id and bump the sequence past it.
also chest offers match through BaseChest, so one silver row covers every arena.
This commit is contained in:
WiseDev 2026-08-23 08:54:43 +03:00
parent 8a745d634c
commit a2bf58a627
5 changed files with 86 additions and 3 deletions

View file

@ -69,7 +69,7 @@ impl AuthApi for AuthService {
message, message,
}); });
} }
let stored = if account.is_zero() || pass_token.is_none() { let stored = if account.is_zero() {
None None
} else { } else {
self.store.get(account).await.map_err(unavailable)? self.store.get(account).await.map_err(unavailable)?
@ -92,6 +92,15 @@ impl AuthApi for AuthService {
} }
existing existing
} }
None if !account.is_zero() => {
let adopted = self
.store
.adopt(account, pass_token.as_deref())
.await
.map_err(unavailable)?;
tracing::info!(account = %adopted.account_ref(), "adopted the account the client asked for");
adopted
}
None => { None => {
let created = self.store.create().await.map_err(unavailable)?; let created = self.store.create().await.map_err(unavailable)?;
tracing::info!(account = %created.account_ref(), "created account"); tracing::info!(account = %created.account_ref(), "created account");

View file

@ -41,6 +41,41 @@ impl AccountStore {
.await?; .await?;
Ok(account_from_row(&row)) Ok(account_from_row(&row))
} }
pub async fn adopt(&self, account: AccountRef, pass_token: Option<&str>) -> Result<Account> {
let now = unix_seconds();
let token = pass_token
.map(str::to_owned)
.filter(|token| !token.is_empty())
.unwrap_or_else(generate_pass_token);
let row = storage::sqlx::query(
"insert into accounts (
account_high, account_low, pass_token, created_at, last_seen_at,
session_count, play_time_seconds, banned
)
values ($1, $2, $3, $4, $4, 0, 0, false)
on conflict (account_high, account_low) do nothing
returning *",
)
.bind(account.high)
.bind(account.low)
.bind(token)
.bind(now)
.fetch_optional(self.database.pool())
.await?;
storage::sqlx::query(
"select setval('accounts_low_seq', greatest($1, (select last_value from accounts_low_seq)))",
)
.bind(account.low.max(1) as i64)
.execute(self.database.pool())
.await?;
match row {
Some(row) => Ok(account_from_row(&row)),
None => self
.get(account)
.await?
.ok_or_else(|| storage::StorageError::corrupt("the account vanished mid adoption")),
}
}
pub async fn get(&self, account: AccountRef) -> Result<Option<Account>> { pub async fn get(&self, account: AccountRef) -> Result<Option<Account>> {
let row = storage::sqlx::query( let row = storage::sqlx::query(
"select * from accounts where account_high = $1 and account_low = $2", "select * from accounts where account_high = $1 and account_low = $2",

View file

@ -66,7 +66,7 @@ impl HomeMode {
} }
fn market_cost(&self, give: &ShopEntry) -> Result<ShopEntry, &'static str> { fn market_cost(&self, give: &ShopEntry) -> Result<ShopEntry, &'static str> {
if let Some(chest) = give.data.as_treasure_chest() { if let Some(chest) = give.data.as_treasure_chest() {
let price = chest.int("ShopPriceWithoutSpeedUp"); let price = chest.shop_price();
if price < 1 { if price < 1 {
return Err("that chest is not sold in the shop"); return Err("that chest is not sold in the shop");
} }

View file

@ -11,7 +11,15 @@ pub struct ShopOffer {
} }
impl ShopOffer { impl ShopOffer {
pub fn sells(&self, data: &logic::LogicDataRef) -> bool { pub fn sells(&self, data: &logic::LogicDataRef) -> bool {
&self.give.data == data if &self.give.data == data {
return true;
}
match (self.give.data.as_treasure_chest(), data.as_treasure_chest()) {
(Some(offered), Some(wanted)) => {
offered.root().global_id() == wanted.root().global_id()
}
_ => false,
}
} }
} }
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]

View file

@ -47,6 +47,37 @@ impl LogicArenaData {
self.int("Arena") self.int("Arena")
} }
} }
impl LogicTreasureChestData {
pub fn base_chest(&self) -> Option<LogicTreasureChestData> {
let base = self.string("BaseChest");
if base.is_empty() || base == self.name() {
return None;
}
LogicDataRef::by_name(crate::data::tables::table::TREASURE_CHESTS, base).as_treasure_chest()
}
pub fn root(&self) -> LogicTreasureChestData {
match self.base_chest() {
Some(base) => base.root(),
None => self.clone(),
}
}
pub fn arena_data(&self) -> LogicDataRef {
LogicDataRef::by_name(crate::data::tables::table::ARENAS, self.string("Arena"))
}
pub fn shop_price(&self) -> i32 {
self.root().int("ShopPriceWithoutSpeedUp")
}
pub fn speed_up_cost(&self) -> i32 {
self.root().int("SpeedUpCost")
}
pub fn unlock_seconds(&self) -> i32 {
let root = self.root();
root.int("TimeTakenDays").saturating_mul(86_400)
+ root.int("TimeTakenHours").saturating_mul(3_600)
+ root.int("TimeTakenMinutes").saturating_mul(60)
+ root.int("TimeTakenSeconds")
}
}
impl LogicResourcePackData { impl LogicResourcePackData {
pub fn resource(&self) -> LogicDataRef { pub fn resource(&self) -> LogicDataRef {
LogicDataRef::by_name( LogicDataRef::by_name(