summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/ContentCoordinator.cs
blob: 86ebc5c3f3e20cf9a07734ba634f6237065da367 (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
175
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.Xna.Framework.Content;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Metadata;
using StardewValley;

namespace StardewModdingAPI.Framework
{
    /// <summary>The central logic for creating content managers, invalidating caches, and propagating asset changes.</summary>
    internal class ContentCoordinator : IDisposable
    {
        /*********
        ** Properties
        *********/
        /// <summary>Encapsulates monitoring and logging.</summary>
        private readonly IMonitor Monitor;

        /// <summary>Provides metadata for core game assets.</summary>
        private readonly CoreAssetPropagator CoreAssets;

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

        /// <summary>The loaded content managers (including the <see cref="MainContentManager"/>).</summary>
        private readonly IList<SContentManager> ContentManagers = new List<SContentManager>();


        /*********
        ** Accessors
        *********/
        /// <summary>The primary content manager used for most assets.</summary>
        public SContentManager MainContentManager { get; private set; }

        /// <summary>The current language as a constant.</summary>
        public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language;

        /// <summary>Interceptors which provide the initial versions of matching assets.</summary>
        public IDictionary<IModMetadata, IList<IAssetLoader>> Loaders { get; } = new Dictionary<IModMetadata, IList<IAssetLoader>>();

        /// <summary>Interceptors which edit matching assets after they're loaded.</summary>
        public IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>();

        /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
        public string FullRootDirectory { get; }


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="serviceProvider">The service provider to use to locate services.</param>
        /// <param name="rootDirectory">The root directory to search for content.</param>
        /// <param name="currentCulture">The current culture for which to localise content.</param>
        /// <param name="monitor">Encapsulates monitoring and logging.</param>
        /// <param name="reflection">Simplifies access to private code.</param>
        public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection)
        {
            this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
            this.Reflection = reflection;
            this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory);
            this.ContentManagers.Add(
                this.MainContentManager = new SContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, isModFolder: false)
            );
            this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.NormaliseAssetName, reflection);
        }

        /// <summary>Get a new content manager which defers loading to the content core.</summary>
        /// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
        /// <param name="isModFolder">Whether this content manager is wrapped around a mod folder.</param>
        /// <param name="rootDirectory">The root directory to search for content (or <c>null</c>. for the default)</param>
        public SContentManager CreateContentManager(string name, bool isModFolder, string rootDirectory = null)
        {
            return new SContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory ?? this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, isModFolder);
        }

        /// <summary>Get the current content locale.</summary>
        public string GetLocale() => this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode);

        /// <summary>Convert an absolute file path into a appropriate asset name.</summary>
        /// <param name="absolutePath">The absolute path to the file.</param>
        public string GetAssetNameFromFilePath(string absolutePath) => this.MainContentManager.GetAssetNameFromFilePath(absolutePath, ContentSource.GameContent);

        /// <summary>Purge assets from the cache that match one of the interceptors.</summary>
        /// <param name="editors">The asset editors for which to purge matching assets.</param>
        /// <param name="loaders">The asset loaders for which to purge matching assets.</param>
        /// <returns>Returns the invalidated asset names.</returns>
        public IEnumerable<string> InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders)
        {
            if (!editors.Any() && !loaders.Any())
                return new string[0];

            // get CanEdit/Load methods
            MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit));
            MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad));
            if (canEdit == null || canLoad == null)
                throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen

            // invalidate matching keys
            return this.InvalidateCache(asset =>
            {
                // check loaders
                MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType);
                if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { asset })))
                    return true;

                // check editors
                MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType);
                return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { asset }));
            });
        }

        /// <summary>Purge matched assets from the cache.</summary>
        /// <param name="predicate">Matches the asset keys to invalidate.</param>
        /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
        /// <returns>Returns the invalidated asset keys.</returns>
        public IEnumerable<string> InvalidateCache(Func<IAssetInfo, bool> predicate, bool dispose = false)
        {
            string locale = this.GetLocale();
            return this.InvalidateCache((assetName, type) =>
            {
                IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.NormaliseAssetName);
                return predicate(info);
            });
        }

        /// <summary>Purge matched assets from the cache.</summary>
        /// <param name="predicate">Matches the asset keys to invalidate.</param>
        /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
        /// <returns>Returns the invalidated asset names.</returns>
        public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
        {
            // invalidate cache
            HashSet<string> removedAssetNames = new HashSet<string>();
            foreach (SContentManager contentManager in this.ContentManagers)
            {
                foreach (string name in contentManager.InvalidateCache(predicate, dispose))
                    removedAssetNames.Add(name);
            }

            // reload core game assets
            int reloaded = 0;
            foreach (string key in removedAssetNames)
            {
                if (this.CoreAssets.Propagate(this.MainContentManager, key)) // use an intercepted content manager
                    reloaded++;
            }

            // report result
            if (removedAssetNames.Any())
                this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace);
            this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace);

            return removedAssetNames;
        }

        /// <summary>Dispose held resources.</summary>
        public void Dispose()
        {
            if (this.MainContentManager == null)
                return; // already disposed

            this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace);
            foreach (SContentManager contentManager in this.ContentManagers)
                contentManager.Dispose();
            this.ContentManagers.Clear();
            this.MainContentManager = null;
        }
    }
}