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

LBP3 Playlists #702

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
79b4ef0
WIP LBP3 Playlist support
Toastbrot236 Nov 27, 2024
e3c8ef4
Rename lbp1 playlist related things to be explicitly lbp1 related to …
Toastbrot236 Nov 29, 2024
c2494d8
bug fixes, 1 more endpoint, some more experiments, some other things
Toastbrot236 Nov 29, 2024
8f7c0a9
bug fixes, a refactor, some other improvements
Toastbrot236 Dec 13, 2024
eae06e0
Refactor some code for more reusability and to replace a hack for lbp…
Toastbrot236 Dec 14, 2024
5a0d739
dont fail to send multiple levels to game completely if atleast one l…
Toastbrot236 Dec 14, 2024
2a2e1f9
Let lbp3 playlists immediately update ingame, remove some no longer n…
Toastbrot236 Dec 14, 2024
36e05d0
nvm, let people reset their playlist name and description in lbp3
Toastbrot236 Dec 14, 2024
d50ce49
Remove some debugging, fixup my code style, slightly easier differenc…
Toastbrot236 Dec 14, 2024
bd67093
Differentiate further between lbp1 and lbp3 specific playlist classes…
Toastbrot236 Dec 14, 2024
b3adc15
Implement custom order for levels in playlists, add some comments
Toastbrot236 Dec 15, 2024
8c85c91
Reword a comment
Toastbrot236 Dec 15, 2024
061a058
Correct another one of my comments, use dedicated method for getting …
Toastbrot236 Dec 15, 2024
966641d
More accurate playlist timestamps (set before write), move foreach in…
Toastbrot236 Dec 16, 2024
6c0c4ab
fore matting, add some comments
Toastbrot236 Dec 16, 2024
ef6f5f9
Reword more comments, always sort levels in playlists, more fore matting
Toastbrot236 Jan 10, 2025
a1328af
Fix some ways of creating recursive playlists not being caught, fix o…
Toastbrot236 Jan 11, 2025
dbdc2a9
Fix that test, sort playlists and playlist levels
Toastbrot236 Jan 11, 2025
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
164 changes: 139 additions & 25 deletions Refresh.GameServer/Database/GameDatabaseContext.Playlists.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,48 @@
using Refresh.GameServer.Extensions;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.Playlists;
using Refresh.GameServer.Types.Relations;
using Refresh.GameServer.Types.UserData;

namespace Refresh.GameServer.Database;

public partial class GameDatabaseContext // Playlists
{
public GamePlaylist CreatePlaylist(GameUser user, SerializedPlaylist createInfo, bool rootPlaylist)
public GamePlaylist CreatePlaylist(GameUser user, SerializedLbp1Playlist createInfo, bool rootPlaylist)
{
GamePlaylist playlist = new()
{
Publisher = user,
Name = createInfo.Name,
Description = createInfo.Description,
IconHash = createInfo.Icon,
LocationX = createInfo.Location.X,
LocationY = createInfo.Location.Y,
IsRoot = rootPlaylist,
};

this.Write(() =>
GamePlaylist playlist = GamePlaylist.ToGamePlaylist(createInfo, user, rootPlaylist);
this.CreatePlaylist(playlist);
return playlist;
}

public GamePlaylist CreatePlaylist(GameUser user, SerializedLbp3Playlist createInfo, bool rootPlaylist)
{
GamePlaylist playlist = GamePlaylist.ToGamePlaylist(createInfo, user, rootPlaylist);
this.CreatePlaylist(playlist);
return playlist;
}

public void CreatePlaylist(GamePlaylist createInfo)
{
DateTimeOffset now = this._time.Now;

this.AddSequentialObject(createInfo, () =>
{
this.AddSequentialObject(playlist);
createInfo.CreationDate = now;
createInfo.LastUpdateDate = now;
});

return playlist;
}

public GamePlaylist? GetPlaylistById(int playlistId)
=> this.GamePlaylists.FirstOrDefault(p => p.PlaylistId == playlistId);

public void UpdatePlaylist(GamePlaylist playlist, SerializedPlaylist updateInfo)
public void UpdatePlaylist(GamePlaylist playlist, SerializedLbp1Playlist updateInfo)
{
DateTimeOffset now = this._time.Now;

this.Write(() =>
{
playlist.LastUpdateDate = now;
playlist.Name = updateInfo.Name;
playlist.Description = updateInfo.Description;
playlist.IconHash = updateInfo.Icon;
Expand All @@ -44,13 +52,26 @@ public void UpdatePlaylist(GamePlaylist playlist, SerializedPlaylist updateInfo)
});
}

public void UpdatePlaylist(GamePlaylist playlist, SerializedLbp3Playlist updateInfo)
{
DateTimeOffset now = this._time.Now;

this.Write(() =>
{
playlist.LastUpdateDate = now;
if (updateInfo.Name != null) playlist.Name = updateInfo.Name;
if (updateInfo.Description != null) playlist.Description = updateInfo.Description;
});
}

public void DeletePlaylist(GamePlaylist playlist)
{
this.Write(() =>
{
// Remove all relations relating to this playlist
this.LevelPlaylistRelations.RemoveRange(l => l.Playlist == playlist);
this.SubPlaylistRelations.RemoveRange(l => l.Playlist == playlist || l.SubPlaylist == playlist);
this.FavouritePlaylistRelations.RemoveRange(l => l.Playlist == playlist);

// Remove the playlist object
this.GamePlaylists.Remove(playlist);
Expand Down Expand Up @@ -101,56 +122,149 @@ public void AddLevelToPlaylist(GameLevel level, GamePlaylist parent)
{
Level = level,
Playlist = parent,
// index of new relation = index of last relation + 1 = relation count (without new relation)
Index = this.GetTotalLevelsInPlaylistCount(parent),
});
});
}

public void RemoveLevelFromPlaylist(GameLevel level, GamePlaylist parent)
{
LevelPlaylistRelation? relation =
this.LevelPlaylistRelations.FirstOrDefault(r => r.Level == level && r.Playlist == parent);

if (relation == null)
return;

// decrease index of every playlist level after this one by 1
this.DecreasePlaylistLevelIndicesAfterIndex(parent, relation.Index);

this.Write(() =>
{
LevelPlaylistRelation? relation =
this.LevelPlaylistRelations.FirstOrDefault(r => r.Level == level && r.Playlist == parent);

if (relation == null)
return;

this.LevelPlaylistRelations.Remove(relation);
});
}

private void DecreasePlaylistLevelIndicesAfterIndex(GamePlaylist playlist, int index)
{
IEnumerable<LevelPlaylistRelation> relations = this.LevelPlaylistRelations
.Where(r => r.Playlist == playlist && r.Index >= index)
.AsEnumerable();

this.Write(() => {
foreach(LevelPlaylistRelation relation in relations)
{
relation.Index--;
}
});
}

public void SetPlaylistLevelIndex(GamePlaylist playlist, GameLevel level, int newIndex)
{
LevelPlaylistRelation relation = this.LevelPlaylistRelations
.First(r => r.Playlist == playlist && r.Level == level);

this.Write(() => {
relation.Index = newIndex;
});
}

public IEnumerable<GamePlaylist> GetPlaylistsContainingPlaylist(GamePlaylist playlist)
// TODO: with postgres this can be IQueryable
=> this.SubPlaylistRelations.Where(p => p.SubPlaylist == playlist).AsEnumerable()
.Reverse()
.Select(r => this.GamePlaylists.First(p => p.PlaylistId == r.Playlist.PlaylistId))
.Where(p => !p.IsRoot);

public IEnumerable<GamePlaylist> GetPlaylistsByAuthorContainingPlaylist(GameUser user, GamePlaylist playlist)
// TODO: with postgres this can be IQueryable
=> this.SubPlaylistRelations.Where(p => p.SubPlaylist == playlist).AsEnumerable()
.Reverse()
.Select(r => this.GamePlaylists.First(p => p.PlaylistId == r.Playlist.PlaylistId))
.Where(p => p.Publisher.UserId == user.UserId)
.Where(p => !p.IsRoot);

public IEnumerable<GameLevel> GetLevelsInPlaylist(GamePlaylist playlist, TokenGame game) =>
public IEnumerable<GameLevel> GetLevelsInPlaylist(GamePlaylist playlist, TokenGame game)
// TODO: When we have postgres, remove the `AsEnumerable` call for performance.
this.LevelPlaylistRelations.Where(l => l.Playlist == playlist).AsEnumerable()
=> this.LevelPlaylistRelations
.Where(l => l.Playlist == playlist)
.OrderBy(r => r.Index)
.AsEnumerable()
.Select(l => l.Level)
.FilterByGameVersion(game);

public int GetTotalLevelsInPlaylistCount(GamePlaylist playlist, TokenGame game) =>
this.LevelPlaylistRelations.Where(l => l.Playlist == playlist).AsEnumerable()
.Select(l => l.Level)
.FilterByGameVersion(game)
.Count();

public int GetTotalLevelsInPlaylistCount(GamePlaylist playlist) =>
this.LevelPlaylistRelations.Where(l => l.Playlist == playlist).AsEnumerable()
.Select(l => l.Level)
.Count();

public IEnumerable<GamePlaylist> GetPlaylistsInPlaylist(GamePlaylist playlist)
// TODO: When we have postgres, remove the `AsEnumerable` call for performance.
=> this.SubPlaylistRelations.Where(p => p.Playlist == playlist).AsEnumerable()
.Reverse()
.Select(l => l.SubPlaylist);

public IEnumerable<GamePlaylist> GetPlaylistsByAuthor(GameUser author)
// TODO: When we have postgres, remove the `AsEnumerable` call for performance.
=> this.GamePlaylists.Where(p => p.Publisher == author).AsEnumerable()
.Where(p => !p.IsRoot)
.OrderByDescending(p => p.LastUpdateDate);

public IEnumerable<GamePlaylist> GetPlaylistsByAuthorContainingLevel(GameUser author, GameLevel level)
// TODO: When we have postgres, remove the `AsEnumerable` call for performance.
=> this.LevelPlaylistRelations.Where(p => p.Level == level).AsEnumerable()
.Reverse()
.Select(r => this.GamePlaylists.First(p => p.PlaylistId == r.Playlist.PlaylistId))
.Where(p => p.Publisher.UserId == author.UserId);

public IEnumerable<GamePlaylist> GetPlaylistsContainingLevel(GameLevel level)
// TODO: When we have postgres, remove the `AsEnumerable` call for performance.
=> this.LevelPlaylistRelations.Where(p => p.Level == level).AsEnumerable()
.Reverse()
.Select(r => this.GamePlaylists.First(p => p.PlaylistId == r.Playlist.PlaylistId));

public int GetFavouriteCountForPlaylist(GamePlaylist playlist)
=> this.FavouritePlaylistRelations
.Count(r => r.Playlist == playlist);

public bool IsPlaylistFavouritedByUser(GamePlaylist playlist, GameUser user)
=> this.FavouritePlaylistRelations.FirstOrDefault(r => r.Playlist == playlist && r.User == user) != null;

public IEnumerable<GamePlaylist> GetPlaylistsFavouritedByUser(GameUser user)
// TODO: When we have postgres, remove the `AsEnumerable` call for performance.
=> this.FavouritePlaylistRelations.Where(r => r.User == user).AsEnumerable()
.Reverse()
.Select(r => r.Playlist);

public bool FavouritePlaylist(GamePlaylist playlist, GameUser user)
{
if (this.IsPlaylistFavouritedByUser(playlist, user)) return false;

FavouritePlaylistRelation relation = new()
{
Playlist = playlist,
User = user,
};
this.Write(() => this.FavouritePlaylistRelations.Add(relation));

return true;
}

public bool UnfavouritePlaylist(GamePlaylist playlist, GameUser user)
{
FavouritePlaylistRelation? relation = this.FavouritePlaylistRelations
.FirstOrDefault(r => r.Playlist == playlist && r.User == user);

if (relation == null) return false;

this.Write(() => this.FavouritePlaylistRelations.Remove(relation));

return true;
}
}
1 change: 1 addition & 0 deletions Refresh.GameServer/Database/GameDatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public partial class GameDatabaseContext : RealmDatabaseContext
private RealmDbSet<GamePlaylist> GamePlaylists => new(this._realm);
private RealmDbSet<LevelPlaylistRelation> LevelPlaylistRelations => new(this._realm);
private RealmDbSet<SubPlaylistRelation> SubPlaylistRelations => new(this._realm);
private RealmDbSet<FavouritePlaylistRelation> FavouritePlaylistRelations => new(this._realm);
private RealmDbSet<GameUserVerifiedIpRelation> GameUserVerifiedIpRelations => new(this._realm);

