Skip to content

Added 3 New LBP3 Categories - #1109

Open
W0lf4llo wants to merge 14 commits into
LBPUnion:mainfrom
W0lf4llo:feature/lbp3-recently-played
Open

Added 3 New LBP3 Categories#1109
W0lf4llo wants to merge 14 commits into
LBPUnion:mainfrom
W0lf4llo:feature/lbp3-recently-played

Conversation

@W0lf4llo

Copy link
Copy Markdown
Contributor

I added support for LBP3's recently played category.

Essentially, LBP2 tracks which levels you've played client side, and just reports it back whenever you click on "Recently Played". LBP3 on the original servers would handle all of this server sided. This essentially works by recording the recently played community levels via the games "gameState" request, then it stores the recently played slot ID's and the timestamps per each user. I set a limit so that it keeps a maximum of 20 recently played levels, when you replay a level that was already on the list, it changes the timestamp to have it re-appear at the top of the list. The game also only applies to users who are playing on LBP3. If you play LBP1 or LBP2, it will not change this list whatsoever.

image

@W0lf4llo W0lf4llo changed the title Added Recently Played support for LittleBigPlanet 3 Added Recently Played & Recommended For You to LBP3 Aug 13, 2026
@W0lf4llo

Copy link
Copy Markdown
Contributor Author

I wrote a fairly simple recommendation system for the server which is VERY similar to how Sony used to handle it. I left my comments in there so you can easily read what everything does and why its there. If anything else is needed to change in this PR please let me know! I just figured I'd stack it on top of this pr as i built this on top of my already existing work.

@Toastbrot236

Copy link
Copy Markdown

Is there a reason you're using /gameState instead of /play to find out what levels a user has played?

@W0lf4llo W0lf4llo changed the title Added Recently Played & Recommended For You to LBP3 Added 3 New LBP3 Categories Aug 13, 2026
@W0lf4llo

Copy link
Copy Markdown
Contributor Author

Also, I forgot to thank you on here for the recommendation, as using /play was a far better way of tracking when a user actually enters a community level. I appreciate it!

@W0lf4llo

W0lf4llo commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Also, this is my last commit to the PR unless an issue has been found.. which I'm sure there will 100% be issues, just feel free to let me know, and ask any questions if needed!

Increased the limit of recently played slots from 20 to 30.

@FeTetra FeTetra left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot more to review, but I'm going to leave this for now.

Comment thread ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs Outdated
Comment thread ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs Outdated
Comment thread ProjectLighthouse.Servers.GameServer/Types/Categories/NewestLevelsCategory.cs Outdated
Comment thread ProjectLighthouse.Servers.GameServer/Types/Categories/NewestLevelsCategory.cs Outdated
W0lf4llo and others added 5 commits August 24, 2026 20:32
…hController.cs

Co-authored-by: FeTetra <166051662+FeTetra@users.noreply.github.com>
…hController.cs

Co-authored-by: FeTetra <166051662+FeTetra@users.noreply.github.com>
…velsCategory.cs

Co-authored-by: FeTetra <166051662+FeTetra@users.noreply.github.com>
…velsCategory.cs

Co-authored-by: FeTetra <166051662+FeTetra@users.noreply.github.com>

@Slendy Slendy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your contribution!

Here are a few more things that should be addressed that aren't covered in the requested changes:

  • Comments should have a space between the two backslashes and the first character of the comment (//comment goes here should be // comment goes here)
  • Indentation of code and comments in certain places is off

This could honestly be split into separate PRs since from what I can tell the recently played and recommended categories have nothing to do with each other, but it's fine given how dead development is now.

{
RecentlyPlayedEntity? recentlyPlayed = await this.RecentlyPlayed.FirstOrDefaultAsync(r => r.UserId == userId);

long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long now = TimeHelper.TimestampMillis;

@@ -0,0 +1,1602 @@
// <auto-generated />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This entire file doesn't need to be included and can be deleted

Comment on lines +23 to +32
public bool ShouldSerializeGameFilter() => !string.IsNullOrWhiteSpace(this.GameFilter);

public bool ShouldSerializeDateFilterType() => !string.IsNullOrWhiteSpace(this.DateFilterType);

public bool ShouldSerializeIncludePlayed() => this.IncludePlayed.HasValue;

public bool ShouldSerializeTeamPicked() => !string.IsNullOrWhiteSpace(this.TeamPicked);

public bool ShouldSerializeBlacklisted() => !string.IsNullOrWhiteSpace(this.Blacklisted);
} No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a bunch of ShouldSerialize you should use

[DefaultValue("")]

on any nullable fields that you don't want to show up if they are empty. If the field is an object then you can use null instead of the empty double quotes

example:

    [DefaultValue("")]
    [XmlElement("gameFilter")]
    public string? GameFilter { get; set; }

There's a couple places that this should be fixed but I'm not going to comment on all of them for brevity.

Suggested change
public bool ShouldSerializeGameFilter() => !string.IsNullOrWhiteSpace(this.GameFilter);
public bool ShouldSerializeDateFilterType() => !string.IsNullOrWhiteSpace(this.DateFilterType);
public bool ShouldSerializeIncludePlayed() => this.IncludePlayed.HasValue;
public bool ShouldSerializeTeamPicked() => !string.IsNullOrWhiteSpace(this.TeamPicked);
public bool ShouldSerializeBlacklisted() => !string.IsNullOrWhiteSpace(this.Blacklisted);
}

