Skip to content

Commit 8791f9a

Browse files
committed
When creating a mod, optionally convert textures
- Older blender (2.79b) can't read the dx10 texture header (supposedly 2.83+ can though) - The conversion flags for each format should be specified in SnapshotProfiles/TexFormat.txt - texconv from the direct x tools project is used to do the conversion and its binary should be in TPLib
1 parent 20b4c53 commit 8791f9a

File tree

4 files changed

+176
-3
lines changed

4 files changed

+176
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,4 @@ Native/mod_stats/__test_process_messages_3.txt
7979
g2025g1_pre.sh
8080
G2025g1
8181
SnapshotProfiles/TestProfile1.yaml
82+
SnapshotProfiles/TexFormat.txt

MMLaunch/CreateModWindow.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,6 @@
5353
<Button
5454
Command="{Binding Path=RemoveSnapshots}"
5555
Content="Remove Snapshots" HorizontalAlignment="Left" Margin="150,195,0,0" VerticalAlignment="Top" Width="125" Height="27"/>
56+
<CheckBox x:Name="convertTex" ToolTip="If needed, convert textures into d3d9 format readable by blender 2.79b" Content="Convert Textures" HorizontalAlignment="Left" Margin="200,368,0,0" VerticalAlignment="Top" Width="116" IsChecked="{Binding Path=ConvertTextures}"/>
5657
</Grid>
5758
</Window>

MMLaunch/CreateModWindow.xaml.fs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type CreateModViewModel() as self =
5656
let mutable modNameTB: TextBox option = None
5757
let mutable mmobjFiles = new ObservableCollection<MMObjFileModel>([])
5858
let mutable addToModIndex = true
59+
let mutable convertTextures = true
5960
let mutable removeSnapshotsFn = ignore
6061

6162
let mutable sdWriteTime = DateTime.Now
@@ -183,6 +184,10 @@ type CreateModViewModel() as self =
183184
with get() = addToModIndex
184185
and set value = addToModIndex <- value
185186

187+
member x.ConvertTextures
188+
with get() = convertTextures
189+
and set value = convertTextures <- value
190+
186191
member x.CanCreate =
187192
let mnvalid =
188193
match validateModName(modName) with
@@ -208,7 +213,7 @@ type CreateModViewModel() as self =
208213
| Err(e),_ -> ViewModelUtil.pushDialog(e)
209214
| _,None -> ()
210215
| Ok(_),Some(file) ->
211-
match (ModUtil.createMod dataDir modName file.FullPath) with
216+
match (ModUtil.createMod dataDir modName convertTextures file.FullPath) with
212217
| Ok(modFile) ->
213218
let createdMessage = sprintf "Import %s into blender to edit." modFile
214219
let modIndexErr =

MMLaunch/ModUtil.fs

Lines changed: 168 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,167 @@ open YamlDotNet.Serialization
2424

2525
open ViewModelUtil
2626

27+
/// Helper for DDS files, mostly LLM generated
28+
module DdsUtil =
29+
open System
30+
open System.IO
31+
open System.Text
32+
33+
type HeaderInfo = { FourCC : string; HasDX10 : bool }
34+
35+
let PathToTexCli = [ "TPlib"; "..\\TPLib"; "..\\..\\TPLib"; "..\\..\\..\\TPLib"; ]
36+
let PathToSnapshotProfiles = [ "SnapshotProfiles"; "..\\SnapshotProfiles"; "..\\..\\SnapshotProfiles"; "..\\..\\..\\SnapshotProfiles" ]
37+
let SnapTexFormatFile = "TexFormat.txt"
38+
let TexConvName = "texconv.exe"
39+
40+
/// Returns the absolute path of the first existing directory in a list of paths, or None if none exist
41+
let findFirstExistingPath paths : string option =
42+
// Map each path to its full path based on the current directory
43+
let fullPaths = paths |> List.map Path.GetFullPath
44+
// Find the first path (if any) that exists from the mapped paths
45+
fullPaths |> List.tryFind Directory.Exists
46+
47+
let readHeader (path : string) : HeaderInfo =
48+
use br = new BinaryReader(File.OpenRead path)
49+
50+
if br.ReadUInt32() <> 0x20534444u then
51+
invalidOp "Not a DDS file."
52+
53+
br.BaseStream.Seek(84L, SeekOrigin.Begin) |> ignore
54+
let fourCCu = br.ReadUInt32()
55+
let fourCC = Encoding.ASCII.GetString(BitConverter.GetBytes fourCCu)
56+
57+
{ FourCC = fourCC; HasDX10 = (fourCC = "DX10") }
58+
59+
/// Read the 4-byte FourCC at offset 84 (0x54) in the DDS header
60+
/// Returns true if it equals "DX10"
61+
let needsConversion (filePath : string) : bool =
62+
(readHeader filePath).HasDX10
63+
64+
open System.Globalization
65+
open System.Diagnostics
66+
67+
let readSnapTexFormatFile () : string * Map<uint32, string> =
68+
// Find the first existing path for SnapTexFormatFile
69+
let filePath =
70+
PathToSnapshotProfiles
71+
|> List.map (fun dir -> Path.Combine(dir, SnapTexFormatFile))
72+
|> List.tryFind File.Exists
73+
74+
// If no path is found, fail with an error
75+
let filePath =
76+
match filePath with
77+
| Some path -> path
78+
| None -> failwithf "%A not found in any of the specified paths: %A (relative to: %A)" SnapTexFormatFile PathToSnapshotProfiles (Environment.CurrentDirectory)
79+
80+
// Read the file and process each line
81+
// syntax: src dxgi input format number (from dxgiformat.h) = list of flags to be supplied to texconv
82+
// example: 71 = -f BC1_UNORM -dx9
83+
let lines = File.ReadAllLines(filePath)
84+
let kvpList =
85+
lines
86+
|> Array.choose (fun line ->
87+
let trimmedLine = line.Trim()
88+
89+
// Skip blank lines or comments
90+
if String.IsNullOrWhiteSpace(trimmedLine) || trimmedLine.StartsWith("//") || trimmedLine.StartsWith("#") then
91+
None
92+
else
93+
// Find the position of '='
94+
let equalIndex = trimmedLine.IndexOf('=')
95+
if equalIndex >= 0 then
96+
let keyPart = trimmedLine.Substring(0, equalIndex).Trim()
97+
let valuePart = trimmedLine.Substring(equalIndex + 1).Trim()
98+
99+
// Parse the key
100+
let key =
101+
if keyPart.ToLowerInvariant().StartsWith("0x") then
102+
let hexValue = keyPart.Substring(2)
103+
UInt32.Parse(hexValue, NumberStyles.HexNumber)
104+
else
105+
UInt32.Parse(keyPart)
106+
107+
// Use the trimmed value as the map value
108+
Some (key, valuePart)
109+
else
110+
None // If no '=' is found, skip the line
111+
)
112+
113+
// Convert the list of key-value pairs to a map
114+
filePath,Map.ofArray kvpList
115+
116+
let readFormat(path:string) =
117+
let hdr = readHeader path
118+
if not hdr.HasDX10 then failwithf "file format read only supported on dx10+ dds texture files"
119+
120+
use br = new BinaryReader(File.OpenRead path)
121+
122+
br.BaseStream.Seek(0x80L, SeekOrigin.Begin) |> ignore
123+
br.ReadUInt32()
124+
125+
/// Converts a DDS file based on its format.
126+
/// - `filePath`: The full absolute path to the DDS texture file.
127+
let convertFile (filePath: string) =
128+
// Read the format of the DDS file.
129+
let format = readFormat filePath
130+
131+
// Read the SnapTexFormatFile map.
132+
let formatFile,formatMap = readSnapTexFormatFile()
133+
134+
// Lookup the read format in the map.
135+
let convertArgs =
136+
match formatMap.TryFind(format) with
137+
| Some flags -> flags // Return the found flags.
138+
| None -> failwithf "DDS DXGI Format %A used by texture was not found in the format map; it needs to be added to %A" format formatFile
139+
140+
// Determine the path to TexConvName using PathToTexCli, fail with a formatted message if not found.
141+
let texConvPath =
142+
match findFirstExistingPath PathToTexCli with
143+
| Some path -> Path.Combine(path, TexConvName)
144+
| None -> failwithf "%s not found in any of the specified paths: %A (relative to: %A)" TexConvName PathToTexCli (Environment.CurrentDirectory)
145+
146+
// Validate that the executable exists at the determined path.
147+
if not (File.Exists(texConvPath)) then
148+
failwithf "TexConv executable not found at %s" texConvPath
149+
150+
// Prepare the command line arguments.
151+
let arguments = sprintf "-y %s \"%s\"" convertArgs filePath
152+
153+
// Set up the process start info.
154+
let startInfo =
155+
ProcessStartInfo(
156+
FileName = texConvPath,
157+
Arguments = arguments,
158+
UseShellExecute = false,
159+
WorkingDirectory = Path.GetDirectoryName(filePath),
160+
RedirectStandardOutput = true,
161+
RedirectStandardError = true,
162+
CreateNoWindow = true
163+
)
164+
165+
// Execute the process.
166+
use proc = new Process()
167+
proc.StartInfo <- startInfo
168+
169+
let mutable errorOut = "";
170+
let mutable stdOut = "";
171+
172+
proc.OutputDataReceived.Add(fun args -> if not (String.IsNullOrWhiteSpace(args.Data)) then stdOut <- stdOut + args.Data)
173+
proc.ErrorDataReceived.Add(fun args -> if not (String.IsNullOrWhiteSpace(args.Data)) then errorOut <- errorOut + args.Data)
174+
175+
// Start the process and begin reading output and error streams asynchronously.
176+
proc.Start() |> ignore
177+
proc.BeginOutputReadLine()
178+
proc.BeginErrorReadLine()
179+
180+
// Wait for the process to exit.
181+
proc.WaitForExit()
182+
183+
// Check if the process exited with a non-zero exit code.
184+
if proc.ExitCode <> 0 then
185+
failwithf "TexConv process failed with exit code %d (error output: %A)" proc.ExitCode errorOut
186+
187+
27188
module ModUtil =
28189

29190
type YamlRef = {
@@ -87,8 +248,8 @@ mods:"""
87248
Ok(())
88249
with
89250
| e -> Err(e.Message)
90-
91-
let createMod (modRoot:string) (modName:string) (srcMMObjFile:string):Result<ModFilePath,Message> =
251+
252+
let createMod (modRoot:string) (modName:string) (convertTextures:bool) (srcMMObjFile:string) : Result<ModFilePath,Message> =
92253
try
93254
let modName = modName.Trim()
94255
if modName = "" then
@@ -156,7 +317,12 @@ mods:"""
156317
let texExt = ".dds"
157318
let texBN = refBasename + texExt
158319
let newTexFile = Path.Combine(modOutDir, texBN)
320+
159321
File.Copy(texSrc,newTexFile)
322+
323+
if convertTextures && DdsUtil.needsConversion texSrc then
324+
DdsUtil.convertFile newTexFile
325+
160326
texBN
161327
else
162328
texFile

0 commit comments

Comments
 (0)