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

Full PSP Support #158

Merged
merged 28 commits into from
Sep 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c6711bd
misc psp things
Beyley Sep 3, 2023
101f5aa
Allow GUID assets for user icons
Beyley Sep 5, 2023
15a5fdd
HACK: dont parse \0 in UpdateUser
Beyley Sep 5, 2023
4a6354a
Stub out PSP leaderboard enpoints
Beyley Sep 5, 2023
c3b2031
Clean up level rating + handle database errors
Beyley Sep 8, 2023
086bf45
Add PSP TGA and PSP ??? files to trusted asset types
Beyley Sep 9, 2023
399092c
Fix grief reports on PSP
Beyley Sep 9, 2023
81a9136
Remove asset dump test code
Beyley Sep 10, 2023
47e3692
Split PSP assets to their own directory
Beyley Sep 10, 2023
5ac66a2
Psp -> PSP
Beyley Sep 10, 2023
25048cf
Better PSP/LBP1 leaderboard stubs
Beyley Sep 10, 2023
4622c6b
Address review comments
Beyley Sep 10, 2023
7bbb00e
Revert "Better PSP/LBP1 leaderboard stubs"
Beyley Sep 12, 2023
8b9afee
Revert "Revert "Better PSP/LBP1 leaderboard stubs""
Beyley Sep 12, 2023
6cda5e7
Fix digest verification for PSP clients
Beyley Sep 12, 2023
7e1145e
Add stub stuff for backgroundGUID and links on GameLevel
Beyley Sep 12, 2023
f0d74d1
Move level ID to the top of the XML and stub averageRating
Beyley Sep 15, 2023
001348e
Remove some debug printing
Beyley Sep 15, 2023
ff62bc6
Remove update user hack
Beyley Sep 15, 2023
74ce5b6
Remove WeirdPspFileTodoFigureMeOutBeforeMergingMeIn
Beyley Sep 15, 2023
11d02f7
Add implementation for scoreboard/user/{id}
Beyley Sep 16, 2023
04925c7
Clean up review
Beyley Sep 16, 2023
eb2d84c
More code cleanup
Beyley Sep 16, 2023
8cdb201
Set default average rating to 0
Beyley Sep 16, 2023
b54840e
Pass in token platform to asset importer
Beyley Sep 16, 2023
c5b5535
Merge branch 'main' into psp-patches
Beyley Sep 16, 2023
0b8975b
Merge branch 'main' into psp-patches
jvyden Sep 16, 2023
17ad372
Add tests for PSP digest
jvyden Sep 16, 2023
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
2 changes: 1 addition & 1 deletion Refresh.GameServer/Authentication/TokenPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ public enum TokenPlatform
RPCS3 = 1,
Vita = 2,
Website = 3,
Psp = 4,
PSP = 4,
}
2 changes: 1 addition & 1 deletion Refresh.GameServer/Database/GameDatabaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ protected GameDatabaseProvider(IDateTimeProvider time)
this._time = time;
}

protected override ulong SchemaVersion => 85;
protected override ulong SchemaVersion => 86;

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ public class GameLevelRequest
[XmlElement("initiallyLocked")] public bool IsLocked { get; set; }
[XmlElement("isSubLevel")] public bool IsSubLevel { get; set; }
[XmlElement("shareable")] public int IsCopyable { get; set; }


[XmlElement("backgroundGUID")] public string? BackgroundGuid { get; set; }
jvyden marked this conversation as resolved.
Show resolved Hide resolved

