-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
You can only create playlists in-game when latest retail LBP1 has the memory address `0x0074c2c0` patched to be `0x01` instead of `0x00`. In the EBOOT this is located at address `0x0073c2c0`. Updates to Refresher and the OFW patchers will need to be made to enable this feature. For testing you also need to make sure that the `/lbp/network_settings.nws` endpoint is being accessed correctly, since playlists require the `AlexDB` option to be set to function. Luckily the game is perfectly happy viewing and playing other's playlists even if you don't have the feature enabled yourself. So there's no worry about un-patched users having issues. I would like to tackle APIv3 support for playlists in a future PR, but aside from that, this is a complete implementation of the feature for LBP1. It should be possible to represent this (at least close enough) in LBP3 by just not showing sub-playlists, and showing all playlists by a user, instead of just playlists inside of your root playlist. For LBP2/PSP/Vita we can just display playlists as a slot results override you trigger from the API, or as a special cased `playlist:name` in text search, although we won't be able to display sub-playlists there either. As a basic technical summary of how this is layed out, all players contain a "Root Playlist". In LBP1, when you view your "My Playlists" view, it will create a root playlist for you, if you don't have one. All playlists you create in the future are then considered "sub-playlists" of that root playlist, or of some other playlist. The game allows you to add any playlist (eg. other's playlists aswell) as a sub-playlist to any of your playlists (except your root playlist). And it also allows you to add any `user` slot to any of your playlists (except your root playlist), as a sub-level of the playlist. The original developer's intention for viewing a user's playlists is that there is a button on the profile labeled "Playlists", and it would show the contents of the root playlist, however I found this to be rather uncreative, so in LBP1 I've injected the contents of your root playlist into your user level list as "sticky" slot results, so it always appears regardless of the page. This seems to work fairly well, and doesn't impact pagination from what I can see.
- Loading branch information
Showing
27 changed files
with
956 additions
and
137 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
Refresh.GameServer/Database/GameDatabaseContext.Playlists.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
using Refresh.GameServer.Authentication; | ||
using Refresh.GameServer.Extensions; | ||
using Refresh.GameServer.Types.Levels; | ||
using Refresh.GameServer.Types.Playlists; | ||
using Refresh.GameServer.Types.UserData; | ||
|
||
namespace Refresh.GameServer.Database; | ||
|
||
public partial class GameDatabaseContext // Playlists | ||
{ | ||
public GamePlaylist CreatePlaylist(GameUser user, SerializedPlaylist 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(() => | ||
{ | ||
this.AddSequentialObject(playlist); | ||
}); | ||
|
||
return playlist; | ||
} | ||
|
||
public GamePlaylist? GetPlaylistById(int playlistId) | ||
=> this.GamePlaylists.FirstOrDefault(p => p.PlaylistId == playlistId); | ||
|
||
public void UpdatePlaylist(GamePlaylist playlist, SerializedPlaylist updateInfo) | ||
{ | ||
this.Write(() => | ||
{ | ||
playlist.Name = updateInfo.Name; | ||
playlist.Description = updateInfo.Description; | ||
playlist.IconHash = updateInfo.Icon; | ||
playlist.LocationX = updateInfo.Location.X; | ||
playlist.LocationY = updateInfo.Location.Y; | ||
}); | ||
} | ||
|
||
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); | ||
|
||
// Remove the playlist object | ||
this.GamePlaylists.Remove(playlist); | ||
}); | ||
} | ||
|
||
public void AddPlaylistToPlaylist(GamePlaylist child, GamePlaylist parent) | ||
{ | ||
this.Write(() => | ||
{ | ||
// Make sure to not create a duplicate object | ||
if (this.SubPlaylistRelations.Any(p => p.SubPlaylist == child && p.Playlist == parent)) | ||
return; | ||
|
||
// Add the relation | ||
this.SubPlaylistRelations.Add(new SubPlaylistRelation | ||
{ | ||
Playlist = parent, | ||
SubPlaylist = child, | ||
}); | ||
}); | ||
} | ||
|
||
public void RemovePlaylistFromPlaylist(GamePlaylist child, GamePlaylist parent) | ||
{ | ||
this.Write(() => | ||
{ | ||
SubPlaylistRelation? relation = | ||
this.SubPlaylistRelations.FirstOrDefault(r => r.SubPlaylist == child && r.Playlist == parent); | ||
|
||
if (relation == null) | ||
return; | ||
|
||
this.SubPlaylistRelations.Remove(relation); | ||
}); | ||
} | ||
|
||
public void AddLevelToPlaylist(GameLevel level, GamePlaylist parent) | ||
{ | ||
this.Write(() => | ||
{ | ||
// Make sure to not create a duplicate object | ||
if (this.LevelPlaylistRelations.Any(p => p.Level == level && p.Playlist == parent)) | ||
return; | ||
|
||
// Add the relation | ||
this.LevelPlaylistRelations.Add(new LevelPlaylistRelation | ||
{ | ||
Level = level, | ||
Playlist = parent, | ||
}); | ||
}); | ||
} | ||
|
||
public void RemoveLevelFromPlaylist(GameLevel level, GamePlaylist parent) | ||
{ | ||
this.Write(() => | ||
{ | ||
LevelPlaylistRelation? relation = | ||
this.LevelPlaylistRelations.FirstOrDefault(r => r.Level == level && r.Playlist == parent); | ||
|
||
if (relation == null) | ||
return; | ||
|
||
this.LevelPlaylistRelations.Remove(relation); | ||
}); | ||
} | ||
|
||
public IEnumerable<GamePlaylist> GetPlaylistsContainingPlaylist(GamePlaylist playlist) | ||
// TODO: with postgres this can be IQueryable | ||
=> this.SubPlaylistRelations.Where(p => p.SubPlaylist == playlist).AsEnumerable() | ||
.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() | ||
.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) => | ||
// TODO: When we have postgres, remove the `AsEnumerable` call for performance. | ||
this.LevelPlaylistRelations.Where(l => l.Playlist == playlist).AsEnumerable() | ||
.Select(l => l.Level) | ||
.FilterByGameVersion(game); | ||
|
||
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() | ||
.Select(l => l.SubPlaylist); | ||
|
||
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() | ||
.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() | ||
.Select(r => this.GamePlaylists.First(p => p.PlaylistId == r.Playlist.PlaylistId)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.