1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
// SPDX-License-Identifier: GPL-3.0-only
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "vfstool",
about = "vfstool allows users to reconstruct and interact with OpenMW's virtual file system in any way they might see fit, using this application to locate files, serialize their VFS to most major text formats, extract files out of the vfs, and even collapse their VFS to a single directory for space savings."
)]
pub struct Cli {
/// Path to openmw.cfg.
///
/// Note this is the directory containing it, not the path to the file itself.
///
/// Example: C:\Documents\My Games\openmw
///
/// This argument assumes the config used is called `openmw.cfg`
/// (case-insensitive).
///
/// If you need to use an openmw.cfg which is named something else,
///
/// set the `OPENMW_CONFIG` variable to the absolute path of your desired config file instead.
#[arg(short, long)]
pub config: Option<PathBuf>,
/// Whether or not to use relative paths in output
#[arg(short = 'r', long)]
pub use_relative: bool,
#[command(subcommand)]
pub command: Commands,
}
/// Subcommands for `vfstool`
#[derive(Subcommand)]
pub enum Commands {
/// Given a target directory, create a set of hardlinks for the entire virtual
/// filesystem inside of it. Skyrim support ;)
Collapse {
/// Target folder to collapse the VFS into
collapse_into: PathBuf,
/// If this is used, any case where hard linking failed or won't work (files in BSA
/// archives), falls back to normal copying operations
#[arg(short, long)]
allow_copying: bool,
/// If enabled, allows extracting files out of BSA/BA2 archives during collapsing
#[arg(short, long)]
extract_archives: bool,
/// Use symbolic instead of hardlinks, to allow cross-device links
#[arg(short, long)]
symbolic: bool,
/// Print the materialization plan instead of writing files
#[arg(long)]
dry_run: bool,
/// Output format for --dry-run.
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
/// Path to save the --dry-run plan to.
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Extract a given file from the VFS into a given directory
Extract {
/// Full relative path to a VFS file, eg `meshes/xbase_anim.nif`
source_file: PathBuf,
/// Directory to extract the file to
target_dir: PathBuf,
},
/// Given some VFS path, like `meshes/xbase_anim.nif`, return its absolute path (if found)
FindFile {
/// Full (relative) VFS Path to query.
/// Returns the absolute path, of the file referenced by this VFS path. EG:
///
/// vfstool find-file `meshes/xbase_anim.nif`
///
/// C:\Games\Morrowind\Data `Files\Meshes\XBase_Anim.nif`
path: PathBuf,
/// If set, only matches files which are NOT inside an archive (BSA/BA2).
/// Exits with code 2 if the file exists but is archived.
#[arg(short = 'p', long = "only_physical")]
only_physical: bool,
/// Simple output, no coloration or formatting. Useful for pipes
#[arg(short, long)]
simple: bool,
},
/// Given some query term, locate all matches in the vfs.
Find {
/// VFS Path to query. Supports regular expressions!
path: PathBuf,
/// Output format when serializing as text.
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
/// Path to save the resulting search tree to.
///
/// If omitted, the result is printed directly to stdout.
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Given an absolute path, return a filtered version of the VFS containing either things
/// replacing it, or files from this directory which are not being replaced
Remaining {
filter_path: PathBuf,
/// If used, show only files replacing contents of this path, instead of ones still in it
#[arg(short, long)]
replacements_only: bool,
/// Output format when serializing as text.
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
/// Path to save the resulting search tree to.
///
/// If omitted, the result is printed directly to stdout.
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Analyse conflict relationships across all sources in the load order
Conflicts {
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Show sources whose files are entirely overridden by higher-priority sources
Shadowed {
/// Include the full list of overridden files for each shadowed source
#[arg(short = 'l', long)]
list_files: bool,
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Explain the full provider chain for a VFS path
Explain {
/// Relative VFS path to query, e.g. `textures/tx_bc_mudcrab.dds`
path: PathBuf,
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
/// List VFS keys with more than one provider
Duplicates {
/// Optional regex over normalized VFS keys, e.g. `^meshes/` or `textures/.*\\.dds$`
pattern: Option<String>,
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
/// List loaded archives and winning entry counts
Archives {
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
/// List entries supplied by one archive
ArchiveList {
/// Archive selector: full path, unique filename, or unique path suffix
#[arg(required_unless_present = "source_index")]
archive: Option<PathBuf>,
/// Archive source index as printed by `vfstool archives`
#[arg(long, conflicts_with = "archive")]
source_index: Option<usize>,
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Report per-source provider contribution counts
Contributions {
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Validate configured paths and referenced files
Validate {
/// Also build the VFS before checking configured file references.
///
/// This lets validation account for configured archives. Omit it for the fast loose-file
/// reference validation path.
#[arg(long)]
full: bool,
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Compare files between two specific data directories
Diff {
/// First directory (absolute path matching a data= entry)
source_a: PathBuf,
/// Second directory (absolute path matching a data= entry)
source_b: PathBuf,
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Dump the VFS to a directory, run a command, then capture new/modified files to data-local.
///
/// Use {} in the command arguments as a placeholder for the merged directory path.
///
/// Example: vfstool run /tmp/merged -- tes3conv {} output.json
Run {
/// Directory to dump the merged VFS into
merged_dir: PathBuf,
/// Command and arguments to execute
#[arg(trailing_var_arg = true, required = true)]
command: Vec<String>,
/// Keep the merged directory after the command exits
#[arg(long)]
keep_merged: bool,
/// Destination for captured files (defaults to data-local from openmw.cfg)
#[arg(long)]
output: Option<PathBuf>,
/// Always copy files instead of hardlinking them.
/// Hardlinks are used by default to avoid duplicating data on disk.
#[arg(long)]
copy: bool,
/// Working directory for the child process.
///
/// Defaults to the current working directory if not set.
#[arg(long)]
working_dir: Option<PathBuf>,
},
/// Emit deterministic lock manifest for current winners.
Lock {
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Compare current VFS state to a lock manifest.
Drift {
/// Path to lock manifest (yaml/json/toml; inferred from extension).
lock_file: PathBuf,
/// If set, drift causes exit code 4.
#[arg(long)]
fail_on_drift: bool,
#[arg(short, long, value_enum, default_value = "yaml")]
format: OutputFormat,
#[arg(short, long)]
output: Option<PathBuf>,
},
}
/// Supported output formats
#[derive(Debug, ValueEnum, Clone, Copy)]
pub enum OutputFormat {
Json,
Yaml,
Toml,
}