diff --git a/launcher/README.md b/launcher/README.md index 66be2e436..02267710f 100644 --- a/launcher/README.md +++ b/launcher/README.md @@ -126,7 +126,8 @@ The opening, then Play ([clip](docs/opening.webm)): each signed-in Microsoft account, the offline names the player keeps, and the name the game made up (`horizon-player.txt`). Picking one plays as it from the next Play. Settings > Account adds offline names (3 to 16 letters, digits or `_`; the game gets `--name`) and forgets them, - skin and cape files included; the Console's Share log takes the names out of the log too. + skin and cape files included, and rolls the game's made-up name for a new one; the Console's + Share log takes the names out of the log too. `horizon-launcher account offline [NAME]`, `account select ` (an account or an offline name), `account forget-offline `. diff --git a/launcher/src/instances.rs b/launcher/src/instances.rs index 0d47386cb..581e88076 100644 --- a/launcher/src/instances.rs +++ b/launcher/src/instances.rs @@ -491,6 +491,34 @@ fn follow_player(game_home: &Path, home: &Path) { } } +/// A fresh made-up name everywhere the game keeps one: `horizon-player.txt` is per game +/// folder (`PLAYER_FILES`), so the same new name goes to the default home and every +/// instance's — the player stays one player in its worlds. The file's `name:` line and the +/// `Player` + five digits scheme are horizon-client's (`offline_name.rs`). +pub fn new_player_name() -> Result { + new_player_name_in(&backend::game_home(), &backend::data_dir()) +} + +fn new_player_name_in(game_home: &Path, data: &Path) -> Result { + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_or(0, |d| d.as_nanos() as u64); + let name = format!( + "Player{:05}", + (nanos ^ u64::from(std::process::id()).rotate_left(32)) % 100_000 + ); + for instance in list_in(game_home, data) { + if instance.home.is_dir() { + fs::write( + instance.home.join("horizon-player.txt"), + format!("name:{name}\n"), + ) + .map_err(|e| format!("{}: {e}", instance.home.display()))?; + } + } + Ok(name) +} + #[cfg(test)] #[path = "instances/tests.rs"] mod tests; diff --git a/launcher/src/instances/tests.rs b/launcher/src/instances/tests.rs index b2e2fe6fe..5ca88eeba 100644 --- a/launcher/src/instances/tests.rs +++ b/launcher/src/instances/tests.rs @@ -67,6 +67,33 @@ fn player_home(home: &Path) { ); } +#[test] +fn a_new_player_name_replaces_the_kept_one_everywhere() { + let root = scratch("newname"); + let (home, data) = (root.join("horizon"), root.join("launcher")); + player_home(&home); + // An own instance that has the old name, and one with no file yet. + put( + &home.join("instances/one/horizon-player.txt"), + "name:Player123\n", + ); + fs::create_dir_all(home.join("instances/two")).unwrap(); + let name = new_player_name_in(&home, &data).unwrap(); + assert_ne!(name, "Player123"); + assert!(name.starts_with("Player") && name.len() == 11); + let kept = |home: &Path| fs::read_to_string(home.join("horizon-player.txt")).unwrap(); + for h in [ + &home, + &home.join("instances/one"), + &home.join("instances/two"), + &home.join("ports/small-pack/home"), + &home.join("horizon-packs/cozy/home"), + ] { + assert_eq!(kept(h), format!("name:{name}\n"), "{}", h.display()); + } + let _ = fs::remove_dir_all(&root); +} + #[test] fn existing_worlds_stay_where_they_are_and_every_folder_becomes_an_instance() { let root = scratch("migrate"); diff --git a/launcher/src/switch_page.rs b/launcher/src/switch_page.rs index c10f2deeb..97c952cab 100644 --- a/launcher/src/switch_page.rs +++ b/launcher/src/switch_page.rs @@ -212,6 +212,37 @@ impl Launcher { )); } rows = rows.children(self.skin_panel(cx)); + // The name the game made up can't be forgotten — it's what an offline player is — + // only rolled for a new one. + if let Some(made_up) = self.offline_name.clone() { + let plays = now == Player::GameName; + n += 1; + rows = rows.child(index_row( + n, + made_up, + Some( + div() + .child(tr(if plays { + "Offline name · the game's own · plays" + } else { + "Offline name · the game's own" + })) + .into_any_element(), + ), + Some( + button("game-name-new", "New name", Kind::Ghost, &t) + .on_click(cx.listener(|this, _: &ClickEvent, _, cx| { + match instances::new_player_name() { + Ok(name) => this.offline_name = Some(name), + Err(e) => this.sign_in_state = Some(SignInState::Done(Err(e))), + } + cx.notify(); + })) + .into_any_element(), + ), + &t, + )); + } n += 1; let add = div() .flex()