Skip to content

Routing

This contains additional information on routing for developers.

It may also provide insight on how a function should be implemented.

Route.Matches

route.matches_no_subfolder checks if the route ends with input.

While also accounting for subfolders.

So if the route is <PATH_TO_GAME_FOLDER>/dvdroot/BGM/EVENT_ADX_E.AFS, route.matches_no_subfolder will return true for EVENT_ADX_E.AFS because it ends with EVENT_ADX_E.AFS.

A Truth table for route.matches_no_subfolder(group.Route).

Standard routes:

route group.Route route.matches(group.Route) Description
a.bin b.bin false b.bin not at end of a.bin
b.bin b.bin true Direct match.
folder/a.bin a.bin true Matches a.bin at end.
folder/a.bin b.bin false b.bin not at end of folder/a.bin

Nested files of same type:

route group.Route route.matches(group.Route) Description
parent.bin/child.bin child.bin true Matches child.bin at end.
parent.bin/child.bin parent.bin/child.bin true Matches parent.bin/child.bin at end.
parent.bin/parentSubfolder/child.bin parent.bin/child.bin false Not direct descendant of parent.bin.
parent.bin/parentSubfolder/child.bin parentSubfolder/child.bin true Matches child.bin at end. May match multiple parent folders/archives.

Nested files of different type:

route group.Route route.matches(group.Route) Description
parent.bin/child.dds child.dds true Matches child.dds at end.
parent.bin/child.dds parent.bin/child.dds true Matches parent.bin/child.dds at end.
parent.bin/parentSubfolder/child.dds parent.bin/child.dds false Not direct descendant of parent.bin.
parent.bin/parentSubfolder/child.dds parentSubfolder/child.dds true Matches child.dds at end. May match multiple parent folders/archives.

Unintended Actions / Collateral Damage:

route group.Route route.matches(group.Route) Description
ModBFolder/child.bin child.bin true Overrides file child.bin in another folder. ❌ Potentially Undesireable.
child.bin/.../ModBFolder/... child.bin false ModBFolder doesn't end with child.bin.

In practice, the route is a full path to a file, with any recursive children tacked on if doing recursive emulation.

e.g. route is

  • C:/Full/Path/To/file.afs

If you are accessing file with path SomeFolder/file.afs/00000.adx while you are already building file.afs, the route will be:

  • C:/Full/Path/To/file.afs/00000.adx

This allows for recursive emulation of files.

Consider writing a diagnostic for undesireable overrides.

We should avoid overriding files outside of game folders when that is not desireable. To avoid this, we should write a diagnostic to ensure people specify top level archives as GameFolderName/file.afs or GameFolderName/data/file.afs rather than just file.afs.

Handling Subfolders

This is a special case.

Sometimes an emulated file may have a hierarchy of internal files. For example, an archive may have multiple nested folders.

For example, a mod may have the path parent.bin/child/child.dds, which should add child/child.dds to parent.bin.

For this we provide specialised method matches_with_subfolder.

route group.Route route.matches(group.Route) Description
parent.bin parent.bin/child true Matched via parent.bin in front.
parent.bin parent.bin/child/child2 true Matched via parent.bin in front.
folder/parent.bin parent.bin/child true Matched via parent.bin in front.
folder/parent.bin parent.bin/child/child2 true Matched via parent.bin in front.
folder/parent.bin folder/parent.bin/child true Matched via folder/parent.bin in front.
folder/parent.bin der/parent.bin/child true Matched via der/parent.bin in front.
folder/parent.bin folder/parent.bin/child/child2 true Matched via folder/parent.bin in front.
folder/parent.bin folder/other/parent.bin/child false folder/parent.bin != folder/other
parent.bin parent.bin_suffix/child false parent.bin is not a prefix of parent.bin_suffix.
folder/parent.bin folder/parent.bin_suffix/child false folder/parent.bin is not a prefix of folder/parent.bin_suffix.
parent.bin parent.bin/otherFolder/parent.bin/child true Recursive parent.bin folders should not throw the logic off.

Code

The algorithm for this is nontrivial, so here is a reference implementation for matches_with_subfolder

use std::path::MAIN_SEPARATOR;
use memchr::memchr_iter;

