world_owner_tool/
world_owner_tool.rs

1use brdb::{Brdb, Guid, IntoReader, OwnerTableSoA, pending::BrPendingFs, schemas::OWNER_TABLE_SOA};
2use std::{collections::HashMap, fs::File, io::Read, path::PathBuf, process};
3use uuid::Uuid;
4
5/// Opens a world and replaces its owners with PUBLIC
6fn main() -> Result<(), Box<dyn std::error::Error>> {
7    let mut args = std::env::args().into_iter().peekable();
8    let cmd = args.next().unwrap();
9    if !args.peek().is_some() {
10        println!("usage: {cmd} show world.brdb");
11        println!("usage: {cmd} apply world.brdb owners.csv");
12        println!(
13            "owners.csv must be `display_name,user_name,user_id,old_user_id` where old_user_id is the one to replace."
14        );
15        process::exit(0);
16    }
17
18    let command = args.next().unwrap();
19    if command != "show" && command != "apply" {
20        eprintln!("unknown command. expected `show` or `apply`");
21        process::exit(1);
22    }
23
24    let Some(file) = args.next() else {
25        eprintln!("missing world file arg");
26        process::exit(1);
27    };
28
29    let dst = PathBuf::from(&file);
30    if !dst.exists() {
31        eprintln!("file {file} does not exist");
32        process::exit(1);
33    }
34
35    let db = Brdb::open(dst)?.into_reader();
36
37    let owners = db.owners_soa()?;
38
39    if command == "show" {
40        if args.peek().is_some() {
41            eprintln!("too many arguments!");
42            process::exit(1);
43        }
44
45        let owners_csv = owners
46            .prop("DisplayNames")?
47            .as_array()?
48            .iter()
49            .zip(owners.prop("UserNames")?.as_array()?.iter())
50            .zip(owners.prop("UserIds")?.as_array()?.iter())
51            .map(|((display_name, user_name), user_id)| {
52                format!(
53                    "{},{},{}",
54                    display_name.as_str().unwrap(),
55                    user_name.as_str().unwrap(),
56                    Guid::try_from(user_id).unwrap().uuid(),
57                )
58            })
59            .collect::<Vec<_>>();
60        println!("display_name,user_name,user_id\n{}", owners_csv.join("\n"));
61    } else if command == "apply" {
62        let Some(apply_file) = args.next() else {
63            eprintln!("missing owners csv file arg");
64            process::exit(1);
65        };
66        if args.peek().is_some() {
67            eprintln!("too many arguments!");
68            process::exit(1);
69        }
70
71        let apply_path = PathBuf::from(&apply_file);
72        if !apply_path.exists() {
73            eprintln!("file {apply_file} does not exist");
74            process::exit(1);
75        }
76
77        let mut display_name_index = None;
78        let mut user_name_index = None;
79        let mut user_id_index = None;
80        let mut old_user_id_index = None;
81        let mut apply_data = String::new();
82        File::open(apply_path)?.read_to_string(&mut apply_data)?;
83        let Some((header, rows)) = apply_data.split_once("\n") else {
84            eprintln!("file {apply_file} does not have any rows");
85            process::exit(1);
86        };
87        for (i, key) in header.split(",").enumerate() {
88            match key.trim().to_ascii_lowercase().as_ref() {
89                "display_name" => {
90                    display_name_index = Some(i);
91                }
92                "user_name" => {
93                    user_name_index = Some(i);
94                }
95                "user_id" => {
96                    user_id_index = Some(i);
97                }
98                "old_user_id" => old_user_id_index = Some(i),
99                other => {
100                    eprintln!("unknown column {other} in {apply_file}");
101                    process::exit(1);
102                }
103            }
104        }
105
106        let missing = [
107            ("display_name", display_name_index.is_none()),
108            ("user_name", user_name_index.is_none()),
109            ("user_id", user_id_index.is_none()),
110            ("old_user_id", old_user_id_index.is_none()),
111        ]
112        .into_iter()
113        .filter_map(|(k, cond)| cond.then_some(k.to_owned()))
114        .collect::<Vec<_>>();
115        if !missing.is_empty() {
116            eprintln!("missing columns: {}", missing.join(","));
117            process::exit(1);
118        }
119
120        let display_name_index = display_name_index.unwrap();
121        let user_name_index = user_name_index.unwrap();
122        let user_id_index = user_id_index.unwrap();
123        let old_user_id_index = old_user_id_index.unwrap();
124
125        // Parse the owners from BrdbValues
126        let mut new_soa = OwnerTableSoA::try_from(&owners.to_value())?;
127        let owners_lut = rows
128            .trim()
129            .split("\n")
130            .map(|r| r.trim().split(",").collect::<Vec<&str>>())
131            .map(|cols| {
132                let user_name = cols[user_name_index];
133                let display_name = cols[display_name_index];
134                let user_id = Uuid::parse_str(&cols[user_id_index])
135                    .expect(&format!("invalid uuid: {}", cols[user_id_index]));
136                let old_user_id = Uuid::parse_str(&cols[old_user_id_index])
137                    .expect(&format!("invalid old uuid: {}", cols[old_user_id_index]));
138                (old_user_id, (user_name, display_name, user_id))
139            })
140            .collect::<HashMap<_, _>>();
141        println!("{owners_lut:?}");
142
143        let mut changes = 0;
144
145        for i in 0..new_soa.user_ids.len() {
146            let old_id = new_soa.user_ids[i].uuid();
147            let Some((user_name, display_name, user_id)) = owners_lut.get(&old_id) else {
148                println!("missing old id for {old_id} - ignoring");
149                continue;
150            };
151            println!("replacing {old_id} with {user_id} - {user_name} ({display_name})");
152            new_soa.user_names[i] = (*user_name).to_owned();
153            new_soa.display_names[i] = (*display_name).to_owned();
154            new_soa.user_ids[i] = Guid::from_uuid((*user_id).clone());
155            changes += 1;
156        }
157
158        if changes == 0 {
159            println!("world left unchanged");
160            std::process::exit(0);
161        }
162
163        // convert the owners struct of arrays into bytes using the owners schema
164        let content = db.owners_schema()?.write_brdb(OWNER_TABLE_SOA, &new_soa)?;
165
166        let patch = BrPendingFs::Root(vec![(
167            "World".to_owned(),
168            BrPendingFs::Folder(Some(vec![(
169                "0".to_string(),
170                BrPendingFs::Folder(Some(vec![(
171                    "Owners.mps".to_string(),
172                    BrPendingFs::File(Some(content)),
173                )])),
174            )])),
175        )]);
176        db.write_pending("Replace owners", db.to_pending_patch()?.with_patch(patch)?)?;
177        println!("revision created")
178    }
179
180    Ok(())
181}