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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
use std::path::PathBuf;
/// Defines the type of value an argument can hold.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ValueType {
Flag,
Float,
Integer,
String,
Path,
}
/// Defines the common interface for a compiler tool.
pub trait Compiler: Default {
/// The specific argument type associated with this compiler.
type Arg: CompilerArg;
/// Returns the human-readable name of the compiler.
fn name(&self) -> &'static str;
/// Returns a brief description of what the compiler does.
fn description(&self) -> &'static str;
/// Returns the default working directory for the compiler.
fn working_dir_template(&self) -> &'static str;
/// Returns a slice of the arguments configured for this compiler instance.
fn get_args(&self) -> &[Self::Arg];
/// Adds a configured argument to this compiler instance.
fn add_arg(&mut self, arg: Self::Arg);
/// Removes all user-configured arguments. Base arguments are unaffected.
fn clear_args(&mut self);
/// Convenience method for getting all metadata at once
fn get_metadata(&self) -> CompilerMetadata {
CompilerMetadata {
name: self.name(),
description: self.description(),
working_dir_template: self.working_dir_template(),
}
}
/// Returns the configured arguments in a structured format.
fn get_structured_args(&self) -> Vec<(&'static str, Option<String>)> {
self.get_args().iter().map(|arg| arg.as_arg()).collect()
}
/// Builds the final, flattened list of command-line arguments for execution.
fn build_args(&self) -> Vec<String> {
let structured_args = self.get_structured_args();
let mut final_args = Vec::with_capacity(structured_args.len() * 2);
for (key, value_opt) in structured_args {
if !key.is_empty() {
final_args.push(key.to_string());
}
if let Some(value) = value_opt {
final_args.push(value);
}
}
final_args
}
/// Build the final command-line string for execution using context values.///
/// If provided, `executable` will be used as the compiler path. Otherwise,
/// it defaults to `<bin_dir>/<compiler_name>.exe`.
fn build_command(&self, context: &CompilerContext, executable: Option<PathBuf>) -> CommandInfo {
let final_args = self
.build_args()
.iter()
.map(|arg| context.replace(arg))
.collect();
let resolved_wd = PathBuf::from(context.replace(self.working_dir_template()));
let compiler_path = if let Some(path) = executable {
path
} else {
context.bin_dir.join(&self.name().to_lowercase()).with_extension("exe")
};
CommandInfo {
name: self.name(),
compiler_path: compiler_path,
args: final_args,
working_dir: resolved_wd,
}
}
}
/// Defines the common interface for a compiler argument.
pub trait CompilerArg: Sized {
/// Returns the human-readable name of the argument.
fn name(&self) -> &'static str;
/// Returns a detailed description of the argument's purpose.
fn description(&self) -> &'static str;
/// Returns the type of value this argument holds (e.g., Flag, Float).
fn value_type(&self) -> ValueType;
/// Returns the default value for this argument, if one is defined.
fn get_default_value(&self) -> Option<Self>;
/// Formats the argument and its value (if any) into a command-line string.
fn as_arg(&self) -> (&'static str, Option<String>);
/// Whether this argument is used by the compiler by default.
fn is_default(&self) -> bool;
/// Returns a slice of game App IDs this argument is compatible with.
/// Returns `None` if the argument is universally compatible (i.e., has no game constraints).
fn compatible_games(&self) -> Option<&'static [u32]>;
/// Checks if this argument is compatible with a specific game App ID.
fn is_compatible_with_game(&self, app_id: u32) -> bool {
match self.compatible_games() {
Some(games) => games.contains(&app_id),
None => true,
}
}
}
/// Holds the concrete values for placeholders used in compiler arguments.
#[derive(Debug, Clone, Default)]
pub struct CompilerContext {
// Base paths
pub bin_dir: PathBuf,
pub game_dir: PathBuf,
pub map_path: PathBuf,
pub out_dir: PathBuf,
// Derived values (computed at creation)
pub map_dir: PathBuf, // Directory containing the map
pub map_name: String, // Filename without extension (e.g., "de_dust2")
pub map_name_ext: String, // Filename with extension (e.g., "de_dust2.vmf")
pub map_ext: String, // File extension (e.g., "vmf")
pub bsp_path: PathBuf, // Full path to the .bsp file
}
impl CompilerContext {
pub fn new(
bin_dir: Option<PathBuf>,
game_dir: Option<PathBuf>,
map_path: Option<PathBuf>,
out_dir: Option<PathBuf>,
) -> Self {
let map_path = map_path.unwrap_or_default();
let map_dir = map_path.parent().map(PathBuf::from).unwrap_or_default();
// If out_dir is not specified, it defaults to the map directory
let out_dir = out_dir.unwrap_or_else(|| map_dir.clone());
// Calculate all derived values
let map_name = map_path.file_stem().and_then(|s| s.to_str()).unwrap_or("").to_string();
let map_name_ext = map_path.file_name().and_then(|s| s.to_str()).unwrap_or("").to_string();
let map_ext = map_path.extension().and_then(|s| s.to_str()).unwrap_or("").to_string();
let bsp_path = map_path.with_extension("bsp");
Self {
bin_dir: bin_dir.unwrap_or_default(),
game_dir: game_dir.unwrap_or_default(),
map_path: map_path.clone(),
out_dir,
map_dir,
map_name,
map_name_ext,
map_ext,
bsp_path,
}
}
/// Replaces placeholders in the string in a single pass and returns a new string.
pub fn replace(&self, input: &str) -> String {
// Pre-allocate memory to avoid reallocations.
// Take the original length + a small buffer.
let mut output = String::with_capacity(input.len() + 32);
let mut last_match_end = 0;
// Iterate over all occurrences of the '$' character
for (start_of_match, _) in input.match_indices('$') {
// Skip if this part of the string has already been processed
if start_of_match < last_match_end {
continue;
}
// Add the part of the string from the end of the last match to the start of the current one
output.push_str(&input[last_match_end..start_of_match]);
// Check what follows the '$'
let remaining = &input[start_of_match..];
let placeholder_value = self.get_placeholder_value(remaining);
if let Some((placeholder_key, value)) = placeholder_value {
// Placeholder found, append its value
output.push_str(&value);
// Advance the cursor by the length of the placeholder (e.g., "$mapName")
last_match_end = start_of_match + placeholder_key.len() + 1; // +1 for '$'
} else {
// This is not a placeholder, just a '$' character. Add it as is.
output.push('$');
last_match_end = start_of_match + 1;
}
}
// Add the remaining tail of the string after the last match
if last_match_end < input.len() {
output.push_str(&input[last_match_end..]);
}
output
}
/// Helper function for matching a string slice with known placeholders.
/// Returns (key, value) on success.
fn get_placeholder_value(&self, remaining_slice: &str) -> Option<(&str, String)> {
// We use starts_with, which is a very fast operation for &str
if remaining_slice.starts_with("$binDir") {
Some(("binDir", self.bin_dir.to_string_lossy().into_owned()))
} else if remaining_slice.starts_with("$gameDir") {
Some(("gameDir", self.game_dir.to_string_lossy().into_owned()))
} else if remaining_slice.starts_with("$mapPath") {
Some(("mapPath", self.map_path.to_string_lossy().into_owned()))
} else if remaining_slice.starts_with("$outDir") {
Some(("outDir", self.out_dir.to_string_lossy().into_owned()))
} else if remaining_slice.starts_with("$mapDir") {
Some(("mapDir", self.map_dir.to_string_lossy().into_owned()))
} else if remaining_slice.starts_with("$mapNameExt") {
Some(("mapNameExt", self.map_name_ext.clone()))
} else if remaining_slice.starts_with("$mapName") { // Important: $mapNameExt must come before $mapName
Some(("mapName", self.map_name.clone()))
} else if remaining_slice.starts_with("$mapExt") {
Some(("mapExt", self.map_ext.clone()))
} else if remaining_slice.starts_with("$bspPath") {
Some(("bspPath", self.bsp_path.to_string_lossy().into_owned()))
}
// Aliases
else if remaining_slice.starts_with("$file") {
Some(("file", self.map_name.clone()))
} else if remaining_slice.starts_with("$path") {
Some(("path", self.map_path.to_string_lossy().into_owned()))
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct CommandInfo {
pub name: &'static str,
/// The path to the compiler executable.
pub compiler_path: PathBuf,
/// The complete, resolved argument list.
pub args: Vec<String>,
/// The final working directory for the command.
pub working_dir: PathBuf,
}
/// A struct for convenience, serialization, and data transfer.
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serialization", derive(serde::Serialize, serde::Deserialize))]
pub struct CompilerMetadata {
/// The human-readable name of the compiler.
pub name: &'static str,
/// A brief description of what the compiler does.
pub description: &'static str,
/// The default working directory for the compiler.
pub working_dir_template: &'static str,
}
#[derive(Debug, PartialEq)]
pub enum ParseArgError {
/// The provided argument was not recognized by the compiler.
UnknownArgument(String),
/// An argument that requires a value was provided without one.
MissingValue(&'static str),
/// A flag argument, which does not accept a value, was provided with one.
UnexpectedValue(&'static str),
/// The value provided for an argument was not in the expected format.
InvalidValue {
argument: &'static str,
value: String,
},
}
impl std::fmt::Display for ParseArgError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownArgument(arg) => write!(f, "unknown argument: {}", arg),
Self::MissingValue(arg) => write!(f, "argument '{}' requires a value, but none was provided", arg),
Self::UnexpectedValue(arg) => write!(f, "argument '{}' is a flag and does not accept a value", arg),
Self::InvalidValue { argument, value } => write!(f, "invalid value '{}' for argument '{}'", value, argument),
}
}
}
impl std::error::Error for ParseArgError {}
include!(concat!(env!("OUT_DIR"), "/generated_compilers.rs"));