internal GameDatabaseContext(IDateTimeProvider time)
Expand Down
20 changes: 18 additions & 2 deletions Refresh.GameServer/Database/GameDatabaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ protected GameDatabaseProvider(IDateTimeProvider time)
this._time = time;
}

protected override ulong SchemaVersion => 161;
protected override ulong SchemaVersion => 163;

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

Expand Down Expand Up @@ -87,6 +87,7 @@ protected GameDatabaseProvider(IDateTimeProvider time)
typeof(GamePlaylist),
typeof(LevelPlaylistRelation),
typeof(SubPlaylistRelation),
typeof(FavouritePlaylistRelation)
];

public override void Warmup()
Expand Down Expand Up @@ -686,7 +687,7 @@ protected override void Migrate(Migration migration, ulong oldVersion)
// We weren't deleting level playlist relations when a level was deleted. Version 160 fixes this.
if (oldVersion < 160)
migration.NewRealm.RemoveRange(migration.NewRealm.All<LevelPlaylistRelation>().Where(r => r.Level == null));

// IQueryable<dynamic>? oldLevelPlaylistRelations = migration.OldRealm.DynamicApi.All("LevelPlaylistRelation");
// IQueryable<LevelPlaylistRelation>? newLevelPlaylistRelations = migration.NewRealm.All<LevelPlaylistRelation>();
// if (oldVersion < 155)
Expand All @@ -704,5 +705,20 @@ protected override void Migrate(Migration migration, ulong oldVersion)
// dynamic oldSubPlaylistRelation = oldSubPlaylistRelations.ElementAt(i);
// SubPlaylistRelation newSubPlaylistRelation = newSubPlaylistRelations.ElementAt(i);
// }

