Typemap / exclusive-lock (+l) manager for Unreal binaries

- Tools -> Exclusive Locks (typemap): view/edit server typemap, one-click add
  recommended binary+l rules for UE asset types (.uasset/.umap/.fbx/textures/etc)
- Per-file 'Set exclusive-lock type (+l)' in the Changes context menu
- Backend p4 typemap -o/-i and reopen -t

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bonchellon
2026-07-08 11:41:24 +03:00
parent b40358e0f5
commit e2ad438ed4
5 changed files with 185 additions and 1 deletions

View File

@ -1314,6 +1314,87 @@ async fn p4_unlock(state: State<'_, AppState>, files: Vec<String>) -> Result<Str
run_text(&conn, &args)
}
/// Read the server typemap as a list of "TYPE PATTERN" entries.
/// (Empty list if the typemap is unset. Requires admin to *change*, not to read.)
#[tauri::command]
async fn p4_typemap_get(state: State<'_, AppState>) -> Result<Vec<String>, String> {
let conn = current(&state)?;
let spec = run_text(&conn, &["typemap", "-o"])?;
let mut entries = Vec::new();
let mut in_map = false;
for line in spec.lines() {
if line.starts_with("TypeMap:") {
in_map = true;
continue;
}
if in_map {
if line.starts_with('\t') || line.starts_with(' ') {
let e = line.trim();
if !e.is_empty() && !e.starts_with('#') {
entries.push(e.to_string());
}
} else if !line.trim().is_empty() {
break; // reached the next spec field
}
}
}
Ok(entries)
}
/// Replace the server typemap with the given "TYPE PATTERN" entries.
/// Requires admin rights on the server.
#[tauri::command]
async fn p4_typemap_set(state: State<'_, AppState>, entries: Vec<String>) -> Result<String, String> {
let conn = current(&state)?;
let mut spec = String::from("TypeMap:\n");
for e in &entries {
let e = e.trim();
if !e.is_empty() {
spec.push('\t');
spec.push_str(e);
spec.push('\n');
}
}
let mut child = p4_cmd(&conn, false)
.args(["typemap", "-i"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to start p4: {e}"))?;
if let Some(si) = child.stdin.as_mut() {
si.write_all(spec.as_bytes()).map_err(|e| e.to_string())?;
}
let out = child.wait_with_output().map_err(|e| e.to_string())?;
if !out.status.success() {
let err = decode(&out.stderr);
let msg = if err.trim().is_empty() { decode(&out.stdout) } else { err };
let low = msg.to_lowercase();
return Err(if low.contains("permission") || low.contains("protect") || low.contains("admin") || low.contains("not allowed") {
"You need admin rights on the server to change the typemap.".to_string()
} else {
msg.trim().to_string()
});
}
Ok(decode(&out.stdout).trim().to_string())
}
/// Change the Perforce file type of opened files (e.g. add "+l" exclusive lock).
/// `filetype` is passed to `p4 reopen -t`, so "+l" adds the modifier and
/// "binary+l" sets the full type.
#[tauri::command]
async fn p4_reopen_type(state: State<'_, AppState>, files: Vec<String>, filetype: String) -> Result<String, String> {
let conn = current(&state)?;
if files.is_empty() {
return Ok(String::new());
}
let mut args: Vec<&str> = vec!["reopen", "-t", &filetype];
for f in &files {
args.push(f.as_str());
}
run_text(&conn, &args)
}
/// Extract the largest embedded PNG from a byte blob — this is how an uncooked
/// Unreal `.uasset` stores its Content-Browser thumbnail (PNG-compressed in the
/// package thumbnail table). We scan for the PNG signature → IEND and keep the
@ -1924,6 +2005,9 @@ pub fn run() {
p4_opened_all,
p4_lock,
p4_unlock,
p4_typemap_get,
p4_typemap_set,
p4_reopen_type,
uasset_thumbnail,
uasset_export_3d,
p4_shelve,