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::characters::CharacterSummary;
7use crate::config::SessionConfig;
8use flatland_client_lib::ConnectOptions;
9
10pub 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#[allow(dead_code)] pub 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 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 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}