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

Implement level tagging #585

Merged
merged 2 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
19 changes: 19 additions & 0 deletions Refresh.GameServer/Database/GameDatabaseContext.Levels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ public void DeleteLevel(GameLevel level)
this.QueueLevelRelations.RemoveRange(r => r.Level == level);
this.RateLevelRelations.RemoveRange(r => r.Level == level);
this.UniquePlayLevelRelations.RemoveRange(r => r.Level == level);
this.TagLevelRelations.RemoveRange(r => r.Level == level);

IQueryable<GameSubmittedScore> scores = this.GameSubmittedScores.Where(r => r.Level == level);

Expand Down Expand Up @@ -241,6 +242,24 @@ public DatabaseList<GameLevel> GetMostHeartedLevels(int count, int skip, GameUse
return new DatabaseList<GameLevel>(mostHeartedLevels, skip, count);
}

[Pure]
public DatabaseList<GameLevel> GetLevelsByTag(int count, int skip, GameUser? user, Tag tag, LevelFilterSettings levelFilterSettings)
{
IQueryable<TagLevelRelation> tagRelations = this.TagLevelRelations;

IEnumerable<GameLevel> filteredTaggedLevels = tagRelations
.Where(x => x._Tag == (int)tag)
.AsEnumerable()
.Select(x => x.Level)
.Distinct()
.Where(l => l._Source == (int)GameLevelSource.User)
.OrderByDescending(l => l.PublishDate)
.FilterByLevelFilterSettings(user, levelFilterSettings)
.FilterByGameVersion(levelFilterSettings.GameVersion);

return new DatabaseList<GameLevel>(filteredTaggedLevels, skip, count);
}

[Pure]
public DatabaseList<GameLevel> GetMostUniquelyPlayedLevels(int count, int skip, GameUser? user, LevelFilterSettings levelFilterSettings)
{
Expand Down
33 changes: 32 additions & 1 deletion Refresh.GameServer/Database/GameDatabaseContext.Relations.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Diagnostics.Contracts;
using Realms;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Endpoints.Game.Levels.FilterSettings;
using Refresh.GameServer.Extensions;
Expand Down Expand Up @@ -493,4 +492,36 @@ public bool RateLevelComment(GameUser user, GameLevelComment comment, RatingType
=> this.RateComment(user, comment, ratingType, this.LevelCommentRelations);

#endregion

#region Tags

public void AddTagRelation(GameUser user, GameLevel level, Tag tag)
{
this.Write(() =>
{
// Remove any old tags from this user on this level
this.TagLevelRelations.RemoveRange(this.TagLevelRelations.Where(t => t.User == user && t.Level == level));

this.TagLevelRelations.Add(new TagLevelRelation
{
Tag = tag,
User = user,
Level = level,
});
});
}

public IEnumerable<TagLevelRelation> GetTagsForLevel(GameLevel level)
{
IQueryable<TagLevelRelation> levelTags = this.TagLevelRelations.Where(t => t.Level == level);

IOrderedEnumerable<TagLevelRelation> tags = levelTags
.AsEnumerable()
.DistinctBy(t => t._Tag)
.OrderByDescending(t => levelTags.Count(levelTag => levelTag._Tag == t._Tag));

return tags;
}

#endregion
}
1 change: 1 addition & 0 deletions Refresh.GameServer/Database/GameDatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public partial class GameDatabaseContext : RealmDatabaseContext
private RealmDbSet<GameReview> GameReviews => new(this._realm);
private RealmDbSet<DisallowedUser> DisallowedUsers => new(this._realm);
private RealmDbSet<RateReviewRelation> RateReviewRelations => new(this._realm);
private RealmDbSet<TagLevelRelation> TagLevelRelations => new(this._realm);

internal GameDatabaseContext(IDateTimeProvider time)
{
Expand Down
1 change: 1 addition & 0 deletions Refresh.GameServer/Database/GameDatabaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ protected GameDatabaseProvider(IDateTimeProvider time)
typeof(GameReview),
typeof(DisallowedUser),
typeof(RateReviewRelation),
typeof(TagLevelRelation),
jvyden marked this conversation as resolved.
Show resolved Hide resolved
};

public override void Warmup()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public class ApiGameLevelResponse : IApiResponse, IDataConvertableFrom<ApiGameLe
public required bool IsSubLevel { get; set; }
public required bool IsCopyable { get; set; }
public required float Score { get; set; }
public required IEnumerable<Tag> Tags { get; set; }

public static ApiGameLevelResponse? FromOld(GameLevel? level, DataContext dataContext)
{
Expand Down Expand Up @@ -86,6 +87,7 @@ public class ApiGameLevelResponse : IApiResponse, IDataConvertableFrom<ApiGameLe
PhotosTaken = dataContext.Database.GetTotalPhotosInLevel(level),
LevelComments = dataContext.Database.GetTotalCommentsForLevel(level),
Reviews = dataContext.Database.GetTotalReviewsForLevel(level),
Tags = dataContext.Database.GetTagsForLevel(level).Select(t => t.Tag),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public class GameLevelResponse : IDataConvertableFrom<GameLevelResponse, GameLev
[XmlElement("reviewsEnabled")] public bool ReviewsEnabled { get; set; } = true;
[XmlElement("commentCount")] public int CommentCount { get; set; } = 0;
[XmlElement("commentsEnabled")] public bool CommentsEnabled { get; set; } = true;
[XmlElement("tags")] public string Tags { get; set; } = "";

/// <summary>
/// Provides a unique level ID for ~1.1 billion hashed levels, uses the hash directly, so this is deterministic
Expand Down Expand Up @@ -174,6 +175,7 @@ public static GameLevelResponse FromHash(string hash, DataContext dataContext)
AverageStarRating = old.CalculateAverageStarRating(dataContext.Database),
ReviewCount = old.Reviews.Count,
CommentCount = dataContext.Database.GetTotalCommentsForLevel(old),
Tags = string.Join(',', dataContext.Database.GetTagsForLevel(old).Select(t => t.Tag.ToLbpString())) ,
};

response.Type = "user";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Xml.Serialization;
using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Bunkum.Core.Endpoints.Debugging;
Expand All @@ -7,8 +6,8 @@
using Bunkum.Protocols.Http;
using Refresh.GameServer.Database;
using Refresh.GameServer.Time;
using Refresh.GameServer.Types;
using Refresh.GameServer.Types.Challenges;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.Roles;
using Refresh.GameServer.Types.UserData;

Expand Down Expand Up @@ -191,4 +190,7 @@ public SerializedGameChallengeList ChallengeConfig(RequestContext context, IDate
Challenges = [],
};
}

