Skip to content

Instantly share code, notes, and snippets.

@barncastle
Last active April 15, 2019 13:48
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/21cde4ae9bba3c850c3271b1d7636d5e to your computer and use it in GitHub Desktop.
Save barncastle/21cde4ae9bba3c850c3271b1d7636d5e 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 TXIDReplacer
{
class Program
{
static Dictionary<uint, string> FileNameLookup;
static void Main(string[] args)
{
LoadLookup();
if (!Directory.Exists(args[0]))
throw new ArgumentException("Invalid directory provided");
var files = Directory.EnumerateFiles(args[0], "*.m2", SearchOption.AllDirectories);
foreach (var file in files)
ProcessFile(file);
Console.ReadLine();
}
static void ProcessFile(string file)
{
Console.WriteLine("Processing " + Path.GetFileName(file));
using (var fsIn = File.OpenRead(file))
using (var br = new BinaryReader(fsIn))
using (var fsOut = new MemoryStream((int)fsIn.Length))
using (var bw = new BinaryWriter(fsOut))
{
// get the MD20 size
br.ReadInt32();
int md20Size = br.ReadInt32();
br.BaseStream.Position = 0;
// scan for TXID
if (!SeekChunk(br, 0x44495854, out int size))
{
Console.WriteLine($"Skipping {Path.GetFileName(file)} : No TXID");
return;
}
// grab the TXIDs
var txids = MemoryMarshal.Cast<byte, uint>(br.ReadBytes(size));
// copy the MD21 chunk to the output stream
byte[] buffer = new byte[8 + md20Size];
fsIn.Position = 0;
fsIn.Read(buffer);
fsOut.Write(buffer);
// write c string filenames at the end of the MD20 datas
// and store their lengths and offsets
// unknown textures are presumed to be in the base directory and named <fildataid>.blp
br.BaseStream.Position = br.BaseStream.Length;
var textureLookup = new M2ArrayHeader[txids.Length];
for (int i = 0; i < txids.Length; i++)
{
if (!FileNameLookup.TryGetValue(txids[i], out string filename))
{
filename = txids[i] + ".blp";
Console.WriteLine($"Unknown filename {txids[i]}");
}
textureLookup[i] = new M2ArrayHeader()
{
Size = filename.Length + 1,
Offset = (uint)br.BaseStream.Position
};
fsOut.Write(System.Text.Encoding.UTF8.GetBytes(filename + "\0"));
}
// update the MD20 size field
fsIn.Position = 4;
bw.Write((uint)(bw.BaseStream.Length - 8));
// parse the MD20 header
br.BaseStream.Position = 8;
M2Header header = MemoryMarshal.Read<M2Header>(br.ReadBytes(Marshal.SizeOf<M2Header>()));
// jump to the textures and update the texture name M2Array header
bw.BaseStream.Position = header.OfsTextures;
buffer = new byte[8];
for (int i = 0; i < textureLookup.Length; i++)
{
bw.BaseStream.Position += 8;
MemoryMarshal.Write(buffer, ref textureLookup[i]);
fsOut.Write(buffer);
}
// copy the other chunks
br.BaseStream.Position = md20Size + 8;
CopyChunks(br, bw, 0x44495854);
// overwrite the input file
fsIn.Close();
using (var fsFinal = File.Create(file))
fsOut.WriteTo(fsFinal);
}
}
private static bool SeekChunk(BinaryReader br, uint token, out int size)
{
uint ctoken = 0;
size = 0;
while (br.BaseStream.Position < br.BaseStream.Length)
{
ctoken = br.ReadUInt32();
size = br.ReadInt32();
if (ctoken == token)
return true;
br.BaseStream.Position += size;
}
return false;
}
private static void CopyChunks(BinaryReader br, BinaryWriter bw, params uint[] ignore)
{
uint token;
int size;
while (br.BaseStream.Position < br.BaseStream.Length)
{
token = br.ReadUInt32();
size = br.ReadInt32();
if (Array.IndexOf(ignore, token) > -1)
{
br.BaseStream.Position += size;
}
else
{
bw.Write(token);
bw.Write(size);
bw.Write(br.ReadBytes(size));
}
}
}
private static void LoadLookup()
{
FileNameLookup = new Dictionary<uint, string>();
string[] parts;
using (var sr = new StreamReader("listfile.csv"))
{
while (!sr.EndOfStream)
{
parts = sr.ReadLine().Split(';', 2);
if (!parts[1].EndsWith("blp"))
continue;
FileNameLookup[uint.Parse(parts[0])] = parts[1];
}
}
}
}
[StructLayout(LayoutKind.Sequential)]
struct M2ArrayHeader
{
public int Size;
public uint Offset;
}
[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