return this.BadRequest();
}

if (token.GameVersion == GameVersion.LittleBigPlanet3 && slotType == "user")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slotType check isn't necessary because story levels are excluded via the check on line 36

Suggested change
if (token.GameVersion == GameVersion.LittleBigPlanet3 && slotType == "user")
if (token.GameVersion == GameVersion.LittleBigPlanet3)

}

public IQueryable<ScoredSlot> GetScoredItems(DatabaseContext database, GameTokenEntity token, SlotQueryBuilder queryBuilder)
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be refactored into 2 separate functions for actually calculating the user's search scores, and then fetching the levels based on the scores should take place here.


long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();

//Level at the top of the recently played category for the user

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment doesn't really make sense, is it supposed to go somewhere else?

Comment on lines +30 to +33
{
Slot = s,
ThumbsUp = database.RatedLevels.Count(r => r.SlotId == s.SlotId && r.Rating == 1),
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{
Slot = s,
ThumbsUp = database.RatedLevels.Count(r => r.SlotId == s.SlotId && r.Rating == 1),
})
{
Slot = s,
ThumbsUp = database.RatedLevels.Count(r => r.SlotId == s.SlotId && r.Rating == 1),
})

Fix indentation here

Comment on lines +18 to +21
public override string[] Sorts { get; } =
{
"relevance",
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public override string[] Sorts { get; } =
{
"relevance",
};
public override string[] Sorts { get; } = ["relevance",];

This change should be implemented in all Category classes where possible

Comment on lines +34 to +37
{
Slot = s,
Hearts = database.HeartedLevels.Count(r => r.SlotId == s.SlotId),
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{
Slot = s,
Hearts = database.HeartedLevels.Count(r => r.SlotId == s.SlotId),
})
{
Slot = s,
Hearts = database.HeartedLevels.Count(r => r.SlotId == s.SlotId),
})

Fix indentation

return database.Slots.Where(_ => false);

List<int> slotIds = recentlyPlayed.SlotIds
.Take(30)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also use the config value instead of hardcoding it

@sudokoko
sudokoko self-requested a review August 25, 2026 18:44

@sudokoko sudokoko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple things I noticed, on top of Slendy's review.

SlotQueryBuilder queryBuilder)
{
RecentlyPlayedEntity? recentlyPlayed = database.RecentlyPlayed
.AsNoTracking()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're using .AsNoTracking() in some places and not others, and additionally - I don't believe we've really ever marked database calls as explicitly untracked.

Suggested change
.AsNoTracking()

{
public int MaxNeighbors { get; set; } = 250;
public int MinimumNeighborOverlap { get; set; } = 2;
public int MaxCandidatePool { get; set; } = 1000;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public int MaxCandidatePool { get; set; } = 1000;

From what I can tell, this isn't used anywhere. I don't know if you intended for it to be.

Comment on lines +124 to +140
IQueryable<ScoredSlot> recommendations =
from slot in database.Slots
.AsNoTracking()
.Where(queryBuilder.Build())
.Where(slot => !database.VisitedLevels.Any(visitedLevel =>
visitedLevel.UserId == token.UserId &&
visitedLevel.SlotId == slot.SlotId))

join score in scores
on slot.SlotId equals score.SlotId

let hearts = database.HeartedLevels.Count(heartedLevel =>
heartedLevel.SlotId == slot.SlotId)

let likes = database.RatedLevels.Count(rating =>
rating.SlotId == slot.SlotId &&
rating.Rating == 1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This query seems to do a lot of unbounded work and performs multiple COUNT operations per slot. Could have performance issues at scale.

Perhaps this is where you intended to use CategoryConfiguration.Instance.Recommended.MaxCandidatePool to limit the candidate set before performing expensive aggregates?

Could also pre-aggregate the likes and hearts in separate grouped queries instead of calling COUNT inside of the projection.

.Take(config.MaxCandidatePool)

#nullable disable

this.PlayerCount = RoomHelper.Rooms.Count(r => r.Slot.SlotType == SlotType.User && r.Slot.SlotId == this.SlotId);
Dictionary<int, int> playerCounts = RoomHelper.GetUserLevelPlayerCounts();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If PrepareSerialization is invoked for multiple slots in parallel, this could result in a performance issue with how this method is implemented. See comment in GameUserSlot.cs.


public static Dictionary<int, int> GetUserLevelPlayerCounts()
{
lock (RoomLock)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continued from the comment on GameUserSlot.cs - method acquires a lock and builds a full dictionary each time. Consider caching the counts for the duration of the request or exposing an API where the caller computes the counts once and reuses them for multiple calls.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants