Skip to content

Instantly share code, notes, and snippets.

@barncastle
Created February 4, 2019 15:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save barncastle/4e2dedb824e4cd2f80d7c84175cb4b74 to your computer and use it in GitHub Desktop.
Save barncastle/4e2dedb824e4cd2f80d7c84175cb4b74 to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Runtime.InteropServices;
namespace ConsoleApp1
{
class FileNameExtractor
{
public HashSet<uint> TextureIDs;
public HashSet<string> TextureNames;
public FileNameExtractor(string filename)
{
TextureIDs = new HashSet<uint>();
TextureNames = new HashSet<string>();
using (var fs = File.OpenRead(filename))
using (var br = new BinaryReader(fs))
{
switch (Path.GetExtension(filename).ToLower())
{
case ".m2":
ProcessM2(br);
break;
case ".wmo": // this should be the root not group WMO
ProcessWMO(br);
break;
case ".adt":
ProcessADT(br);
break;
}
}
}
/// <summary>
/// Scan an M2 for a TXID and any inline filenames
/// </summary>
/// <param name="br"></param>
private void ProcessM2(BinaryReader br)
{
uint token = br.ReadUInt32();
uint MD20offset = 0;
// if MD21 read TXID if it exists
if (token == 0x3132444D)
{
MD20offset = 8; // MD20 is within the MD21 chunk
// scan for TXID - this is an array if FileDataIds
br.BaseStream.Position = 0; // reset stream pos
if (SeekChunk(br, 0x44495854, out int size))
AddRange(TextureIDs, MemoryMarshal.Cast<byte, uint>(br.ReadBytes(size)).ToArray());
}
// read the MD20 header
br.BaseStream.Position = MD20offset;
var header = MemoryMarshal.Read<M2Header>(br.ReadBytes(Marshal.SizeOf<M2Header>()));
// check if any filenames exist
if (header.NTextures > 0)
{
br.BaseStream.Position = header.OfsTextures;
// read the M2Texture segments
int textureSize = Marshal.SizeOf<M2Texture>() * header.NTextures;
M2Texture[] textures = MemoryMarshal.Cast<byte, M2Texture>(br.ReadBytes(textureSize)).ToArray();
// iterate the offsets and read the names - ignore \0 suffix
foreach (var tex in textures)
{
// 0 indicates a reference to TXID so no string available
if (tex.FileNameSize == 0)
continue;
br.BaseStream.Position = tex.FileNameOffset;
TextureNames.Add(System.Text.Encoding.ASCII.GetString(br.ReadBytes(tex.FileNameSize - 1)));
}
}
}
/// <summary>
/// Scan a WMO for inline filenames fallingback to reading MOMT FileDataIds
/// </summary>
/// <param name="br"></param>
private void ProcessWMO(BinaryReader br)
{
bool hasMOTX; // if MOTX then ignore MOMT
// scan for MOTX
if (hasMOTX = SeekChunk(br, 0x4D4F5458, out int size))
{
string textures = System.Text.Encoding.ASCII.GetString(br.ReadBytes(size));
AddRange(TextureNames, textures.Split('\0', StringSplitOptions.RemoveEmptyEntries));
}
// scan for MOMT if no MOTX chunk
br.BaseStream.Position = 0;
if (!hasMOTX && SeekChunk(br, 0x4D4F4D54, out size))
{
// BfA MOMT indices into MOTX are FileDataIds
var MOMTs = MemoryMarshal.Cast<byte, MOMT>(br.ReadBytes(size));
foreach (var momt in MOMTs)
{
if (momt.DiffuseNameIndex > 0)
TextureIDs.Add(momt.DiffuseNameIndex);
if (momt.EnvNameIndex > 0)
TextureIDs.Add(momt.EnvNameIndex);
if (momt.Unk_Texture > 0)
TextureIDs.Add(momt.Unk_Texture);
}
}
}
/// <summary>
/// Scan an ADT for inline filenames and FileDataIds
/// <para>MTEX, MDID, MHID, MMDX, MWMO</para>
/// </summary>
/// <param name="br"></param>
private void ProcessADT(BinaryReader br)
{
// MTEX
br.BaseStream.Position = 0;
if (SeekChunk(br, 0x5845544D, out int size))
{
string textures = System.Text.Encoding.ASCII.GetString(br.ReadBytes(size));
AddRange(TextureNames, textures.Split('\0', StringSplitOptions.RemoveEmptyEntries));
}
// MDID
br.BaseStream.Position = 0;
if (SeekChunk(br, 0x4449444D, out size))
AddRange(TextureIDs, MemoryMarshal.Cast<byte, uint>(br.ReadBytes(size)).ToArray());
// MHID
br.BaseStream.Position = 0;
if (SeekChunk(br, 0x4449484d, out size))
AddRange(TextureIDs, MemoryMarshal.Cast<byte, uint>(br.ReadBytes(size)).ToArray());
// MMDX
br.BaseStream.Position = 0;
if (SeekChunk(br, 0x58444D4D, out size))
{
string textures = System.Text.Encoding.ASCII.GetString(br.ReadBytes(size));
AddRange(TextureNames, textures.Split('\0', StringSplitOptions.RemoveEmptyEntries));
}
// MWMO
if (SeekChunk(br, 0x4F4D574D, out size))
{
string textures = System.Text.Encoding.ASCII.GetString(br.ReadBytes(size));
AddRange(TextureNames, textures.Split('\0', StringSplitOptions.RemoveEmptyEntries));
}
}
/// <summary>
/// Scans for a specific token in the file
/// </summary>
/// <param name="br"></param>
/// <param name="token"></param>
/// <param name="size"></param>
/// <returns></returns>
private bool SeekChunk(BinaryReader br, uint token, out int size)
{
long length = br.BaseStream.Length;
uint ctoken = br.ReadUInt32();
size = br.ReadInt32();
while (br.BaseStream.Position + size < length)
{
br.BaseStream.Position += size;
ctoken = br.ReadUInt32();
size = br.ReadInt32();
if (ctoken == token)
return true;
}
return false;
}
/// <summary>
/// Lazy sugar syntax for hashset addrange cba making an extension method
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="hashset"></param>
/// <param name="values"></param>
private void AddRange<T>(HashSet<T> hashset, IEnumerable<T> values)
{
foreach (var v in values)
hashset.Add(v);
}
}
[StructLayout(LayoutKind.Sequential)]
struct MOMT
{
public uint flags;
public int Shader;
public int BlendMode;
public uint DiffuseNameIndex;
public uint SidnColor;
public uint FrameSidnColor;
public uint EnvNameIndex;
public uint DiffColor;
public uint GroundType;
public uint Unk_Texture;
public uint Unk_Color;
public uint Unk_Ref;
private readonly int RunTimeData1;
private readonly int RunTimeData2;
private readonly int RunTimeData3;
private readonly int RunTimeData4;
}
[StructLayout(LayoutKind.Sequential)]
struct M2Texture
{
public uint Type;
public uint Flags;
public int FileNameSize; // M2ArrayHeader ...
public uint FileNameOffset;
}
[StructLayout(LayoutKind.Sequential)]
struct M2Header
{
public int Magic;
public int Version;
public int LenName;
public int OfsName;
public int GlobalFlags;
public int NGlobalSequences;
public int OfsGlobalSequences;
public int NAnimations;
public int OfsAnimations;
public int NAnimLookup;
public int OfsAnimLookup;
public int NBones;
public int OfsBones;
public int NKeyBoneLookup;
public int OfsKeyBoneLookup;
public int NVertices;
public int OfsVertices;
public int NViews;
public int NSubmeshAnimations;
public int OfsSubmeshAnimations;
public int NTextures;
public int OfsTextures;
public int NTransparencies;
public int OfsTransparencies;
public int NUvAnimation;
public int OfsUvAnimation;
public int NTexReplace;
public int OfsTexReplace;
public int NRenderFlags;
public int OfsRenderFlags;
public int NBoneLookupTable;
public int OfsBoneLookupTable;
public int NTexLookup;
public int OfsTexLookup;
public int NTexUnits;
public int OfsTexUnits;
public int NTransLookup;
public int OfsTransLookup;
public int NUvAnimLookup;
public int OfsUvAnimLookup;
public Vector3 VertexBoxMin;
public Vector3 VertexBoxMax;
public float VertexRadius;
public Vector3 BoundingBoxMin;
public Vector3 BoundingBoxMax;
public float BoundingRadius;
public int NBoundingTriangles;
public int OfsBoundingTriangles;
public int NBoundingVertices;
public int OfsBoundingVertices;
public int NBoundingNormals;
public int OfsBoundingNormals;
public int NAttachments;
public int OfsAttachments;
public int NAttachLookup;
public int OfsAttachLookup;
public int NEvents;
public int OfsEvents;
public int NLights;
public int OfsLights;
public int NCameras;
public int OfsCameras;
public int NCameraLookup;
public int OfsCameraLookup;
public int NRibbonEmitters;
public int OfsRibbonEmitters;
public int NParticleEmitters;
public int OfsParticleEmitters;
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment