using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
#if SMAPI_FOR_WINDOWS
using System.Windows.Forms;
#endif
using Newtonsoft.Json;
using StardewModdingAPI.Enums;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Input;
using StardewModdingAPI.Framework.Logging;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.ModHelpers;
using StardewModdingAPI.Framework.ModLoading;
using StardewModdingAPI.Framework.Networking;
using StardewModdingAPI.Framework.Patching;
using StardewModdingAPI.Framework.PerformanceMonitoring;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Rendering;
using StardewModdingAPI.Framework.Serialization;
using StardewModdingAPI.Framework.StateTracking.Comparers;
using StardewModdingAPI.Framework.StateTracking.Snapshots;
using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Patches;
using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
using StardewModdingAPI.Utilities;
using StardewValley;
using xTile.Display;
using SObject = StardewValley.Object;
namespace StardewModdingAPI.Framework
{
/// The core class which initializes and manages SMAPI.
internal class SCore : IDisposable
{
/*********
** Fields
*********/
/****
** Low-level components
****/
/// Tracks whether the game should exit immediately and any pending initialization should be cancelled.
private readonly CancellationTokenSource CancellationToken = new CancellationTokenSource();
/// Manages the SMAPI console window and log file.
private readonly LogManager LogManager;
/// The core logger and monitor for SMAPI.
private Monitor Monitor => this.LogManager.Monitor;
/// Simplifies access to private game code.
private readonly Reflector Reflection = new Reflector();
/// Encapsulates access to SMAPI core translations.
private readonly Translator Translator = new Translator();
/// The SMAPI configuration settings.
private readonly SConfig Settings;
/// The mod toolkit used for generic mod interactions.
private readonly ModToolkit Toolkit = new ModToolkit();
/****
** Higher-level components
****/
/// Manages console commands.
private readonly CommandManager CommandManager = new CommandManager();
/// The underlying game instance.
private SGameRunner Game;
/// SMAPI's content manager.
private ContentCoordinator ContentCore;
/// The game's core multiplayer utility for the main player.
private SMultiplayer Multiplayer;
/// Tracks the installed mods.
/// This is initialized after the game starts.
private readonly ModRegistry ModRegistry = new ModRegistry();
/// Manages SMAPI events for mods.
private readonly EventManager EventManager;
/****
** State
****/
/// The path to search for mods.
private string ModsPath => Constants.ModsPath;
/// Whether the game is currently running.
private bool IsGameRunning;
/// Whether the program has been disposed.
private bool IsDisposed;
/// Whether the next content manager requested by the game will be for .
private bool NextContentManagerIsMain;
/// Whether post-game-startup initialization has been performed.
private bool IsInitialized;
/// Whether the player just returned to the title screen.
public bool JustReturnedToTitle { get; set; }
/// The maximum number of consecutive attempts SMAPI should make to recover from an update error.
private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second
/// Whether custom content was removed from the save data to avoid a crash.
private bool IsSaveContentRemoved;
/// Asset interceptors added or removed since the last tick.
private readonly List ReloadAssetInterceptorsQueue = new List();
/// A list of queued commands to execute.
/// This property must be thread-safe, since it's accessed from a separate console input thread.
public ConcurrentQueue CommandQueue { get; } = new ConcurrentQueue();
/*********
** Accessors
*********/
/// Manages deprecation warnings.
/// This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.
internal static DeprecationManager DeprecationManager { get; private set; }
/// Manages performance counters.
/// This is initialized after the game starts. This is non-private for use by Console Commands.
internal static PerformanceMonitor PerformanceMonitor { get; private set; }
/// The number of update ticks which have already executed. This is similar to , but incremented more consistently for every tick.
internal static uint TicksElapsed { get; private set; }
/*********
** Public methods
*********/
/// Construct an instance.
/// The path to search for mods.
/// Whether to output log messages to the console.
public SCore(string modsPath, bool writeToConsole)
{
// init paths
this.VerifyPath(modsPath);
this.VerifyPath(Constants.LogDir);
Constants.ModsPath = modsPath;
// init log file
this.PurgeNormalLogs();
string logPath = this.GetLogPath();
// init basics
this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath));
if (File.Exists(Constants.ApiUserConfigPath))
JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings);
this.LogManager = new LogManager(logPath: logPath, colorConfig: this.Settings.ConsoleColors, writeToConsole: writeToConsole, isVerbose: this.Settings.VerboseLogging, isDeveloperMode: this.Settings.DeveloperMode, getScreenIdForLog: this.GetScreenIdForLog);
SCore.PerformanceMonitor = new PerformanceMonitor(this.Monitor);
this.EventManager = new EventManager(this.ModRegistry, SCore.PerformanceMonitor);
SCore.PerformanceMonitor.InitializePerformanceCounterCollections(this.EventManager);
SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
SDate.Translations = this.Translator;
// log SMAPI/OS info
this.LogManager.LogIntro(modsPath, this.Settings.GetCustomSettings());
// validate platform
#if SMAPI_FOR_WINDOWS
if (Constants.Platform != Platform.Windows)
{
this.Monitor.Log("Oops! You're running Windows, but this version of SMAPI is for Linux or Mac. Please reinstall SMAPI to fix this.", LogLevel.Error);
this.LogManager.PressAnyKeyToExit();
}
#else
if (Constants.Platform == Platform.Windows)
{
this.Monitor.Log($"Oops! You're running {Constants.Platform}, but this version of SMAPI is for Windows. Please reinstall SMAPI to fix this.", LogLevel.Error);
this.LogManager.PressAnyKeyToExit();
}
#endif
}
/// Launch SMAPI.
[HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions
public void RunInteractively()
{
// initialize SMAPI
try
{
JsonConverter[] converters = {
new ColorConverter(),
new PointConverter(),
new Vector2Converter(),
new RectangleConverter()
};
foreach (JsonConverter converter in converters)
this.Toolkit.JsonHelper.JsonSettings.Converters.Add(converter);
// add error handlers
#if SMAPI_FOR_WINDOWS
Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error);
Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
#endif
AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error);
// add more lenient assembly resolver
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name);
// hook locale event
LocalizedContentManager.OnLanguageChange += locale => this.OnLocaleChanged();
// override game
this.Multiplayer = new SMultiplayer(this.Monitor, this.EventManager, this.Toolkit.JsonHelper, this.ModRegistry, this.Reflection, this.OnModMessageReceived, this.Settings.LogNetworkTraffic);
SGame.CreateContentManagerImpl = this.CreateContentManager; // must be static since the game accesses it before the SGame constructor is called
this.Game = new SGameRunner(
monitor: this.Monitor,
reflection: this.Reflection,
eventManager: this.EventManager,
modHooks: new SModHooks(this.OnNewDayAfterFade),
multiplayer: this.Multiplayer,
exitGameImmediately: this.ExitGameImmediately,
onGameContentLoaded: this.OnGameContentLoaded,
onGameUpdating: this.OnGameUpdating,
onPlayerInstanceUpdating: this.OnPlayerInstanceUpdating,
onGameExiting: this.OnGameExiting
);
StardewValley.GameRunner.instance = this.Game;
// apply game patches
new GamePatcher(this.Monitor).Apply(
new EventErrorPatch(this.LogManager.MonitorForGame),
new DialogueErrorPatch(this.LogManager.MonitorForGame, this.Reflection),
new ObjectErrorPatch(),
new LoadContextPatch(this.Reflection, this.OnLoadStageChanged),
new LoadErrorPatch(this.Monitor, this.OnSaveContentRemoved),
new ScheduleErrorPatch(this.LogManager.MonitorForGame)
);
// add exit handler
this.CancellationToken.Token.Register(() =>
{
if (this.IsGameRunning)
{
this.LogManager.WriteCrashLog();
this.Game.Exit();
}
});
// set window titles
this.SetWindowTitles(
game: $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}",
smapi: $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"
);
}
catch (Exception ex)
{
this.Monitor.Log($"SMAPI failed to initialize: {ex.GetLogSummary()}", LogLevel.Error);
this.LogManager.PressAnyKeyToExit();
return;
}
// log basic info
this.LogManager.HandleMarkerFiles();
this.LogManager.LogSettingsHeader(this.Settings.DeveloperMode, this.Settings.CheckForUpdates);
// set window titles
this.SetWindowTitles(
game: $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion}",
smapi: $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"
);
// start game
this.Monitor.Log("Starting game...", LogLevel.Debug);
try
{
this.IsGameRunning = true;
StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window
this.Game.Run();
}
catch (Exception ex)
{
this.LogManager.LogFatalLaunchError(ex);
this.LogManager.PressAnyKeyToExit();
}
finally
{
this.Dispose();
}
}
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
public void Dispose()
{
// skip if already disposed
if (this.IsDisposed)
return;
this.IsDisposed = true;
this.Monitor.Log("Disposing...");
// dispose mod data
foreach (IModMetadata mod in this.ModRegistry.GetAll())
{
try
{
(mod.Mod as IDisposable)?.Dispose();
}
catch (Exception ex)
{
mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn);
}
}
// dispose core components
this.IsGameRunning = false;
this.ContentCore?.Dispose();
this.CancellationToken?.Dispose();
this.Game?.Dispose();
this.LogManager?.Dispose(); // dispose last to allow for any last-second log messages
// end game (moved from Game1.OnExiting to let us clean up first)
Process.GetCurrentProcess().Kill();
}
/*********
** Private methods
*********/
/// Initialize mods before the first game asset is loaded. At this point the core content managers are loaded (so mods can load their own assets), but the game is mostly uninitialized.
private void InitializeBeforeFirstAssetLoaded()
{
if (this.CancellationToken.IsCancellationRequested)
{
this.Monitor.Log("SMAPI shutting down: aborting initialization.", LogLevel.Warn);
return;
}
// init TMX support
xTile.Format.FormatManager.Instance.RegisterMapFormat(new TMXTile.TMXFormat(Game1.tileSize / Game1.pixelZoom, Game1.tileSize / Game1.pixelZoom, Game1.pixelZoom, Game1.pixelZoom));
// load mod data
ModToolkit toolkit = new ModToolkit();
ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath);
// load mods
{
this.Monitor.Log("Loading mod metadata...");
ModResolver resolver = new ModResolver();
// log loose files
{
string[] looseFiles = new DirectoryInfo(this.ModsPath).GetFiles().Select(p => p.Name).ToArray();
if (looseFiles.Any())
this.Monitor.Log($" Ignored loose files: {string.Join(", ", looseFiles.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}");
}
// load manifests
IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase).ToArray();
// filter out ignored mods
foreach (IModMetadata mod in mods.Where(p => p.IsIgnored))
this.Monitor.Log($" Skipped {mod.GetRelativePathWithRoot()} (folder name starts with a dot).");
mods = mods.Where(p => !p.IsIgnored).ToArray();
// load mods
resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl);
mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
// check for updates
this.CheckForUpdatesAsync(mods);
}
// update window titles
int modsLoaded = this.ModRegistry.GetAll().Count();
this.SetWindowTitles(
game: $"Stardew Valley {Constants.GameVersion} - running SMAPI {Constants.ApiVersion} with {modsLoaded} mods",
smapi: $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"
);
}
/// Raised after the game finishes initializing.
private void OnGameInitialized()
{
// validate XNB integrity
if (!this.ValidateContentIntegrity())
this.Monitor.Log("SMAPI found problems in your game's content files which are likely to cause errors or crashes. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Error);
// start SMAPI console
new Thread(
() => this.LogManager.RunConsoleInputLoop(
commandManager: this.CommandManager,
reloadTranslations: this.ReloadTranslations,
handleInput: input => this.CommandQueue.Enqueue(input),
continueWhile: () => this.IsGameRunning && !this.CancellationToken.IsCancellationRequested
)
).Start();
}
/// Raised after the game finishes loading its initial content.
private void OnGameContentLoaded()
{
// override map display device
Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, Game1.game1.GraphicsDevice);
// log GPU info
#if SMAPI_FOR_WINDOWS
this.Monitor.Log($"Running on GPU: {Game1.game1.GraphicsDevice?.Adapter?.Description ?? ""}");
#endif
}
/// Raised when the game is updating its state (roughly 60 times per second).
/// A snapshot of the game timing state.
/// Invoke the game's update logic.
private void OnGameUpdating(GameTime gameTime, Action runGameUpdate)
{
try
{
/*********
** Safe queued work
*********/
// print warnings/alerts
SCore.DeprecationManager.PrintQueued();
SCore.PerformanceMonitor.PrintQueuedAlerts();
/*********
** First-tick initialization
*********/
if (!this.IsInitialized)
{
this.IsInitialized = true;
this.OnGameInitialized();
}
/*********
** Special cases
*********/
// Abort if SMAPI is exiting.
if (this.CancellationToken.IsCancellationRequested)
{
this.Monitor.Log("SMAPI shutting down: aborting update.");
return;
}
/*********
** Reload assets when interceptors are added/removed
*********/
if (this.ReloadAssetInterceptorsQueue.Any())
{
// get unique interceptors
AssetInterceptorChange[] interceptors = this.ReloadAssetInterceptorsQueue
.GroupBy(p => p.Instance, new ObjectReferenceComparer