diff --git a/Refresh.Common/RefreshContext.cs b/Refresh.Common/RefreshContext.cs index 033642bb6..540f15a21 100644 --- a/Refresh.Common/RefreshContext.cs +++ b/Refresh.Common/RefreshContext.cs @@ -12,4 +12,5 @@ public enum RefreshContext Aipi, Presence, Database, + CacheService, } \ No newline at end of file diff --git a/Refresh.Core/Extensions/GameAssetExtensions.cs b/Refresh.Core/Extensions/GameAssetExtensions.cs index 12cefc149..6bf064efe 100644 --- a/Refresh.Core/Extensions/GameAssetExtensions.cs +++ b/Refresh.Core/Extensions/GameAssetExtensions.cs @@ -151,7 +151,11 @@ private static string TransformImage(this GameAsset asset, TokenGame game, IData dataContext.DataStore, _ => null, () => asset.AsMainlinePhotoHash, - hash => dataContext.Database.SetMainlinePhotoHash(asset, hash), + hash => + { + dataContext.Database.SetMainlinePhotoHash(asset, hash); + dataContext.Cache.CacheAsset(asset.AssetHash, asset); + }, () => throw new NotSupportedException(), _ => throw new NotSupportedException() ); @@ -172,9 +176,17 @@ private static string TransformImage(this GameAsset asset, TokenGame game, IData dataContext.DataStore, CropToIcon, () => asset.AsMainlineIconHash, - hash => dataContext.Database.SetMainlineIconHash(asset, hash), + hash => + { + dataContext.Database.SetMainlineIconHash(asset, hash); + dataContext.Cache.CacheAsset(asset.AssetHash, asset); + }, () => asset.AsMipIconHash, - hash => dataContext.Database.SetMipIconHash(asset, hash) + hash => + { + dataContext.Database.SetMipIconHash(asset, hash); + dataContext.Cache.CacheAsset(asset.AssetHash, asset); + } ); } diff --git a/Refresh.Core/Extensions/PhotoExtensions.cs b/Refresh.Core/Extensions/PhotoExtensions.cs index a5aa6d72a..7713d78c0 100644 --- a/Refresh.Core/Extensions/PhotoExtensions.cs +++ b/Refresh.Core/Extensions/PhotoExtensions.cs @@ -15,9 +15,9 @@ public static SerializedPhoto FromGamePhoto(GamePhoto photo, DataContext dataCon // NOTE: we usually would do `if psp, prepend psp/ to the hashes`, // but since we are converting the psp TGA assets to PNG in FillInExtraData, we don't need to! // also, I think the game would get mad if we did that - LargeHash = dataContext.Database.GetAssetFromHash(photo.LargeAsset.AssetHash)?.GetAsPhoto(dataContext.Game, dataContext) ?? photo.LargeAsset.AssetHash, - MediumHash = dataContext.Database.GetAssetFromHash(photo.MediumAsset.AssetHash)?.GetAsPhoto(dataContext.Game, dataContext) ?? photo.MediumAsset.AssetHash, - SmallHash = dataContext.Database.GetAssetFromHash(photo.SmallAsset.AssetHash)?.GetAsPhoto(dataContext.Game, dataContext) ?? photo.SmallAsset.AssetHash, + LargeHash = photo.LargeAsset.GetAsPhoto(dataContext.Game, dataContext) ?? photo.LargeAsset.AssetHash, + MediumHash = photo.MediumAsset.GetAsPhoto(dataContext.Game, dataContext) ?? photo.MediumAsset.AssetHash, + SmallHash = photo.SmallAsset.GetAsPhoto(dataContext.Game, dataContext) ?? photo.SmallAsset.AssetHash, PlanHash = photo.PlanHash, PhotoSubjects = [], }; diff --git a/Refresh.Core/Services/CacheService.cs b/Refresh.Core/Services/CacheService.cs new file mode 100644 index 000000000..bee8a0e00 --- /dev/null +++ b/Refresh.Core/Services/CacheService.cs @@ -0,0 +1,442 @@ +// Uncomment to enable extra logging. +// Do not enable in production as this will flood your console and might be memory-heavy. +//#define CACHE_DEBUG + +#if !DEBUG +#undef CACHE_DEBUG +#endif + +using System.Diagnostics; +using Bunkum.Core.Services; +using MongoDB.Bson; +using NotEnoughLogs; +using Refresh.Common; +using Refresh.Core.Types.Cache; +using Refresh.Core.Types.Relations; +using Refresh.Database; +using Refresh.Database.Models.Assets; +using Refresh.Database.Models.Levels; +using Refresh.Database.Models.Users; + +namespace Refresh.Core.Services; + +public class CacheService : EndpointService +{ + private readonly TimeProviderService _time; + private readonly Dictionary> _cachedAssetData = []; // hash -> data + private readonly Dictionary>> _cachedSkillRewards = []; // level ID -> data + private readonly Dictionary>> _cachedLevelTags = []; // level ID -> data + + private readonly Dictionary>> _cachedOwnUserRelations = []; // source user UUID -> target user UUID -> data + private readonly Dictionary>> _cachedOwnLevelRelations = []; // source user UUID -> level ID -> data + + private const int LevelCacheDurationSeconds = 60 * 10; + private const int AssetCacheDurationSeconds = 60 * 60; // GameAssets basically never change for now + + // TODO: unit tests for this + public CacheService(Logger logger, TimeProviderService time) : base(logger) + { + this._time = time; + } + + [Conditional("CACHE_DEBUG")] + private void Log(ReadOnlySpan format, params object[] args) + { + this.Logger.LogDebug(RefreshContext.CacheService, format, args); + } + + public override void Initialize() + { + base.Initialize(); + + // periodically automatically remove expired data if it hasn't been removed somehow else before, + // to not keep unused data for a potentially undefined time + Thread expirationThread = new(() => + { + while(true) + { + Thread.Sleep(1000 * 60 * 2); + this.RemoveExpiredCache(); + } + }); + expirationThread.Start(); + } + + private void RemoveExpiredCache() + { + this.Log("Automatically removing expired cache..."); + DateTimeOffset now = this._time.TimeProvider.Now; + + foreach(KeyValuePair> cache in this._cachedAssetData) + { + if (this.HasCacheExpired(cache.Value, now)) + { + this.Log("Automatically removing expired GameAsset taken by hash {0}", cache.Key); + this._cachedAssetData.Remove(cache.Key); + } + } + + foreach(KeyValuePair>> cache in this._cachedSkillRewards) + { + if (this.HasCacheExpired(cache.Value, now)) + { + this.Log("Automatically removing expired GameSkillReward list taken from level {0}", cache.Key); + this._cachedSkillRewards.Remove(cache.Key); + } + } + + foreach(KeyValuePair>> cache in this._cachedLevelTags) + { + if (this.HasCacheExpired(cache.Value, now)) + { + this.Log("Automatically removing expired Tag list taken from level {0}", cache.Key); + this._cachedLevelTags.Remove(cache.Key); + } + } + + foreach(KeyValuePair>> cache in this._cachedOwnUserRelations) + { + foreach(KeyValuePair> innerCache in cache.Value) + { + if (this.HasCacheExpired(innerCache.Value, now)) + { + this.Log("Automatically removing expired OwnUserRelations by user {0} for user {1}", cache.Key, innerCache.Key); + cache.Value.Remove(innerCache.Key); + } + } + + if (cache.Value.Count <= 0) + { + this.Log("Automatically removing empty OwnUserRelations dictionary by user {0}", cache.Key); + this._cachedOwnUserRelations.Remove(cache.Key); + } + } + + foreach(KeyValuePair>> cache in this._cachedOwnLevelRelations) + { + foreach(KeyValuePair> innerCache in cache.Value) + { + if (this.HasCacheExpired(innerCache.Value, now)) + { + this.Log("Automatically removing expired OwnLevelRelations by user {0} for level {1}", cache.Key, innerCache.Key); + cache.Value.Remove(innerCache.Key); + } + } + + if (cache.Value.Count <= 0) + { + this.Log("Automatically removing empty OwnLevelRelations dictionary by user {0}", cache.Key); + this._cachedOwnLevelRelations.Remove(cache.Key); + } + } + } + + private bool HasCacheExpired(CachedData? cached, DateTimeOffset? now = null) + => cached == null || cached.ExpiresAt < (now ?? this._time.TimeProvider.Now); + + #region Assets + public void CacheAsset(string hash, GameAsset? asset) + { + if (hash.StartsWith('g') || hash.IsBlankHash()) return; + + DateTimeOffset expiresAt = this._time.TimeProvider.Now.AddSeconds(AssetCacheDurationSeconds); + this.Log("Refreshing GameAsset {0} cache, it will expire at {1}", hash, expiresAt); + this._cachedAssetData[hash] = new(asset, expiresAt); + } + + // currently not necessary to return these with CachedReturn... + public GameAsset? GetAssetInfo(string hash, GameDatabaseContext database) + { + if (hash.StartsWith('g') || hash.IsBlankHash()) return null; + + this.Log("Looking up GameAsset {0} in cache", hash); + CachedData? fromCache = this._cachedAssetData.GetValueOrDefault(hash); + + if (this.HasCacheExpired(fromCache)) + { + this.Log("Looking up GameAsset {0} in DB", hash); + GameAsset? refreshed = database.GetAssetFromHash(hash); + + // Cache anyway, even if asset was not found in DB, for the same reason we're caching everything else + // If the asset ever happens to be added to DB, it will be added to cache aswell by whatever is handling the upload/import anyway + this.CacheAsset(hash, refreshed); + return refreshed; + } + + this.Log("Found unexpired GameAsset {0} in cache, it will expire at {1}", hash, fromCache!.ExpiresAt); + return fromCache!.Content; + } + + #endregion + + public void RemoveLevelData(GameLevel level) + { + this.RemoveSkillRewards(level); + this.RemoveTags(level); + } + + #region Skill Rewards + + public void CacheSkillRewards(GameLevel level, List rewards) + { + DateTimeOffset expiresAt = this._time.TimeProvider.Now.AddSeconds(LevelCacheDurationSeconds); + this.Log("Refreshing GameSkillRewards cache for level {0}, they will expire at {1}", level, expiresAt); + this._cachedSkillRewards[level.LevelId] = new(rewards, expiresAt); + } + + public void RemoveSkillRewards(GameLevel level) + { + this.Log("Removing GameSkillRewards cache for level {0}", level); + this._cachedSkillRewards.Remove(level.LevelId); + } + + public List GetSkillRewards(GameLevel level, GameDatabaseContext database) + { + this.Log("Looking up GameSkillRewards for level {0} in cache", level); + CachedData>? fromCache = this._cachedSkillRewards.GetValueOrDefault(level.LevelId); + + if (this.HasCacheExpired(fromCache)) + { + this.Log("Looking up GameSkillRewards for level {0} in DB", level); + List refreshed = database.GetSkillRewardsForLevel(level).ToList(); + + this.CacheSkillRewards(level, refreshed); + return refreshed; + } + + this.Log("Found unexpired GameSkillRewards for level {0} in cache, they will expire at {1}", level, fromCache!.ExpiresAt); + return fromCache!.Content; + } + + #endregion + #region Tags + + public void CacheTags(GameLevel level, List tags) + { + DateTimeOffset expiresAt = this._time.TimeProvider.Now.AddSeconds(LevelCacheDurationSeconds); + this.Log("Refreshing Tags cache for level {0}, they will expire at {1}", level, expiresAt); + this._cachedLevelTags[level.LevelId] = new(tags, expiresAt); + } + + // Make service re-get tags the next time they're requested, incase a tag is added or removed. + // Can't just lazily add or remove tag to/from the cache because they're grouped and sorted by frequency (data we don't have here). + public void RemoveTags(GameLevel level) + { + this.Log("Removing Tags cache for level {0}", level); + this._cachedLevelTags.Remove(level.LevelId); + } + + public List GetTags(GameLevel level, GameDatabaseContext database) + { + this.Log("Looking up Tags for level {0} in cache", level); + CachedData>? fromCache = this._cachedLevelTags.GetValueOrDefault(level.LevelId); + + if (this.HasCacheExpired(fromCache)) + { + this.Log("Looking up Tags for level {0} in DB", level); + List refreshed = database.GetTagsForLevel(level).ToList(); + + this.CacheTags(level, refreshed); + return refreshed; + } + + this.Log("Found unexpired GameSkillRewards for level {0} in cache, they will expire at {1}", level, fromCache!.ExpiresAt); + return fromCache!.Content; + } + + #endregion + + #region Own User Relations + + public void CacheOwnUserRelations(GameUser source, GameUser target, OwnUserRelations newData) + { + DateTimeOffset expiresAt = this._time.TimeProvider.Now.AddSeconds(LevelCacheDurationSeconds); + if (!this._cachedOwnUserRelations.ContainsKey(source.UserId)) + this._cachedOwnUserRelations[source.UserId] = []; + + this.Log("Refreshing OwnUserRelations cache by user {0} for user {1}, they will expire at {2}", source, target, expiresAt); + this._cachedOwnUserRelations[source.UserId][target.UserId] = new(newData, expiresAt); + } + + public void RemoveOwnUserRelations(GameUser source, GameUser target) + { + this.Log("Resetting OwnUserRelations by user {0}", source); + this._cachedOwnUserRelations.GetValueOrDefault(source.UserId)?.Remove(target.UserId); + } + + public CachedReturn GetOwnUserRelations(GameUser source, GameUser target, GameDatabaseContext database) + { + this.Log("Looking up OwnUserRelations by user {0} for user {0} in cache", source, target); + CachedData? fromCache = this._cachedOwnUserRelations.GetValueOrDefault(source.UserId)?.GetValueOrDefault(target.UserId); + + if (this.HasCacheExpired(fromCache)) + { + this.Log("Looking up OwnUserRelations by user {0} for user {1} in DB", source, target); + OwnUserRelations refreshed = new() + { + IsHearted = database.IsUserFavouritedByUser(target, source), + }; + + this.CacheOwnUserRelations(source, target, refreshed); + return new(refreshed, true); + } + + this.Log("Found unexpired OwnUserRelations by user {0} for user {1} in cache, they will expire at {2}", source, target, fromCache!.ExpiresAt); + return new(fromCache!.Content, false); + } + + public void UpdateUserHeartedStatusByUser(GameUser source, GameUser target, bool newValue, GameDatabaseContext database) + { + CachedReturn fromCache = this.GetOwnUserRelations(source, target, database); + if (fromCache.WasRefreshed) return; // value is already up-to-date + + // dictionaries are already ensured to exist + this.Log("Lazily setting hearted status by user {0} for user {1} to {2}", source, target, newValue); + this._cachedOwnUserRelations[source.UserId][target.UserId].Content.IsHearted = newValue; + } + + #endregion + #region Own Level Relations + + public void CacheOwnLevelRelations(GameUser source, GameLevel target, OwnLevelRelations newData) + { + DateTimeOffset expiresAt = this._time.TimeProvider.Now.AddSeconds(LevelCacheDurationSeconds); + this.Log("Refreshing OwnLevelRelations cache by user {0} for level {1}, they will expire at {2}", source, target, expiresAt); + + if (!this._cachedOwnLevelRelations.ContainsKey(source.UserId)) + this._cachedOwnLevelRelations[source.UserId] = []; + + this._cachedOwnLevelRelations[source.UserId][target.LevelId] = new(newData, expiresAt); + } + + public CachedReturn GetOwnLevelRelations(GameUser source, GameLevel target, GameDatabaseContext database) + { + this.Log("Looking up OwnLevelRelations by user {0} for level {0} in cache", source, target); + CachedData? fromCache = this._cachedOwnLevelRelations.GetValueOrDefault(source.UserId)?.GetValueOrDefault(target.LevelId); + + if (this.HasCacheExpired(fromCache)) + { + this.Log("Looking up OwnLevelRelations by user {0} for level {1} in DB", source, target); + OwnLevelRelations refreshed = new() + { + IsHearted = database.IsLevelFavouritedByUser(target, source), + IsQueued = database.IsLevelQueuedByUser(target, source), + LevelRating = (int?)database.GetRatingByUser(target, source) ?? 0, + TotalPlayCount = database.GetTotalPlaysForLevelByUser(target, source), + TotalCompletionCount = database.GetTotalCompletionsForLevelByUser(target, source), + PhotoCount = database.GetTotalPhotosInLevelByUser(target, source), + }; + + this.CacheOwnLevelRelations(source, target, refreshed); + return new(refreshed, true); + } + + this.Log("Found unexpired OwnLevelRelations by user {0} for level {1} in cache, they will expire at {2}", source, target, fromCache!.ExpiresAt); + return new(fromCache!.Content, false); + } + + public void UpdateLevelHeartedStatusByUser(GameUser source, GameLevel target, bool newValue, GameDatabaseContext database) + { + CachedReturn fromCache = this.GetOwnLevelRelations(source, target, database); + if (fromCache.WasRefreshed) return; // value is already up-to-date + + // dictionaries are already ensured to exist + this.Log("Lazily setting hearted status by user {0} for level {1} to {2}", source, target, newValue); + this._cachedOwnLevelRelations[source.UserId][target.LevelId].Content.IsHearted = newValue; + } + + public void UpdateLevelQueuedStatusByUser(GameUser source, GameLevel target, bool newValue, GameDatabaseContext database) + { + CachedReturn fromCache = this.GetOwnLevelRelations(source, target, database); + if (fromCache.WasRefreshed) return; // value is already up-to-date + + // dictionaries are already ensured to exist + this.Log("Lazily setting queued status by user {0} for level {1} to {2}", source, target, newValue); + this._cachedOwnLevelRelations[source.UserId][target.LevelId].Content.IsQueued = newValue; + } + + public void ClearQueueByUser(GameUser source) + { + this.Log("Resetting queued stati by user {0}", source); + + foreach (KeyValuePair> relations in this._cachedOwnLevelRelations.GetValueOrDefault(source.UserId)?.ToArray() ?? []) + { + // will be refreshed later anyway; don't want too many DB calls here (e.g. user had 100 levels queued, this method would in that case send 600 DB queries) + if (this.HasCacheExpired(relations.Value)) continue; + + this.Log("Resetting queued status by user {0} for level {1}", source, relations.Key); + relations.Value.Content.IsQueued = false; + this._cachedOwnLevelRelations[source.UserId][relations.Key] = relations.Value; + } + } + + public void UpdateLevelRatingByUser(GameUser source, GameLevel target, int newRating, GameDatabaseContext database) + { + CachedReturn fromCache = this.GetOwnLevelRelations(source, target, database); + if (fromCache.WasRefreshed) return; // value is already up-to-date + + // dictionaries are already ensured to exist + this.Log("Lazily setting level rating by user {0} for level {1} to {2}", source, target, newRating); + this._cachedOwnLevelRelations[source.UserId][target.LevelId].Content.LevelRating = newRating; + } + + public void IncrementLevelTotalPlaysByUser(GameUser source, GameLevel target, int incrementor, GameDatabaseContext database) + { + CachedReturn fromCache = this.GetOwnLevelRelations(source, target, database); + if (fromCache.WasRefreshed) return; // value is already up-to-date + + // dictionaries are already ensured to exist + this.Log("Lazily incrementing total plays by user {0} for level {1} to {2}", source, target, incrementor); + this._cachedOwnLevelRelations[source.UserId][target.LevelId].Content.TotalPlayCount += incrementor; + } + + public void IncrementLevelTotalCompletionsByUser(GameUser source, GameLevel target, int incrementor, GameDatabaseContext database) + { + CachedReturn fromCache = this.GetOwnLevelRelations(source, target, database); + if (fromCache.WasRefreshed) return; // value is already up-to-date + + // dictionaries are already ensured to exist + this.Log("Lazily incrementing total completions by user {0} for level {1} to {2}", source, target, incrementor); + this._cachedOwnLevelRelations[source.UserId][target.LevelId].Content.TotalCompletionCount += incrementor; + } + + public void ResetLevelCompletionCountByUser(GameUser source) + { + this.Log("Resetting total completions by user {0}", source); + + foreach (KeyValuePair> relations in this._cachedOwnLevelRelations.GetValueOrDefault(source.UserId)?.ToArray() ?? []) + { + if (this.HasCacheExpired(relations.Value)) continue; + + this.Log("Resetting total completions by user {0} for level {1}", source, relations.Key); + relations.Value.Content.TotalCompletionCount = 0; + this._cachedOwnLevelRelations[source.UserId][relations.Key] = relations.Value; + } + } + + public void IncrementLevelPhotosByUser(GameUser source, GameLevel target, int incrementor, GameDatabaseContext database) + { + CachedReturn fromCache = this.GetOwnLevelRelations(source, target, database); + if (fromCache.WasRefreshed) return; // value is already up-to-date + + // dictionaries are already ensured to exist + this.Log("Lazily incrementing total photos by user {0} for level {1} to {2}", source, target, incrementor); + this._cachedOwnLevelRelations[source.UserId][target.LevelId].Content.PhotoCount += incrementor; + } + + public void ResetLevelPhotoCountsByUser(GameUser source) + { + this.Log("Resetting total photos by user {0}", source); + + foreach (KeyValuePair> relations in this._cachedOwnLevelRelations.GetValueOrDefault(source.UserId)?.ToArray() ?? []) + { + if (this.HasCacheExpired(relations.Value)) continue; + + this.Log("Resetting total photos by user {0} for level {1}", source, relations.Key); + relations.Value.Content.PhotoCount = 0; + this._cachedOwnLevelRelations[source.UserId][relations.Key] = relations.Value; + } + } + + #endregion +} \ No newline at end of file diff --git a/Refresh.Core/Types/Cache/CachedData.cs b/Refresh.Core/Types/Cache/CachedData.cs new file mode 100644 index 000000000..76cb0e5ce --- /dev/null +++ b/Refresh.Core/Types/Cache/CachedData.cs @@ -0,0 +1,13 @@ +namespace Refresh.Core.Types.Cache; + +public class CachedData +{ + public TData Content { get; set; } + public DateTimeOffset ExpiresAt { get; set; } + + public CachedData(TData content, DateTimeOffset expiresAt) + { + Content = content; + ExpiresAt = expiresAt; + } +} \ No newline at end of file diff --git a/Refresh.Core/Types/Cache/CachedReturn.cs b/Refresh.Core/Types/Cache/CachedReturn.cs new file mode 100644 index 000000000..2087351ef --- /dev/null +++ b/Refresh.Core/Types/Cache/CachedReturn.cs @@ -0,0 +1,13 @@ +namespace Refresh.Core.Types.Cache; + +public class CachedReturn +{ + public TData Content { get; set; } + public bool WasRefreshed { get; set; } + + public CachedReturn(TData content, bool wasRefreshed) + { + Content = content; + WasRefreshed = wasRefreshed; + } +} \ No newline at end of file diff --git a/Refresh.Core/Types/Data/DataContext.cs b/Refresh.Core/Types/Data/DataContext.cs index c89bdc1fb..90889edc9 100644 --- a/Refresh.Core/Types/Data/DataContext.cs +++ b/Refresh.Core/Types/Data/DataContext.cs @@ -14,6 +14,7 @@ public class DataContext : IDataContext public required IDataStore DataStore { get; init; } public required MatchService Match { get; init; } public required GuidCheckerService GuidChecker { get; init; } + public required CacheService Cache { get; init; } public required Token? Token; public GameUser? User => this.Token?.User; @@ -22,6 +23,6 @@ public class DataContext : IDataContext public string GetIconFromHash(string hash) { - return this.Database.GetAssetFromHash(hash)?.GetAsIcon(this.Game, this) ?? hash; + return this.Cache.GetAssetInfo(hash, this.Database)?.GetAsIcon(this.Game, this) ?? hash; } } \ No newline at end of file diff --git a/Refresh.Core/Types/Data/DataContextService.cs b/Refresh.Core/Types/Data/DataContextService.cs index a35cdf4bf..0757c7a5c 100644 --- a/Refresh.Core/Types/Data/DataContextService.cs +++ b/Refresh.Core/Types/Data/DataContextService.cs @@ -16,13 +16,15 @@ public class DataContextService : Service private readonly MatchService _matchService; private readonly AuthenticationService _authService; private readonly GuidCheckerService _guidCheckerService; + private readonly CacheService _cacheService; - public DataContextService(StorageService storage, MatchService match, AuthenticationService auth, Logger logger, GuidCheckerService guidChecker) : base(logger) + public DataContextService(StorageService storage, MatchService match, AuthenticationService auth, Logger logger, GuidCheckerService guidChecker, CacheService cache) : base(logger) { this._storageService = storage; this._matchService = match; this._authService = auth; this._guidCheckerService = guidChecker; + this._cacheService = cache; } private static T StealInjection(Service service, ListenerContext? context = null, Lazy? database = null, string name = "") @@ -42,6 +44,7 @@ private static T StealInjection(Service service, ListenerContext? context = n Match = this._matchService, Token = StealInjection(this._authService, context, database), GuidChecker = this._guidCheckerService, + Cache = this._cacheService, }; } diff --git a/Refresh.Core/Types/Relations/OwnLevelRelations.cs b/Refresh.Core/Types/Relations/OwnLevelRelations.cs new file mode 100644 index 000000000..d0ae3f04a --- /dev/null +++ b/Refresh.Core/Types/Relations/OwnLevelRelations.cs @@ -0,0 +1,11 @@ +namespace Refresh.Core.Types.Relations; + +public class OwnLevelRelations +{ + public bool IsHearted { get; set; } + public bool IsQueued { get; set; } + public int LevelRating { get; set; } + public int TotalPlayCount { get; set; } + public int TotalCompletionCount { get; set; } + public int PhotoCount { get; set; } +} \ No newline at end of file diff --git a/Refresh.Core/Types/Relations/OwnUserRelations.cs b/Refresh.Core/Types/Relations/OwnUserRelations.cs new file mode 100644 index 000000000..03f121cc4 --- /dev/null +++ b/Refresh.Core/Types/Relations/OwnUserRelations.cs @@ -0,0 +1,6 @@ +namespace Refresh.Core.Types.Relations; + +public class OwnUserRelations +{ + public bool IsHearted { get; set; } +} \ No newline at end of file diff --git a/Refresh.Database/GameDatabaseContext.Levels.cs b/Refresh.Database/GameDatabaseContext.Levels.cs index caccd7315..e56703739 100644 --- a/Refresh.Database/GameDatabaseContext.Levels.cs +++ b/Refresh.Database/GameDatabaseContext.Levels.cs @@ -624,9 +624,10 @@ public IEnumerable GetSkillRewardsForLevel(GameLevel level) return this.GameSkillRewards.Where(r => r.LevelId == level.LevelId); } - public void UpdateSkillRewardsForLevel(GameLevel level, IEnumerable rewards) + public List UpdateSkillRewardsForLevel(GameLevel level, IEnumerable rewards) { this.GameSkillRewards.RemoveRange(this.GetSkillRewardsForLevel(level)); + List ret = []; this.Write(() => { @@ -643,8 +644,11 @@ public void UpdateSkillRewardsForLevel(GameLevel level, IEnumerable { user.Statistics!.QueueCount = 0; diff --git a/Refresh.Database/Models/Levels/GameLevel.cs b/Refresh.Database/Models/Levels/GameLevel.cs index 173b7bab3..aa3317989 100644 --- a/Refresh.Database/Models/Levels/GameLevel.cs +++ b/Refresh.Database/Models/Levels/GameLevel.cs @@ -129,4 +129,10 @@ public GameLevel Clone() { return (GameLevel)this.MemberwiseClone(); } + + public override string ToString() + { + string shortenedTitle = this.Title.Length > 20 ? string.Concat(this.Title.AsSpan(0, 20), "-") : this.Title; + return $"'{shortenedTitle}' ({this.GameVersion} / {this.LevelId})"; + } } \ No newline at end of file diff --git a/Refresh.GameServer/RefreshGameServer.cs b/Refresh.GameServer/RefreshGameServer.cs index 18e3a6080..21b211df4 100644 --- a/Refresh.GameServer/RefreshGameServer.cs +++ b/Refresh.GameServer/RefreshGameServer.cs @@ -168,6 +168,7 @@ protected override void SetupServices() this.Server.AddService(); this.Server.AddService(); this.Server.AddService(); + this.Server.AddService(); if(this._configStore.Integration!.AipiEnabled) this.Server.AddService(); diff --git a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminLeaderboardApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminLeaderboardApiEndpoints.cs index 2b8059256..183ff2352 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminLeaderboardApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminLeaderboardApiEndpoints.cs @@ -3,6 +3,7 @@ using Bunkum.Core.Endpoints; using Bunkum.Protocols.Http; using Refresh.Core.Authentication.Permission; +using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models.Levels.Scores; using Refresh.Database.Models.Users; @@ -17,13 +18,14 @@ public class AdminLeaderboardApiEndpoints : EndpointGroup [ApiV3Endpoint("admin/scores/{uuid}", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)] [DocSummary("Removes a score by the score's UUID.")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.ScoreMissingErrorWhen)] - public ApiOkResponse DeleteScore(RequestContext context, GameDatabaseContext database, + public ApiOkResponse DeleteScore(RequestContext context, GameDatabaseContext database, DataContext dataContext, [DocSummary("The UUID of the score")] string uuid) { GameScore? score = database.GetScoreByUuid(uuid); if (score == null) return ApiNotFoundError.Instance; database.DeleteScore(score); + dataContext.Cache.IncrementLevelTotalCompletionsByUser(score.Publisher, score.Level, -1, database); return new ApiOkResponse(); } @@ -31,7 +33,7 @@ public ApiOkResponse DeleteScore(RequestContext context, GameDatabaseContext dat [ApiV3Endpoint("admin/users/{idType}/{id}/scores", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)] [DocSummary("Deletes all scores set by a user, specified by UUID or username.")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] - public ApiOkResponse DeleteScoresSetByUser(RequestContext context, GameDatabaseContext database, + public ApiOkResponse DeleteScoresSetByUser(RequestContext context, GameDatabaseContext database, DataContext dataContext, [DocSummary(SharedParamDescriptions.UserIdParam)] string id, [DocSummary(SharedParamDescriptions.UserIdTypeParam)] string idType) { @@ -39,6 +41,7 @@ public ApiOkResponse DeleteScoresSetByUser(RequestContext context, GameDatabaseC if (user == null) return ApiNotFoundError.UserMissingError; database.DeleteScoresSetByUser(user); + dataContext.Cache.ResetLevelCompletionCountByUser(user); return new ApiOkResponse(); } } \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminLevelApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminLevelApiEndpoints.cs index 77e6ec27a..29dd9bb12 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminLevelApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminLevelApiEndpoints.cs @@ -73,12 +73,13 @@ public ApiResponse EditLevelById(RequestContext context, G [ApiV3Endpoint("admin/levels/id/{id}", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)] [DocSummary("Deletes a level.")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)] - public ApiOkResponse DeleteLevel(RequestContext context, GameDatabaseContext database, int id) + public ApiOkResponse DeleteLevel(RequestContext context, GameDatabaseContext database, int id, DataContext dataContext) { GameLevel? level = database.GetLevelById(id); if (level == null) return ApiNotFoundError.LevelMissingError; database.DeleteLevel(level); + dataContext.Cache.RemoveLevelData(level); return new ApiOkResponse(); } diff --git a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminPhotoApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminPhotoApiEndpoints.cs index 8493d75ad..eb526b023 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminPhotoApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminPhotoApiEndpoints.cs @@ -3,6 +3,7 @@ using Bunkum.Core.Endpoints; using Bunkum.Protocols.Http; using Refresh.Core.Authentication.Permission; +using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models.Photos; using Refresh.Database.Models.Users; @@ -17,7 +18,7 @@ public class AdminPhotoApiEndpoints : EndpointGroup [ApiV3Endpoint("admin/users/{idType}/{id}/photos", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)] [DocSummary("Deletes all photos posted by a user. Gets user by their UUID or username.")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.UserMissingErrorWhen)] - public ApiOkResponse DeletePhotosPostedByUser(RequestContext context, GameDatabaseContext database, + public ApiOkResponse DeletePhotosPostedByUser(RequestContext context, GameDatabaseContext database, DataContext dataContext, [DocSummary(SharedParamDescriptions.UserIdParam)] string id, [DocSummary(SharedParamDescriptions.UserIdTypeParam)] string idType) { @@ -25,18 +26,22 @@ public ApiOkResponse DeletePhotosPostedByUser(RequestContext context, GameDataba if (user == null) return ApiNotFoundError.UserMissingError; database.DeletePhotosPostedByUser(user); + dataContext.Cache.ResetLevelPhotoCountsByUser(user); return new ApiOkResponse(); } [ApiV3Endpoint("admin/photos/id/{id}", HttpMethods.Delete), MinimumRole(GameUserRole.Moderator)] [DocSummary("Deletes a photo.")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.PhotoMissingErrorWhen)] - public ApiOkResponse DeletePhoto(RequestContext context, GameDatabaseContext database, int id) + public ApiOkResponse DeletePhoto(RequestContext context, GameDatabaseContext database, int id, DataContext dataContext) { GamePhoto? photo = database.GetPhotoById(id); if (photo == null) return ApiNotFoundError.PhotoMissingError; database.RemovePhoto(photo); + if (photo.Level != null) + dataContext.Cache.IncrementLevelPhotosByUser(photo.Publisher, photo.Level, -1, database); + return new ApiOkResponse(); } } \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs index ec74e0de3..6c526ed67 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelOwnRelationsResponse.cs @@ -1,4 +1,5 @@ using Refresh.Core.Types.Data; +using Refresh.Core.Types.Relations; using Refresh.Database.Models.Levels; namespace Refresh.Interfaces.APIv3.Endpoints.DataTypes.Response.Levels; @@ -21,16 +22,17 @@ public class ApiGameLevelOwnRelationsResponse : IApiResponse { if (dataContext.User == null) return null; + + OwnLevelRelations relations = dataContext.Cache.GetOwnLevelRelations(dataContext.User, level, dataContext.Database).Content; return new() { - // TODO: Probably cache these stats aswell - IsHearted = dataContext.Database.IsLevelFavouritedByUser(level, dataContext.User), - IsQueued = dataContext.Database.IsLevelQueuedByUser(level, dataContext.User), - LevelRating = (int?)dataContext.Database.GetRatingByUser(level, dataContext.User) ?? 0, - MyPlaysCount = dataContext.Database.GetTotalPlaysForLevelByUser(level, dataContext.User), - CompletionCount = dataContext.Database.GetTotalCompletionsForLevelByUser(level, dataContext.User), - PhotoCount = dataContext.Database.GetTotalPhotosInLevelByUser(level, dataContext.User) + IsHearted = relations.IsHearted, + IsQueued = relations.IsQueued, + LevelRating = relations.LevelRating, + MyPlaysCount = relations.TotalPlayCount, + CompletionCount = relations.TotalCompletionCount, + PhotoCount = relations.PhotoCount }; } } diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelResponse.cs index 8bf90e61e..b2361b26f 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelResponse.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Levels/ApiGameLevelResponse.cs @@ -83,7 +83,7 @@ public class ApiGameLevelResponse : IApiResponse, IDataConvertableFrom EditLevelById(RequestContext context, [DocSummary("Deletes a level by the level's numerical ID")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)] [DocError(typeof(ApiAuthenticationError), ApiAuthenticationError.NoPermissionsForObjectWhen)] - public ApiOkResponse DeleteLevelById(RequestContext context, GameDatabaseContext database, GameUser user, + public ApiOkResponse DeleteLevelById(RequestContext context, GameDatabaseContext database, GameUser user, DataContext dataContext, [DocSummary("The ID of the level")] int id) { GameLevel? level = database.GetLevelById(id); @@ -101,6 +101,7 @@ public ApiOkResponse DeleteLevelById(RequestContext context, GameDatabaseContext return ApiAuthenticationError.NoPermissionsForObject; database.DeleteLevel(level); + dataContext.Cache.RemoveLevelData(level); return new ApiOkResponse(); } @@ -157,47 +158,50 @@ public ApiResponse GetLevelRelationsOfUser(Req } [ApiV3Endpoint("levels/id/{id}/heart", HttpMethods.Post)] - [DocSummary("Adds a specific level by it's ID to your hearted levels")] + [DocSummary("Adds a specific level by its ID to your hearted levels")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)] [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] public ApiOkResponse FavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, - [DocSummary("The ID of the level")] int id) + [DocSummary("The ID of the level")] int id, DataContext dataContext) { GameLevel? level = database.GetLevelById(id); if (level == null) return ApiNotFoundError.LevelMissingError; database.FavouriteLevel(level, user); + dataContext.Cache.UpdateLevelHeartedStatusByUser(user, level, true, database); return new ApiOkResponse(); } [ApiV3Endpoint("levels/id/{id}/unheart", HttpMethods.Post)] - [DocSummary("Removes a specific level by it's ID from your hearted levels")] + [DocSummary("Removes a specific level by its ID from your hearted levels")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)] [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] public ApiOkResponse UnheartLevel(RequestContext context, GameDatabaseContext database, GameUser user, - [DocSummary("The ID of the level")] int id) + [DocSummary("The ID of the level")] int id, DataContext dataContext) { GameLevel? level = database.GetLevelById(id); if (level == null) return ApiNotFoundError.LevelMissingError; database.UnfavouriteLevel(level, user); + dataContext.Cache.UpdateLevelHeartedStatusByUser(user, level, false, database); return new ApiOkResponse(); } [ApiV3Endpoint("levels/id/{id}/queue", HttpMethods.Post)] - [DocSummary("Adds a specific level by it's ID to your queue")] + [DocSummary("Adds a specific level by its ID to your queue")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)] [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] public ApiOkResponse QueueLevel(RequestContext context, GameDatabaseContext database, GameUser user, - [DocSummary("The ID of the level")] int id) + [DocSummary("The ID of the level")] int id, DataContext dataContext) { GameLevel? level = database.GetLevelById(id); if (level == null) return ApiNotFoundError.LevelMissingError; bool success = database.QueueLevel(level, user); + dataContext.Cache.UpdateLevelQueuedStatusByUser(user, level, true, database); // Only give pin if the level was queued without having already been queued. // Won't protect against spam, but this way the pin objective is more accurately implemented. @@ -208,17 +212,18 @@ public ApiOkResponse QueueLevel(RequestContext context, GameDatabaseContext data } [ApiV3Endpoint("levels/id/{id}/dequeue", HttpMethods.Post)] - [DocSummary("Removes a specific level by it's ID from your queue")] + [DocSummary("Removes a specific level by its ID from your queue")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)] [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] public ApiOkResponse DequeueLevel(RequestContext context, GameDatabaseContext database, GameUser user, - [DocSummary("The ID of the level")] int id) + [DocSummary("The ID of the level")] int id, DataContext dataContext) { GameLevel? level = database.GetLevelById(id); if (level == null) return ApiNotFoundError.LevelMissingError; database.DequeueLevel(level, user); + dataContext.Cache.UpdateLevelQueuedStatusByUser(user, level, false, database); return new ApiOkResponse(); } @@ -230,6 +235,7 @@ public ApiOkResponse ClearQueuedLevels(RequestContext context, GameDatabaseConte IDataStore dataStore, GameUser user, DataContext dataContext) { database.ClearQueue(user); + dataContext.Cache.ClearQueueByUser(user); return new ApiOkResponse(); } } \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/PhotoApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/PhotoApiEndpoints.cs index 1b602b566..91602e70c 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/PhotoApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/PhotoApiEndpoints.cs @@ -25,7 +25,7 @@ public class PhotoApiEndpoints : EndpointGroup [DocSummary("Deletes an uploaded photo")] [DocError(typeof(ApiNotFoundError), ApiNotFoundError.PhotoMissingErrorWhen)] [DocError(typeof(ApiValidationError), ApiValidationError.NoPhotoDeletionPermissionErrorWhen)] - public ApiResponse DeletePhoto(RequestContext context, GameDatabaseContext database, GameUser user, int id) + public ApiResponse DeletePhoto(RequestContext context, GameDatabaseContext database, GameUser user, int id, DataContext dataContext) { GamePhoto? photo = database.GetPhotoById(id); if (photo == null) return ApiNotFoundError.PhotoMissingError; @@ -34,6 +34,8 @@ public ApiResponse DeletePhoto(RequestContext context, GameDat return ApiValidationError.NoPhotoDeletionPermissionError; database.RemovePhoto(photo); + if (photo.Level != null) + dataContext.Cache.IncrementLevelPhotosByUser(photo.Publisher, photo.Level, -1, database); return new ApiOkResponse(); } diff --git a/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs index 8f3382b18..782c8cdc7 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/ResourceApiEndpoints.cs @@ -76,7 +76,7 @@ public Response DownloadPspGameAsset(RequestContext context, IDataStore dataStor [DocError(typeof(ApiValidationError), ApiValidationError.HashMissingErrorWhen)] [RateLimitSettings(RequestTimeoutDuration, ImageAssetRequestAmount, RequestBlockDuration, ImageAssetBucket)] public Response DownloadGameAssetAsImage(RequestContext context, IDataStore dataStore, GameDatabaseContext database, - [DocSummary("The SHA1 hash of the asset")] string hash, ImageImporter imageImport, AssetImporter assetImport) + [DocSummary("The SHA1 hash of the asset")] string hash, ImageImporter imageImport, AssetImporter assetImport, DataContext dataContext) { bool isPspAsset = hash.StartsWith("psp/"); @@ -99,7 +99,7 @@ public Response DownloadGameAssetAsImage(RequestContext context, IDataStore data else { //Import the asset as normal - GameAsset? asset = database.GetAssetFromHash(realHash); + GameAsset? asset = dataContext.Cache.GetAssetInfo(realHash, database); imageImport.ImportAsset(realHash, isPspAsset, asset?.AssetType, dataStore); } } @@ -118,8 +118,8 @@ public Response DownloadGameAssetAsImage(RequestContext context, IDataStore data [DocError(typeof(ApiValidationError), ApiValidationError.HashMissingErrorWhen)] [RateLimitSettings(RequestTimeoutDuration, ImageAssetRequestAmount, RequestBlockDuration, ImageAssetBucket)] public Response DownloadPspGameAssetAsImage(RequestContext context, IDataStore dataStore, GameDatabaseContext database, - [DocSummary("The SHA1 hash of the asset")] string hash, ImageImporter imageImport, AssetImporter assetImport) - => this.DownloadGameAssetAsImage(context, dataStore, database, $"psp/{hash}", imageImport, assetImport); + [DocSummary("The SHA1 hash of the asset")] string hash, ImageImporter imageImport, AssetImporter assetImport, DataContext dataContext) + => this.DownloadGameAssetAsImage(context, dataStore, database, $"psp/{hash}", imageImport, assetImport, dataContext); [ApiV3Endpoint("assets/{hash}"), Authentication(false)] [DocSummary("Gets information from the database about a particular hash. Includes user who uploaded, dependencies, timestamps, etc.")] @@ -138,7 +138,7 @@ public ApiResponse GetAssetInfo(RequestContext context, Ga if (!CommonPatterns.Sha1Regex().IsMatch(realHash)) return ApiValidationError.HashInvalidError; if (string.IsNullOrWhiteSpace(realHash)) return ApiValidationError.HashMissingError; - GameAsset? asset = database.GetAssetFromHash(realHash); + GameAsset? asset = dataContext.Cache.GetAssetInfo(realHash, database); if (asset == null) return ApiNotFoundError.Instance; return ApiGameAssetResponse.FromOld(asset, dataContext); @@ -182,7 +182,7 @@ IntegrationConfig integration if (dataStore.ExistsInStore(hash)) { - GameAsset? existingAsset = database.GetAssetFromHash(hash); + GameAsset? existingAsset = dataContext.Cache.GetAssetInfo(hash, database); if (existingAsset == null) return ApiInternalError.HashNotFoundInDatabaseError; @@ -212,6 +212,7 @@ IntegrationConfig integration } database.AddAssetToDatabase(gameAsset); + dataContext.Cache.CacheAsset(gameAsset.AssetHash, gameAsset); return new ApiResponse(ApiGameAssetResponse.FromOld(gameAsset, dataContext)!, Created); } diff --git a/Refresh.Interfaces.APIv3/Endpoints/ReviewApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/ReviewApiEndpoints.cs index a04c89156..d26363d84 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/ReviewApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/ReviewApiEndpoints.cs @@ -126,6 +126,7 @@ public ApiResponse PostReviewToLevel(RequestContext conte return ApiValidationError.RatingParseError; database.RateLevel(level, user, body.LevelRating.Value); + dataContext.Cache.UpdateLevelRatingByUser(user, level, (int)body.LevelRating.Value, database); } if (body.Content != null && body.Content.Length > UgcLimits.CommentLimit) @@ -167,6 +168,7 @@ public ApiResponse UpdateReviewById(RequestContext contex return ApiValidationError.RatingParseError; database.RateLevel(review.Level, user, body.LevelRating.Value); + dataContext.Cache.UpdateLevelRatingByUser(user, review.Level, (int)body.LevelRating.Value, database); } if (body.Content != null && body.Content.Length > UgcLimits.CommentLimit) diff --git a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs index d53c3b6c1..76eedc8c5 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/UserApiEndpoints.cs @@ -55,6 +55,7 @@ public ApiOkResponse HeartUser(RequestContext context, GameDatabaseContext datab if(target == null) return ApiNotFoundError.UserMissingError; bool success = database.FavouriteUser(target, user); + dataContext.Cache.UpdateUserHeartedStatusByUser(user, target, true, database); // Only give pin if the user was hearted without having already been hearted. // Won't protect against spam, but this way the pin objective is more accurately implemented. @@ -77,6 +78,7 @@ public ApiOkResponse UnheartUser(RequestContext context, GameDatabaseContext dat if(target == null) return ApiNotFoundError.UserMissingError; database.UnfavouriteUser(target, user); + dataContext.Cache.UpdateUserHeartedStatusByUser(user, target, false, database); return new ApiOkResponse(); } diff --git a/Refresh.Interfaces.APIv3/Extensions/GameLevelExtensions.cs b/Refresh.Interfaces.APIv3/Extensions/GameLevelExtensions.cs index 450c7f922..3fecdbaa3 100644 --- a/Refresh.Interfaces.APIv3/Extensions/GameLevelExtensions.cs +++ b/Refresh.Interfaces.APIv3/Extensions/GameLevelExtensions.cs @@ -8,7 +8,7 @@ public static class GameLevelExtensions { public static string GetIconHash(this GameLevel level, DataContext dataContext) { - string hash = dataContext.Database.GetAssetFromHash(level.IconHash)?.GetAsIcon(TokenGame.Website, dataContext) ?? level.IconHash; + string hash = dataContext.GetIconFromHash(level.IconHash); return level.GameVersion == TokenGame.LittleBigPlanetPSP ? "psp/" + hash : hash; diff --git a/Refresh.Interfaces.APIv3/Extensions/StringExtensions.cs b/Refresh.Interfaces.APIv3/Extensions/StringExtensions.cs index 202772847..56f74c45f 100644 --- a/Refresh.Interfaces.APIv3/Extensions/StringExtensions.cs +++ b/Refresh.Interfaces.APIv3/Extensions/StringExtensions.cs @@ -28,7 +28,7 @@ public static (string?, ApiError?) ValidateIcon(this string? iconReference, Data } else { - GameAsset? asset = dataContext.Database.GetAssetFromHash(iconReference); + GameAsset? asset = dataContext.Cache.GetAssetInfo(iconReference, dataContext.Database); if (asset == null) return (null, ApiValidationError.IconMissingError); diff --git a/Refresh.Interfaces.Game/Endpoints/DataTypes/Response/GameLevelResponse.cs b/Refresh.Interfaces.Game/Endpoints/DataTypes/Response/GameLevelResponse.cs index 721df6cd5..6e19b125d 100644 --- a/Refresh.Interfaces.Game/Endpoints/DataTypes/Response/GameLevelResponse.cs +++ b/Refresh.Interfaces.Game/Endpoints/DataTypes/Response/GameLevelResponse.cs @@ -144,7 +144,7 @@ private static GameLevelResponse FromMinimal(GameMinimalLevelResponse minimal) if (dataContext.Game is TokenGame.LittleBigPlanetVita or TokenGame.BetaBuild) { - GameAsset? rootResourceAsset = dataContext.Database.GetAssetFromHash(response.RootResource); + GameAsset? rootResourceAsset = dataContext.Cache.GetAssetInfo(response.RootResource, dataContext.Database); if (rootResourceAsset != null) { rootResourceAsset.TraverseDependenciesRecursively(dataContext.Database, (_, asset) => @@ -154,7 +154,7 @@ private static GameLevelResponse FromMinimal(GameMinimalLevelResponse minimal) }); } - response.SkillRewards = dataContext.Database.GetSkillRewardsForLevel(old).ToList(); + response.SkillRewards = dataContext.Cache.GetSkillRewards(old, dataContext.Database); } return response; diff --git a/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs index 64df00cd3..959fbfcdf 100644 --- a/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs @@ -10,6 +10,7 @@ using Refresh.Common.Time; using Refresh.Core.Authentication.Permission; using Refresh.Core.Configuration; +using Refresh.Core.Types.Data; using Refresh.Database; using Refresh.Database.Models.Levels; using Refresh.Database.Models.Users; @@ -47,7 +48,7 @@ public SerializedPrivacySettings SetPrivacySettings(RequestContext context, Seri [GameEndpoint("npdata", ContentType.Xml, HttpMethods.Post)] [RateLimitSettings(480, 8, 420, "game-npdata")] - public Response SetFriendData(RequestContext context, GameUser user, GameDatabaseContext database, SerializedFriendData body) + public Response SetFriendData(RequestContext context, GameUser user, GameDatabaseContext database, SerializedFriendData body, DataContext dataContext) { IEnumerable friends = body.FriendsList.Names .Take(128) // should be way more than enough - we'll see if this becomes a problem @@ -55,7 +56,10 @@ public Response SetFriendData(RequestContext context, GameUser user, GameDatabas .Where(u => u != null)!; foreach (GameUser userToFavourite in friends) + { database.FavouriteUser(userToFavourite, user); + dataContext.Cache.RemoveOwnUserRelations(user, userToFavourite); // really don't want to refresh the relations of up to 128 users in the worst case + } return OK; } diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs index b33cb3aea..d74dfa661 100644 --- a/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Levels/LeaderboardEndpoints.cs @@ -34,7 +34,7 @@ public class LeaderboardEndpoints : EndpointGroup [MinimumRole(GameUserRole.Restricted)] [RateLimitSettings(PlayLevelEndpointLimits.TimeoutDuration, PlayLevelEndpointLimits.RequestAmount, PlayLevelEndpointLimits.BlockDuration, PlayLevelEndpointLimits.RequestBucket)] - public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseContext database, string slotType, int id) + public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseContext database, string slotType, int id, DataContext dataContext) { GameLevel? level = database.GetLevelByIdAndType(slotType, id); if (level == null) return NotFound; @@ -61,6 +61,7 @@ public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseCon } database.PlayLevel(level, user, count); + dataContext.Cache.IncrementLevelTotalPlaysByUser(user, level, count, database); return OK; } @@ -185,6 +186,7 @@ public Response SubmitScore(RequestContext context, GameUser user, GameServerCon DatabaseList scores = database.GetRankedScoresAroundScore(score, 5); this.AwardScoreboardPins(scores, dataContext, user, level); + dataContext.Cache.IncrementLevelTotalCompletionsByUser(user, level, 1, database); return new Response(SerializedScoreLeaderboardList.FromDatabaseList(scores, dataContext), ContentType.Xml); } diff --git a/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs index c5144c5c3..2720fe612 100644 --- a/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/Levels/PublishEndpoints.cs @@ -192,7 +192,7 @@ public Response PublishLevel(RequestContext context, //Make sure the root resource exists in the data store if (!dataContext.DataStore.ExistsInStore(rootResourcePath)) return NotFound; - GameAsset? asset = dataContext.Database.GetAssetFromHash(body.RootResource); + GameAsset? asset = dataContext.Cache.GetAssetInfo(body.RootResource, dataContext.Database); if (asset != null && dataContext.Game != TokenGame.LittleBigPlanetPSP) { // ReSharper disable once ConvertIfStatementToSwitchStatement @@ -240,12 +240,14 @@ public Response PublishLevel(RequestContext context, dataContext.Database.UpdateLevelModdedStatus(levelToUpdate); } - dataContext.Database.UpdateSkillRewardsForLevel(levelToUpdate, body.SkillRewards); + List updatedRewards = dataContext.Database.UpdateSkillRewardsForLevel(levelToUpdate, body.SkillRewards); + dataContext.Cache.CacheSkillRewards(levelToUpdate, updatedRewards); return new Response(GameLevelResponse.FromOld(levelToUpdate, dataContext)!, ContentType.Xml); } GameLevel newLevel = dataContext.Database.AddLevel(body, dataContext.Game, user); - dataContext.Database.UpdateSkillRewardsForLevel(newLevel, body.SkillRewards); + List newRewards = dataContext.Database.UpdateSkillRewardsForLevel(newLevel, body.SkillRewards); + dataContext.Cache.CacheSkillRewards(newLevel, newRewards); context.Logger.LogInfo(BunkumCategory.UserContent, "User {0} (id: {1}) uploaded level id {2}", user.Username, user.UserId, newLevel.LevelId); @@ -266,7 +268,7 @@ public Response PublishLevel(RequestContext context, } [GameEndpoint("unpublish/{id}", ContentType.Xml, HttpMethods.Post)] - public Response DeleteLevel(RequestContext context, GameUser user, GameDatabaseContext database, int id) + public Response DeleteLevel(RequestContext context, GameUser user, GameDatabaseContext database, int id, DataContext dataContext) { GameLevel? level = database.GetLevelById(id); if (level == null) return NotFound; @@ -274,6 +276,7 @@ public Response DeleteLevel(RequestContext context, GameUser user, GameDatabaseC if (level.Publisher?.UserId != user.UserId) return Unauthorized; database.DeleteLevel(level); + dataContext.Cache.RemoveLevelData(level); return OK; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Game/Endpoints/MatchingEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/MatchingEndpoints.cs index 66715c02d..43ccc56fe 100644 --- a/Refresh.Interfaces.Game/Endpoints/MatchingEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/MatchingEndpoints.cs @@ -134,6 +134,7 @@ public Response EnterLevel(RequestContext context, Token token, MatchService mat if (level != null && roomSlotType == RoomSlotType.Online && room.LevelId != id && room.LevelType != roomSlotType) { dataContext.Database.PlayLevel(level, token.User, 1); + dataContext.Cache.IncrementLevelTotalPlaysByUser(token.User, level, 1, dataContext.Database); } room.LevelType = roomSlotType; diff --git a/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs index 658e15846..71e877b09 100644 --- a/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs @@ -55,7 +55,7 @@ public Response UploadPhoto(RequestContext context, SerializedPhoto body, GameDa List hashes = [body.LargeHash, body.MediumHash, body.SmallHash]; foreach (string hash in hashes.Distinct()) { - GameAsset? gameAsset = database.GetAssetFromHash(hash); + GameAsset? gameAsset = dataContext.Cache.GetAssetInfo(hash, database); if(gameAsset == null) continue; if (aipi != null && aipi.ScanAndHandleAsset(dataContext, gameAsset)) return Unauthorized; @@ -64,11 +64,14 @@ public Response UploadPhoto(RequestContext context, SerializedPhoto body, GameDa GameLevel? level = body.Level == null ? null : database.GetLevelByIdAndType(body.Level.Type, body.Level.LevelId); database.UploadPhoto(body, body.PhotoSubjects, user, level); + if (level != null) + dataContext.Cache.IncrementLevelPhotosByUser(user, level, 1, database); + return OK; } [GameEndpoint("deletePhoto/{id}", HttpMethods.Post)] - public Response DeletePhoto(RequestContext context, GameDatabaseContext database, GameUser user, int id) + public Response DeletePhoto(RequestContext context, GameDatabaseContext database, GameUser user, int id, DataContext dataContext) { GamePhoto? photo = database.GetPhotoById(id); if (photo == null) return NotFound; @@ -77,6 +80,9 @@ public Response DeletePhoto(RequestContext context, GameDatabaseContext database return Unauthorized; database.RemovePhoto(photo); + if (photo.Level != null) + dataContext.Cache.IncrementLevelPhotosByUser(photo.Publisher, photo.Level, -1, database); + return OK; } diff --git a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs index a38364584..26c4c1ff7 100644 --- a/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/RelationEndpoints.cs @@ -24,7 +24,7 @@ public class RelationEndpoints : EndpointGroup [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] public Response FavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, - int id, GameServerConfig config) + int id, GameServerConfig config, DataContext dataContext) { if (user.IsWriteBlocked(config)) return context.IsPSP() ? OK : Unauthorized; // See comment below @@ -35,6 +35,7 @@ public Response FavouriteLevel(RequestContext context, GameDatabaseContext datab if (level == null) return context.IsPSP() ? OK : NotFound; database.FavouriteLevel(level, user); + dataContext.Cache.UpdateLevelHeartedStatusByUser(user, level, true, database); return OK; } @@ -43,7 +44,7 @@ public Response FavouriteLevel(RequestContext context, GameDatabaseContext datab [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, - string slotType, int id, GameServerConfig config) + string slotType, int id, GameServerConfig config, DataContext dataContext) { if (user.IsWriteBlocked(config)) return context.IsPSP() ? OK : Unauthorized; // See comment below @@ -54,6 +55,7 @@ public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext dat if (level == null) return context.IsPSP() ? OK : NotFound; database.UnfavouriteLevel(level, user); + dataContext.Cache.UpdateLevelHeartedStatusByUser(user, level, false, database); return OK; } @@ -62,7 +64,7 @@ public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext dat [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] public Response FavouriteUser(RequestContext context, GameDatabaseContext database, GameUser user, string username, - GameServerConfig config) + GameServerConfig config, DataContext dataContext) { if (user.IsWriteBlocked(config)) return context.IsPSP() ? OK : Unauthorized; // See comment below @@ -73,6 +75,7 @@ public Response FavouriteUser(RequestContext context, GameDatabaseContext databa if (userToFavourite == null) return context.IsPSP() ? OK : NotFound; database.FavouriteUser(userToFavourite, user); + dataContext.Cache.UpdateUserHeartedStatusByUser(user, userToFavourite, true, database); return OK; } @@ -81,7 +84,7 @@ public Response FavouriteUser(RequestContext context, GameDatabaseContext databa [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] public Response UnfavouriteUser(RequestContext context, GameDatabaseContext database, GameUser user, - string username, GameServerConfig config) + string username, GameServerConfig config, DataContext dataContext) { if (user.IsWriteBlocked(config)) return context.IsPSP() ? OK : Unauthorized; // See comment below @@ -92,6 +95,7 @@ public Response UnfavouriteUser(RequestContext context, GameDatabaseContext data if (userToFavourite == null) return context.IsPSP() ? OK : NotFound; database.UnfavouriteUser(userToFavourite, user); + dataContext.Cache.UpdateUserHeartedStatusByUser(user, userToFavourite, false, database); return OK; } @@ -116,12 +120,13 @@ public Response UnfavouriteUser(RequestContext context, GameDatabaseContext data [RequireEmailVerified] [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] - public Response QueueLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id) + public Response QueueLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, DataContext dataContext) { GameLevel? level = database.GetLevelByIdAndType(slotType, id); if (level == null) return NotFound; database.QueueLevel(level, user); + dataContext.Cache.UpdateLevelQueuedStatusByUser(user, level, true, database); return OK; } @@ -129,12 +134,13 @@ public Response QueueLevel(RequestContext context, GameDatabaseContext database, [RequireEmailVerified] [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] - public Response DequeueLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id) + public Response DequeueLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, DataContext dataContext) { GameLevel? level = database.GetLevelByIdAndType(slotType, id); if (level == null) return NotFound; database.DequeueLevel(level, user); + dataContext.Cache.UpdateLevelQueuedStatusByUser(user, level, false, database); return OK; } @@ -142,9 +148,10 @@ public Response DequeueLevel(RequestContext context, GameDatabaseContext databas [RequireEmailVerified] [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] - public Response ClearQueue(RequestContext context, GameDatabaseContext database, GameUser user) + public Response ClearQueue(RequestContext context, GameDatabaseContext database, GameUser user, DataContext dataContext) { database.ClearQueue(user); + dataContext.Cache.ClearQueueByUser(user); return OK; } @@ -152,7 +159,7 @@ public Response ClearQueue(RequestContext context, GameDatabaseContext database, [RequireEmailVerified] [RateLimitSettings(LevelTaggingEndpointLimits.TimeoutDuration, LevelTaggingEndpointLimits.RequestAmount, LevelTaggingEndpointLimits.BlockDuration, LevelTaggingEndpointLimits.RequestBucket)] - public Response SubmitTagsForLevel(RequestContext context, GameDatabaseContext database, GameUser user, + public Response SubmitTagsForLevel(RequestContext context, GameDatabaseContext database, GameUser user, DataContext dataContext, string slotType, int id, string body, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -174,6 +181,7 @@ public Response SubmitTagsForLevel(RequestContext context, GameDatabaseContext d return BadRequest; database.AddTagRelation(user, level, tag.Value); + dataContext.Cache.RemoveTags(level); // Reset return OK; } diff --git a/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs index a5527283c..4dc19a830 100644 --- a/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ResourceEndpoints.cs @@ -32,7 +32,8 @@ public class ResourceEndpoints : EndpointGroup [SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")] [RateLimitSettings(450, 180, 300, "game-asset-upload")] public Response UploadAsset(RequestContext context, string hash, string type, byte[] body, IDataStore dataStore, - GameDatabaseContext database, GameUser user, AssetImporter importer, GameServerConfig config, IDateTimeProvider timeProvider, Token token) + GameDatabaseContext database, GameUser user, AssetImporter importer, GameServerConfig config, IDateTimeProvider timeProvider, Token token, + DataContext dataContext) { if (user.IsWriteBlocked(config)) return Unauthorized; @@ -90,6 +91,7 @@ public Response UploadAsset(RequestContext context, string hash, string type, by gameAsset.OriginalUploader = user; database.AddAssetToDatabase(gameAsset); + dataContext.Cache.CacheAsset(gameAsset.AssetHash, gameAsset); database.IncrementUserFilesizeQuota(user, body.Length); @@ -121,7 +123,7 @@ public Response GetResource(RequestContext context, GameUser user, Token token, // Part of a workaround to prevent LBP Hub from breaking challenge ghost replay. // See ChallengeGhostRateLimitService's summary for more information. - if (token.TokenGame == TokenGame.BetaBuild && dataContext.Database.GetAssetFromHash(hash)?.AssetType == GameAssetType.ChallengeGhost) + if (token.TokenGame == TokenGame.BetaBuild && dataContext.Cache.GetAssetInfo(hash, dataContext.Database)?.AssetType == GameAssetType.ChallengeGhost) { if (ghostService.IsUserRateLimited(user.UserId)) { diff --git a/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs index c081605f4..afbad46fd 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReviewEndpoints.cs @@ -27,7 +27,7 @@ public class ReviewEndpoints : EndpointGroup [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] public Response SubmitRating(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, - int id, GameServerConfig config) + int id, GameServerConfig config, DataContext dataContext) { if (user.IsWriteBlocked(config)) return Unauthorized; @@ -47,6 +47,8 @@ public Response SubmitRating(RequestContext context, GameDatabaseContext databas if (rating is > 1 or < -1) return BadRequest; bool rated = database.RateLevel(level, user, (RatingType)rating); + dataContext.Cache.UpdateLevelRatingByUser(user, level, rating, database); + return rated ? OK : Unauthorized; } @@ -55,7 +57,8 @@ public Response SubmitRating(RequestContext context, GameDatabaseContext databas [RequireEmailVerified] [RateLimitSettings(CommonRelationEndpointLimits.TimeoutDuration, CommonRelationEndpointLimits.RequestAmount, CommonRelationEndpointLimits.BlockDuration, CommonRelationEndpointLimits.RequestBucket)] - public Response RateUserLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id) + public Response RateUserLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, + DataContext dataContext) { GameLevel? level = database.GetLevelByIdAndType(slotType, id); if (level == null) return NotFound; @@ -87,7 +90,9 @@ public Response RateUserLevel(RequestContext context, GameDatabaseContext databa return BadRequest; } - return database.RateLevel(level, user, rating) ? OK : Unauthorized; + database.RateLevel(level, user, rating); + dataContext.Cache.UpdateLevelRatingByUser(user, level, (int)rating, database); + return OK; } [GameEndpoint("reviewsFor/{slotType}/{id}", ContentType.Xml)] @@ -179,6 +184,7 @@ public Response PostReviewForLevel(RequestContext context, // Update the user's rating if valid if (body.Thumb is > 1 or < -1) return BadRequest; database.RateLevel(level, user, (RatingType)body.Thumb); + dataContext.Cache.UpdateLevelRatingByUser(user, level, body.Thumb, database); // Return the review return new Response(SerializedGameReview.FromOld(review, dataContext), ContentType.Xml); diff --git a/Refresh.Interfaces.Game/Types/Levels/GameMinimalLevelResponse.cs b/Refresh.Interfaces.Game/Types/Levels/GameMinimalLevelResponse.cs index 2dd0b1915..9e7b46b0e 100644 --- a/Refresh.Interfaces.Game/Types/Levels/GameMinimalLevelResponse.cs +++ b/Refresh.Interfaces.Game/Types/Levels/GameMinimalLevelResponse.cs @@ -200,7 +200,7 @@ public static GameMinimalLevelResponse FromHash(string hash, DataContext dataCon if (dataContext.Game is TokenGame.LittleBigPlanet1 or TokenGame.BetaBuild) { - response.Tags = string.Join(',', dataContext.Database.GetTagsForLevel(old).Select(t => t.ToLbpString())); + response.Tags = string.Join(',', dataContext.Cache.GetTags(old, dataContext.Database).Select(t => t.ToLbpString())); } if (dataContext.Game is not TokenGame.LittleBigPlanet1 or TokenGame.LittleBigPlanetPSP) @@ -210,7 +210,7 @@ public static GameMinimalLevelResponse FromHash(string hash, DataContext dataCon response.PublisherLabels = old.Labels.ToLbpCommaList(dataContext.Game); } - response.IconHash = dataContext.Database.GetAssetFromHash(old.IconHash)?.GetAsIcon(dataContext.Game, dataContext) ?? old.IconHash; + response.IconHash = dataContext.GetIconFromHash(old.IconHash); return response; } diff --git a/Refresh.Interfaces.Game/Types/Playlists/SerializedLbp1Playlist.cs b/Refresh.Interfaces.Game/Types/Playlists/SerializedLbp1Playlist.cs index ee406de88..9343f8888 100644 --- a/Refresh.Interfaces.Game/Types/Playlists/SerializedLbp1Playlist.cs +++ b/Refresh.Interfaces.Game/Types/Playlists/SerializedLbp1Playlist.cs @@ -36,7 +36,7 @@ public class SerializedLbp1Playlist : IDataConvertableFrom(); this.Server.AddService(); this.Server.AddService(); + this.Server.AddService(); // Must always be last, see comment in RefreshGameServer this.Server.AddService(); diff --git a/RefreshTests.GameServer/TestContext.cs b/RefreshTests.GameServer/TestContext.cs index 852b9dde5..0775b6265 100644 --- a/RefreshTests.GameServer/TestContext.cs +++ b/RefreshTests.GameServer/TestContext.cs @@ -227,6 +227,7 @@ public DataContext GetDataContext(Token? token = null) Match = this.GetService(), Token = token, GuidChecker = this.GetService(), + Cache = this.GetService(), }; } diff --git a/RefreshTests.GameServer/Tests/Matching/MatchingTests.cs b/RefreshTests.GameServer/Tests/Matching/MatchingTests.cs index daa8b89df..3489c8562 100644 --- a/RefreshTests.GameServer/Tests/Matching/MatchingTests.cs +++ b/RefreshTests.GameServer/Tests/Matching/MatchingTests.cs @@ -44,6 +44,7 @@ public void CreatesRooms() Match = match, GuidChecker = null!, Token = token1, + Cache = null!, }, config); match.ExecuteMethod("CreateRoom", roomData, new DataContext { @@ -53,6 +54,7 @@ public void CreatesRooms() Match = match, Token = token2, GuidChecker = null!, + Cache = null!, }, config); Assert.Multiple(() => @@ -101,6 +103,7 @@ public void DoesntMatchIfNoRooms() DataStore = null!, //this isn't accessed by matching Match = match, Token = token1, + Cache = null!, }, config); // Tell user1 to try to find a room @@ -118,6 +121,7 @@ public void DoesntMatchIfNoRooms() DataStore = null!, //this isn't accessed by matching Match = match, Token = token1, + Cache = null!, }, config); // Deserialize the result @@ -162,6 +166,7 @@ public void StrictNatCantJoinStrict() Match = match, GuidChecker = null!, Token = token1, + Cache = null!, }, config); match.ExecuteMethod("CreateRoom", roomData, new DataContext { @@ -171,6 +176,7 @@ public void StrictNatCantJoinStrict() Match = match, Token = token2, GuidChecker = null!, + Cache = null!, }, config); // Tell user2 to try to find a room @@ -181,6 +187,7 @@ public void StrictNatCantJoinStrict() Match = match, GuidChecker = null!, Token = token2, + Cache = null!, }, config); //Deserialize the result @@ -234,6 +241,7 @@ public void StrictNatCanJoinOpen() Match = match, Token = token1, GuidChecker = null!, + Cache = null!, }, config); match.ExecuteMethod("CreateRoom", roomData2, new DataContext { @@ -243,6 +251,7 @@ public void StrictNatCanJoinOpen() Match = match, Token = token2, GuidChecker = null!, + Cache = null!, }, config); // Tell user2 to try to find a room @@ -259,6 +268,7 @@ public void StrictNatCanJoinOpen() Match = match, Token = token2, GuidChecker = null!, + Cache = null!, }, config); Assert.That(response.StatusCode, Is.EqualTo(OK)); } @@ -295,6 +305,7 @@ public void MatchesPlayersTogether() Match = match, Token = token1, GuidChecker = null!, + Cache = null!, }, config); match.ExecuteMethod("CreateRoom", roomData, new DataContext { @@ -304,6 +315,7 @@ public void MatchesPlayersTogether() Match = match, Token = token2, GuidChecker = null!, + Cache = null!, }, config); // Tell user2 to try to find a room @@ -320,6 +332,7 @@ public void MatchesPlayersTogether() Match = match, Token = token2, GuidChecker = null!, + Cache = null!, }, config); Assert.That(response.StatusCode, Is.EqualTo(OK)); } @@ -358,6 +371,7 @@ public void HostCanSetPlayersInRoom() Match = match, Token = token1, GuidChecker = null!, + Cache = null!, }, config); match.ExecuteMethod("CreateRoom", roomData, new DataContext { @@ -367,6 +381,7 @@ public void HostCanSetPlayersInRoom() Match = match, Token = token2, GuidChecker = null!, + Cache = null!, }, config); // Get user1 and user2 in the same room @@ -384,6 +399,7 @@ public void HostCanSetPlayersInRoom() Match = match, Token = token1, GuidChecker = null!, + Cache = null!, }, config); GameRoom? room = match.RoomAccessor.GetRoomByUser(user1); Assert.Multiple(() => @@ -427,6 +443,7 @@ public void PlayersCanLeaveAndSplitIntoNewRoom() Match = match, Token = token1, GuidChecker = null!, + Cache = null!, }, config); match.ExecuteMethod("CreateRoom", roomData, new DataContext { @@ -436,6 +453,7 @@ public void PlayersCanLeaveAndSplitIntoNewRoom() Match = match, Token = token2, GuidChecker = null!, + Cache = null!, }, config); // Get user1 and user2 in the same room @@ -454,6 +472,7 @@ public void PlayersCanLeaveAndSplitIntoNewRoom() Match = match, Token = token1, GuidChecker = null!, + Cache = null!, }, config); GameRoom? user1Room = match.RoomAccessor.GetRoomByUser(user1); Assert.That(user1Room, Is.Not.Null); @@ -469,6 +488,7 @@ public void PlayersCanLeaveAndSplitIntoNewRoom() Match = match, Token = token2, GuidChecker = null!, + Cache = null!, }, config); GameRoom? user1Room = match.RoomAccessor.GetRoomByUser(user1); GameRoom? user2Room = match.RoomAccessor.GetRoomByUser(user2);