launcher: the game's made-up name can be rolled for a new one

The game's own name (horizon-player.txt) isn't a kept offline name, so
Forget never applied to it and there was no way to be rid of it. Settings
> Account shows it with "New name", which writes a fresh PlayerNNNNN to
every existing game home — the default and each instance's — so the
player stays the same (new) player in its worlds, as PLAYER_FILES' copies
do.

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
socksprox
2026-10-27 18:13:55 +01:00
parent 8cfc13fe2c
commit 9b6faeb8cf
4 changed files with 88 additions and 1 deletions
+2 -1
View File
@@ -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 <name>` (an account or an offline name), `account forget-offline
<name>`.
+28
View File
@@ -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<String, String> {
new_player_name_in(&backend::game_home(), &backend::data_dir())
}
fn new_player_name_in(game_home: &Path, data: &Path) -> Result<String, String> {
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;
+27
View File
@@ -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");
+31
View File
@@ -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()