world_owner_tool/
world_owner_tool.rs1use 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
5fn 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 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 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}