using System; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using OmegaLeo.HelperLib.Shared.Attributes; namespace OmegaLeo.HelperLib.Steamworks { [Documentation(nameof(SteamManager), "Manages Steamworks initialization, callback dispatch, and shutdown for applications using the Steam API.", null, @"```csharp SteamManager.ConfigureLogger(logger); SteamManager.ConfigureLibraryPath(pluginFolder); if (SteamManager.RestartAppIfNecessary(480)) { Quit(); return; } var steam = SteamManager.Instance; steam.OverlayActivated += overlay => Pause(overlay.Active); steam.Update(); ```")] public class SteamManager { private static SteamManager _instance; private static uint? _configuredAppId; private static ILogger _logger = NullLogger.Instance; private static readonly SteamCallbackDispatcher CallbackDispatcher = new SteamCallbackDispatcher(); private static readonly SteamAppsManager AppsManager = new SteamAppsManager(); private static readonly SteamUgcManager UgcManager = new SteamUgcManager(); private static readonly SteamRemotePlayManager RemotePlayManager = new SteamRemotePlayManager(); private static readonly SteamControllerManager ControllerManager = new SteamControllerManager(); private static readonly SteamFriendsManager FriendsManager = new SteamFriendsManager(); private static readonly SteamGameCoordinatorManager GameCoordinatorManager = new SteamGameCoordinatorManager(); private static readonly SteamGameServerManager GameServerManager = new SteamGameServerManager(); private static readonly SteamGameServerStatsManager GameServerStatsManager = new SteamGameServerStatsManager(); private static readonly SteamHtmlSurfaceManager HtmlSurfaceManager = new SteamHtmlSurfaceManager(); private static readonly SteamInputManager InputManager = new SteamInputManager(); private static readonly SteamInventoryManager InventoryManager = new SteamInventoryManager(); private static readonly SteamMatchmakingManager MatchmakingManager = new SteamMatchmakingManager(); private static readonly SteamMatchmakingServersManager MatchmakingServersManager = new SteamMatchmakingServersManager(); private static readonly SteamMusicManager MusicManager = new SteamMusicManager(); private static readonly SteamPartiesManager PartiesManager = new SteamPartiesManager(); private static readonly SteamRemoteStorageManager RemoteStorageManager = new SteamRemoteStorageManager(); private static readonly SteamScreenshotsManager ScreenshotsManager = new SteamScreenshotsManager(); private static readonly SteamUserStatsManager UserStatsManager = new SteamUserStatsManager(); private static readonly SteamUtilsManager UtilsManager = new SteamUtilsManager(); private static readonly SteamVideoManager VideoManager = new SteamVideoManager(); [Documentation(nameof(Instance), "Singleton access to the Steam manager.", null, null)] public static SteamManager Instance => _instance ??= new SteamManager(); internal static ILogger Logger => _logger; internal static SteamCallbackDispatcher Dispatcher => CallbackDispatcher; [Documentation(nameof(ConfigureLogger), "Configures the logger used by Steamworks helper classes.", new[] { "logger: ILogger instance to receive Steamworks logs." }, null)] public static void ConfigureLogger(ILogger logger) { _logger = logger ?? NullLogger.Instance; } [Documentation(nameof(ConfigureLibraryPath), "Adds a folder (or full file path) to search first for the Steam API native library (steam_api64.dll / libsteam_api.so / libsteam_api.dylib). Can be called several times; paths are tried in order, then the default locations. Must be called before Instance or RestartAppIfNecessary. Returns false when the library is already loaded.", new[] { "directoryOrFile: Folder containing the native library, or the full path to it." }, @"```csharp SteamManager.ConfigureLibraryPath(Path.Combine(gameDataFolder, ""Plugins"", ""x86_64"")); ```")] public static bool ConfigureLibraryPath(string directoryOrFile) { if (!SteamNativeApi.AddLibrarySearchPath(directoryOrFile)) { _logger.LogWarning("ConfigureLibraryPath ignored for '{SteamLibraryPath}': the path is empty or the Steam API library is already loaded.", directoryOrFile); return false; } return true; } [Documentation(nameof(ConfigureManualDispatch), "Enables or disables manual callback dispatch (enabled by default). When disabled, Update() uses SteamAPI_RunCallbacks and no callback events or async call results are delivered. Must be called before Instance.", new[] { "enabled: True to use manual dispatch." }, null)] public static bool ConfigureManualDispatch(bool enabled) { return _instance == null && SteamNativeApi.ConfigureManualDispatch(enabled); } [Documentation(nameof(RestartAppIfNecessary), "Calls SteamAPI_RestartAppIfNecessary. Returns true when the game was not launched through Steam and Steam is relaunching it: quit immediately in that case. Call it before Instance and before ConfigureAppId (steam_appid.txt, and possibly the SteamAppId environment variable, suppress the relaunch). Returns false when the native library cannot be loaded.", new[] { "appId: Your Steam AppId." }, @"```csharp if (SteamManager.RestartAppIfNecessary(480)) { Quit(); return; } ```")] public static bool RestartAppIfNecessary(uint appId) { if (appId == 0) { return false; } if (_configuredAppId.HasValue) { _logger.LogWarning("RestartAppIfNecessary was called after ConfigureAppId; the AppId environment variables or steam_appid.txt may stop Steam from relaunching the game."); } var restarting = SteamNativeApi.RestartAppIfNecessary(appId); if (restarting) { _logger.LogInformation("Steam is relaunching the game through the Steam client; the current process should exit."); } return restarting; } [Documentation(nameof(ConfigureAppId), "Configures the Steam AppId used during initialization by setting the SteamAppId/SteamGameId environment variables. Does not write steam_appid.txt. Must be called before accessing the singleton instance.", new[] { "appId: Steam application identifier." }, null)] public static bool ConfigureAppId(uint appId) { return ConfigureAppId(appId, false); } [Documentation(nameof(ConfigureAppId), "Configures the Steam AppId used during initialization. Optionally writes steam_appid.txt next to the executable and in the working directory (useful in editors and development builds; leave false for shipped builds). Must be called before accessing the singleton instance.", new[] { "appId: Steam application identifier.", "writeSteamAppIdFile: True to also write steam_appid.txt." }, null)] public static bool ConfigureAppId(uint appId, bool writeSteamAppIdFile) { if (appId == 0) { return false; } // AppId must be configured before the singleton is created. if (_instance != null) { return false; } _configuredAppId = appId; SteamNativeApi.ConfigureAppId(appId, writeSteamAppIdFile); return true; } [Documentation(nameof(IsSteamworksInitialized), "Indicates whether Steamworks is initialized and ready for API calls.", null, null)] public bool IsSteamworksInitialized { get; private set; } [Documentation(nameof(InitErrorMessage), "The reason initialization failed, as reported by SteamAPI_InitFlat when available. Empty after a successful init.", null, null)] public string InitErrorMessage { get; private set; } = string.Empty; [Documentation(nameof(Callbacks), "Provides callback subscriptions and async call results through manual dispatch.", null, null)] public SteamCallbackDispatcher Callbacks => CallbackDispatcher; [Documentation(nameof(OverlayActivated), "Raised from Update() when the Steam overlay opens or closes. Same as Callbacks.OverlayActivated.", null, null)] public event Action OverlayActivated { add => CallbackDispatcher.OverlayActivated += value; remove => CallbackDispatcher.OverlayActivated -= value; } [Documentation(nameof(Apps), "Provides access to ISteamApps bindings (ownership, DLC, language).", null, null)] public SteamAppsManager Apps => AppsManager; [Documentation(nameof(Ugc), "Provides access to Steam Workshop (UGC) helper operations.", null, null)] public SteamUgcManager Ugc => UgcManager; [Documentation(nameof(RemotePlay), "Provides access to Steam Remote Play helper operations.", null, null)] public SteamRemotePlayManager RemotePlay => RemotePlayManager; [Documentation(nameof(Controller), "Provides access to ISteamController bindings.", null, null)] public SteamControllerManager Controller => ControllerManager; [Documentation(nameof(Friends), "Provides access to ISteamFriends bindings.", null, null)] public SteamFriendsManager Friends => FriendsManager; [Documentation(nameof(GameCoordinator), "Provides access to ISteamGameCoordinator bindings.", null, null)] public SteamGameCoordinatorManager GameCoordinator => GameCoordinatorManager; [Documentation(nameof(GameServer), "Provides access to ISteamGameServer bindings.", null, null)] public SteamGameServerManager GameServer => GameServerManager; [Documentation(nameof(GameServerStats), "Provides access to ISteamGameServerStats bindings.", null, null)] public SteamGameServerStatsManager GameServerStats => GameServerStatsManager; [Documentation(nameof(HtmlSurface), "Provides access to ISteamHTMLSurface bindings.", null, null)] public SteamHtmlSurfaceManager HtmlSurface => HtmlSurfaceManager; [Documentation(nameof(Input), "Provides access to ISteamInput bindings.", null, null)] public SteamInputManager Input => InputManager; [Documentation(nameof(Inventory), "Provides access to ISteamInventory bindings.", null, null)] public SteamInventoryManager Inventory => InventoryManager; [Documentation(nameof(Matchmaking), "Provides access to ISteamMatchmaking bindings.", null, null)] public SteamMatchmakingManager Matchmaking => MatchmakingManager; [Documentation(nameof(MatchmakingServers), "Provides access to ISteamMatchmakingServers bindings.", null, null)] public SteamMatchmakingServersManager MatchmakingServers => MatchmakingServersManager; [Documentation(nameof(Music), "Provides access to ISteamMusic bindings.", null, null)] public SteamMusicManager Music => MusicManager; [Documentation(nameof(Parties), "Provides access to ISteamParties bindings.", null, null)] public SteamPartiesManager Parties => PartiesManager; [Documentation(nameof(RemoteStorage), "Provides access to ISteamRemoteStorage bindings.", null, null)] public SteamRemoteStorageManager RemoteStorage => RemoteStorageManager; [Documentation(nameof(Screenshots), "Provides access to ISteamScreenshots bindings.", null, null)] public SteamScreenshotsManager Screenshots => ScreenshotsManager; [Documentation(nameof(UserStats), "Provides access to ISteamUserStats bindings.", null, null)] public SteamUserStatsManager UserStats => UserStatsManager; [Documentation(nameof(Utils), "Provides access to ISteamUtils bindings.", null, null)] public SteamUtilsManager Utils => UtilsManager; [Documentation(nameof(Video), "Provides access to ISteamVideo bindings.", null, null)] public SteamVideoManager Video => VideoManager; private SteamManager() { try { // Attempt to initialize Steamworks if (SteamNativeApi.Initialize(_configuredAppId)) { IsSteamworksInitialized = true; _logger.LogInformation("Steamworks initialized successfully (manual callback dispatch: {ManualDispatchActive}).", SteamNativeApi.IsManualDispatchActive); } else { InitErrorMessage = SteamNativeApi.LastInitErrorMessage; _logger.LogWarning("Steamworks initialization failed: {InitErrorMessage} Ensure Steam is running and AppId context is valid.", InitErrorMessage); } } catch (Exception e) { _logger.LogError(e, "An exception occurred during Steamworks initialization."); } } // This must be called from your game's main loop (e.g., Update in Unity/Godot) [Documentation(nameof(Update), "Executes Steamworks callbacks (manual dispatch, or SteamAPI_RunCallbacks when manual dispatch is unavailable) and delivers events and async call results. Call every frame from your game loop's main thread.", null, null)] public void Update() { if (!IsSteamworksInitialized) { return; } if (SteamNativeApi.IsManualDispatchActive) { CallbackDispatcher.RunFrame(); } else { SteamNativeApi.RunCallbacks(); } } [Documentation(nameof(Shutdown), "Shuts down Steamworks and clears the initialized state. Pending async call results complete with IoFailure = true.", null, null)] public void Shutdown() { if (IsSteamworksInitialized) { CallbackDispatcher.FailPendingCallResults(); SteamNativeApi.Shutdown(); IsSteamworksInitialized = false; } } [Documentation(nameof(GetSteamName), "Gets the current Steam persona name. Returns \"Player\" when Steamworks is unavailable.", null, null)] public string GetSteamName() { if (IsSteamworksInitialized) { return SteamNativeApi.GetPersonaName(); } return "Player"; } } }