Skip to content
Open
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
62 changes: 42 additions & 20 deletions Godbert/ViewModels/TerritoryViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,16 @@ private void _Export(Territory territory, Ookii.Dialogs.Wpf.ProgressDialog progr
Dictionary<string, bool> exportedPaths = new Dictionary<string, bool>();
UInt64 vs = 1, vt = 1, vn = 1, i = 0;
Matrix IdentityMatrix = Matrix.Identity;
const int VertLineFlushThreshold = 50000;

void FlushVertLines(bool force = false) {
if (!force && vertStr.Count < VertLineFlushThreshold)
return;
if (vertStr.Count == 0)
return;
System.IO.File.AppendAllLines(_ExportFileName, vertStr);
vertStr.Clear();
}

void ExportMaterials(Material m, string path) {
vertStr.Add($"mtllib {path}.mtl");
Expand All @@ -164,14 +174,26 @@ void ExportMaterials(Material m, string path) {
if (mtlName.Contains("_dummy_"))
continue;

var ddsBytes = SaintCoinach.Imaging.ImageConverter.GetDDS(img);
byte[] ddsBytes = null;
var fileExt = ".png";
try {
ddsBytes = SaintCoinach.Imaging.ImageConverter.GetDDS(img);
fileExt = ddsBytes != null ? ".dds" : ".png";

var fileExt = ddsBytes != null ? ".dds" : ".png";

if (fileExt == ".dds")
System.IO.File.WriteAllBytes($"{_ExportDirectory}/{mtlName}.dds", ddsBytes);
else
SaintCoinach.Imaging.ImageConverter.Convert(img).Save($"{_ExportDirectory}/{mtlName}.png");
if (fileExt == ".dds") {
System.IO.File.WriteAllBytes($"{_ExportDirectory}/{mtlName}.dds", ddsBytes);
}
else {
using (var convertedImage = SaintCoinach.Imaging.ImageConverter.Convert(img)) {
convertedImage.Save($"{_ExportDirectory}/{mtlName}.png", System.Drawing.Imaging.ImageFormat.Png);
}
}
}
catch (Exception texEx) {
System.Diagnostics.Debug.WriteLine(
$"Failed to export texture '{img.Path}' ({img.Width}x{img.Height}, {img.Format}) for material '{path}': {texEx.Message}");
continue;
}


if (mtlName.Contains("_n.tex")) {
Expand Down Expand Up @@ -235,20 +257,23 @@ void ExportMesh(ref Mesh mesh, ref Matrix lgbTransform, ref string materialName,
vertStr.Add($"vt {v.UV.Value.X} {v.UV.Value.Y * -1.0}".Replace(',', '.'));
tempVt++;
}

if (vertStr.Count >= VertLineFlushThreshold)
FlushVertLines();
}
vertStr.Add($"g {modelFilePath}_{i.ToString()}_{k.ToString()}");
vertStr.Add($"usemtl {materialName}");
for (UInt64 j = 0; j + 3 < (UInt64)mesh.Indices.Length + 1; j += 3) {
for (var j = 0; j + 2 < mesh.Indices.Length; j += 3) {
vertStr.Add(
$"f " +
$"{mesh.Indices[j] + vs}/{mesh.Indices[j] + vt}/{mesh.Indices[j] + vn} " +
$"{mesh.Indices[j + 1] + vs}/{mesh.Indices[j + 1] + vt}/{mesh.Indices[j + 1] + vn} " +
$"{mesh.Indices[j + 2] + vs}/{mesh.Indices[j + 2] + vt}/{mesh.Indices[j + 2] + vn}");

if (vertStr.Count >= VertLineFlushThreshold)
FlushVertLines();
}
if (i % 1000 == 0) {
System.IO.File.AppendAllLines(_ExportFileName, vertStr);
vertStr.Clear();
}
FlushVertLines();
vs += tempVs;
vn += tempVn;
vt += tempVt;
Expand Down Expand Up @@ -339,8 +364,7 @@ void ExportSgbModels(SaintCoinach.Graphics.Sgb.SgbFile sgbFile, ref Matrix lgbTr
}
}

System.IO.File.AppendAllLines(_ExportFileName, vertStr);
vertStr.Clear();
FlushVertLines(true);
vs = 1; vn = 1; vt = 1; i = 0;
foreach (var lgb in territory.LgbFiles) {
foreach (var lgbGroup in lgb.Groups) {
Expand All @@ -356,8 +380,7 @@ void ExportSgbModels(SaintCoinach.Graphics.Sgb.SgbFile sgbFile, ref Matrix lgbTr

newGroup = false;

System.IO.File.AppendAllLines(_ExportFileName, vertStr);
vertStr.Clear();
FlushVertLines(true);

//vertStr.Add($"o {lgbGroup.Name}");

Expand Down Expand Up @@ -461,8 +484,7 @@ void ExportSgbModels(SaintCoinach.Graphics.Sgb.SgbFile sgbFile, ref Matrix lgbTr
lightStrs.Clear();
}
}
System.IO.File.AppendAllLines(_ExportFileName, vertStr);
vertStr.Clear();
FlushVertLines(true);
System.IO.File.AppendAllLines(lightsFileName, lightStrs);
lightStrs.Clear();
System.Windows.Forms.MessageBox.Show("Finished exporting " + territory.Name, "", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Information);
Expand All @@ -471,8 +493,8 @@ void ExportSgbModels(SaintCoinach.Graphics.Sgb.SgbFile sgbFile, ref Matrix lgbTr
System.Windows.Forms.MessageBox.Show(e.Message, $"Canceled {teriName} export");
}
catch (Exception e) {
System.Diagnostics.Debug.WriteLine(e.StackTrace);
System.Windows.Forms.MessageBox.Show(e.StackTrace, $"Unable to export {teriName}");
System.Diagnostics.Debug.WriteLine(e.ToString());
System.Windows.Forms.MessageBox.Show($"{e.GetType().FullName}: {e.Message}{Environment.NewLine}{e.StackTrace}", $"Unable to export {teriName}");
}
}
#endregion
Expand Down
18 changes: 14 additions & 4 deletions SaintCoinach/ByteArrayExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public static T ToStructure<T>(this byte[] bytes, int offset) where T : struct {
public static T ToStructure<T>(this byte[] bytes, ref int offset) where T : struct {
var t = typeof(T);
var size = Marshal.SizeOf(t);
if (offset < 0 || size < 0 || offset > bytes.Length - size)
throw new System.IO.InvalidDataException($"Structure read out of range. Type={t.Name}, Offset={offset}, Size={size}, BufferLength={bytes.Length}.");
IntPtr ptr = Marshal.AllocHGlobal(size);
try {
Marshal.Copy(bytes, offset, ptr, size);
Expand All @@ -38,13 +40,21 @@ public static string ReadString(this byte[] buffer, int offset) {
return ReadString(buffer, ref offset);
}
public static string ReadString(this byte[] buffer, ref int offset) {
var strEnd = offset - 1;
while (buffer[++strEnd] != 0) { }
if (offset < 0 || offset >= buffer.Length)
return string.Empty;

var strEnd = Array.IndexOf(buffer, (byte)0, offset);
if (strEnd < 0)
strEnd = buffer.Length;

var size = strEnd - offset;
if (size <= 0) {
offset = strEnd < buffer.Length ? strEnd + 1 : buffer.Length;
return string.Empty;
}

var value = Encoding.ASCII.GetString(buffer, offset, size);

offset = strEnd + 1;
offset = strEnd < buffer.Length ? strEnd + 1 : buffer.Length;
return value;
}
}
Expand Down
37 changes: 32 additions & 5 deletions SaintCoinach/Graphics/Lgb/LgbModelEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,45 @@ public struct HeaderData {
public Pcb.PcbFile CollisionFile { get; private set; }
#endregion

private static string NormalizePath(string value, string extension) {
if (string.IsNullOrWhiteSpace(value))
return string.Empty;

var path = value.Trim().Trim('\0').Replace('\\', '/');
var roots = new[] { "bg/", "bgcommon/", "common/", "chara/", "vfx/", "cut/" };

var start = -1;
foreach (var root in roots) {
var index = path.IndexOf(root, StringComparison.OrdinalIgnoreCase);
if (index >= 0 && (start < 0 || index < start))
start = index;
}
if (start > 0)
path = path.Substring(start);

var extIndex = path.IndexOf(extension, StringComparison.OrdinalIgnoreCase);
if (extIndex >= 0)
path = path.Substring(0, extIndex + extension.Length);

return path;
}

#region Constructor
public LgbModelEntry(IO.PackCollection packs, byte[] buffer, int offset) {
this.Header = buffer.ToStructure<HeaderData>(offset);
this.Name = buffer.ReadString(offset + Header.NameOffset);

ModelFilePath = buffer.ReadString(offset + Header.ModelFileOffset);
CollisionFilePath = buffer.ReadString(offset + Header.CollisionFileOffset);
ModelFilePath = NormalizePath(buffer.ReadString(offset + Header.ModelFileOffset), ".mdl");
CollisionFilePath = NormalizePath(buffer.ReadString(offset + Header.CollisionFileOffset), ".pcb");

if (!string.IsNullOrWhiteSpace(ModelFilePath)) {
SaintCoinach.IO.File mdlFile;
if (packs.TryGetFile(ModelFilePath, out mdlFile))
this.Model = new TransformedModel(((Graphics.ModelFile)mdlFile).GetModelDefinition(), Header.Translation, Header.Rotation, Header.Scale);
try {
SaintCoinach.IO.File mdlFile;
if (packs.TryGetFile(ModelFilePath, out mdlFile))
this.Model = new TransformedModel(((Graphics.ModelFile)mdlFile).GetModelDefinition(), Header.Translation, Header.Rotation, Header.Scale);
} catch (Exception ex) {
Debug.WriteLine($"{Name} at 0x{offset:X} model '{ModelFilePath}' failure: {ex.Message}");
}
}

if (!string.IsNullOrWhiteSpace(CollisionFilePath)) {
Expand Down
44 changes: 42 additions & 2 deletions SaintCoinach/Graphics/Territory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,54 @@ public Territory(Xiv.TerritoryType type) : this(type.Sheet.Collection.PackCollec
public Territory(IO.PackCollection packs, string name, string levelPath) {
this.Packs = packs;
this.Name = name;
var i = levelPath.IndexOf("/level/");
this.BasePath = "bg/" + levelPath.Substring(0, i + 1);
this.BasePath = ResolveBasePath(levelPath);

Build();
}
#endregion

#region Build
private string ResolveBasePath(string levelPath) {
var normalized = (levelPath ?? string.Empty).Replace('\\', '/').Trim().Trim('/');
if (normalized.StartsWith("bg/", StringComparison.OrdinalIgnoreCase))
normalized = normalized.Substring(3);

var candidates = new List<string>();

var levelSegmentIndex = normalized.IndexOf("/level/", StringComparison.OrdinalIgnoreCase);
if (levelSegmentIndex >= 0)
candidates.Add("bg/" + normalized.Substring(0, levelSegmentIndex + 1));

if (!string.IsNullOrEmpty(normalized))
candidates.Add("bg/" + normalized.TrimEnd('/') + "/");

if (normalized.EndsWith("/level", StringComparison.OrdinalIgnoreCase)) {
var withoutLevel = normalized.Substring(0, normalized.Length - "/level".Length).TrimEnd('/');
if (!string.IsNullOrEmpty(withoutLevel))
candidates.Add("bg/" + withoutLevel + "/");
}

candidates.Add("bg/");

foreach (var candidate in candidates.Distinct(StringComparer.OrdinalIgnoreCase)) {
if (LooksLikeValidBasePath(candidate)) {
return candidate;
}
}

var fallback = candidates[0];
System.Diagnostics.Debug.WriteLine(
string.Format("Could not verify territory base path candidates. Bg='{0}', Fallback='{1}'", levelPath, fallback));
return fallback;
}

private bool LooksLikeValidBasePath(string basePath) {
return Packs.FileExists(basePath + "bgplate/terrain.tera")
|| Packs.FileExists(basePath + "level/bg.lgb")
|| Packs.FileExists(basePath + "level/planmap.lgb")
|| Packs.FileExists(basePath + "level/planevent.lgb");
}

private void Build() {
var terrainPath = BasePath + "bgplate/terrain.tera";
if (Packs.TryGetFile(terrainPath, out var terrainFile))
Expand Down
23 changes: 22 additions & 1 deletion SaintCoinach/IO/Directory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,28 @@ public bool TryGetFile(uint key, out File file) {
return true;

if (Index.Files.TryGetValue(key, out var index)) {
var theFile = FileFactory.Get(this.Pack, index);
File theFile;
try {
theFile = FileFactory.Get(this.Pack, index);
} catch (System.IO.InvalidDataException ex) {
System.Diagnostics.Debug.WriteLine(
string.Format("Failed to parse file in TryGetFile. Pack={0}, Dat={1}, Offset=0x{2:X}. {3}",
this.Pack.Id, index.DatFile, index.Offset, ex.Message));
file = null;
return false;
} catch (System.IO.EndOfStreamException ex) {
System.Diagnostics.Debug.WriteLine(
string.Format("Unexpected end of stream in TryGetFile. Pack={0}, Dat={1}, Offset=0x{2:X}. {3}",
this.Pack.Id, index.DatFile, index.Offset, ex.Message));
file = null;
return false;
} catch (System.NotSupportedException ex) {
System.Diagnostics.Debug.WriteLine(
string.Format("Unsupported file format in TryGetFile. Pack={0}, Dat={1}, Offset=0x{2:X}. {3}",
this.Pack.Id, index.DatFile, index.Offset, ex.Message));
file = null;
return false;
}
_Files.AddOrUpdate(key,
k => new WeakReference<File>(theFile),
(k, r) => {
Expand Down
Loading