Add custom card handling probably

This commit is contained in:
Ethan O'Brien
2026-07-31 20:32:02 -05:00
parent d077210efe
commit 26926e1a28
23 changed files with 3746 additions and 67 deletions

View File

@@ -1,6 +1,9 @@
use jzon::{array, object, JsonValue};
use lazy_static::lazy_static;
use std::collections::HashMap;
use crate::router::userdata;
use crate::router::global;
use crate::router::{card, custom_card, databases};
fn get_clear_count(user: &JsonValue, level: i32) -> i64 {
let mut rv = 0;
@@ -67,34 +70,66 @@ pub enum UserView {
const DEFAULT_CARD: i64 = 10010001;
fn proxy_card_id(id: i64) -> i64 {
let prefix = id / 10000;
if prefix < 10000 {
return id;
}
let rv = if prefix < 14000 {
(prefix - 9000) * 10000 + 1
} else {
DEFAULT_CARD
lazy_static! {
// Each character's lowest official card id: the stand-in shown to viewers
// who can't resolve a custom card of that character
static ref OFFICIAL_CARD_BY_CHARACTER: HashMap<i64, i64> = {
let mut rv: HashMap<i64, i64> = HashMap::new();
for entry in databases::CARD_LIST.entries() {
let Some(id) = entry.1["id"].as_i64() else { continue; };
if card::is_custom(id) {
continue;
}
let Some(character) = entry.1["masterCharacterId"].as_i64() else { continue; };
let slot = rv.entry(character).or_insert(id);
if id < *slot {
*slot = id;
}
}
rv
};
// Not every prefix has a real character behind it
if crate::router::databases::CARD_LIST[rv.to_string()].is_empty() {
return DEFAULT_CARD;
}
rv
}
pub fn proxy_user_cards(user: &mut JsonValue) {
// The character behind any card id: baked masterdata first, then the runtime
// custom-card db, then the imported band's id arithmetic (prefix - 9000)
fn card_character(id: i64) -> Option<i64> {
let card = &databases::CARD_LIST[id.to_string()];
if !card.is_empty() {
return card["masterCharacterId"].as_i64();
}
if custom_card::is_custom_runtime(id) {
return custom_card::character_of(id);
}
Some(id / 10000 - 9000)
}
// A custom card the viewer can't resolve shows as its character's base
// official card - the right face, never a crash. Characters with no official
// card (custom ones included) fall back to the default
fn proxy_card_id(id: i64) -> i64 {
if !card::is_custom(id) {
return id;
}
let Some(character) = card_character(id) else {
return DEFAULT_CARD;
};
let Some(rv) = OFFICIAL_CARD_BY_CHARACTER.get(&character) else {
return DEFAULT_CARD;
};
*rv
}
pub fn proxy_user_cards(user: &mut JsonValue, protocol: u32) {
for key in ["favorite_master_card_id", "guest_smile_master_card_id", "guest_cool_master_card_id", "guest_pure_master_card_id"] {
let id = user["user"][key].as_i64().unwrap_or(0);
if crate::router::card::is_custom(id) {
if !custom_card::viewer_can_resolve(id, protocol) {
user["user"][key] = proxy_card_id(id).into();
}
}
// A card's id is its master_card_id, so both sides need proxying
for key in ["favorite_card", "guest_smile_card", "guest_cool_card", "guest_pure_card"] {
let id = user[key]["master_card_id"].as_i64().unwrap_or(0);
if crate::router::card::is_custom(id) {
if !custom_card::viewer_can_resolve(id, protocol) {
user[key]["id"] = proxy_card_id(id).into();
user[key]["master_card_id"] = proxy_card_id(id).into();
}
@@ -102,7 +137,8 @@ pub fn proxy_user_cards(user: &mut JsonValue) {
if !user["main_deck_detail"].is_empty() {
let mut used = array![];
for id in user["main_deck_detail"]["deck"]["main_card_ids"].members_mut() {
let card = proxy_card_id(id.as_i64().unwrap_or(0));
let raw = id.as_i64().unwrap_or(0);
let card = if custom_card::viewer_can_resolve(raw, protocol) { raw } else { proxy_card_id(raw) };
// Whole characters share one proxy, and the client can't hold the
// same card twice
if card == 0 || used.contains(card) {
@@ -116,7 +152,7 @@ pub fn proxy_user_cards(user: &mut JsonValue) {
let mut ids = array![];
for card in user["main_deck_detail"]["card_list"].members() {
let id = card["master_card_id"].as_i64().unwrap_or(0);
let proxy = proxy_card_id(id);
let proxy = if custom_card::viewer_can_resolve(id, protocol) { id } else { proxy_card_id(id) };
if ids.contains(proxy) {
continue;
}
@@ -132,7 +168,7 @@ pub fn proxy_user_cards(user: &mut JsonValue) {
}
}
pub fn get_user(id: i64, friends: &JsonValue, view: UserView, custom_cards: bool) -> JsonValue {
pub fn get_user(id: i64, friends: &JsonValue, view: UserView, protocol: u32) -> JsonValue {
let user = userdata::get_acc_from_uid(id);
if !user["error"].is_empty() {
return object!{};
@@ -182,9 +218,124 @@ pub fn get_user(id: i64, friends: &JsonValue, view: UserView, custom_cards: bool
}
}
if !custom_cards {
proxy_user_cards(&mut rv);
}
proxy_user_cards(&mut rv, protocol);
rv
}
#[cfg(test)]
mod tests {
use super::*;
use crate::database::custom_card as db;
fn wipe(owner: i64) {
for card in db::get_cards_by_owner(owner).members() {
db::delete_card(card["master_card_id"].as_i64().unwrap());
}
for character in db::get_characters_by_owner(owner).members() {
db::delete_character(character["master_character_id"].as_i64().unwrap());
}
}
// The imported band proxies to its own character's base card, not to a
// single default (the old prefix arithmetic collapsed 14000+ to Honoka)
#[test]
fn proxies_resolve_through_the_character() {
let _lock = crate::runtime::lock_test_data_path();
wipe(5101);
assert_eq!(proxy_card_id(10010001), 10010001);
assert_eq!(proxy_card_id(0), 0);
assert_eq!(proxy_card_id(100010001), 10010001);
assert_eq!(proxy_card_id(100090001), 10090001);
assert_eq!(proxy_card_id(110010001), 20010001);
assert_eq!(proxy_card_id(130010001), 40010001);
assert_eq!(proxy_card_id(140010001), DEFAULT_CARD);
// A runtime card on an official character proxies to that character
let id = db::next_card_id();
db::insert_card(id, 2003, 5101, &jzon::object!{ "master_card_id": id, "rarity": 1 }, true, false);
assert_eq!(proxy_card_id(id), 20030001);
// On a custom character (no official card) it falls to the default
let orphan = db::next_card_id();
db::insert_card(orphan, db::FIRST_CHARACTER_ID, 5101, &jzon::object!{ "master_card_id": orphan, "rarity": 1 }, true, false);
assert_eq!(proxy_card_id(orphan), DEFAULT_CARD);
// A deleted/unknown runtime id can't resolve a character either
let unknown = db::next_card_id() + 5000;
assert_eq!(proxy_card_id(unknown), DEFAULT_CARD);
// A proxy is never zero and always a real row
for id in [100010001i64, 141720001, 149990001, id, orphan, unknown] {
let proxy = proxy_card_id(id);
assert_ne!(proxy, 0, "id {}", id);
assert!(!databases::CARD_LIST[proxy.to_string()].is_empty(), "id {}", id);
}
wipe(5101);
}
// Even a protocol-3 viewer gets a proxy for a draft - published is what
// makes a runtime card resolvable, and older viewers proxy everything
#[test]
fn drafts_and_old_viewers_get_proxies() {
let _lock = crate::runtime::lock_test_data_path();
wipe(5102);
let published = db::next_card_id();
db::insert_card(published, 2003, 5102, &jzon::object!{ "master_card_id": published, "rarity": 1 }, true, false);
let draft = db::next_card_id();
db::insert_card(draft, 2003, 5102, &jzon::object!{ "master_card_id": draft, "rarity": 1 }, false, false);
assert!(custom_card::viewer_can_resolve(published, custom_card::PROTOCOL_VERSION));
assert!(!custom_card::viewer_can_resolve(draft, custom_card::PROTOCOL_VERSION));
let mut user = jzon::object!{
"user": {
"favorite_master_card_id": draft,
"guest_smile_master_card_id": published
},
"favorite_card": { "id": draft, "master_card_id": draft },
"guest_smile_card": { "id": published, "master_card_id": published },
"main_deck_detail": {
"deck": { "main_card_ids": [draft, published, 10010001, 0, 0] },
"card_list": [
{ "id": draft, "master_card_id": draft },
{ "id": published, "master_card_id": published },
{ "id": 10010001, "master_card_id": 10010001 }
]
}
};
proxy_user_cards(&mut user, custom_card::PROTOCOL_VERSION);
assert_eq!(user["user"]["favorite_master_card_id"].as_i64(), Some(20030001));
assert_eq!(user["user"]["guest_smile_master_card_id"].as_i64(), Some(published));
assert_eq!(user["favorite_card"]["master_card_id"].as_i64(), Some(20030001));
assert_eq!(user["guest_smile_card"]["master_card_id"].as_i64(), Some(published));
assert_eq!(user["main_deck_detail"]["deck"]["main_card_ids"][0].as_i64(), Some(20030001));
assert_eq!(user["main_deck_detail"]["deck"]["main_card_ids"][1].as_i64(), Some(published));
assert_eq!(user["main_deck_detail"]["deck"]["main_card_ids"][2].as_i64(), Some(10010001));
assert_eq!(user["main_deck_detail"]["card_list"].len(), 3);
for card in user["main_deck_detail"]["card_list"].members() {
assert_ne!(card["master_card_id"].as_i64(), Some(draft));
}
// Protocol 0-2 viewers proxy even the published runtime card
for protocol in [0u32, 1, 2] {
let mut user = jzon::object!{
"user": { "favorite_master_card_id": published },
"favorite_card": { "id": published, "master_card_id": published }
};
proxy_user_cards(&mut user, protocol);
assert_eq!(user["user"]["favorite_master_card_id"].as_i64(), Some(20030001), "protocol {}", protocol);
}
// The baked import band needs only protocol 2
let mut user = jzon::object!{
"user": { "favorite_master_card_id": 100010001 },
"favorite_card": { "id": 100010001, "master_card_id": 100010001 }
};
proxy_user_cards(&mut user, 2);
assert_eq!(user["user"]["favorite_master_card_id"].as_i64(), Some(100010001));
proxy_user_cards(&mut user, 1);
assert_eq!(user["user"]["favorite_master_card_id"].as_i64(), Some(10010001));
wipe(5102);
}
}