Skip to content
Open
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
111 changes: 99 additions & 12 deletions ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ public async Task<IActionResult> GetUser(string username)
UserEntity? user = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username);
if (user == null) return this.NotFound();

return this.Ok(GameUser.CreateFromEntity(user, this.GetToken().GameVersion));
GameVersion gameVersion = this.GetToken().GameVersion;

UserProfilePinsEntity? profilePins = await this.database.UserProfilePins.FirstOrDefaultAsync(p => p.UserId == user.UserId && p.GameVersion == gameVersion);

GameUser profile = GameUser.CreateFromEntity(user, gameVersion);

profile.ProfilePins = profilePins?.Pins ?? user.Pins;

return this.Ok(profile);
}

[HttpGet("users")]
Expand Down Expand Up @@ -173,24 +181,103 @@ public async Task<IActionResult> UpdateUser()
[Produces("text/json")]
public async Task<IActionResult> UpdateMyPins()
{
UserEntity? user = await this.database.UserFromGameToken(this.GetToken());
GameTokenEntity token = this.GetToken();

UserEntity? user = await this.database.UserFromGameToken(token);
if (user == null) return this.Forbid();

if (token.GameVersion is not (GameVersion.LittleBigPlanet2 or GameVersion.LittleBigPlanet3 or GameVersion.LittleBigPlanetVita))
{
return this.BadRequest();
}

string bodyString = await this.ReadBodyAsync();

Pins? pinJson = JsonSerializer.Deserialize<Pins>(bodyString);
if (pinJson?.ProfilePins == null) return this.BadRequest();
Pins? pinJson;

try
{
pinJson = JsonSerializer.Deserialize<Pins>(bodyString);
}
catch (JsonException)
{
return this.BadRequest();
}

if (pinJson == null)
return this.BadRequest();

if (!PinUploadParser.TryParse(pinJson, out PinUploadParser.ParsedPinUpload parsed))
return this.BadRequest();

PinSet pinSet = token.GameVersion.ToPinSet();

Dictionary<uint, UserPinProgressEntity> storedProgress = await this.database.UserPinProgress
.Where(p => p.UserId == user.UserId && p.PinSet == pinSet)
.ToDictionaryAsync(p => p.ProgressType);

// Sometimes the update gets called periodically as pin progress updates via playing,
// may not affect equipped profile pins however, so check before setting it.
string currentPins = user.Pins;
string newPins = string.Join(",", pinJson.ProfilePins.Distinct());
foreach (KeyValuePair<uint, double> uploaded in parsed.Progress)
{
if (storedProgress.TryGetValue(uploaded.Key, out UserPinProgressEntity? stored))
{
stored.Value = PinProgressRules.Merge(uploaded.Key, stored.Value, uploaded.Value);

continue;
}

UserPinProgressEntity entity = new()
{
UserId = user.UserId,
PinSet = pinSet,
ProgressType = uploaded.Key,
Value = uploaded.Value,
};

this.database.UserPinProgress.Add(entity);
storedProgress.Add(uploaded.Key, entity);
}

if (parsed.ProfilePins != null)
{
string newPins = string.Join(",", parsed.ProfilePins);

if (string.Equals(currentPins, newPins)) return this.Ok("[{\"StatusCode\":200}]");
UserProfilePinsEntity? profilePins = await this.database.UserProfilePins
.FirstOrDefaultAsync(p => p.UserId == user.UserId && p.GameVersion == token.GameVersion);

if (profilePins == null)
{
profilePins = new UserProfilePinsEntity
{
UserId = user.UserId,
GameVersion = token.GameVersion,
Pins = newPins,
};

this.database.UserProfilePins.Add(profilePins);
}
else if (!string.Equals(profilePins.Pins, newPins))
{
profilePins.Pins = newPins;
}
}

user.Pins = newPins;
await this.database.SaveChangesAsync();

return this.Ok("[{\"StatusCode\":200}]");
List<object> responsePins = [];

foreach (UserPinProgressEntity progress in storedProgress.Values.OrderBy(p => p.ProgressType))
{
responsePins.Add(progress.ProgressType);
responsePins.Add(progress.Value);
}

return new JsonResult(new
{
progress = responsePins,
awards = responsePins,
})
{
ContentType = "text/json",
};
}
}
}
131 changes: 131 additions & 0 deletions ProjectLighthouse.Servers.GameServer/Types/Users/PinUploadParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#nullable enable
using System.Text.Json;

namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Users;

public static class PinUploadParser
{
public sealed class ParsedPinUpload
{
public Dictionary<uint, double> Progress { get; } = [];

// Null means profile_pins was omitted
public List<uint>? ProfilePins { get; internal set; }
}

public static bool TryParse(Pins upload, out ParsedPinUpload result)
{
result = new ParsedPinUpload();

if (!TryParseProgress(upload.Progress, result))
return false;

if (!TryParseAwards(upload.Awards, result))
return false;

if (!TryParseProfilePins(upload.ProfilePins, result))
return false;

return true;
}

private static bool TryParseProgress(JsonElement[]? values, ParsedPinUpload result)
{
if (values == null)
return true;

if ((values.Length & 1) != 0)
return false;

for (int i = 0; i < values.Length; i += 2)
{
if (values[i].ValueKind != JsonValueKind.Number || !values[i].TryGetUInt32(out uint progressType))
{
return false;
}

if (values[i + 1].ValueKind != JsonValueKind.Number || !values[i + 1].TryGetDouble(out double value))
{
return false;
}

if (!double.IsFinite(value))
return false;

if (!result.Progress.TryAdd(progressType, value))
return false;
}

return true;
}

private static bool TryParseAwards(JsonElement[]? values, ParsedPinUpload result)
{
if (values == null)
return true;

if ((values.Length & 1) != 0)
return false;

HashSet<uint> seenProgressTypes = [];

for (int i = 0; i < values.Length; i += 2)
{
if (values[i].ValueKind != JsonValueKind.Number || !values[i].TryGetUInt32(out uint progressType))
{
return false;
}

if (values[i + 1].ValueKind != JsonValueKind.Number || !values[i + 1].TryGetInt64(out long count))
{
return false;
}

if (count < 0)
return false;

if (!seenProgressTypes.Add(progressType))
return false;

if (result.Progress.TryGetValue(progressType, out double existingValue))
{
result.Progress[progressType] = Math.Max(existingValue, count);
}
else
{
result.Progress.Add(progressType, count);
}
}

return true;
}

private static bool TryParseProfilePins(JsonElement[]? values, ParsedPinUpload result)
{
if (values == null)
return true;

if (values.Length > 3)
return false;

List<uint> profilePins = new(values.Length);
HashSet<uint> seenPinIds = [];

foreach (JsonElement value in values)
{
if (value.ValueKind != JsonValueKind.Number || !value.TryGetUInt32(out uint pinId))
{
return false;
}

if (!seenPinIds.Add(pinId))
return false;

profilePins.Add(pinId);
}

result.ProfilePins = profilePins;

return true;
}
}
10 changes: 6 additions & 4 deletions ProjectLighthouse.Servers.GameServer/Types/Users/Pins.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
#nullable enable
using System.Text.Json;
using System.Text.Json.Serialization;

namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Users;

public class Pins
{
[JsonPropertyName("progress")]
public long[]? Progress { get; set; }
public JsonElement[]? Progress { get; set; }

[JsonPropertyName("awards")]
public long[]? Awards { get; set; }
public JsonElement[]? Awards { get; set; }

[JsonPropertyName("profile_pins")]
public long[]? ProfilePins { get; set; }
}
public JsonElement[]? ProfilePins { get; set; }
}
Loading