Spent some time diving into the code to figure out the name obfuscation algorithm. The description from @sylvester334 helped a ton with this, but also cheers to smilegate for leaving the appropriately named FFileManagerWindows::ObfuscateFilename symbol in the EFGame DLL

In short, the names are shifted around by a fixed amount based on the length of the string. Certain characters are first escaped before they are obfuscated, using a pair of characters based on the position % 4 inside the input string. The following table is used for that purpose:
TABLE = {
"Q": ["QP", "QD", "QW", "Q4"],
"-": ["QL", "QB", "QO", "Q5"],
"_": ["QC", "QN", "QT", "Q9"],
"X": ["XU", "XN", "XH", "X3"],
"!": ["XW", "XS", "XZ", "X0"],
}
If Q, -, _, X or ! appear in the input string, they will be replaced by one of these based on the position in the string, % 4. In other words, if the 3rd character (idx = 2) is a _, it will be replaced by QT (since 2 % 4 == 2, TABLE['_'][2] == "QT").
Some examples:
LV_LUT_Commander_Valtan_Extreme becomes LVQTLUTQ9COMMANDERQTVALTANQTEXNTREME
LV_ELG_Kayangel_ED_01_H becomes LVQTELGQ9KAYANGELQNEDQN01QNH
SGintro_Warning becomes SGINTROQ9WARNING
Afterwards, the ascii values will be rotated according to a simple caesar cipher, where rotation is offset by the length of the escaped string. This works for all strings longer than 20 characters.
For inputs shorter than 20 characters, the input will be padded by adding a `!` and several `.`s until 20 characters are reached. This string is then escaped. The obfuscation loop then overwrites the period characters with earlier emitted characters, roughly according to the following pseudocode:
if input[i + unpadded_length] == '.' {
input[i + unpadded_length] = output[i]
}
For deobfuscation of filenames, you simply reverse the shift, undo the escaping (by checking every pair of characters, seeing if they are in the table and the position % 4 matches the expected substitution, and then replacing the pair with the original character), and possibly split by ! if the original input was padded.
Here's an implementation in Rust capable of replicating both obfuscation and deobfuscation, matching 1-to-1 with the values produced by the game:
https://gist.github.com/molenzwiebel/284318d9e963672b239f9fca901be89a. The result after renaming is pretty good!