Alright, so I guess the first thing that should be mentioned is that Windows like to "block" downloads, and the wrappers for S3PI don't work unless they're unblocked. Many users use the default Windows archive extractor, which extracts all the contents of a blocked archive as blocked, and many of them come complaining, so I found a technical solution to this which is to unblock the contents of the folder of your program.
Below I have a class for platform-specific things, which is needed to determine whether a user is on Windows, so as to only unblock on Windows, as it'll throw a fatal exception on other platforms:
Code:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
public static class Platform
{
public static bool IsLinux
{
get
{
return (OS & OSFlags.Linux) != 0;
}
}
public static bool IsMacOS
{
get
{
return (OS & OSFlags.Darwin) != 0;
}
}
public static bool IsRunningUnderWine
{
get
{
return IsWindows && WineDetector.IsRunningUnderWine;
}
}
public static bool IsUnix
{
get
{
return (OS & OSFlags.Unix) != 0;
}
}
public static bool IsWindows
{
get
{
return (OS & OSFlags.Windows) != 0;
}
}
public static OSFlags OS
{
get
{
switch ((int)Environment.OSVersion.Platform)
{
case 4:
case 128:
var os = OSFlags.Unix;
var uname = GetCommandOutput("uname").TrimEnd('\n');
switch (uname)
{
case "Darwin":
case "Linux":
os |= (OSFlags)Enum.Parse(typeof(OSFlags), uname);
break;
}
return os;
default:
return OSFlags.Windows;
}
}
}
[Flags]
public enum OSFlags
{
Windows = 1,
Unix,
Linux = 4,
Darwin = 8
}
static class FileUnblocker
{
[DllImport("kernel32", CharSet = CharSet.Unicode, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool DeleteFile(string name);
public static bool Unblock(string filename)
{
return DeleteFile(filename + ":Zone.Identifier");
}
}
static class WineDetector
{
[DllImport("ntdll.dll", EntryPoint = "wine_get_version")]
static extern IntPtr WineGetVersion();
public static bool IsRunningUnderWine
{
get
{
try
{
return WineGetVersion() != IntPtr.Zero;
}
catch (EntryPointNotFoundException)
{
return false;
}
}
}
}
public static string GetCommandOutput(string command, string arguments = "")
{
using (var process = new Process
{
StartInfo = new ProcessStartInfo
{
Arguments = arguments,
CreateNoWindow = true,
FileName = command,
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false
}
})
{
process.Start();
string error = process.StandardError.ReadToEnd(),
output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
return string.IsNullOrEmpty(error) ? output : string.Format("Error: {0}\nOutput: {1}", error, output);
}
}
public static bool UnblockFile(string filename)
{
return FileUnblocker.Unblock(filename);
}
}
Notice in the above snippet the "FileUnblocker" subclass.
This should be called for everything within the program's folder when the program first launches, as follows:
Code:
if (Platform.IsWindows)
{
foreach (var filename in System.IO.Directory.GetFiles(System.AppDomain.CurrentDomain.BaseDirectory))
{
Platform.UnblockFile(filename);
}
}
Alright, I haven't been contributing to this thread like I promised @zoe22 I would, so allow me to add some things:
This is a very crappy approach, as it requires two libraries and depends on an exception to check if a texture uses R8G8_UNorm. As of writing this, I'm looking for a better approach that works on all platforms
For reading DDS images, I used GDImageLibrary (for compressed textures) and Teximp.Net (for R8G8_UNorm textures). Below is a snippet that is a catch-all for reading DDS images from _IMG resources:
Code:
public static Bitmap GetTexture(this IPackage package, IResourceIndexEntry resourceIndexEntry)
{
Bitmap image;
var resource = WrapperDealer.GetResource(0, package, resourceIndexEntry);
try
{
image = GDImageLibrary._DDS.LoadImage(resource.AsBytes);
}
catch (ArgumentNullException)
{
using (var dds = TeximpNet.DDS.DDSFile.Read(resource.Stream))
{
var mipmap = dds.MipChains[0][0];
var pixelFormat = PixelFormat.Format32bppArgb;
image = new Bitmap(mipmap.Width, mipmap.Height, pixelFormat);
var bitmapData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.WriteOnly, pixelFormat);
var byteArray = new byte[mipmap.SizeInBytes];
Marshal.Copy(mipmap.Data, byteArray, 0, byteArray.Length);
if (dds.Format == TeximpNet.DDS.DXGIFormat.R8G8_UNorm)
{
var tempByteArray = new byte[byteArray.Length * 2];
for (var i = 0; i < byteArray.Length; i += 2)
{
tempByteArray[i * 2] = tempByteArray[i * 2 + 1] = tempByteArray[i * 2 + 2] = byteArray[i];
tempByteArray[i * 2 + 3] = byteArray[i + 1];
}
byteArray = tempByteArray;
}
Marshal.Copy(byteArray, 0, bitmapData.Scan0, byteArray.Length);
image.UnlockBits(bitmapData);
}
}
return image;
}
If you ever need to reference the game packages, I have found this to be a convenient way to do so (make sure GameFolders.xml is in your compiled application's folder):
Code:
static Dictionary<PackageTag, IPackage> sGameContentPackages, sGameImageResourcePackages, sGameThumbnailResourcePackages;
static object sLock = new object();
public static Dictionary<PackageTag, IPackage> GameContentPackages
{
get
{
if (sGameContentPackages == null)
{
lock (sLock)
{
sGameContentPackages = new Dictionary<PackageTag, IPackage>();
foreach (var game in GameFolders.Games)
{
var enumerator = game.GameContent.GetEnumerator();
while (enumerator.MoveNext())
{
sGameContentPackages.Add(enumerator.Current, s3pi.Package.Package.OpenPackage(0, enumerator.Current.Path));
}
}
}
}
return sGameContentPackages;
}
}
public static Dictionary<PackageTag, IPackage> GameImageResourcePackages
{
get
{
if (sGameImageResourcePackages == null)
{
lock (sLock)
{
sGameImageResourcePackages = new Dictionary<PackageTag, IPackage>();
foreach (var game in GameFolders.Games)
{
var enumerator = game.DDSImages.GetEnumerator();
while (enumerator.MoveNext())
{
sGameImageResourcePackages.Add(enumerator.Current, s3pi.Package.Package.OpenPackage(0, enumerator.Current.Path));
}
}
}
}
return sGameImageResourcePackages;
}
}
public static Dictionary<PackageTag, IPackage> GameThumbnailResourcePackages
{
get
{
if (sGameThumbnailResourcePackages == null)
{
lock (sLock)
{
sGameThumbnailResourcePackages = new Dictionary<PackageTag, IPackage>();
foreach (var game in GameFolders.Games)
{
var enumerator = game.Thumbnails.GetEnumerator();
while (enumerator.MoveNext())
{
sGameThumbnailResourcePackages.Add(enumerator.Current, s3pi.Package.Package.OpenPackage(0, enumerator.Current.Path));
}
}
}
}
return sGameThumbnailResourcePackages;
}
}
Below is an example of finding a resource in the game files by its instance ID from the packages of the specified dictionary:
Code:
public static Tuple<IPackage, IResourceIndexEntry> FindResourceByInstance(Dictionary<PackageTag, IPackage> gamePackages, ulong instance)
{
foreach (var gamePackage in gamePackages.Values)
{
var found = gamePackage.FindAll(x => x.Instance == instance);
if (found.Count > 0)
{
return new Tuple<IPackage, IResourceIndexEntry>(gamePackage, found[0]);
}
}
return null;
}
I also recommend using the attached modified version of GameFolders.xml for support by case-sensitive filesystems and by games that have been decompressed with TurboTravel.