flatland_cli/
game_session.rs1use flatland_protocol::AuthCredential;
2use uuid::Uuid;
3
4use crate::api::ControlPlaneClient;
5use crate::auth_context::{load_play_auth, PlayAuth, PlayAuthKind};
6use crate::config::SessionConfig;
7use flatland_client_lib::ConnectOptions;
8
9#[derive(serde::Deserialize)]
10struct CharacterList {
11 characters: Vec<CharacterRow>,
12}
13
14#[derive(serde::Deserialize, Clone)]
15struct CharacterRow {
16 id: Uuid,
17 name: String,
18}
19
20pub async fn prepare_remote_play(
24 character_name: Option<&str>,
25 server: std::net::SocketAddr,
26) -> anyhow::Result<ConnectOptions> {
27 let auth = load_play_auth()?;
28 let client = ControlPlaneClient::new(&auth.api_base);
29 let character = resolve_existing_character(&auth, &client, character_name).await?;
30
31 if auth.kind == PlayAuthKind::Session {
32 if let Ok(mut session) = SessionConfig::load() {
33 let _ = session.remember_character(character.id);
34 }
35 }
36
37 let credential = match auth.kind {
38 PlayAuthKind::Session => AuthCredential::Session { token: auth.bearer },
39 PlayAuthKind::ApiToken => AuthCredential::ApiToken {
40 token: auth.bearer,
41 character_id: character.id,
42 },
43 };
44
45 Ok(
47 ConnectOptions::to_server(&character.name, server)
48 .with_auth(credential, Some(character.id)),
49 )
50}
51
52async fn resolve_existing_character(
53 auth: &PlayAuth,
54 client: &ControlPlaneClient,
55 character_name: Option<&str>,
56) -> anyhow::Result<CharacterRow> {
57 let list: CharacterList = client.get("/v1/characters", &auth.bearer).await?;
58
59 if list.characters.is_empty() {
60 anyhow::bail!(
61 "no characters on this account.\n\
62 Create one first (requires login):\n\
63 flatland3 character create <Name>\n\
64 Then play with:\n\
65 flatland3 --name <Name>"
66 );
67 }
68
69 if let Ok(id_str) = std::env::var("FLATLAND_CHARACTER_ID") {
70 if !id_str.is_empty() {
71 let id: Uuid = id_str
72 .parse()
73 .map_err(|_| anyhow::anyhow!("FLATLAND_CHARACTER_ID must be a valid UUID"))?;
74 if let Some(row) = list.characters.iter().find(|c| c.id == id) {
75 return Ok(row.clone());
76 }
77 anyhow::bail!("no character with FLATLAND_CHARACTER_ID={id}");
78 }
79 }
80
81 if let Some(raw) = character_name.map(str::trim).filter(|s| !s.is_empty()) {
82 if let Some(existing) = list.characters.iter().find(|c| c.name == raw) {
83 return Ok(existing.clone());
84 }
85 let names: Vec<_> = list.characters.iter().map(|c| c.name.as_str()).collect();
86 anyhow::bail!(
87 "character \"{raw}\" not found on this account.\n\
88 Existing: {}\n\
89 Create with: flatland3 character create {raw}\n\
90 Or list with: flatland3 character list",
91 names.join(", ")
92 );
93 }
94
95 if auth.kind == PlayAuthKind::Session {
97 if let Ok(session) = SessionConfig::load() {
98 if let Some(id) = session.last_character_id {
99 if let Some(row) = list.characters.iter().find(|c| c.id == id) {
100 return Ok(row.clone());
101 }
102 }
103 }
104 }
105
106 if list.characters.len() == 1 {
107 return Ok(list.characters[0].clone());
108 }
109
110 let names: Vec<_> = list.characters.iter().map(|c| c.name.as_str()).collect();
111 anyhow::bail!(
112 "multiple characters — pick one:\n\
113 flatland3 --name <Name>\n\
114 Available: {}\n\
115 Or: flatland3 character list",
116 names.join(", ")
117 )
118}