[GameEndpoint("tags")]
public string Tags(RequestContext context) => TagExtensions.AllTags;
}
23 changes: 23 additions & 0 deletions Refresh.GameServer/Endpoints/Game/RelationEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,27 @@ public Response ClearQueue(RequestContext context, GameDatabaseContext database,
database.ClearQueue(user);
return OK;
}

[GameEndpoint("tag/{slotType}/{id}", HttpMethods.Post)]
public Response SubmitTagsForLevel(RequestContext context, GameDatabaseContext database, GameUser user, string slotType, int id, string body)
{
GameLevel? level = database.GetLevelByIdAndType(slotType, id);

if (level == null)
return NotFound;

// The format of the POST body is `t=TAG_Name`, so assert this is followed
if (!body.StartsWith("t="))
return BadRequest;

Tag? tag = TagExtensions.FromLbpString(body[2..]);

// If it was an invalid tag, return BadRequest
if (tag == null)
return BadRequest;

database.AddTagRelation(user, level, tag.Value);

return OK;
}
}
37 changes: 37 additions & 0 deletions Refresh.GameServer/Types/Levels/Categories/ByTagCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Bunkum.Core;
using Refresh.GameServer.Database;
using Refresh.GameServer.Endpoints.Game.Levels.FilterSettings;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.UserData;

namespace Refresh.GameServer.Types.Levels.Categories;

public class ByTagCategory : LevelCategory
{
internal ByTagCategory() : base("tag", "tag", false)
{
// Technically this category can apply to any user, but since we fallback to the regular user this name & description still applies
this.Name = "Tag Search";
this.Description = "Search for levels using tags given by users like you!";
this.IconHash = "g820605";
this.FontAwesomeIcon = "tag";
this.Hidden = true; // The by-tag category is not meant to be shown, as it requires a special implementation on all frontends
}

public override DatabaseList<GameLevel>? Fetch(RequestContext context, int skip, int count,
MatchService matchService, GameDatabaseContext database, GameUser? accessor,
LevelFilterSettings levelFilterSettings, GameUser? user)
{
string? tagStr = context.QueryString["tag"];

if (tagStr == null)
return null;

Tag? tag = TagExtensions.FromLbpString(tagStr);

if (tag == null)
return null;

return database.GetLevelsByTag(count, skip, user, tag.Value, levelFilterSettings);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class CategoryService : EndpointService
new QueuedLevelsByUserCategory(),

new SearchLevelCategory(),
new ByTagCategory(),
new DeveloperLevelsCategory(),
new ContestCategory(),
];
Expand Down
4 changes: 3 additions & 1 deletion Refresh.GameServer/Types/Levels/GameMinimalLevelResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public class GameMinimalLevelResponse : IDataConvertableFrom<GameMinimalLevelRes
[XmlElement("initiallyLocked")] public bool IsLocked { get; set; }
[XmlElement("isSubLevel")] public bool IsSubLevel { get; set; }
[XmlElement("shareable")] public int IsCopyable { get; set; }

[XmlElement("tags")] public string Tags { get; set; } = "";

private GameMinimalLevelResponse() {}

/// <summary>
Expand Down Expand Up @@ -99,6 +100,7 @@ public static GameMinimalLevelResponse FromHash(string hash, DataContext dataCon
IsSubLevel = level.IsSubLevel,
IsCopyable = level.IsCopyable,
PlayerCount = dataContext.Match.GetPlayerCountForLevel(RoomSlotType.Online, level.LevelId),
Tags = level.Tags,
};
}

Expand Down
Loading
Loading