using System; using System.IO; using System.Runtime.InteropServices; using System.Text; using UnityEngine; public class AAssit { public static T ReadFromBinaryOf(Stream stream, ref long readBytes, long fileOffset = -1) { // If fileOffset >= 0, seek to that position in the file if (fileOffset >= 0) { stream.Seek(fileOffset, SeekOrigin.Begin); } int size = Marshal.SizeOf(typeof(T)); byte[] buffer = new byte[size]; // Read `size` bytes into `buffer[0..size]` int bytesRead = stream.Read(buffer, 0, size); if (bytesRead < size) { Console.WriteLine($"ERROR::Not enough data in stream to read {typeof(T).Name}. Expected {size} bytes, but only read {bytesRead}."); return default(T); } // Accumulate the number of bytes read readBytes += bytesRead; // Pin and marshal GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); try { return (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); } finally { handle.Free(); } } public static T[] ReadArrayFromBinary(Stream stream, int arraySize, ref long readBytes, long fileOffset = -1) { T[] array = new T[arraySize]; for (int i = 0; i < arraySize; i++) { array[i] = ReadFromBinaryOf(stream, ref readBytes, fileOffset); } return array; } public static T[] ReadArrayFromBinary(FileStream stream, ref long readBytes, long fileOffset = -1) { // seek to the fileOffset if it's >= 0 if (fileOffset >= 0) { stream.Seek(fileOffset, SeekOrigin.Begin); } // Read the first 4 bytes to get the array size int arraySize = GetIntFromFileStream(stream, ref readBytes); if (arraySize <= 0) return null; T[] array = new T[arraySize]; for (int i = 0; i < arraySize; i++) { array[i] = ReadFromBinaryOf(stream, ref readBytes, fileOffset); } return array; } public static byte[] ReadByArray(Stream stream, ref long readBytes, int size, long fileOffset = -1) { // If fileOffset >= 0, seek to that position in the file if (fileOffset >= 0) { stream.Seek(fileOffset, SeekOrigin.Begin); } byte[] buffer = new byte[size]; // Read `size` bytes into `buffer[0..size]` int bytesRead = stream.Read(buffer, 0, size); if (bytesRead < size) { Console.WriteLine($"ERROR::Not enough data in stream to read byte array. Expected {size} bytes, but only read {bytesRead}."); return null; } // Accumulate the number of bytes read readBytes += bytesRead; return buffer; } public static bool GetBoolFromFileStream(FileStream fs, ref long readBytes) { byte[] buffer = new byte[1]; int bytesRead = fs.Read(buffer, 0, 1); if (bytesRead < 1) throw new EndOfStreamException("Not enough data in stream to read bool."); readBytes += bytesRead; return buffer[0] != 0; } public static int GetIntFromFileStream(FileStream fs, ref long readBytes) { byte[] buffer = new byte[4]; int bytesRead = fs.Read(buffer, 0, 4); if (bytesRead < 4) throw new EndOfStreamException("Not enough data in stream to read int."); readBytes += bytesRead; return BitConverter.ToInt32(buffer, 0); } public static uint GetUIntFromFileStream(FileStream fs, ref long readBytes) { byte[] buffer = new byte[4]; int bytesRead = fs.Read(buffer, 0, 4); if (bytesRead < 4) throw new EndOfStreamException("Not enough data in stream to read uint."); readBytes += bytesRead; return BitConverter.ToUInt32(buffer, 0); } public static long GetLongFromFileStream(FileStream fs, ref long readBytes) { byte[] buffer = new byte[8]; int bytesRead = fs.Read(buffer, 0, 8); if (bytesRead < 8) throw new EndOfStreamException("Not enough data in stream to read long."); readBytes += bytesRead; return BitConverter.ToInt64(buffer, 0); } /// /// Get the substring after if starts with . /// /// The full string to check. /// The tag string to look for at the beginning of . /// /// Outputs the substring of that appears immediately after , /// if does indeed start with . /// /// /// True if starts with ; false otherwise. /// public static bool GetStringFromCharsAfter(char[] buffer, string tag, out string result) { // from buffer to string string source = new string(buffer); return GetStringAfter(source, tag, out result); } public static bool GetStringAfter(string source, string tag, out string result) { // Initialize the output in case we return false result = string.Empty; // Check if buffer starts with the specified tag if (!source.StartsWith(tag)) return false; // If it starts with tag, skip tag.Length characters // and copy the rest to result result = source.Substring(tag.Length); return true; } /// /// Reads a string from a binary stream at the position indicated by . /// 1) Seeks to from the start of the stream (SeekOrigin.Begin). /// 2) Reads 4 bytes (an int) that specify the string length (in bytes). /// 3) If length is 0, returns "". /// 4) Otherwise, reads the specified number of bytes and decodes to a string. /// 5) Updates to the new position after reading. /// /// The stream from which to read. /// /// On entry, this indicates where to seek in the stream (from the beginning). /// On exit, this will be updated to the new position after reading. /// /// Outputs the decoded string. /// True if successful; otherwise false (e.g., not enough data). public static bool ReadString(Stream stream, ref long readBytes, out string result) { result = ReadString(stream, ref readBytes); return result != null; } public static string ReadString(Stream stream, ref long readBytes) { var result = string.Empty; try { int length = 0; byte[] buffer = new byte[4]; int byteRead = stream.Read(buffer, 0, 4); if (byteRead < 4) return null; readBytes += byteRead; length = BitConverter.ToInt32(buffer, 0); if (length == 0) return null; buffer = new byte[length]; byteRead = stream.Read(buffer, 0, length); if (byteRead < length) return null; readBytes += byteRead; // Encoding encoding = result = Encoding.GetEncoding(936).GetString(buffer); return result; } catch (Exception e) { // Could be EndOfStreamException, IOException, etc. // Handle or log the exception as needed return null; } } public static void ReadLine(Stream stream, ref long bytesRead, out string result) { result = Fgets(stream, 1024, Encoding.GetEncoding(936)); bytesRead += result.Length; } /// /// Reads a null-terminated string from , starting at . /// - Seeks to in the file. /// - Reads one byte at a time until encountering a null (0x00) or reaching - 1. /// - Decodes the collected bytes using the specified . /// - Appends how many bytes it actually read to . /// /// Returns true on success (string in ), or false if an error occurs /// (EOF, buffer overflow, etc.). /// /// Open, readable . /// Absolute offset in the file to seek before reading. /// Maximum number of characters to collect (like the C++ dwBufferLength). /// Accumulates how many bytes we've read so far across calls. /// Encoding used to interpret the raw bytes (e.g. ). /// Outputs the decoded string (excluding the null terminator). /// True if a string was successfully read, false otherwise. public static bool ReadString(Stream stream,long fileOffset,int bufferLength,ref long alreadyReadBytes, Encoding encoding,out string result) { result = null; // Basic argument checks if (stream == null) throw new ArgumentNullException(nameof(stream)); if (!stream.CanRead) throw new ArgumentException("Stream must be readable.", nameof(stream)); if (bufferLength < 2) throw new ArgumentOutOfRangeException(nameof(bufferLength), "Buffer length must be >= 2."); if (encoding == null) throw new ArgumentNullException(nameof(encoding)); // 1) Seek to the specified offset from the beginning of the file stream.Seek(fileOffset, SeekOrigin.Begin); // 2) We will collect bytes into this temporary array (exclude the null terminator) byte[] buffer = new byte[bufferLength - 1]; int index = 0; // Try reading first byte int firstByte = stream.ReadByte(); if (firstByte == -1) { // EOF immediately return false; } // We have read 1 byte so far in this call long localRead = 1; byte b = (byte)firstByte; // 3) Read until we hit a zero byte (null terminator) or fill the buffer while (b != 0) { buffer[index++] = b; // Check for overflow if (index >= bufferLength - 1) { // We would overflow if we read more data return false; } int nextByte = stream.ReadByte(); if (nextByte == -1) { // EOF before we found a null terminator return false; } localRead++; b = (byte)nextByte; // Loop continues until b == 0 } // If we get here, b == 0 => null terminator found. // localRead already includes the byte we used for the null terminator // 4) Update total bytes read (across calls) alreadyReadBytes += localRead; // 5) Convert the collected bytes to a string result = encoding.GetString(buffer, 0, index); return true; } /// /// Reads up to - 1 characters from , /// stopping at newline or EOF. Returns the line (including the newline) or null if no bytes were read. /// /// An open readable Stream. /// /// The maximum number of characters to read (like the buffer size in fgets). /// The actual string can have up to maxLength - 1 characters plus a null terminator in C terms. /// /// The character encoding (ASCII, UTF8, etc.). /// A string containing the line, or null if no data was read. public static string Fgets(Stream stream, int maxLength, Encoding encoding) { if (stream == null) throw new ArgumentNullException(nameof(stream)); if (!stream.CanRead) throw new ArgumentException("Stream is not readable.", nameof(stream)); if (maxLength <= 1) throw new ArgumentOutOfRangeException(nameof(maxLength), "Must be at least 2."); // We'll store the raw bytes in a temporary buffer. // maxLength - 1 because, in the C analogy, one char is reserved for '\0'. // But in C#, we'll just use that as a ceiling. byte[] buffer = new byte[maxLength - 1]; int totalRead = 0; while (true) { int b = stream.ReadByte(); if (b == -1) { // EOF reached, stop reading break; } // Put this byte into our buffer buffer[totalRead++] = (byte)b; // If we hit a newline, stop (mimicking fgets which stops at '\n') if (b == '\n') { break; } // If we've filled up the buffer (maxLength - 1 bytes), stop if (totalRead >= buffer.Length) { break; } } // If we didn't read anything at all, return null (like fgets returning NULL on EOF) if (totalRead == 0) { return null; } // Convert the bytes we read into a string return encoding.GetString(buffer, 0, totalRead); } private static readonly uint[] l_aCRC32Table = new uint[] { 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d }; public static uint MakeIDFromString(string str) { if (str == null) throw new ArgumentNullException(nameof(str)); uint c = 0xffffffff; // shift register contents byte[] bytes = Encoding.ASCII.GetBytes(str); foreach (byte b in bytes) { c = l_aCRC32Table[(c ^ b) & 0xff] ^ (c >> 8); } return c ^ 0xffffffff; } // load the DXT texture from the byte array. // The byte array is loaded after loading the A3DSkin public static Texture2D LoadTextureDXT(byte[] ddsBytes) { // from 72 to 76 is the texture format // get the texture format from the ddsBytes byte[] formatBytes = new byte[4]; Array.Copy(ddsBytes, 84, formatBytes, 0, 4); // Convert the byte array to a string (assume it's ASCII-encoded) string textureFormat = Encoding.UTF8.GetString(formatBytes); TextureFormat.TryParse(textureFormat, out TextureFormat textureFormatEnum); if (textureFormatEnum != TextureFormat.DXT1 && textureFormatEnum != TextureFormat.DXT5) { Console.WriteLine($"ERROR::Expected DXT1 or DXT5 texture format, but got {textureFormat}. Using DXT5 instead."); textureFormatEnum = TextureFormat.DXT5; } byte ddsSizeCheck = ddsBytes[4]; if (ddsSizeCheck != 124) { Console.WriteLine("ERROR::Invalid DDS DXTn texture. Unable to read"); //this header byte should be 124 for DDS image files return Texture2D.whiteTexture; } int height = ddsBytes[13] * 256 + ddsBytes[12]; int width = ddsBytes[17] * 256 + ddsBytes[16]; int DDS_HEADER_SIZE = 128; byte[] dxtBytes = new byte[ddsBytes.Length - DDS_HEADER_SIZE]; Buffer.BlockCopy(ddsBytes, DDS_HEADER_SIZE, dxtBytes, 0, ddsBytes.Length - DDS_HEADER_SIZE); Texture2D texture = new Texture2D(width, height, textureFormatEnum, false); texture.LoadRawTextureData(dxtBytes); texture.Apply(); return (texture); } public static Texture2D LoadTextureTGA(string filePath) { if (!File.Exists(filePath)) return Texture2D.whiteTexture; using (var tgaStream = File.OpenRead(filePath)) using (BinaryReader r = new BinaryReader(tgaStream)) { // Skip some header info we don't care about. // Even if we did care, we have to move the stream seek point to the beginning, // as the previous method in the workflow left it at the end. r.BaseStream.Seek(12, SeekOrigin.Begin); short width = r.ReadInt16(); short height = r.ReadInt16(); int bitDepth = r.ReadByte(); // Skip a byte of header information we don't care about. r.BaseStream.Seek(1, SeekOrigin.Current); Texture2D tex = new Texture2D(width, height); Color32[] pulledColors = new Color32[width * height]; if (bitDepth == 32) { for (int i = 0; i < width * height; i++) { byte red = r.ReadByte(); byte green = r.ReadByte(); byte blue = r.ReadByte(); byte alpha = r.ReadByte(); pulledColors[i] = new Color32(blue, green, red, alpha); } } else if (bitDepth == 24) { for (int i = 0; i < width * height; i++) { byte red = r.ReadByte(); byte green = r.ReadByte(); byte blue = r.ReadByte(); pulledColors[i] = new Color32(blue, green, red, 1); } } else { throw new Exception("TGA texture had non 32/24 bit depth."); } tex.SetPixels32(pulledColors); tex.Apply(); return tex; } } /// /// Saves a Texture2D to a PNG file at the specified absolute path. /// If a file already exists at the path, returns true without overwriting. /// If the file doesn't exist, creates a new PNG file and saves the texture. /// /// The Texture2D to save as PNG /// The absolute file path where to save the PNG /// True if file already exists or was successfully saved, false on error public static bool SaveTexture2DToPNG(Texture2D texture, string absolutePath, bool compressedTexture = false) { // Check if texture parameter is valid if (texture == null) { Debug.LogError("SaveTexture2DToPNG: Texture is null"); return false; } // Check if the absolute path is valid if (string.IsNullOrEmpty(absolutePath)) { Debug.LogError("SaveTexture2DToPNG: Absolute path is null or empty"); return false; } // Check if file already exists if (File.Exists(absolutePath)) { return true; // File already exists, return true } try { // Ensure the directory exists string directory = Path.GetDirectoryName(absolutePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } if (compressedTexture) { // Create a temporary RenderTexture with same dimensions as source RenderTexture tempRT = RenderTexture.GetTemporary( texture.width, texture.height, 0, RenderTextureFormat.ARGB32 ); // Copy compressed texture to RT, decompressing on GPU Graphics.Blit(texture, tempRT); // Create new uncompressed texture Texture2D uncompressedTex = new Texture2D( texture.width, texture.height, TextureFormat.ARGB32, false ); // Store active RT and switch to temp RenderTexture prevRT = RenderTexture.active; RenderTexture.active = tempRT; // Read pixels from RT into uncompressed texture uncompressedTex.ReadPixels(new Rect(0, 0, tempRT.width, tempRT.height), 0, 0); uncompressedTex.Apply(); // Restore previous RT and release temp RenderTexture.active = prevRT; RenderTexture.ReleaseTemporary(tempRT); // Use uncompressed texture instead of original texture = uncompressedTex; } // Encode texture to PNG byte[] pngBytes = texture.EncodeToPNG(); if (pngBytes == null || pngBytes.Length == 0) { Debug.LogError("SaveTexture2DToPNG: Failed to encode texture to PNG"); return false; } // Write PNG bytes to file File.WriteAllBytes(absolutePath, pngBytes); Debug.Log($"SaveTexture2DToPNG: Successfully saved texture to {absolutePath}"); return true; } catch (Exception e) { Debug.LogError($"SaveTexture2DToPNG: Error saving texture to {absolutePath}: {e.Message}"); return false; } } /// /// This is for when we load a byte buffer for string. \ /// We have to remove the empty byte at the end so the string can be decoded correctly. /// /// public static void RemoveEmptyByte(ref byte[] buffer) { int firstEmptyByte = Array.IndexOf(buffer, (byte)0); if (firstEmptyByte != -1) { Array.Resize(ref buffer, firstEmptyByte); } } } public struct StringStruct { [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2048)] public char[] value; }