Skip to content

Instantly share code, notes, and snippets.

@Reelix
Created January 9, 2019 08:02
Show Gist options
  • Save Reelix/7b1a837386dba0f9fe9a58c79b2c8591 to your computer and use it in GitHub Desktop.
Save Reelix/7b1a837386dba0f9fe9a58c79b2c8591 to your computer and use it in GitHub Desktop.
C# Torchlight 1 Save Game Viewer
internal class Program
{
private static void Main(string[] args)
{
// https://code.google.com/archive/p/torchtools/wikis/TorchlightFileSpec.wiki
Console.Title = "Reelix's TL1 Save Game Viewer";
byte[] fileBytes = File.ReadAllBytes(@"C:\Users\Reelix\AppData\Roaming\runic games\torchlight\save\0.SVT");
// Remove first 4 - Unsure
fileBytes = RemoveFirstBytes(fileBytes, 4);
// Class Name
short classNameLength = GetShort(fileBytes);
fileBytes = RemoveFirstBytes(fileBytes, 2); // Short
string className = GetString(fileBytes, classNameLength);
fileBytes = RemoveFirstBytes(fileBytes, classNameLength * 2); // UTF-16LE - Spare 00 after each byte
Console.WriteLine("Class Name: " + className);
// Difficulty
int difficulty = int.Parse(fileBytes[0].ToString());
fileBytes = RemoveFirstByte(fileBytes);
Console.Write("Difficulty: ");
switch (difficulty)
{
case 0: Console.WriteLine("Easy"); break;
case 1: Console.WriteLine("Normal"); break;
case 2: Console.WriteLine("Hard"); break;
case 3: Console.WriteLine("Very Hard"); break;
}
// Remove 3 bytes - Unsure
fileBytes = RemoveFirstBytes(fileBytes, 3);
// Hardcore
int isHardcore = fileBytes[0];
fileBytes = RemoveFirstByte(fileBytes);
Console.WriteLine("Hardcore: " + (isHardcore == 1 ? "True" : "False"));
// Remove 4 bytes - Unsure
fileBytes = RemoveFirstBytes(fileBytes, 4);
// Retired
int isRetired = fileBytes[0];
fileBytes = RemoveFirstByte(fileBytes);
Console.WriteLine("Retired: " + (isRetired == 1 ? "True" : "False"));
// Remove 30 bytes - Unsure
fileBytes = RemoveFirstBytes(fileBytes, 30);
// Cheater
string isCheaterHex = fileBytes[0].ToString("X2");
Console.Write("Cheater: ");
if (isCheaterHex == "4E")
{
Console.WriteLine("False");
}
else if (isCheaterHex == "D6")
{
Console.WriteLine("True");
}
else
{
Console.WriteLine("Unknown");
}
fileBytes = RemoveFirstByte(fileBytes);
// Remove 40 bytes - Unsure
fileBytes = RemoveFirstBytes(fileBytes, 40);
// Character Name
short characterNameLength = GetShort(fileBytes);
fileBytes = RemoveFirstBytes(fileBytes, 2);
string characterName = GetString(fileBytes, characterNameLength);
fileBytes = RemoveFirstBytes(fileBytes, characterNameLength * 2);
Console.WriteLine("Character Name: " + characterName);
// Remove 84 bytes - Unsure
fileBytes = RemoveFirstBytes(fileBytes, 84);
// Character Level
int characterLevel = GetInt(fileBytes);
fileBytes = RemoveFirstBytes(fileBytes, 4); // An int is 4 bytes
Console.WriteLine("Character Level: " + characterLevel);
// XP
int characterXP = GetInt(fileBytes);
fileBytes = RemoveFirstBytes(fileBytes, 4);
Console.WriteLine("Character XP: " + characterXP);
// Fame Level
int fameLevel = GetInt(fileBytes);
fileBytes = RemoveFirstBytes(fileBytes, 4);
Console.WriteLine("Fame Level: " + fameLevel);
// Fame XP
int fameXP = GetInt(fileBytes);
fileBytes = RemoveFirstBytes(fileBytes, 4);
Console.WriteLine("Fame XP: " + fameXP);
// Remove 4 bytes - Unsure
fileBytes = RemoveFirstBytes(fileBytes, 4);
// HP
int characterHP = GetInt(fileBytes);
fileBytes = RemoveFirstBytes(fileBytes, 4);
Console.WriteLine("HP: " + characterHP);
// Bonus HP
int bonusHP = GetInt(fileBytes);
fileBytes = RemoveFirstBytes(fileBytes, 4);
Console.WriteLine("Bonus HP: " + bonusHP); // Always 0 ?
// Remove 2 bytes - Unsure
fileBytes = RemoveFirstBytes(fileBytes, 2);
// Dead
short isDead = GetShort(fileBytes);
fileBytes = RemoveFirstBytes(fileBytes, 2);
Console.WriteLine("Is Dead: " + (isDead == 0 ? "Yes" : "No"));
// Mana
int characterMP = GetInt(fileBytes);
fileBytes = RemoveFirstBytes(fileBytes, 4);
Console.WriteLine("MP: " + characterMP);
}
private static byte[] RemoveFirstByte(byte[] byteArray)
{
return RemoveFirstBytes(byteArray, 1);
}
private static byte[] RemoveFirstBytes(byte[] byteArray, int num)
{
byte[] newArray = new byte[byteArray.Length - num];
Array.Copy(byteArray, num, newArray, 0, newArray.Length);
return newArray;
}
private static short GetShort(byte[] byteArray)
{
byte[] shortLength = new byte[2];
Array.Copy(byteArray, 0, shortLength, 0, 2);
short shortVal = BitConverter.ToInt16(shortLength, 0);
return shortVal;
}
private static int GetInt(byte[] byteArray)
{
byte[] intLength = new byte[4];
Array.Copy(byteArray, 0, intLength, 0, 4);
int shortVal = BitConverter.ToInt32(intLength, 0);
return shortVal;
}
private static string GetString(byte[] byteArray, int length)
{
byte[] stringBytes = new byte[length * 2]; // UTF-16LE - Spare 00 after each byte
Array.Copy(byteArray, 0, stringBytes, 0, length * 2);
return Encoding.Unicode.GetString(stringBytes);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment