399 lines
13 KiB
C#
399 lines
13 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace OmegaLeo.HelperLib.Steamworks
|
|
{
|
|
internal static class SteamNativeApi
|
|
{
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
[return: MarshalAs(UnmanagedType.I1)]
|
|
private delegate bool SteamApiInitDelegate();
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
private delegate void SteamApiVoidDelegate();
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
private delegate IntPtr SteamFriendsAccessorDelegate();
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
private delegate IntPtr SteamUserStatsAccessorDelegate();
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
private delegate IntPtr SteamFriendsGetPersonaNameDelegate(IntPtr steamFriends);
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
[return: MarshalAs(UnmanagedType.I1)]
|
|
private delegate bool SteamUserStatsSetAchievementDelegate(IntPtr steamUserStats, [MarshalAs(UnmanagedType.LPStr)] string achievementId);
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
[return: MarshalAs(UnmanagedType.I1)]
|
|
private delegate bool SteamUserStatsStoreStatsDelegate(IntPtr steamUserStats);
|
|
|
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
|
[return: MarshalAs(UnmanagedType.I1)]
|
|
private delegate bool SteamUserStatsGetAchievementDelegate(IntPtr steamUserStats, [MarshalAs(UnmanagedType.LPStr)] string achievementId, [MarshalAs(UnmanagedType.I1)] out bool achieved);
|
|
|
|
private static readonly object SyncRoot = new object();
|
|
|
|
private static IntPtr _libraryHandle;
|
|
private static bool _libraryLoaded;
|
|
private static bool _initialized;
|
|
|
|
private static SteamApiInitDelegate _steamApiInit;
|
|
private static SteamApiVoidDelegate _steamApiShutdown;
|
|
private static SteamApiVoidDelegate _steamApiRunCallbacks;
|
|
private static SteamFriendsAccessorDelegate _steamFriendsAccessor;
|
|
private static SteamUserStatsAccessorDelegate _steamUserStatsAccessor;
|
|
private static SteamFriendsGetPersonaNameDelegate _steamFriendsGetPersonaName;
|
|
private static SteamUserStatsSetAchievementDelegate _steamUserStatsSetAchievement;
|
|
private static SteamUserStatsStoreStatsDelegate _steamUserStatsStoreStats;
|
|
private static SteamUserStatsGetAchievementDelegate _steamUserStatsGetAchievement;
|
|
|
|
private static IntPtr _steamFriends;
|
|
private static IntPtr _steamUserStats;
|
|
|
|
public static bool IsAvailable => EnsureLoaded();
|
|
|
|
public static bool Initialize(uint? appId = null)
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
if (appId.HasValue && appId.Value > 0)
|
|
{
|
|
ConfigureAppId(appId.Value);
|
|
}
|
|
|
|
if (_initialized)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!EnsureLoaded())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_steamApiInit == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_initialized = _steamApiInit();
|
|
if (_initialized)
|
|
{
|
|
CacheInterfaces();
|
|
}
|
|
|
|
return _initialized;
|
|
}
|
|
}
|
|
|
|
public static void ConfigureAppId(uint appId)
|
|
{
|
|
if (appId == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
lock (SyncRoot)
|
|
{
|
|
ApplyAppIdEnvironment(appId);
|
|
TryWriteSteamAppIdFile(appId);
|
|
}
|
|
}
|
|
|
|
public static void Shutdown()
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
if (!_initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_steamApiShutdown?.Invoke();
|
|
_initialized = false;
|
|
_steamFriends = IntPtr.Zero;
|
|
_steamUserStats = IntPtr.Zero;
|
|
}
|
|
}
|
|
|
|
public static void RunCallbacks()
|
|
{
|
|
if (!_initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_steamApiRunCallbacks?.Invoke();
|
|
}
|
|
|
|
public static string GetPersonaName()
|
|
{
|
|
if (!EnsureReady())
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var namePtr = _steamFriendsGetPersonaName?.Invoke(_steamFriends) ?? IntPtr.Zero;
|
|
return namePtr == IntPtr.Zero ? string.Empty : Marshal.PtrToStringAnsi(namePtr) ?? string.Empty;
|
|
}
|
|
|
|
public static bool SetAchievement(string achievementId)
|
|
{
|
|
if (!EnsureReady())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return _steamUserStatsSetAchievement?.Invoke(_steamUserStats, achievementId) ?? false;
|
|
}
|
|
|
|
public static bool StoreStats()
|
|
{
|
|
if (!EnsureReady())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return _steamUserStatsStoreStats?.Invoke(_steamUserStats) ?? false;
|
|
}
|
|
|
|
public static bool GetAchievement(string achievementId, out bool achieved)
|
|
{
|
|
achieved = false;
|
|
|
|
if (!EnsureReady())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return _steamUserStatsGetAchievement?.Invoke(_steamUserStats, achievementId, out achieved) ?? false;
|
|
}
|
|
|
|
private static bool EnsureReady()
|
|
{
|
|
if (!EnsureLoaded())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!_initialized)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_steamFriends == IntPtr.Zero || _steamUserStats == IntPtr.Zero)
|
|
{
|
|
CacheInterfaces();
|
|
}
|
|
|
|
return _steamFriends != IntPtr.Zero && _steamUserStats != IntPtr.Zero;
|
|
}
|
|
|
|
private static bool EnsureLoaded()
|
|
{
|
|
lock (SyncRoot)
|
|
{
|
|
if (_libraryLoaded)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
foreach (var candidate in GetLibraryCandidates())
|
|
{
|
|
try
|
|
{
|
|
_libraryHandle = LoadUnmanagedLibrary(candidate);
|
|
_libraryLoaded = _libraryHandle != IntPtr.Zero;
|
|
if (_libraryLoaded)
|
|
{
|
|
BindDelegates();
|
|
return true;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// Try the next candidate.
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static void BindDelegates()
|
|
{
|
|
_steamApiInit =
|
|
Bind<SteamApiInitDelegate>("SteamAPI_Init")
|
|
?? Bind<SteamApiInitDelegate>("SteamAPI_InitSafe");
|
|
_steamApiShutdown = Bind<SteamApiVoidDelegate>("SteamAPI_Shutdown");
|
|
_steamApiRunCallbacks = Bind<SteamApiVoidDelegate>("SteamAPI_RunCallbacks");
|
|
_steamFriendsAccessor = BindInterfaceAccessor<SteamFriendsAccessorDelegate>("SteamFriends", "ISteamFriends");
|
|
_steamUserStatsAccessor = BindInterfaceAccessor<SteamUserStatsAccessorDelegate>("SteamUserStats", "ISteamUserStats");
|
|
_steamFriendsGetPersonaName = Bind<SteamFriendsGetPersonaNameDelegate>("SteamAPI_ISteamFriends_GetPersonaName");
|
|
_steamUserStatsSetAchievement = Bind<SteamUserStatsSetAchievementDelegate>("SteamAPI_ISteamUserStats_SetAchievement");
|
|
_steamUserStatsStoreStats = Bind<SteamUserStatsStoreStatsDelegate>("SteamAPI_ISteamUserStats_StoreStats");
|
|
_steamUserStatsGetAchievement = Bind<SteamUserStatsGetAchievementDelegate>("SteamAPI_ISteamUserStats_GetAchievement");
|
|
}
|
|
|
|
private static T Bind<T>(string exportName) where T : class
|
|
{
|
|
return TryGetExport(_libraryHandle, exportName, out var export)
|
|
? Marshal.GetDelegateForFunctionPointer(export, typeof(T)) as T
|
|
: null;
|
|
}
|
|
|
|
private static T BindInterfaceAccessor<T>(string friendlyName, string interfaceName) where T : class
|
|
{
|
|
for (var version = 1; version <= 64; version++)
|
|
{
|
|
var exportName = $"SteamAPI_{friendlyName}_v{version:000}";
|
|
if (TryGetExport(_libraryHandle, exportName, out var export) ||
|
|
TryGetExport(_libraryHandle, $"SteamAPI_{interfaceName}_v{version:000}", out export))
|
|
{
|
|
return Marshal.GetDelegateForFunctionPointer(export, typeof(T)) as T;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static void CacheInterfaces()
|
|
{
|
|
_steamFriends = _steamFriendsAccessor?.Invoke() ?? IntPtr.Zero;
|
|
_steamUserStats = _steamUserStatsAccessor?.Invoke() ?? IntPtr.Zero;
|
|
}
|
|
|
|
private static string[] GetLibraryCandidates()
|
|
{
|
|
var libraryName = GetLibraryFileName();
|
|
var runtimeIdentifier = GetRuntimeIdentifier();
|
|
|
|
return new[]
|
|
{
|
|
Path.Combine(AppContext.BaseDirectory, "runtimes", runtimeIdentifier, "native", libraryName),
|
|
Path.Combine(AppContext.BaseDirectory, libraryName),
|
|
libraryName
|
|
};
|
|
}
|
|
|
|
private static string GetRuntimeIdentifier()
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
return Environment.Is64BitProcess ? "win-x64" : "win-x86";
|
|
}
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
{
|
|
return Environment.Is64BitProcess ? "osx-x64" : "osx-x86";
|
|
}
|
|
|
|
return Environment.Is64BitProcess ? "linux-x64" : "linux-x86";
|
|
}
|
|
|
|
private static string GetLibraryFileName()
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
return Environment.Is64BitProcess ? "steam_api64.dll" : "steam_api.dll";
|
|
}
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
{
|
|
return "libsteam_api.dylib";
|
|
}
|
|
|
|
return "libsteam_api.so";
|
|
}
|
|
|
|
private static void ApplyAppIdEnvironment(uint appId)
|
|
{
|
|
var appIdValue = appId.ToString();
|
|
Environment.SetEnvironmentVariable("SteamAppId", appIdValue);
|
|
Environment.SetEnvironmentVariable("SteamGameId", appIdValue);
|
|
}
|
|
|
|
private static void TryWriteSteamAppIdFile(uint appId)
|
|
{
|
|
var appIdValue = appId.ToString();
|
|
var candidatePaths = new[]
|
|
{
|
|
Path.Combine(AppContext.BaseDirectory, "steam_appid.txt"),
|
|
Path.Combine(Directory.GetCurrentDirectory(), "steam_appid.txt")
|
|
};
|
|
|
|
foreach (var path in candidatePaths)
|
|
{
|
|
try
|
|
{
|
|
File.WriteAllText(path, appIdValue);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore write failures and continue with other candidates.
|
|
}
|
|
}
|
|
}
|
|
|
|
private static IntPtr LoadUnmanagedLibrary(string path)
|
|
{
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
return LoadLibraryWindows(path);
|
|
}
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
|
{
|
|
return DlopenMac(path, RtldNow);
|
|
}
|
|
|
|
return DlopenLinux(path, RtldNow);
|
|
}
|
|
|
|
private static bool TryGetExport(IntPtr libraryHandle, string exportName, out IntPtr export)
|
|
{
|
|
if (libraryHandle == IntPtr.Zero)
|
|
{
|
|
export = IntPtr.Zero;
|
|
return false;
|
|
}
|
|
|
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
{
|
|
export = GetProcAddressWindows(libraryHandle, exportName);
|
|
return export != IntPtr.Zero;
|
|
}
|
|
|
|
export = RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
|
|
? DlsymMac(libraryHandle, exportName)
|
|
: DlsymLinux(libraryHandle, exportName);
|
|
return export != IntPtr.Zero;
|
|
}
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, EntryPoint = "LoadLibraryW")]
|
|
private static extern IntPtr LoadLibraryWindows(string lpFileName);
|
|
|
|
[DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true, EntryPoint = "GetProcAddress")]
|
|
private static extern IntPtr GetProcAddressWindows(IntPtr hModule, string procName);
|
|
|
|
[DllImport("libdl.so.2", EntryPoint = "dlopen")]
|
|
private static extern IntPtr DlopenLinux(string fileName, int flags);
|
|
|
|
[DllImport("libdl.so.2", EntryPoint = "dlsym")]
|
|
private static extern IntPtr DlsymLinux(IntPtr handle, string symbol);
|
|
|
|
[DllImport("libSystem.B.dylib", EntryPoint = "dlopen")]
|
|
private static extern IntPtr DlopenMac(string fileName, int flags);
|
|
|
|
[DllImport("libSystem.B.dylib", EntryPoint = "dlsym")]
|
|
private static extern IntPtr DlsymMac(IntPtr handle, string symbol);
|
|
|
|
private const int RtldNow = 2;
|
|
|
|
}
|
|
}
|
|
|