// Version 163 added indices for LevelPlaylistRelations for custom playlist level order in LBP3
IQueryable<dynamic>? oldLevelPlaylistRelations = migration.OldRealm.DynamicApi.All("LevelPlaylistRelation");
IQueryable<LevelPlaylistRelation>? newLevelPlaylistRelations = migration.NewRealm.All<LevelPlaylistRelation>();
if (oldVersion < 163)
for (int i = 0; i < newLevelPlaylistRelations.Count(); i++)
{
dynamic oldLevelPlaylistRelation = oldLevelPlaylistRelations.ElementAt(i);
LevelPlaylistRelation newLevelPlaylistRelation = newLevelPlaylistRelations.ElementAt(i);

if (oldVersion < 163)
{
newLevelPlaylistRelation.Index = 0;
}
}
}
}
6 changes: 5 additions & 1 deletion Refresh.GameServer/Endpoints/Game/Levels/LevelEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,11 @@ public class LevelEndpoints : EndpointGroup

foreach (string levelIdStr in levelIds)
{
if (!int.TryParse(levelIdStr, out int levelId)) return null;
// Sometimes, in playlists for example, LBP3 refers to developer levels by taking their level (not story) id
// and prepending a 'd' to it.
// We need to remove it in order to be able to parse the id and get the level.
// If parsing fails anyway, skip over the level id and continue with the next one.
if (!int.TryParse(levelIdStr.StartsWith('d') ? levelIdStr[1..] : levelIdStr, out int levelId)) continue;
GameLevel? level = database.GetLevelById(levelId);

if (level == null) continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@

namespace Refresh.GameServer.Endpoints.Game.Playlists;

public class PlaylistEndpoints : EndpointGroup
public class PlaylistLbp1Endpoints : EndpointGroup
{
// Creates a playlist, with an optional parent ID
[GameEndpoint("createPlaylist", HttpMethods.Post, ContentType.Xml)]
[RequireEmailVerified]
public Response CreatePlaylist(RequestContext context, DataContext dataContext, SerializedPlaylist body)
public Response CreatePlaylist(RequestContext context, DataContext dataContext, SerializedLbp1Playlist body)
{
GameUser user = dataContext.User!;

Expand Down Expand Up @@ -55,7 +55,7 @@ public Response CreatePlaylist(RequestContext context, DataContext dataContext,
dataContext.Database.SetUserRootPlaylist(user, playlist);

// Create the new playlist, returning the data
return new Response(SerializedPlaylist.FromOld(playlist, dataContext), ContentType.Xml);
return new Response(SerializedLbp1Playlist.FromOld(playlist, dataContext), ContentType.Xml);
}

// Gets the slots contained within a playlist
Expand Down Expand Up @@ -147,7 +147,7 @@ public Response CreatePlaylist(RequestContext context, DataContext dataContext,

[GameEndpoint("setPlaylistMetaData/{id}", HttpMethods.Post, ContentType.Xml)]
[RequireEmailVerified]
public Response UpdatePlaylistMetadata(RequestContext context, GameDatabaseContext database, GameUser user, int id, SerializedPlaylist body)
public Response UpdatePlaylistMetadata(RequestContext context, GameDatabaseContext database, GameUser user, int id, SerializedLbp1Playlist body)
{
GamePlaylist? playlist = database.GetPlaylistById(id);
if (playlist == null)
Expand Down Expand Up @@ -211,20 +211,34 @@ public Response AddSlotToPlaylist(RequestContext context, GameDatabaseContext da
if (childPlaylist.PlaylistId == parentPlaylist.PlaylistId)
return BadRequest;

// Add the child playlist to the parent before checking for recursive playlists below to catch cases where a loop would
// only happen once the child playlist is added to its new parent
database.AddPlaylistToPlaylist(childPlaylist, parentPlaylist);

// If the parent contains the child in its parent tree, block the request to prevent recursive playlists
// This would be a `BadRequest`, but the game has a bug and will do this when creating sub-playlists,
// so lets not upset it and just return OK, I dont expect this to be a common problem for people to run into.
bool recursive = false;
IEnumerable<GamePlaylist> traversedPlaylists = [childPlaylist];
parentPlaylist.TraverseParentsRecursively(database, delegate(GamePlaylist playlist)
{
if (playlist.PlaylistId == childPlaylist.PlaylistId)
recursive = true;
// If we have already traversed this playlist before, we have found a loop. Stop traversing in that case.
if (traversedPlaylists.Contains(playlist))
{
recursive = true;
return false;
}
else
{
// Remember this playlist for loop detection
traversedPlaylists = traversedPlaylists.Append(playlist);
return true;
}
});
if (recursive) return OK;
if (recursive) {
// If adding this playlist to its parent has caused a loop which was not there before, remove it from its parent
database.RemovePlaylistFromPlaylist(childPlaylist, parentPlaylist);
return BadRequest;
}

// Add the playlist to the parent
database.AddPlaylistToPlaylist(childPlaylist, parentPlaylist);

// ReSharper disable once ExtractCommonBranchingCode see like 3 lines below (line count subject to change)
return OK;
}
Expand Down
Loading
Loading