Skip to main content.

How Skyrim handles asset file paths

The Elder Scrolls V: Skyrim's 2011 release has multiple functions that normalize asset paths in different and sometimes mutually incompatible ways. This document lists those that I've found.

All subroutine offsets provided below are for the most recent version of the 2011 release. All strings are written with C++ string literal syntax with string escapes to avoid ambiguity, i.e. the text "Hello\World" would be listed as "Hello\\World".

Method 1

The subroutine at 0x00687F60 has the following signature:

const char* NormalizeAssetPath(char* path, uint32_t start_from = 0);

This subroutine modifies the argument string in-place, returning a pointer to the "start from" char. It converts all ASCII letters to uppercase, and converts all slashes to backslashes.

Method 2

The subroutine at 0x00C70F80 has the following signature:

void NormalizeAssetPath(
   char* out,
   const char* original_path,
   const char* suffix,
   bool prepend_data_if_absent
);

This subroutine modifies the argument string in-place.

The function returns immediately if the path doesn't contain a '.' symbol. Otherwise, the function treats all content at and past the '.' symbol as a file extension, copying it into a ten-byte buffer. The copy operation uses strcpy_s, and so fails if the file extension (including the leading '.' and a null terminator) appears to be longer than the buffer can hold. AFAIK a failure here would result in file extensions longer than eight letters being cleared, unless Bethesda changed the CRT's invalid parameter handler.

The subroutine then copies the first MAX_PATH (260) bytes' worth of original_path into a stack-allocated buffer, again using strcpy_s, so excessively long paths will fail, too. It prepends "Data\\" if the arguments so specify, if the path doesn't already start with "data" (case-insensitive), and if the path is neither absolute nor relative to a drive (i.e. the first character is not a backslash and the second is not a colon). The final path (besides the Data folder) consists of the path up to the last '.', followed by the suffix argument and then the truncated extension, and is copied to the buffer pointed to by the out argument.

Here are examples of the function's output, using "ABC" as the suffix:

Input Output
"textures\\" "" (no output if no extension)
"textures/foo.dds" "Data\\textures/fooABC.dds"
"folder.name\\test123.dds" "Data\\folder.name\\test123ABC.dds"
"folder.name\\hi" "Data\\folderABC.name\\hi"
"folder.name\\subfolder" "Data\\folderABC"
"textures/foo.abcdefghijklmnop" "Data\\textures/fooABC"
"a.b.c.d.dds" "Data\\a.b.c.dABC.dds"
"C:foo.dds" "C:fooABC.dds"
"/foo.dds" "/fooABC.dds"
"DataStarTrek\\foo.dds" "DataStarTrek\\fooABC.dds"

Method 3

The subroutine at 0x00AFDE00 has the following signature:

void NormalizeArmorAssetPath(
   char* out,
   const char* original_path,
   const char* filename_suffix
);

This subroutine is similar to the previously discussed subroutine, but it's specialized for armor mesh paths.

As with the previous subroutine, this one does nothing if the path doesn't contain a '.'. The last '.' glyph indicates the file extension, which is retained if it's fewer than ten chars or discarded otherwise. The path before the file extension is copied into a buffer that is MAX_PATH bytes long, and then "DATA\\" — caps, this time, and it's not optional — gets prepended. Where this function diverges from the other is in its handling of underscores. If the path contains an underscore, and either has no backslashes or has the last underscore after the last backslash, then the filename will be truncated just before that underscore. This truncation happens after capturing the file extension, which, as with the last subroutine, involves blindly cutting at the last-seen period.

This fits with what I've dug up when reverse-engineering the code which applies ArmorAddons to actors' 3D models. The game assumes that the model filenames (not including the extensions) will end with "_0" or "_1"; the suffix argument here likely exists in order to be able to choose from one of those as desired, regardless of which of the two possible names is specified in a form's paths.

Here are examples of the function's output, using "ABC" as the suffix:

Input Output
"textures\\" "" (no output if no extension)
"textures/foo.dds" "DATA\\textures\\fooABC.dds"
"archmagebootssm_0.nif" "DATA\\archmagebootssmABC.nif"
"abc_def\\ghi.nif" "DATA\\abc_def\\ghiABC.nif"
"abc_def/ghi.nif" "DATA\\abcABC.nif"
"abc_def_0.nif" "DATA\\abc_defABC.nif"
"folder.name\\test_0.nif" "DATA\\folder.name\\testABC.nif"
"folder.name\\hi" "Data\\folderABC.name\\hi"
"folder.name\\subfolder" "Data\\folderABC"
"C:foo.dds" "C:fooABC.dds"
"/foo.dds" "/fooABC.dds"
"DataStarTrek\\foo.dds" "DataStarTrek\\fooABC.dds"

Method 4

The subroutine at 0x00A5A7F0 has the following signature:

const char* MakePathRelativeToDataDirectory(const char* path);

This subroutine conditionally advances the input pointer and returns it. It'll skip a leading slash, if there is one, and then attempt to skip the Data directory including the slash. It only matches "Data\\" and "data\\" while failing to catch any other cases.

Here are examples of the function's output:

Input Output
"\\abc.dds" "abc.dds"
"Data\\abc.dds" "abc.dds"
"data\\abc.dds" "abc.dds"
"\\Data\\abc.dds" "abc.dds"
"dATA\\abc.dds" "dATA\\abc.dds"
"datA\\abc.dds" "datA\\abc.dds"
"/abc.dds" "/abc.dds"
"data/abc.dds" "data/abc.dds"
"\\data/abc.dds" "data/abc.dds"
"C:\\abc.dds" "C:\\abc.dds"

Conclusions

This behavior is quite uneven; the specifics of path normalization will depend on what game system is even performing the lookup. However, we can come up with a few rules:

  • Use backslashes as path separators when loading and saving paths.
  • Don't allow periods inside of folder names.
  • In general, don't allow paths that are longer than 254 characters (MAX_PATH, or 260, minus 1 for a null terminator and 5 for a Data-directory prefix).
  • If a path contains the Data directory, it must always be rendered as "Data" or "data", and never with any other letter case.
  • The name of the first folder in a path should never be allowed to start with "data" as a case-insensitive prefix; for example, "dataaaa/" is unsafe, as the game may properly fail to prefix it with "Data/" when necessary.
  • The Data directory is optional in some cases, and may be added if missing. This implies that the path "Data/Data/foo.dds" may in some cases be encoded as "Data/foo.dds", and may in turn end up resolving to "Data/foo.dds"; double Data directories, then, are unsafe.
  • NIF filenames for ArmorAddon meshes should only be allowed to contain any underscores if they also end in "_0" or "_1"; underscores without that suffix can be truncated and will produce improper results.
  • If a NIF filename for an ArmorAddon mesh does not already contain a "_0" or "_1" suffix, then the file path shouldn't be allowed to be longer than 252 characters (MAX_PATH, or 260, minus 1 for a null terminator, minus 5 for a Data-directory prefix, and minus 2 for the missing weight suffix).