pub fn route_matches(route: &str, group: &str) -> bool {
    // Neither should be empty, this should be disabled in release builds.
    #[cfg(debug_assertions)]
    {
        if route.is_empty() {
            panic!("Route cannot be empty");
        }
        if group.is_empty() {
            panic!("Group cannot be empty");
        }
        if group.starts_with(MAIN_SEPARATOR) {
            panic!("Group cannot start with forwrard slash");
        }
    }

    // We don't care about semantics of encoding, this is a pure byte match.
    route_matches_impl(route.as_bytes(), group.as_bytes())
}

pub fn route_matches_impl(route: &[u8], group: &[u8]) -> bool {
    let mut forward_slash_iter = memchr_iter(MAIN_SEPARATOR as u8, group);

    while let Some(group_index) = forward_slash_iter.next(){
        let current_group_slice = &group[..group_index];
        if route.ends_with(current_group_slice) {
            return true;
        }
    }

    // This may be a hot path depending on use case.
    return route.ends_with(group);
}

#[cfg(test)]
mod tests {
    use super::*;
    use rstest::rstest;

    #[rstest]
    #[case("a.bin", "b.bin", false, "`b.bin` not at end of `a.bin`")]
    #[case("b.bin", "b.bin", true, "Direct match")]
    #[case("folder/a.bin", "a.bin", true, "Matches `a.bin` at end")]
    #[case("folder/a.bin", "b.bin", false, "`b.bin` not at end of `folder/a.bin`")]
    fn test_standard_routes(
        #[case] route: &str,
        #[case] pattern: &str,
        #[case] expected: bool,
        #[case] description: &str,
    ) {
        assert_eq!(
            route_matches(route, pattern),
            expected,
            "Failed assertion for case: {}\nReason: {}",
            format!("{} , {}", route, pattern),
            description
        );
    }

