Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add level reviews #349

Merged
merged 20 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Refresh.GameServer/Database/GameDatabaseContext.Relations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,55 @@ public bool RateLevel(GameLevel level, GameUser user, RatingType type)
});
return true;
}

/// <summary>
/// Adds a review to the database, deleting any old ones by the user on that level.
/// </summary>
/// <param name="review">The review to add</param>
/// <param name="level">The level the review is for</param>
/// <param name="user">The user who made the review</param>
public void AddReviewToLevel(GameReview review, GameLevel level)
{
List<GameReview> toRemove = level.Reviews.Where(r => r.Publisher.UserId == review.Publisher.UserId).ToList();
if (toRemove.Count > 0)
{
this._realm.Write(() =>
{
foreach (GameReview reviewToDelete in toRemove)
{
level.Reviews.Remove(reviewToDelete);
this._realm.Remove(reviewToDelete);
}
});
}

this.AddSequentialObject(review, level.Reviews);
}

public DatabaseList<GameReview> GetReviewsByUser(GameUser user, int count, int skip)
{
return new DatabaseList<GameReview>(this._realm.All<GameReview>()
.Where(r => r.Publisher == user), skip, count);
}

public int GetTotalReviewsByUser(GameUser user)
=> this._realm.All<GameReview>().Count(r => r.Publisher == user);

public void DeleteReview(GameReview review)
{
this._realm.Remove(review);
}

public GameReview? GetReviewByLevelAndUser(GameLevel level, GameUser user)
{
return level.Reviews.FirstOrDefault(r => r.Publisher.UserId == user.UserId);
}

public DatabaseList<GameReview> GetReviewsForLevel(GameLevel level, int count, int skip)
{
return new DatabaseList<GameReview>(this._realm.All<GameReview>()
.Where(r => r.Level == level), skip, count);
}

#endregion

Expand Down
4 changes: 3 additions & 1 deletion Refresh.GameServer/Database/GameDatabaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Refresh.GameServer.Types.Notifications;
using Refresh.GameServer.Types.Relations;
using Refresh.GameServer.Types.Report;
using Refresh.GameServer.Types.Reviews;
using Refresh.GameServer.Types.UserData.Leaderboard;
using GamePhoto = Refresh.GameServer.Types.Photos.GamePhoto;
using GamePhotoSubject = Refresh.GameServer.Types.Photos.GamePhotoSubject;
Expand All @@ -32,7 +33,7 @@ protected GameDatabaseProvider(IDateTimeProvider time)
this._time = time;
}

protected override ulong SchemaVersion => 112;
protected override ulong SchemaVersion => 115;

protected override string Filename => "refreshGameServer.realm";

Expand Down Expand Up @@ -72,6 +73,7 @@ protected GameDatabaseProvider(IDateTimeProvider time)
typeof(ScreenElements),
typeof(ScreenRect),
typeof(Slot),
typeof(GameReview),
};

public override void Warmup()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.Reviews;

namespace Refresh.GameServer.Endpoints.ApiV3.DataTypes.Response;

[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))]
public class ApiGameReviewResponse : IApiResponse, IDataConvertableFrom<ApiGameReviewResponse, GameReview>
{
public required int ReviewId { get; set; }
public required ApiGameLevelResponse Level { get; set; }
public required ApiGameUserResponse Publisher { get; set; }
public required DateTimeOffset PostedAt { get; set; }
public required string Labels { get; set; }
public required string Text { get; set; }
public static ApiGameReviewResponse? FromOld(GameReview? old)
{
if (old == null) return null;
return new ApiGameReviewResponse
{
ReviewId = old.ReviewId,
Level = ApiGameLevelResponse.FromOld(old.Level)!,
Publisher = ApiGameUserResponse.FromOld(old.Publisher)!,
PostedAt = old.PostedAt,
Labels = old.Labels,
Text = old.Content,
};
}

public static IEnumerable<ApiGameReviewResponse> FromOldList(IEnumerable<GameReview> oldList) => oldList.Select(FromOld).ToList()!;
}
35 changes: 35 additions & 0 deletions Refresh.GameServer/Endpoints/ApiV3/ReviewApiEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using AttribDoc.Attributes;
using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Bunkum.Core.Storage;
using Refresh.GameServer.Database;
using Refresh.GameServer.Documentation.Attributes;
using Refresh.GameServer.Endpoints.ApiV3.ApiTypes;
using Refresh.GameServer.Endpoints.ApiV3.ApiTypes.Errors;
using Refresh.GameServer.Endpoints.ApiV3.DataTypes.Response;
using Refresh.GameServer.Extensions;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.Reviews;

namespace Refresh.GameServer.Endpoints.ApiV3;

public class ReviewApiEndpoints : EndpointGroup
{
[ApiV3Endpoint("levels/id/{id}/reviews"), Authentication(false)]
[DocUsesPageData, DocSummary("Gets a list of the reviews posted to a level.")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)]
public ApiListResponse<ApiGameReviewResponse> GetTopScoresForLevel(RequestContext context,
GameDatabaseContext database, IDataStore dataStore,
[DocSummary("The ID of the level")] int id)
{
GameLevel? level = database.GetLevelById(id);
if (level == null) return ApiNotFoundError.LevelMissingError;

(int skip, int count) = context.GetPageData(true);

DatabaseList<GameReview> reviews = database.GetReviewsForLevel(level, count, skip);
DatabaseList<ApiGameReviewResponse> ret = DatabaseList<ApiGameScoreResponse>.FromOldList<ApiGameReviewResponse, GameReview>(reviews);

return ret;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ public class GameLevelResponse : IDataConvertableFrom<GameLevelResponse, GameLev
[XmlElement("thumbsdown")] public required int BooCount { get; set; }
[XmlElement("yourRating")] public int YourStarRating { get; set; }

// 1 by default since this will break reviews if set to 0 for GameLevelResponses that do not have extra data being filled in
[XmlElement("yourlbp2PlayCount")] public int YourLbp2PlayCount { get; set; } = 1;

[XmlArray("customRewards")]
[XmlArrayItem("customReward")]
public required List<GameSkillReward> SkillRewards { get; set; }
Expand All @@ -70,6 +73,10 @@ public class GameLevelResponse : IDataConvertableFrom<GameLevelResponse, GameLev
[XmlElement("links")] public string? Links { get; set; }
[XmlElement("averageRating")] public double AverageStarRating { get; set; }
[XmlElement("sizeOfResources")] public int SizeOfResourcesInBytes { get; set; }
[XmlElement("reviewCount")] public int ReviewCount { get; set; }
[XmlElement("reviewsEnabled")] public bool ReviewsEnabled { get; set; } = true;
[XmlElement("commentCount")] public int CommentCount { get; set; } = 0;
[XmlElement("commentsEnabled")] public bool CommentsEnabled { get; set; } = true;

public static GameLevelResponse? FromOldWithExtraData(GameLevel? old, GameDatabaseContext database, MatchService matchService, GameUser user, IDataStore dataStore, TokenGame game)
{
Expand Down Expand Up @@ -120,6 +127,8 @@ public class GameLevelResponse : IDataConvertableFrom<GameLevelResponse, GameLev
BackgroundGuid = old.BackgroundGuid,
Links = "",
AverageStarRating = old.CalculateAverageStarRating(),
ReviewCount = old.Reviews.Count,
CommentCount = old.LevelComments.Count,
};

response.Type = "user";
Expand Down Expand Up @@ -150,6 +159,7 @@ private void FillInExtraData(GameDatabaseContext database, MatchService matchSer

this.YourRating = rating?.ToDPad() ?? (int)RatingType.Neutral;
this.YourStarRating = rating?.ToLBP1() ?? 0;
this.YourLbp2PlayCount = level.AllPlays.Count(p => p.User == user);
this.PlayerCount = matchService.GetPlayerCountForLevel(RoomSlotType.Online, this.LevelId);

GameAsset? rootResourceAsset = database.GetAssetFromHash(this.RootResource);
Expand All @@ -163,5 +173,7 @@ private void FillInExtraData(GameDatabaseContext database, MatchService matchSer
}

this.IconHash = database.GetAssetFromHash(this.IconHash)?.GetAsIcon(game, database, dataStore) ?? this.IconHash;

this.CommentCount = level.LevelComments.Count;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class GameUserResponse : IDataConvertableFrom<GameUserResponse, GameUser>
[XmlElement("npHandle")] public required SerializedUserHandle Handle { get; set; }
[XmlElement("commentCount")] public int CommentCount { get; set; }
[XmlElement("commentsEnabled")] public bool CommentsEnabled { get; set; }
[XmlElement("reviewCount")] public int ReviewCount { get; set; }
[XmlElement("favouriteSlotCount")] public int FavouriteLevelCount { get; set; }
[XmlElement("favouriteUserCount")] public int FavouriteUserCount { get; set; }
[XmlElement("lolcatftwCount")] public int QueuedLevelCount { get; set; }
Expand Down Expand Up @@ -108,6 +109,8 @@ private void FillInExtraData(GameUser old, TokenGame gameVersion, GameDatabaseCo

return;
}

this.ReviewCount = database.GetTotalReviewsByUser(old);

this.PlanetsHash = gameVersion switch
{
Expand Down
88 changes: 87 additions & 1 deletion Refresh.GameServer/Endpoints/Game/ReviewEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Bunkum.Core.Endpoints.Debugging;
using Bunkum.Core.Responses;
using Bunkum.Listener.Protocol;
using Bunkum.Protocols.Http;
using Refresh.GameServer.Database;
using Refresh.GameServer.Extensions;
using Refresh.GameServer.Time;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.Reviews;
using Refresh.GameServer.Types.UserData;
Expand Down Expand Up @@ -56,5 +59,88 @@ public Response RateUserLevel(RequestContext context, GameDatabaseContext databa
}

return database.RateLevel(level, user, rating) ? OK : Unauthorized;
}
}

[GameEndpoint("reviewsFor/{slotType}/{levelId}", ContentType.Xml)]
[AllowEmptyBody]
public Response GetReviewsForLevel(RequestContext context, GameDatabaseContext database, string slotType, int levelId)
{
GameLevel? level;
switch (slotType)
{
case "developer":
level = database.GetStoryLevelById(levelId);
break;
case "user":
level = database.GetLevelById(levelId);
break;
default:
return BadRequest;
}

if (level == null)
return NotFound;

(int skip, int count) = context.GetPageData();

return new Response(new SerializedGameReviewResponse(items: SerializedGameReview.FromOldList(database.GetReviewsForLevel(level, count, skip).Items).ToList()), ContentType.Xml);
}

[GameEndpoint("reviewsBy/{username}", ContentType.Xml)]
[AllowEmptyBody]
public Response GetReviewsForLevel(RequestContext context, GameDatabaseContext database, string username)
{
GameUser? user = database.GetUserByUsername(username);

if (user == null)
return NotFound;

(int skip, int count) = context.GetPageData();

return new Response(new SerializedGameReviewResponse(SerializedGameReview.FromOldList(database.GetReviewsByUser(user, count, skip).Items).ToList()), ContentType.Xml);
}

[GameEndpoint("postReview/{slotType}/{levelId}", ContentType.Xml, HttpMethods.Post)]
public Response PostReviewForLevel(
RequestContext context,
GameDatabaseContext database,
string slotType,
int levelId,
SerializedGameReview body,
GameUser user,
IDateTimeProvider timeProvider
)
{
GameLevel? level;
switch (slotType)
{
case "developer":
level = database.GetStoryLevelById(levelId);
break;
case "user":
level = database.GetLevelById(levelId);
break;
default:
return BadRequest;
}

if (level == null)
return NotFound;

//You cant review a level you haven't played.
if (!database.HasUserPlayedLevel(level, user))
return BadRequest;

//Add the review to the database
database.AddReviewToLevel(new GameReview
{
Publisher = user,
Level = level,
PostedAt = timeProvider.Now,
Labels = body.Labels,
Content = body.Text,
}, level);

return OK;
}
}
2 changes: 2 additions & 0 deletions Refresh.GameServer/Types/Levels/GameLevel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ [Ignored] public GameLevelSource Source
// ReSharper disable once InconsistentNaming
public IList<GameSkillReward> _SkillRewards { get; }

public IList<GameReview> Reviews { get; }

#nullable restore

[XmlArray("customRewards")]
Expand Down
13 changes: 13 additions & 0 deletions Refresh.GameServer/Types/Levels/GameLevelSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,17 @@ public enum GameLevelSource
/// A level created by the server to represent a game story level.
/// </summary>
Story,
}

public static class GameLevelSourceExtensions
{
public static string ToGameType(this GameLevelSource source)
{
return source switch
{
GameLevelSource.User => "user",
GameLevelSource.Story => "developer",
_ => throw new ArgumentOutOfRangeException(nameof(source), source, null),
};
}
}
5 changes: 5 additions & 0 deletions Refresh.GameServer/Types/Levels/GameMinimalLevelResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public class GameMinimalLevelResponse : IDataConvertableFrom<GameMinimalLevelRes
[XmlElement("yourDPadRating")] public int YourRating { get; set; }

[XmlElement("playerCount")] public int PlayerCount { get; set; }
[XmlElement("reviewsEnabled")] public bool ReviewsEnabled { get; set; } = true;
[XmlElement("reviewCount")] public int ReviewCount { get; set; } = 0;
[XmlElement("commentsEnabled")] public bool CommentsEnabled { get; set; } = true;
[XmlElement("commentCount")] public int CommentCount { get; set; } = 0;

[XmlElement("initiallyLocked")] public bool IsLocked { get; set; }
[XmlElement("isSubLevel")] public bool IsSubLevel { get; set; }
Expand Down Expand Up @@ -97,6 +101,7 @@ private GameMinimalLevelResponse() {}
YourStarRating = level.YourStarRating,
YourRating = level.YourRating,
AverageStarRating = level.AverageStarRating,
CommentCount = level.CommentCount,
IsLocked = level.IsLocked,
IsSubLevel = level.IsSubLevel,
IsCopyable = level.IsCopyable,
Expand Down
3 changes: 2 additions & 1 deletion Refresh.GameServer/Types/Lists/SerializedList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ public abstract class SerializedList<TItem>
[XmlAttribute("total")]
public int Total { get; set; }

[XmlAttribute("hint_start")] public int NextPageStart { get; set; }
[XmlAttribute("hint_start")]
public int NextPageStart { get; set; }

[XmlIgnore]
public abstract List<TItem> Items { get; set; }
Expand Down
Loading
Loading