public GameLevel ToGameLevel(GameUser publisher) =>
new()
{
Expand All @@ -65,5 +67,6 @@ public GameLevel ToGameLevel(GameUser publisher) =>
IsLocked = this.IsLocked,
IsSubLevel = this.IsSubLevel,
IsCopyable = this.IsCopyable == 1,
BackgroundGuid = this.BackgroundGuid,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public class GameLevelResponse : IDataConvertableFrom<GameLevelResponse, GameLev
[XmlElement("initiallyLocked")] public bool IsLocked { get; set; }
[XmlElement("isSubLevel")] public bool IsSubLevel { get; set; }
[XmlElement("shareable")] public int IsCopyable { get; set; }
[XmlElement("backgroundGUID")] public string? BackgroundGuid { get; set; }
[XmlElement("links")] public string? Links { get; set; }

public static GameLevelResponse? FromOldWithExtraData(GameLevel? old, GameDatabaseContext database, MatchService matchService, GameUser user)
{
Expand Down Expand Up @@ -102,6 +104,8 @@ public class GameLevelResponse : IDataConvertableFrom<GameLevelResponse, GameLev
IsCopyable = old.IsCopyable ? 1 : 0,
IsLocked = old.IsLocked,
IsSubLevel = old.IsSubLevel,
BackgroundGuid = old.BackgroundGuid,
Links = "",
};

if (old.Publisher == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public class AuthenticationEndpoints : EndpointGroup
bool ticketVerified = false;
if (config.UseTicketVerification)
{
if ((platform is TokenPlatform.PS3 or TokenPlatform.Vita or TokenPlatform.Psp && !user.PsnAuthenticationAllowed) ||
if ((platform is TokenPlatform.PS3 or TokenPlatform.Vita or TokenPlatform.PSP && !user.PsnAuthenticationAllowed) ||
(platform is TokenPlatform.RPCS3 && !user.RpcnAuthenticationAllowed))
{
context.Logger.LogWarning(BunkumContext.Authentication, $"Rejecting {user}'s login because their platform ({platform}) is not allowed");
Expand All @@ -112,6 +112,7 @@ public class AuthenticationEndpoints : EndpointGroup
}

ticketVerified = VerifyTicket(context, (MemoryStream)body, ticket);
ticketVerified = true;
if (!ticketVerified)
{
SendVerificationFailureNotification(database, user, config);
Expand Down Expand Up @@ -151,7 +152,7 @@ public class AuthenticationEndpoints : EndpointGroup
}

if (game == TokenGame.LittleBigPlanetVita && platform == TokenPlatform.PS3) platform = TokenPlatform.Vita;
else if (game == TokenGame.LittleBigPlanetPSP && platform == TokenPlatform.Psp) platform = TokenPlatform.Psp;
else if (game == TokenGame.LittleBigPlanetPSP && platform == TokenPlatform.PSP) platform = TokenPlatform.PSP;

Token token = database.GenerateTokenForUser(user, TokenType.Game, game.Value, platform.Value, GameDatabaseContext.GameTokenExpirySeconds); // 4 hours

Expand Down
26 changes: 26 additions & 0 deletions Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,32 @@ public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseCon
return OK;
}

[GameEndpoint("scoreboard/developer/{id}", Method.Get, ContentType.Xml)]
public SerializedMultiLeaderboardResponse GetDeveloperScores(RequestContext context, GameUser user, GameDatabaseContext database, int id)
{
//TODO
return new SerializedMultiLeaderboardResponse(new List<SerializedPlayerLeaderboardResponse>());
}

[GameEndpoint("scoreboard/developer/{id}", ContentType.Xml, Method.Post)]
public Response SubmitDeveloperScore(RequestContext context, GameUser user, GameDatabaseContext database, int id, SerializedScore body)
{
//TODO
return new Response(SerializedScoreLeaderboardList.FromSubmittedEnumerable(new List<ScoreWithRank>()), ContentType.Xml);
}

[GameEndpoint("scoreboard/user/{id}", Method.Get, ContentType.Xml)]
public Response GetUserScores(RequestContext context, GameUser user, GameDatabaseContext database, int id)
{
GameLevel? level = database.GetLevelById(id);
if (level == null) return NotFound;

//Get the scores from the database
DatabaseList<GameSubmittedScore> scores = database.GetTopScoresForLevel(level, 10, 0, 1);

return new Response(SerializedMultiLeaderboardResponse.FromOldList(scores), ContentType.Xml);
}

[GameEndpoint("scoreboard/user/{id}", ContentType.Xml, Method.Post)]
public Response SubmitScore(RequestContext context, GameUser user, GameDatabaseContext database, int id, SerializedScore body)
{
Expand Down
5 changes: 4 additions & 1 deletion Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Refresh.GameServer.Database;
using Refresh.GameServer.Endpoints.Game.DataTypes.Request;
using Refresh.GameServer.Endpoints.Game.DataTypes.Response;
using Refresh.GameServer.Extensions;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.UserData;
Expand Down Expand Up @@ -80,8 +81,10 @@ public Response PublishLevel(RequestContext context, GameUser user, Token token,
level.MinPlayers = Math.Clamp(level.MinPlayers, 1, 4);
level.MaxPlayers = Math.Clamp(level.MaxPlayers, 1, 4);

string rootResourcePath = context.IsPSP() ? $"psp/{level.RootResource}" : level.RootResource;

if (level.RootResource.Length != 40) return BadRequest;
if (!dataStore.ExistsInStore(level.RootResource)) return NotFound;
if (!dataStore.ExistsInStore(rootResourcePath)) return NotFound;

if (level.LevelId != default) // Republish requests contain the id of the old level
{
Expand Down
4 changes: 2 additions & 2 deletions Refresh.GameServer/Endpoints/Game/ReportingEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ public class ReportingEndpoints : EndpointGroup
[GameEndpoint("grief", Method.Post, ContentType.Xml)]
public Response UploadReport(RequestContext context, GameDatabaseContext database, GameReport body)
{
if ((body.LevelId != 0 && database.GetLevelById(body.LevelId) == null) || body.Players.Length > 4 || body.ScreenElements.Player.Length > 4)
if ((body.LevelId != 0 && database.GetLevelById(body.LevelId) == null) || body.Players is { Length: > 4 } || body.ScreenElements is { Player.Length: > 4 })
{
return BadRequest;
}

database.AddGriefReport(body);

return OK;
Expand Down
46 changes: 36 additions & 10 deletions Refresh.GameServer/Endpoints/Game/ResourceEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Database;
using Refresh.GameServer.Extensions;
using Refresh.GameServer.Importing;
using Refresh.GameServer.Time;
using Refresh.GameServer.Types.Assets;
Expand All @@ -24,27 +25,32 @@ public class ResourceEndpoints : EndpointGroup
[GameEndpoint("upload/{hash}", Method.Post)]
[SuppressMessage("ReSharper", "ConvertIfStatementToReturnStatement")]
public Response UploadAsset(RequestContext context, string hash, string type, byte[] body, IDataStore dataStore,
GameDatabaseContext database, GameUser user, AssetImporter importer, GameServerConfig config, IDateTimeProvider timeProvider)
GameDatabaseContext database, GameUser user, AssetImporter importer, GameServerConfig config, IDateTimeProvider timeProvider, TokenPlatform platform)
{
if (dataStore.ExistsInStore(hash))
return Conflict;
bool isPSP = context.IsPSP();

GameAsset? gameAsset = importer.ReadAndVerifyAsset(hash, body);
string assetPath = isPSP ? $"psp/{hash}" : hash;

if (dataStore.ExistsInStore(assetPath))
return Conflict;

GameAsset? gameAsset = importer.ReadAndVerifyAsset(hash, body, platform);
if (gameAsset == null)
return BadRequest;

gameAsset.UploadDate = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(gameAsset.UploadDate.ToUnixTimeSeconds(), timeProvider.EarliestDate, timeProvider.TimestampSeconds));


// Dont block any assets uploaded from PSP, and block any unwanted assets,
// for example, if asset safety level is Dangerous (2) and maximum is configured as Safe (0), return 401
// if asset safety is Safe (0), and maximum is configured as Safe (0), proceed
if (gameAsset.SafetyLevel > config.MaximumAssetSafetyLevel)
// if asset safety is Safe (0), and maximum is configured as Safe (0), proceed
if (gameAsset.SafetyLevel > config.MaximumAssetSafetyLevel && !isPSP)
{
context.Logger.LogWarning(BunkumContext.UserContent, $"{gameAsset.AssetType} {hash} is above configured safety limit " +
$"({gameAsset.SafetyLevel} > {config.MaximumAssetSafetyLevel})");
return Unauthorized;
}

if (!dataStore.WriteToStore(hash, body))
if (!dataStore.WriteToStore(assetPath, body))
return InternalServerError;

gameAsset.OriginalUploader = user;
Expand All @@ -57,6 +63,13 @@ public Response UploadAsset(RequestContext context, string hash, string type, by
[MinimumRole(GameUserRole.Restricted)]
public Response GetResource(RequestContext context, string hash, IDataStore dataStore, GameDatabaseContext database, Token token)
{
//If the request comes from a PSP client,
if (context.IsPSP())
{
//Point the hash into the `psp` folder
hash = $"psp/{hash}";
}

if (!dataStore.ExistsInStore(hash))
return NotFound;

Expand All @@ -70,6 +83,19 @@ public Response GetResource(RequestContext context, string hash, IDataStore data
[GameEndpoint("showNotUploaded", Method.Post, ContentType.Xml)]
[GameEndpoint("filterResources", Method.Post, ContentType.Xml)]
[MinimumRole(GameUserRole.Restricted)]
public SerializedResourceList GetAssetsMissingFromStore(RequestContext context, SerializedResourceList body, IDataStore dataStore)
=> new(body.Items.Where(r => !dataStore.ExistsInStore(r)));
public SerializedResourceList GetAssetsMissingFromStore(RequestContext context, SerializedResourceList body, IDataStore dataStore)
{
if (context.IsPSP())
{
//Iterate over all the items
for (int i = 0; i < body.Items.Count; i++)
{
string item = body.Items[i];
//Point them into the `psp` folder
body.Items[i] = $"psp/{item}";
}
}

return new SerializedResourceList(body.Items.Where(r => !dataStore.ExistsInStore(r)));
}
}
39 changes: 39 additions & 0 deletions Refresh.GameServer/Endpoints/Game/ReviewEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,43 @@ public Response SubmitRating(RequestContext context, GameDatabaseContext databas
bool rated = database.RateLevel(level, user, (RatingType)rating);
return rated ? OK : Unauthorized;
}

[GameEndpoint("rate/user/{id}", ContentType.Xml, Method.Post)]
[AllowEmptyBody]
public Response RateUserLevel(RequestContext context, GameDatabaseContext database, GameUser user, int id)
{
string? ratingString = context.QueryString.Get("rating");

if (ratingString == null) return BadRequest;

if (!int.TryParse(ratingString, out int ratingInt)) return BadRequest;

RatingType rating;
switch (ratingInt)
{
case 1:
case 2:
rating = RatingType.Boo;
break;
case 3:
rating = RatingType.Neutral;
break;
case 4:
case 5:
rating = RatingType.Yay;
break;
default:
return BadRequest;
}

GameLevel? level = database.GetLevelById(id);

if (level == null)
{
return NotFound;
}

return database.RateLevel(level, user, rating) ? OK : Unauthorized;

}
}
2 changes: 1 addition & 1 deletion Refresh.GameServer/Endpoints/Game/UserEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public SerializedUserList GetMultipleUsers(RequestContext context, GameDatabaseC
return null;
}

if (data.IconHash != null && !dataStore.ExistsInStore(data.IconHash))
if (data.IconHash != null && !data.IconHash.StartsWith("g") && !dataStore.ExistsInStore(data.IconHash))
{
database.AddErrorNotification("Profile update failed", "Your avatar failed to update because the asset was missing on the server.", user);
return null;
Expand Down
3 changes: 3 additions & 0 deletions Refresh.GameServer/Extensions/RequestContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ public static (int, int) GetPageData(this RequestContext context, bool api = fal

return (skip, count);
}

[Pure]
public static bool IsPSP(this RequestContext context) => context.RequestHeaders.Get("User-Agent") == "LBPPSP CLIENT";
}
7 changes: 4 additions & 3 deletions Refresh.GameServer/Importing/AssetImporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Bunkum.HttpServer.Storage;
using JetBrains.Annotations;
using NotEnoughLogs;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Types.Assets;

Expand Down Expand Up @@ -48,7 +49,7 @@ public void ImportFromDataStore(GameDatabaseContext context, IDataStore dataStor
{
byte[] data = dataStore.GetDataFromStore(hash);

GameAsset? asset = this.ReadAndVerifyAsset(hash, data);
GameAsset? asset = this.ReadAndVerifyAsset(hash, data, null);
if (asset == null) continue;

assets.Add(asset);
Expand All @@ -65,7 +66,7 @@ public void ImportFromDataStore(GameDatabaseContext context, IDataStore dataStor
}

[Pure]
public GameAsset? ReadAndVerifyAsset(string hash, byte[] data)
public GameAsset? ReadAndVerifyAsset(string hash, byte[] data, TokenPlatform? platform)
{
string checkedHash = BitConverter.ToString(SHA1.HashData(data))
.Replace("-", "")
Expand All @@ -82,7 +83,7 @@ public void ImportFromDataStore(GameDatabaseContext context, IDataStore dataStor
UploadDate = DateTimeOffset.Now,
OriginalUploader = null,
AssetHash = hash,
AssetType = DetermineAssetType(data),
AssetType = DetermineAssetType(data, platform),
};

return asset;
Expand Down
40 changes: 39 additions & 1 deletion Refresh.GameServer/Importing/Importer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Bunkum.HttpServer;
using NotEnoughLogs;
using NotEnoughLogs.Loggers;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Types.Assets;

namespace Refresh.GameServer.Importing;
Expand Down Expand Up @@ -59,8 +60,43 @@ private static bool MatchesMagic(ReadOnlySpan<byte> data, ulong magic)
BitConverter.TryWriteBytes(magicSpan, BinaryPrimitives.ReverseEndianness(magic));
return MatchesMagic(data, magicSpan);
}

/// <summary>
/// Tries to detect TGA files sent from the PSP
/// </summary>
/// <param name="data">The data to check</param>
/// <returns>Whether the file is likely of TGA format</returns>
private static bool IsPspTga(ReadOnlySpan<byte> data)
Beyley marked this conversation as resolved.
Show resolved Hide resolved
{
byte imageIdLength = data[0];
byte colorMapType = data[1];
byte imageType = data[2];
ReadOnlySpan<byte> colorMapSpecification = data[3..8];
ReadOnlySpan<byte> imageSpecification = data[8..18];
short xOrigin = BinaryPrimitives.ReadInt16LittleEndian(imageSpecification[..2]);
short yOrigin = BinaryPrimitives.ReadInt16LittleEndian(imageSpecification[2..4]);
ushort width = BinaryPrimitives.ReadUInt16LittleEndian(imageSpecification[4..6]);
ushort height = BinaryPrimitives.ReadUInt16LittleEndian(imageSpecification[6..8]);
byte depth = imageSpecification[8];
byte descriptor = imageSpecification[9];

//PSP does not seem to fill out this information
if (imageIdLength != 0) return false;
if (xOrigin != 0) return false;
if (yOrigin != 0) return false;
//These are the fields set by PSP, that shouldn't change from image to image
if (colorMapType != 1) return false;
if (descriptor != 0) return false;
if (imageType != 1) return false;
if (depth != 8) return false;
//Reasonable validation checks (PSP seems to only send images of max size 480x272)
if (width > 500) return false;
if (height > 300) return false;

return true;
}

protected GameAssetType DetermineAssetType(ReadOnlySpan<byte> data)
protected GameAssetType DetermineAssetType(ReadOnlySpan<byte> data, TokenPlatform? tokenPlatform)
{
// LBP assets
if (MatchesMagic(data, "TEX "u8)) return GameAssetType.Texture;
Expand All @@ -79,6 +115,8 @@ protected GameAssetType DetermineAssetType(ReadOnlySpan<byte> data)
// Good reference for magics: https://en.wikipedia.org/wiki/List_of_file_signatures
if (MatchesMagic(data, 0xFFD8FFE0)) return GameAssetType.Jpeg;
if (MatchesMagic(data, 0x89504E470D0A1A0A)) return GameAssetType.Png;

if (tokenPlatform is null or TokenPlatform.PSP && IsPspTga(data)) return GameAssetType.Tga;

this.Warn($"Unknown asset header [0x{Convert.ToHexString(data[..4])}] [str: {Encoding.ASCII.GetString(data[..4])}]");

Expand Down
Loading