Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ dirs = "6.0.0"
fs_extra = "1.3.0"
fs-err = "3.3.1"
git2 = "0.21.0"
glob = "0.3.4"
indexmap = "2.14.0"
insta = "1.48.0"
interprocess = "2.4.2"
Expand Down
2 changes: 1 addition & 1 deletion bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ dirs = { workspace = true }
fs_extra = { workspace = true }
fs-err = { workspace = true }
git2 = { workspace = true }
glob = "0.3.4"
glob = { workspace = true }
image = "0.25.10"
indicatif = "0.18.6"
interprocess = { workspace = true }
Expand Down
2 changes: 2 additions & 0 deletions bin/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub enum Error {
Dialoguer(#[from] dialoguer::Error),
#[error("Git Error: {0}")]
Git(#[from] git2::Error),
#[error("Config file selection error: {0}")]
ConfigFiles(#[from] hemtt_config::files::Error),
#[error("Glob Error: {0}")]
GlobError(#[from] glob::GlobError),
#[error("Glob Pattern Error: {0}")]
Expand Down
65 changes: 6 additions & 59 deletions bin/src/modules/rapifier.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{collections::HashMap, path::PathBuf, sync::RwLock};
use std::{collections::HashMap, sync::RwLock};

use hemtt_config::{
Config,
Expand All @@ -11,7 +11,6 @@ use hemtt_workspace::{
addons::{Addon, Location},
};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use vfs::VfsFileType;

use crate::{context::Context, error::Error, progress::progress_bar, report::Report};

Expand Down Expand Up @@ -53,39 +52,12 @@ impl Module for Rapifier {
fn pre_build(&self, ctx: &Context) -> Result<Report, Error> {
ctx.state().set(AddonConfigs::default());
let mut report = Report::new();
let glob_options = glob::MatchOptions {
require_literal_separator: true,
..Default::default()
};
let mut entries = Vec::new();
ctx.addons()
.iter()
.map(|addon| {
let mut globs = Vec::new();
if let Some(config) = addon.config() {
if !config.rapify().enabled() {
debug!("rapify disabled for {}", addon.name());
return Ok(());
}
for file in config.rapify().exclude() {
globs.push(glob::Pattern::new(file)?);
}
}
for entry in ctx.workspace_path().join(addon.folder())?.walk_dir()? {
if entry.metadata()?.file_type == VfsFileType::File && can_rapify(&entry)? {
if globs
.iter()
.any(|pat| pat.matches_with(entry.as_str(), glob_options))
{
debug!("skipping {}", entry.as_str());
continue;
}
entries.push((addon, entry));
}
}
Ok(())
})
.collect::<Result<Vec<_>, Error>>()?;
for addon in ctx.addons() {
for entry in hemtt_config::files::checkable(ctx.workspace_path(), addon)? {
entries.push((addon, entry));
}
}

let progress = progress_bar(entries.len() as u64).with_message("Rapifying Configs");
let reports = entries
Expand Down Expand Up @@ -196,28 +168,3 @@ pub fn rapify(addon: &Addon, path: &WorkspacePath, ctx: &Context) -> Result<Repo
}
Ok(report)
}

pub fn can_rapify(entry: &WorkspacePath) -> Result<bool, Error> {
let path = entry.as_str();
let pathbuf = PathBuf::from(&path);
let ext = pathbuf
.extension()
.unwrap_or_else(|| std::ffi::OsStr::new(""))
.to_str()
.expect("osstr should be valid utf8");
if ext == "cpp" && pathbuf.file_name() != Some(std::ffi::OsStr::new("config.cpp")) {
warn!(
"{} - cpp files other than config.cpp are usually not intentional. use hpp for includes",
path.trim_start_matches('/')
);
}
if !["cpp", "rvmat", "ext", "sqm", "bikb", "bisurf"].contains(&ext) {
return Ok(false);
}
let mut buffer = vec![0; 4];
if entry.open_file()?.read_exact(&mut buffer).is_err() {
// The file is less than 4 bytes, so it is not rapified
return Ok(true);
}
Ok(buffer != b"\0raP")
}
26 changes: 20 additions & 6 deletions hls/src/config/lints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{
};

use hemtt_preprocessor::Processor;
use hemtt_workspace::{WorkspacePath, reporting::WorkspaceFiles};
use hemtt_workspace::{WorkspacePath, addons::Addon, reporting::WorkspaceFiles};
use tokio::{sync::RwLock, task::JoinSet};
use tower_lsp::Client;
use tracing::{debug, warn};
Expand Down Expand Up @@ -38,12 +38,26 @@ impl Cache {

fn check_addons(workspace: &EditorWorkspace, client: Client) {
let mut futures = JoinSet::new();
for config in workspace.root().addons() {
let Ok(source) = workspace.root().join(config.as_str()) else {
warn!("failed to join config {:?}", config);
continue;
// Every rapifiable file, not just `config.cpp`, and honouring the addon's
// `rapify` settings - the same set the CLI checks
let addons = match Addon::scan(workspace.root_disk()) {
Ok(addons) => addons,
Err(e) => {
warn!("not checking configs, failed to scan addons: {e}");
return;
}
};
for addon in addons {
let files = match hemtt_config::files::checkable(workspace.root(), &addon) {
Ok(files) => files,
Err(e) => {
warn!("not checking `{}`: {e}", addon.folder());
continue;
}
};
futures.spawn(check_addon(source, workspace.clone()));
for source in files {
futures.spawn(check_addon(source, workspace.clone()));
}
}
tokio::spawn(async move {
futures.join_all().await;
Expand Down
3 changes: 3 additions & 0 deletions libs/config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ automod = { workspace = true }
byteorder = { workspace = true }
chumsky = { workspace = true }
fs-err = { workspace = true }
glob = { workspace = true }
indexmap = { workspace = true }
linkme = { workspace = true }
lsp-types = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }

serde = { workspace = true, features = ["derive"], optional = true }

Expand Down
90 changes: 90 additions & 0 deletions libs/config/src/files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! Which files in an addon HEMTT config-checks.
//!
//! Shared so the CLI and the language server cannot disagree about it. They
//! did: the CLI checked every rapifiable file and honoured the addon's
//! `rapify` settings, while the editor only ever looked at `config.cpp` and
//! ignored `exclude` and `enabled` entirely.

use std::io::Read;

use hemtt_workspace::{WorkspacePath, addons::Addon};

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Workspace error: {0}")]
Workspace(#[from] hemtt_workspace::Error),
#[error("Invalid rapify exclude pattern: {0}")]
Pattern(#[from] glob::PatternError),
}

/// Extensions that HEMTT rapifies.
const EXTENSIONS: &[&str] = &["cpp", "rvmat", "ext", "sqm", "bikb", "bisurf"];

/// Can this file be rapified?
///
/// A file that is already rapified is skipped - it starts with `\0raP`.
///
/// # Errors
/// [`hemtt_workspace::Error`] if the file cannot be read
pub fn can_rapify(entry: &WorkspacePath) -> Result<bool, hemtt_workspace::Error> {
let path = entry.as_str();
let pathbuf = std::path::PathBuf::from(&path);
let Some(ext) = pathbuf.extension().and_then(std::ffi::OsStr::to_str) else {
return Ok(false);
};
if ext == "cpp" && pathbuf.file_name() != Some(std::ffi::OsStr::new("config.cpp")) {
tracing::warn!(
"{} - cpp files other than config.cpp are usually not intentional. use hpp for includes",
path.trim_start_matches('/')
);
}
if !EXTENSIONS.contains(&ext) {
return Ok(false);
}
let mut buffer = vec![0; 4];
if entry.open_file()?.read_exact(&mut buffer).is_err() {
// The file is less than 4 bytes, so it is not rapified
return Ok(true);
}
Ok(buffer != b"\0raP")
}

/// The files in an addon that should be config-checked.
///
/// Honours the addon's `rapify` settings: nothing if it is disabled, and
/// anything matching an `exclude` pattern is left out.
///
/// # Errors
/// [`Error::Workspace`] if the addon cannot be walked
/// [`Error::Pattern`] if an `exclude` pattern is not a valid glob
pub fn checkable(root: &WorkspacePath, addon: &Addon) -> Result<Vec<WorkspacePath>, Error> {
let mut excludes = Vec::new();
if let Some(config) = addon.config() {
if !config.rapify().enabled() {
tracing::debug!("rapify disabled for {}", addon.name());
return Ok(Vec::new());
}
for pattern in config.rapify().exclude() {
excludes.push(glob::Pattern::new(pattern)?);
}
}
let options = glob::MatchOptions {
require_literal_separator: true,
..Default::default()
};
let mut files = Vec::new();
for entry in root.join(addon.folder())?.walk_dir()? {
if !entry.is_file()? || !can_rapify(&entry)? {
continue;
}
if excludes
.iter()
.any(|pattern| pattern.matches_with(entry.as_str(), options))
{
tracing::debug!("skipping {}", entry.as_str());
continue;
}
files.push(entry);
}
Ok(files)
}
1 change: 1 addition & 0 deletions libs/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::{

pub mod analyze;
pub mod display;
pub mod files;
mod model;
pub mod parse;
pub mod rapify;
Expand Down
63 changes: 63 additions & 0 deletions libs/config/tests/files.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#![allow(clippy::unwrap_used)]

//! Which files get config checked. Both the CLI and the language server rely
//! on this answer, so a change here changes what the editor shows.

use hemtt_config::files::{can_rapify, checkable};
use hemtt_workspace::{LayerType, addons::Addon};

const ROOT: &str = "tests/files";

fn workspace() -> hemtt_workspace::WorkspacePath {
hemtt_workspace::Workspace::builder()
.physical(&std::path::PathBuf::from(ROOT), LayerType::Source)
.finish(None, false, &hemtt_common::config::PDriveOption::Disallow)
.unwrap()
}

fn addon() -> Addon {
Addon::new(
&std::path::PathBuf::from(ROOT),
"main".to_string(),
hemtt_workspace::addons::Location::Addons,
)
.unwrap()
}

fn names(mut files: Vec<hemtt_workspace::WorkspacePath>) -> Vec<String> {
files.sort_by_key(hemtt_workspace::WorkspacePath::filename);
files
.iter()
.map(hemtt_workspace::WorkspacePath::filename)
.collect()
}

#[test]
fn rapifiable_extensions() {
let workspace = workspace();
for name in ["config.cpp", "checked.rvmat", "mission.ext"] {
let path = workspace.join(format!("/addons/main/{name}")).unwrap();
assert!(can_rapify(&path).unwrap(), "{name} should be rapifiable");
}
let path = workspace.join("/addons/main/notes.txt").unwrap();
assert!(!can_rapify(&path).unwrap(), "notes.txt is not a config");
}

/// The editor used to check only `config.cpp`, so an error in a `.rvmat` or
/// `.ext` was reported by `hemtt check` and never shown.
#[test]
fn checks_every_rapifiable_file() {
let files = names(checkable(&workspace(), &addon()).unwrap());
assert!(files.contains(&"config.cpp".to_string()), "{files:?}");
assert!(files.contains(&"checked.rvmat".to_string()), "{files:?}");
assert!(files.contains(&"mission.ext".to_string()), "{files:?}");
assert!(!files.contains(&"notes.txt".to_string()), "{files:?}");
}

/// The editor used to ignore `rapify.exclude`, so it reported diagnostics on
/// files the project had deliberately excluded.
#[test]
fn honours_exclude() {
let files = names(checkable(&workspace(), &addon()).unwrap());
assert!(!files.contains(&"skipped.rvmat".to_string()), "{files:?}");
}
1 change: 1 addition & 0 deletions libs/config/tests/files/addons/main/$PBOPREFIX$
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
z\test\addons\main
2 changes: 2 additions & 0 deletions libs/config/tests/files/addons/main/addon.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[rapify]
exclude = ["/addons/main/skipped.rvmat"]
1 change: 1 addition & 0 deletions libs/config/tests/files/addons/main/checked.rvmat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class Stage0 { texture = "a.paa"; };
3 changes: 3 additions & 0 deletions libs/config/tests/files/addons/main/config.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class CfgPatches {
class test_main {};
};
1 change: 1 addition & 0 deletions libs/config/tests/files/addons/main/mission.ext
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class Params {};
1 change: 1 addition & 0 deletions libs/config/tests/files/addons/main/notes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
not a config
1 change: 1 addition & 0 deletions libs/config/tests/files/addons/main/skipped.rvmat
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class Stage0 { texture = "b.paa"; };