using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Reflection; using StardewValley; namespace StardewModdingAPI.Framework { /// SMAPI's extension of the game's core , used to inject SMAPI components. internal class SGameRunner : GameRunner { /********* ** Fields *********/ /// Encapsulates monitoring and logging for SMAPI. private readonly Monitor Monitor; /// Manages SMAPI events for mods. private readonly EventManager Events; /// Simplifies access to private game code. private readonly Reflector Reflection; /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. private readonly Action ExitGameImmediately; /// The core SMAPI mod hooks. private readonly SModHooks ModHooks; /// The core multiplayer logic. private readonly SMultiplayer Multiplayer; /// Raised after the game finishes loading its initial content. private readonly Action OnGameContentLoaded; /// Raised when XNA is updating (roughly 60 times per second). private readonly Action OnGameUpdating; /// Raised when the game instance for a local split-screen player is updating (once per per player). private readonly Action OnPlayerInstanceUpdating; /// Raised before the game exits. private readonly Action OnGameExiting; /********* ** Public methods *********/ /// The singleton instance. public static SGameRunner Instance => (SGameRunner)GameRunner.instance; /********* ** Public methods *********/ /// Construct an instance. /// Encapsulates monitoring and logging for SMAPI. /// Simplifies access to private game code. /// Manages SMAPI events for mods. /// Handles mod hooks provided by the game. /// The core multiplayer logic. /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// Raised after the game finishes loading its initial content. /// Raised when XNA is updating its state (roughly 60 times per second). /// Raised when the game instance for a local split-screen player is updating (once per per player). /// Raised before the game exits. public SGameRunner(Monitor monitor, Reflector reflection, EventManager eventManager, SModHooks modHooks, SMultiplayer multiplayer, Action exitGameImmediately, Action onGameContentLoaded, Action onGameUpdating, Action onPlayerInstanceUpdating, Action onGameExiting) { // init XNA Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // hook into game this.ModHooks = modHooks; // init SMAPI this.Monitor = monitor; this.Events = eventManager; this.Reflection = reflection; this.Multiplayer = multiplayer; this.ExitGameImmediately = exitGameImmediately; this.OnGameContentLoaded = onGameContentLoaded; this.OnGameUpdating = onGameUpdating; this.OnPlayerInstanceUpdating = onPlayerInstanceUpdating; this.OnGameExiting = onGameExiting; } /// Create a game instance for a local player. /// The player index. /// The instance index. public override Game1 CreateGameInstance(PlayerIndex playerIndex = PlayerIndex.One, int instanceIndex = 0) { SInputState inputState = new(); return new SGame(playerIndex, instanceIndex, this.Monitor, this.Reflection, this.Events, inputState, this.ModHooks, this.Multiplayer, this.ExitGameImmediately, this.OnPlayerInstanceUpdating, this.OnGameContentLoaded); } /// public override void AddGameInstance(PlayerIndex playerIndex) { base.AddGameInstance(playerIndex); EarlyConstants.LogScreenId = Context.ScreenId; this.UpdateForSplitScreenChanges(); } /// public override void RemoveGameInstance(Game1 gameInstance) { base.RemoveGameInstance(gameInstance); if (this.gameInstances.Count <= 1) EarlyConstants.LogScreenId = null; this.UpdateForSplitScreenChanges(); } /// Get the screen ID for a given player ID, if the player is local. /// The player ID to check. public int? GetScreenId(long playerId) { return this.gameInstances .FirstOrDefault(p => ((SGame)p).PlayerId == playerId) ?.instanceId; } /********* ** Protected methods *********/ /// Perform cleanup logic when the game exits. /// The event sender. /// The event args. /// This overrides the logic in to let SMAPI clean up before exit. protected override void OnExiting(object sender, EventArgs args) { this.OnGameExiting(); } /// The method called when the game is updating its state (roughly 60 times per second). /// A snapshot of the game timing state. protected override void Update(GameTime gameTime) { this.OnGameUpdating(gameTime, () => base.Update(gameTime)); } /// Update metadata when a split screen is added or removed. private void UpdateForSplitScreenChanges() { HashSet oldScreenIds = new(Context.ActiveScreenIds); // track active screens Context.ActiveScreenIds.Clear(); foreach (var screen in this.gameInstances) Context.ActiveScreenIds.Add(screen.instanceId); // remember last removed screen foreach (int id in oldScreenIds) { if (!Context.ActiveScreenIds.Contains(id)) Context.LastRemovedScreenId = id; } } } }