Skip to main content

flatland_cli/
game_session.rs

1use flatland_protocol::AuthCredential;
2use uuid::Uuid;
3
4use crate::api::ControlPlaneClient;
5use crate::auth_context::{load_play_auth, PlayAuth, PlayAuthKind};
6use crate::characters::CharacterSummary;
7use crate::config::SessionConfig;
8use flatland_client_lib::ConnectOptions;
9
10/// Resolve auth + an **existing** character for remote play.
11///
12/// Never creates characters. Use `flatland3 character create <Name>` first.
13pub async fn prepare_remote_play(
14    character_name: Option<&str>,
15    server: std::net::SocketAddr,
16) -> anyhow::Result<ConnectOptions> {
17    let auth = load_play_auth()?;
18    let client = ControlPlaneClient::new(&auth.api_base);
19    let character = resolve_existing_character(&auth, &client, character_name).await?;
20    if auth.kind == PlayAuthKind::Session {
21        if let Ok(mut session) = SessionConfig::load() {
22            let _ = session.remember_character(character.id);
23        }
24    }
25    connect_options_for(&auth, &character, server)
26}
27
28/// Connect with a character already chosen by gfx onboarding (or similar).
29#[allow(dead_code)] // used by flatland3-gfx via the lib crate
30pub async fn prepare_remote_play_as(
31    character: &CharacterSummary,
32    server: std::net::SocketAddr,
33) -> anyhow::Result<ConnectOptions> {
34    let auth = load_play_auth()?;
35    if auth.kind == PlayAuthKind::Session {
36        if let Ok(mut session) = SessionConfig::load() {
37            let _ = session.remember_character(character.id);
38        }
39    }
40    connect_options_for(&auth, character, server)
41}
42
43fn connect_options_for(
44    auth: &PlayAuth,
45    character: &CharacterSummary,
46    server: std::net::SocketAddr,
47) -> anyhow::Result<ConnectOptions> {
48    let credential = match auth.kind {
49        PlayAuthKind::Session => AuthCredential::Session {
50            token: auth.bearer.clone(),
51        },
52        PlayAuthKind::ApiToken => AuthCredential::ApiToken {
53            token: auth.bearer.clone(),
54            character_id: character.id,
55        },
56    };
57
58    // Hello.client_name is display-only; character_id is authoritative for persistence.
59    Ok(
60        ConnectOptions::to_server(&character.name, server)
61            .with_auth(credential, Some(character.id)),
62    )
63}
64
65async fn resolve_existing_character(
66    auth: &PlayAuth,
67    client: &ControlPlaneClient,
68    character_name: Option<&str>,
69) -> anyhow::Result<CharacterSummary> {
70    #[derive(serde::Deserialize)]
71    struct CharacterList {
72        characters: Vec<CharacterSummary>,
73    }
74
75    let list: CharacterList = client.get("/v1/characters", &auth.bearer).await?;
76
77    if list.characters.is_empty() {
78        anyhow::bail!(
79            "no characters on this account.\n\
80             Create one first (requires login):\n\
81               flatland3 character create <Name>\n\
82             Then play with:\n\
83               flatland3 --name <Name>"
84        );
85    }
86
87    if let Ok(id_str) = std::env::var("FLATLAND_CHARACTER_ID") {
88        if !id_str.is_empty() {
89            let id: Uuid = id_str
90                .parse()
91                .map_err(|_| anyhow::anyhow!("FLATLAND_CHARACTER_ID must be a valid UUID"))?;
92            if let Some(row) = list.characters.iter().find(|c| c.id == id) {
93                return Ok(row.clone());
94            }
95            anyhow::bail!("no character with FLATLAND_CHARACTER_ID={id}");
96        }
97    }
98
99    if let Some(raw) = character_name.map(str::trim).filter(|s| !s.is_empty()) {
100        if let Some(existing) = list.characters.iter().find(|c| c.name == raw) {
101            return Ok(existing.clone());
102        }
103        let names: Vec<_> = list.characters.iter().map(|c| c.name.as_str()).collect();
104        anyhow::bail!(
105            "character \"{raw}\" not found on this account.\n\
106             Existing: {}\n\
107             Create with: flatland3 character create {raw}\n\
108             Or list with: flatland3 character list",
109            names.join(", ")
110        );
111    }
112
113    // No --name: use last played character, else the only character, else ask.
114    if auth.kind == PlayAuthKind::Session {
115        if let Ok(session) = SessionConfig::load() {
116            if let Some(id) = session.last_character_id {
117                if let Some(row) = list.characters.iter().find(|c| c.id == id) {
118                    return Ok(row.clone());
119                }
120            }
121        }
122    }
123
124    if list.characters.len() == 1 {
125        return Ok(list.characters[0].clone());
126    }
127
128    let names: Vec<_> = list.characters.iter().map(|c| c.name.as_str()).collect();
129    anyhow::bail!(
130        "multiple characters — pick one:\n\
131           flatland3 --name <Name>\n\
132         Available: {}\n\
133         Or: flatland3 character list",
134        names.join(", ")
135    )
136}