summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/SGameRunner.cs
blob: 45e7369c80fa629c69b35dac6e37f9c7c0487e1b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
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
{
    /// <summary>SMAPI's extension of the game's core <see cref="GameRunner"/>, used to inject SMAPI components.</summary>
    internal class SGameRunner : GameRunner
    {
        /*********
        ** Fields
        *********/
        /// <summary>Encapsulates monitoring and logging for SMAPI.</summary>
        private readonly Monitor Monitor;

        /// <summary>Manages SMAPI events for mods.</summary>
        private readonly EventManager Events;

        /// <summary>Simplifies access to private game code.</summary>
        private readonly Reflector Reflection;

        /// <summary>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.</summary>
        private readonly Action<string> ExitGameImmediately;

        /// <summary>The core SMAPI mod hooks.</summary>
        private readonly SModHooks ModHooks;

        /// <summary>The core multiplayer logic.</summary>
        private readonly SMultiplayer Multiplayer;

        /// <summary>Raised after the game finishes loading its initial content.</summary>
        private readonly Action OnGameContentLoaded;

        /// <summary>Raised when XNA is updating (roughly 60 times per second).</summary>
        private readonly Action<GameTime, Action> OnGameUpdating;

        /// <summary>Raised when the game instance for a local split-screen player is updating (once per <see cref="OnGameUpdating"/> per player).</summary>
        private readonly Action<SGame, GameTime, Action> OnPlayerInstanceUpdating;

        /// <summary>Raised before the game exits.</summary>
        private readonly Action OnGameExiting;


        /*********
        ** Public methods
        *********/
        /// <summary>The singleton instance.</summary>
        public static SGameRunner Instance => (SGameRunner)GameRunner.instance;


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="monitor">Encapsulates monitoring and logging for SMAPI.</param>
        /// <param name="reflection">Simplifies access to private game code.</param>
        /// <param name="eventManager">Manages SMAPI events for mods.</param>
        /// <param name="modHooks">Handles mod hooks provided by the game.</param>
        /// <param name="multiplayer">The core multiplayer logic.</param>
        /// <param name="exitGameImmediately">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.</param>
        /// <param name="onGameContentLoaded">Raised after the game finishes loading its initial content.</param>
        /// <param name="onGameUpdating">Raised when XNA is updating its state (roughly 60 times per second).</param>
        /// <param name="onPlayerInstanceUpdating">Raised when the game instance for a local split-screen player is updating (once per <see cref="OnGameUpdating"/> per player).</param>
        /// <param name="onGameExiting">Raised before the game exits.</param>
        public SGameRunner(Monitor monitor, Reflector reflection, EventManager eventManager, SModHooks modHooks, SMultiplayer multiplayer, Action<string> exitGameImmediately, Action onGameContentLoaded, Action<GameTime, Action> onGameUpdating, Action<SGame, GameTime, 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;
        }

        /// <summary>Create a game instance for a local player.</summary>
        /// <param name="playerIndex">The player index.</param>
        /// <param name="instanceIndex">The instance index.</param>
        public override Game1 CreateGameInstance(PlayerIndex playerIndex = PlayerIndex.One, int instanceIndex = 0)
        {
            SInputState inputState = new SInputState();
            return new SGame(playerIndex, instanceIndex, this.Monitor, this.Reflection, this.Events, inputState, this.ModHooks, this.Multiplayer, this.ExitGameImmediately, this.OnPlayerInstanceUpdating);
        }

        /// <inheritdoc />
        public override void AddGameInstance(PlayerIndex playerIndex)
        {
            base.AddGameInstance(playerIndex);

            EarlyConstants.LogScreenId = Context.ScreenId;
            this.UpdateForSplitScreenChanges();
        }

        /// <inheritdoc />
        public override void RemoveGameInstance(Game1 gameInstance)
        {
            base.RemoveGameInstance(gameInstance);

            if (this.gameInstances.Count <= 1)
                EarlyConstants.LogScreenId = null;
            this.UpdateForSplitScreenChanges();
        }

        /// <summary>Get the screen ID for a given player ID, if the player is local.</summary>
        /// <param name="playerId">The player ID to check.</param>
        public int? GetScreenId(long playerId)
        {
            return this.gameInstances
                .FirstOrDefault(p => ((SGame)p).PlayerId == playerId)
                ?.instanceId;
        }


        /*********
        ** Protected methods
        *********/
        /// <summary>Load content when the game is launched.</summary>
        protected override void LoadContent()
        {
            base.LoadContent();

            this.OnGameContentLoaded();
        }

        /// <summary>Perform cleanup logic when the game exits.</summary>
        /// <param name="sender">The event sender.</param>
        /// <param name="args">The event args.</param>
        /// <remarks>This overrides the logic in <see cref="Game1.exitEvent"/> to let SMAPI clean up before exit.</remarks>
        protected override void OnExiting(object sender, EventArgs args)
        {
            this.OnGameExiting();
        }

        /// <summary>The method called when the game is updating its state (roughly 60 times per second).</summary>
        /// <param name="gameTime">A snapshot of the game timing state.</param>
        protected override void Update(GameTime gameTime)
        {
            this.OnGameUpdating(gameTime, () => base.Update(gameTime));
        }

        /// <summary>Update metadata when a split screen is added or removed.</summary>
        private void UpdateForSplitScreenChanges()
        {
            HashSet<int> oldScreenIds = new HashSet<int>(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;
            }
        }
    }
}