    #[rstest]
    #[case(
        "/full/path/to/a.bin",
        "b.bin",
        false,
        "`b.bin` not at end of `/full/path/to/a.bin`"
    )]
    #[case("/full/path/to/b.bin", "b.bin", true, "Direct match")]
    #[case("/full/path/to/folder/a.bin", "a.bin", true, "Matches `a.bin` at end")]
    #[case(
        "/full/path/to/folder/a.bin",
        "b.bin",
        false,
        "`b.bin` not at end of `/full/path/to/folder/a.bin`"
    )]
    fn test_standard_routes_with_full_path_routes(
        #[case] route: &str,
        #[case] pattern: &str,
        #[case] expected: bool,
        #[case] description: &str,
    ) {
        assert_eq!(
            route_matches(route, pattern),
            expected,
            "Failed assertion for case: {}\nReason: {}",
            format!("{} , {}", route, pattern),
            description
        );
    }

    #[rstest]
    #[case(
        "parent.bin/child.bin",
        "child.bin",
        true,
        "Matches `child.bin` at end"
    )]
    #[case(
        "parent.bin/child.bin",
        "parent.bin/child.bin",
        true,
        "Matches `parent.bin/child.bin` at end"
    )]
    #[case(
        "parent.bin/parentSubfolder/child.bin",
        "parent.bin/child.bin",
        false,
        "Not direct descendant of `parent.bin`"
    )]
    #[case(
        "parent.bin/parentSubfolder/child.bin",
        "parentSubfolder/child.bin",
        true,
        "Matches `child.bin` at end. May match multiple parent folders/archives"
    )]
    fn test_nested_files_same_type(
        #[case] route: &str,
        #[case] pattern: &str,
        #[case] expected: bool,
        #[case] description: &str,
    ) {
        assert_eq!(
            route_matches(route, pattern),
            expected,
            "Failed assertion for case: {}\nReason: {}",
            format!("{} , {}", route, pattern),
            description
        );
    }

    #[rstest]
    #[case(
        "/full/path/to/parent.bin/child.bin",
        "child.bin",
        true,
        "Matches `child.bin` at end"
    )]
    #[case(
        "/full/path/to/parent.bin/child.bin",
        "parent.bin/child.bin",
        true,
        "Matches `parent.bin/child.bin` at end"
    )]
    #[case(
        "/full/path/to/parent.bin/parentSubfolder/child.bin",
        "parent.bin/child.bin",
        false,
        "Not direct descendant of `parent.bin`"
    )]
    #[case(
        "/full/path/to/parent.bin/parentSubfolder/child.bin",
        "parentSubfolder/child.bin",
        true,
        "Matches `child.bin` at end. May match multiple parent folders/archives"
    )]
    fn test_nested_files_same_type_with_full_path_routes(
        #[case] route: &str,
        #[case] pattern: &str,
        #[case] expected: bool,
        #[case] description: &str,
    ) {
        assert_eq!(
            route_matches(route, pattern),
            expected,
            "Failed assertion for case: {}\nReason: {}",
            format!("{} , {}", route, pattern),
            description
        );
    }

    #[rstest]
    #[case(
        "parent.bin/child.dds",
        "child.dds",
        true,
        "Matches `child.dds` at end"
    )]
    #[case(
        "parent.bin/child.dds",
        "parent.bin/child.dds",
        true,
        "Matches `parent.bin/child.dds` at end"
    )]
    #[case(
        "parent.bin/parentSubfolder/child.dds",
        "parent.bin/child.dds",
        false,
        "Not direct descendant of `parent.bin`"
    )]
    #[case(
        "parent.bin/parentSubfolder/child.dds",
        "parentSubfolder/child.dds",
        true,
        "Matches `child.dds` at end. May match multiple parent folders/archives"
    )]
    fn test_nested_files_different_type(
        #[case] route: &str,
        #[case] pattern: &str,
        #[case] expected: bool,
        #[case] description: &str,
    ) {
        assert_eq!(
            route_matches(route, pattern),
            expected,
            "Failed assertion for case: {}\nReason: {}",
            format!("{} , {}", route, pattern),
            description
        );
    }

    #[rstest]
    #[case(
        "/full/path/to/parent.bin/child.dds",
        "child.dds",
        true,
        "Matches `child.dds` at end"
    )]
    #[case(
        "/full/path/to/parent.bin/child.dds",
        "parent.bin/child.dds",
        true,
        "Matches `parent.bin/child.dds` at end"
    )]
    #[case(
        "/full/path/to/parent.bin/parentSubfolder/child.dds",
        "parent.bin/child.dds",
        false,
        "Not direct descendant of `parent.bin`"
    )]
    #[case(
        "/full/path/to/parent.bin/parentSubfolder/child.dds",
        "parentSubfolder/child.dds",
        true,
        "Matches `child.dds` at end. May match multiple parent folders/archives"
    )]
    fn test_nested_files_different_type_with_full_path_routes(
        #[case] route: &str,
        #[case] pattern: &str,
        #[case] expected: bool,
        #[case] description: &str,
    ) {
        assert_eq!(
            route_matches(route, pattern),
            expected,
            "Failed assertion for case: {}\nReason: {}",
            format!("{} , {}", route, pattern),
            description
        );
    }

    #[rstest]
    #[case(
        "ModBFolder/child.bin",
        "child.bin",
        true,
        "Overrides file `child.bin` in another folder. ❌ Potentially Undesireable"
    )]
    #[case(
        "child.bin/.../ModBFolder/...",
        "child.bin",
        false,
        "ModBFolder doesn't end with `child.bin`"
    )]
    fn test_unintended_actions(
        #[case] route: &str,
        #[case] pattern: &str,
        #[case] expected: bool,
        #[case] description: &str,
    ) {
        assert_eq!(
            route_matches(route, pattern),
            expected,
            "Failed assertion for case: {}\nReason: {}",
            format!("{} , {}", route, pattern),
            description
        );
    }

    #[rstest]
    #[case(
        "/full/path/to/ModBFolder/child.bin",
        "child.bin",
        true,
        "Overrides file `child.bin` in another folder. ❌ Potentially Undesireable"
    )]
    #[case(
        "/full/path/to/child.bin/.../ModBFolder/...",
        "child.bin",
        false,
        "ModBFolder doesn't end with `child.bin`"
    )]
    fn test_unintended_actions_with_full_path_routes(
        #[case] route: &str,
        #[case] pattern: &str,
        #[case] expected: bool,
        #[case] description: &str,
    ) {
        assert_eq!(
            route_matches(route, pattern),
            expected,
            "Failed assertion for case: {}\nReason: {}",
            format!("{} , {}", route, pattern),
            description
        );
    }

    #[rstest]
    #[case(
        "parent.bin",
        "parent.bin/child",
        true,
        "Matched via `parent.bin` in front"
    )]
    #[case(
        "parent.bin",
        "parent.bin/child/child2",
        true,
        "Matched via `parent.bin` in front"
    )]
    #[case(
        "folder/parent.bin",
        "parent.bin/child",
        true,
        "Matched via `parent.bin` in front"
    )]
    #[case(
        "folder/parent.bin",
        "parent.bin/child/child2",
        true,
        "Matched via `parent.bin` in front"
    )]
    #[case(
        "folder/parent.bin",
        "folder/parent.bin/child",
        true,
        "Matched via `folder/parent.bin` in front"
    )]
    #[case(
        "folder/parent.bin",
        "der/parent.bin/child",
        true,
        "Matched via `der/parent.bin` in front"
    )]
    #[case(
        "folder/parent.bin",
        "folder/parent.bin/child/child2",
        true,
        "Matched via `folder/parent.bin` in front"
    )]
    #[case(
        "folder/parent.bin",
        "folder/other/parent.bin/child",
        false,
        "`folder/parent.bin` != `folder/other`"
    )]
    #[case(
        "parent.bin",
        "parent.bin_suffix/child",
        false,
        "`parent.bin` is not a prefix of `parent.bin_suffix`"
    )]
    #[case(
        "folder/parent.bin",
        "folder/parent.bin_suffix/child",
        false,
        "`folder/parent.bin` is not a prefix of `folder/parent.bin_suffix`"
    )]
    #[case(
        "parent.bin",
        "parent.bin/otherFolder/parent.bin/child",
        true,
        "Recursive `parent.bin` folders should not throw the logic off"
    )]
    fn test_handling_subfolders(
        #[case] route: &str,
        #[case] pattern: &str,
        #[case] expected: bool,
        #[case] description: &str,
    ) {
        assert_eq!(
            route_matches(route, pattern),
            expected,
            "Failed assertion for case: {}\nReason: {}",
            format!("{} , {}", route, pattern),
            description
        );
    }

    #[rstest]
    #[case(
        "/full/path/to/parent.bin",
        "parent.bin/child",
        true,
        "Matched via `parent.bin` in front"
    )]
    #[case(
        "/full/path/to/parent.bin",
        "parent.bin/child/child2",
        true,
        "Matched via `parent.bin` in front"
    )]
    #[case(
        "/full/path/to/folder/parent.bin",
        "parent.bin/child",
        true,
        "Matched via `parent.bin` in front"
    )]
    #[case(
        "/full/path/to/folder/parent.bin",
        "parent.bin/child/child2",
        true,
        "Matched via `parent.bin` in front"
    )]
    #[case(
        "/full/path/to/folder/parent.bin",
        "full/path/to/folder/parent.bin/child",
        true,
        "Matched via `/full/path/to/folder/parent.bin` in front"
    )]
    #[case(
        "/full/path/to/folder/parent.bin",
        "other/full/path/to/folder/parent.bin/child",
        false,
        "Does not match because of 'other' folder at front."
    )]
    #[case(
        "/full/path/to/parent.bin",
        "parent.bin_suffix/child",
        false,
        "`parent.bin` is not a prefix of `parent.bin_suffix`"
    )]
    #[case(
        "/full/path/to/folder/parent.bin",
        "folder/parent.bin_suffix/child",
        false,
        "`/full/path/to/folder/parent.bin` is not a prefix of `folder/parent.bin_suffix`"
    )]
    #[case(
        "/full/path/to/parent.bin",
        "parent.bin/otherFolder/parent.bin/child",
        true,
        "Recursive `parent.bin` folders should not throw the logic off"
    )]
    fn test_handling_subfolders_with_full_path_routes(
        #[case] route: &str,
        #[case] pattern: &str,
        #[case] expected: bool,
        #[case] description: &str,
    ) {
        assert_eq!(
            route_matches(route, pattern),
            expected,
            "Failed assertion for case: {}\nReason: {}",
            format!("{} , {}", route, pattern),
            description
        );
    }
}

The algorithm below is technically O(n^2), however in practice it's O(n) because the number of directory separarators which we test against is 1, or very close to 1.

When ported over to the actual emulator implementation, this should be benched, because with short strings used for directory names (likely <8 chars), doing a byte by byte check may be faster latency wise. (memchr is more optimised for throughput)