Skip to content

Commit 7f1b0e5

Browse files
authored
Photo upload rework (#1042)
Partially reworks saving `GamePhoto`s in DB by doing the following: - The DB method now takes interface-type parameters instead of `SerializedPhoto` (making it work more similarly to other similar methods), and this PR moves boundary parsing logic from `SerializedPhotoSubject` to `PhotoHelper`, in order to start untangling photo-related game endpoint classes from the DB project - The level the photo is supposed to be attributed to is now passed as a parameter to the DB method - The DB method now returns the newly created `GamePhoto` - levels of photos created from reports will now retain their original level ID and slot type, even if it's not a user/developer level (e.g. pod), or if it doesn't exist on the server
2 parents 867a89a + ba23004 commit 7f1b0e5

14 files changed

Lines changed: 357 additions & 115 deletions

File tree

Refresh.Database/GameDatabaseContext.Photos.cs

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using Refresh.Database.Models.Users;
44
using Refresh.Database.Models.Levels;
55
using Refresh.Database.Models.Photos;
6+
using Refresh.Database.Query;
7+
using Refresh.Database.Helpers;
68

79
namespace Refresh.Database;
810

@@ -26,7 +28,7 @@ public partial class GameDatabaseContext // Photos
2628
.Include(p => p.Subject4User)
2729
.Include(p => p.Subject4User!.Statistics);
2830

29-
public void UploadPhoto(SerializedPhoto photo, GameUser publisher)
31+
public GamePhoto UploadPhoto(IPhotoUpload photo, IEnumerable<IPhotoUploadSubject> subjects, GameUser publisher, GameLevel? level)
3032
{
3133
GamePhoto newPhoto = new()
3234
{
@@ -36,33 +38,24 @@ public void UploadPhoto(SerializedPhoto photo, GameUser publisher)
3638
PlanHash = photo.PlanHash,
3739

3840
Publisher = publisher,
39-
LevelType = photo.Level?.Type ?? "",
40-
OriginalLevelId = photo.Level?.LevelId ?? 0,
41-
OriginalLevelName = photo.Level?.Title ?? "",
41+
Level = level,
42+
LevelType = photo.LevelType ?? "",
43+
OriginalLevelId = photo.LevelId ?? 0,
44+
OriginalLevelName = photo.LevelTitle ?? "",
4245

4346
TakenAt = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(photo.Timestamp, this._time.EarliestDate, this._time.TimestampSeconds)),
4447
PublishedAt = this._time.Now,
4548
};
4649

47-
GameLevel? level = null;
48-
49-
if (photo.Level?.Type is "user" or "developer")
50-
{
51-
level = this.GetLevelByIdAndType(photo.Level.Type, photo.Level.LevelId);
52-
newPhoto.Level = level;
53-
}
54-
55-
float[] bounds = new float[SerializedPhotoSubject.FloatCount];
56-
57-
List<GamePhotoSubject> gameSubjects = new(photo.PhotoSubjects.Count);
58-
foreach (SerializedPhotoSubject subject in photo.PhotoSubjects)
50+
List<GamePhotoSubject> gameSubjects = new(subjects.Count());
51+
foreach (IPhotoUploadSubject subject in subjects)
5952
{
6053
GameUser? subjectUser = null;
6154

6255
if (!string.IsNullOrEmpty(subject.Username))
6356
subjectUser = this.GetUserByUsername(subject.Username);
6457

65-
SerializedPhotoSubject.ParseBoundsList(subject.BoundsList, bounds);
58+
float[] bounds = PhotoHelper.ParseBoundsList(subject.BoundsList);
6659

6760
gameSubjects.Add(new GamePhotoSubject(subjectUser, subject.DisplayName, bounds));
6861

@@ -96,6 +89,7 @@ public void UploadPhoto(SerializedPhoto photo, GameUser publisher)
9689
}
9790

9891
this.CreatePhotoUploadEvent(publisher, newPhoto);
92+
return newPhoto;
9993
}
10094

10195
public void RemovePhoto(GamePhoto photo)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Globalization;
2+
3+
namespace Refresh.Database.Helpers;
4+
5+
public abstract class PhotoHelper
6+
{
7+
public const byte SubjectBoundaryCount = 4;
8+
9+
public static float[] ParseBoundsList(string input)
10+
{
11+
if (string.IsNullOrWhiteSpace(input))
12+
throw new ArgumentException($"Bounds are empty");
13+
14+
string[] boundsStr = input.Split(',');
15+
if (boundsStr.Length != SubjectBoundaryCount)
16+
throw new ArgumentException($"Number of bounds is invalid ({boundsStr.Length} instead of {SubjectBoundaryCount})");
17+
18+
float[] boundsParsed = new float[SubjectBoundaryCount];
19+
for (int i = 0; i < SubjectBoundaryCount; i++)
20+
{
21+
// No OutOfRangeExceptions as both arrays are ensured to be as long as the constant
22+
string boundaryStr = boundsStr[i];
23+
24+
if (!float.TryParse(boundaryStr, NumberFormatInfo.InvariantInfo, out float f))
25+
throw new FormatException($"Boundary {boundaryStr} ({i+1}/{SubjectBoundaryCount}) is not a float");
26+
27+
boundsParsed[i] = f;
28+
}
29+
30+
return boundsParsed;
31+
}
32+
}

Refresh.Database/Models/Photos/SerializedPhoto.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
using System.Xml.Serialization;
2+
using Refresh.Database.Query;
23

34
namespace Refresh.Database.Models.Photos;
45

56
#nullable disable
67

78
[XmlRoot("photo")]
89
[XmlType("photo")]
9-
public class SerializedPhoto
10+
public class SerializedPhoto : IPhotoUpload
1011
{
1112
[XmlAttribute("timestamp")]
1213
public long Timestamp { get; set; }
@@ -21,8 +22,16 @@ public class SerializedPhoto
2122
[XmlElement("medium")] public string MediumHash { get; set; }
2223
[XmlElement("large")] public string LargeHash { get; set; }
2324
[XmlElement("plan")] public string PlanHash { get; set; }
25+
26+
#nullable restore
2427

25-
[XmlElement("slot")] public SerializedPhotoLevel Level { get; set; }
28+
/// <remarks>
29+
/// Should never be null, but marked as nullable anyway in order to prevent another bad implementation from breaking
30+
/// </remarks>
31+
[XmlElement("slot")] public SerializedPhotoLevel? Level { get; set; }
32+
[XmlIgnore] public int? LevelId => this.Level?.LevelId;
33+
[XmlIgnore] public string? LevelType => this.Level?.Type;
34+
[XmlIgnore] public string? LevelTitle => this.Level?.Title;
2635

27-
[XmlArray("subjects")] public List<SerializedPhotoSubject> PhotoSubjects { get; set; }
36+
[XmlArray("subjects")] public List<SerializedPhotoSubject> PhotoSubjects { get; set; } = [];
2837
}
Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
using System.Globalization;
21
using System.Xml.Serialization;
2+
using Refresh.Database.Query;
33

44
namespace Refresh.Database.Models.Photos;
55

66
#nullable disable
77

88
[XmlRoot("subject")]
99
[XmlType("subject")]
10-
public class SerializedPhotoSubject
10+
public class SerializedPhotoSubject : IPhotoUploadSubject
1111
{
1212
[XmlElement("npHandle")]
1313
public string Username { get; set; }
@@ -17,31 +17,4 @@ public class SerializedPhotoSubject
1717

1818
[XmlElement("bounds")]
1919
public string BoundsList { get; set; }
20-
21-
public const byte FloatCount = 4;
22-
23-
public static void ParseBoundsList(ReadOnlySpan<char> input, float[] floats)
24-
{
25-
byte start = 0;
26-
byte floatIndex = 0;
27-
28-
for (byte i = 0; i < input.Length; i++)
29-
{
30-
if(floatIndex == FloatCount - 1) break; // won't catch the last float - not worth reading
31-
if (input[i] != ',') continue;
32-
33-
if (!float.TryParse(input.Slice(start, i - start), NumberFormatInfo.InvariantInfo, out float f))
34-
throw new FormatException("Invalid format");
35-
36-
floats[floatIndex] = f;
37-
floatIndex++;
38-
start = (byte)(i + 1);
39-
}
40-
41-
// parse the last float value - bounds does not end with comma
42-
if (!float.TryParse(input[start..], NumberFormatInfo.InvariantInfo, out float lastFloat))
43-
throw new FormatException("Invalid format");
44-
45-
floats[FloatCount - 1] = lastFloat;
46-
}
4720
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Refresh.Database.Query;
2+
3+
public interface IPhotoUpload
4+
{
5+
int? LevelId { get; }
6+
string? LevelType { get; }
7+
string? LevelTitle { get; }
8+
string SmallHash { get; }
9+
string MediumHash { get;}
10+
string LargeHash { get; }
11+
string PlanHash { get; }
12+
long Timestamp { get; }
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace Refresh.Database.Query;
2+
3+
public interface IPhotoUploadSubject
4+
{
5+
string Username { get; }
6+
string DisplayName { get; }
7+
string BoundsList { get; }
8+
}

Refresh.Interfaces.Game/Endpoints/PhotoEndpoints.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,9 @@ public Response UploadPhoto(RequestContext context, SerializedPhoto body, GameDa
5858
return Unauthorized;
5959
}
6060

61-
database.UploadPhoto(body, user);
61+
GameLevel? level = body.Level == null ? null : database.GetLevelByIdAndType(body.Level.Type, body.Level.LevelId);
6262

63+
database.UploadPhoto(body, body.PhotoSubjects, user, level);
6364
return OK;
6465
}
6566

Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,7 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas
5353
}
5454

5555
string jpegHash = body.JpegHash;
56-
57-
//If the level is specified but its invalid, set it to 0, to indicate the level is unknown
58-
//This case is hit when someone makes a grief report from a non-existent level, which we allow
59-
if (body.LevelId != 0 && level == null)
60-
body.LevelId = 0;
61-
56+
6257
//Basic validation
6358
if (body.Players is { Length: > 4 } || body.ScreenElements is { Player.Length: > 4 })
6459
// Return OK on PSP, since if we dont, it will error when trying to access the community moon and soft-lock the save file
@@ -84,18 +79,14 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas
8479
MediumHash = jpegHash,
8580
LargeHash = jpegHash,
8681
PlanHash = "0",
87-
Level = body.LevelId == 0 || level == null ? null : new SerializedPhotoLevel
82+
Level = new SerializedPhotoLevel
8883
{
89-
LevelId = level.LevelId,
90-
Title = level.Title,
91-
Type = level.SlotType switch {
92-
GameSlotType.User => "user",
93-
GameSlotType.Story => "developer",
94-
_ => throw new ArgumentOutOfRangeException(),
95-
},
84+
LevelId = body.LevelId,
85+
Title = level?.Title ?? "",
86+
Type = body.LevelType
9687
},
9788
PhotoSubjects = subjects,
98-
}, user);
89+
}, subjects, user, level);
9990

10091
return OK; // just upload photo
10192
}

RefreshTests.GameServer/TestContext.cs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Refresh.Interfaces.Game.Endpoints.DataTypes.Request;
1717
using Refresh.Database.Models.Playlists;
1818
using Refresh.Interfaces.APIv3.Endpoints.DataTypes.Request;
19+
using Refresh.Database.Models.Photos;
1920

2021
namespace RefreshTests.GameServer;
2122

@@ -144,7 +145,7 @@ public GameLevel CreateLevel(GameUser author, string title, string description,
144145
return level;
145146
}
146147

147-
public void CreatePhotoWithSubject(GameUser author, string imageHash)
148+
public GamePhoto CreatePhotoWithSubject(GameUser author, string imageHash, GameLevel? level = null)
148149
{
149150
// TODO: Return newly created GamePhoto
150151
if (this.Database.GetAssetFromHash(imageHash) == null)
@@ -155,22 +156,25 @@ public void CreatePhotoWithSubject(GameUser author, string imageHash)
155156
});
156157
}
157158

158-
this.Database.UploadPhoto(new()
159+
SerializedPhoto photo = new()
159160
{
160161
AuthorName = author.Username,
161162
SmallHash = imageHash,
162163
MediumHash = imageHash,
163164
LargeHash = imageHash,
164-
PhotoSubjects =
165-
[
166-
new()
167-
{
168-
Username = author.Username,
169-
DisplayName = author.Username,
170-
BoundsList = "10,10,10,10"
171-
}
172-
]
173-
}, author);
165+
166+
};
167+
IEnumerable<SerializedPhotoSubject> subjects =
168+
[
169+
new()
170+
{
171+
Username = author.Username,
172+
DisplayName = author.Username,
173+
BoundsList = "10,10,10,10"
174+
}
175+
];
176+
177+
return this.Database.UploadPhoto(photo, subjects, author, level);
174178
}
175179

176180
public void FillLeaderboard(GameLevel level, int count, byte type)

RefreshTests.GameServer/Tests/Levels/LevelDeletionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public void DeletesLevelWithPhotos()
7373
],
7474
};
7575

76-
context.Database.UploadPhoto(photo, author);
76+
context.Database.UploadPhoto(photo, photo.PhotoSubjects, author, level);
7777
context.Database.Refresh();
7878

7979
level = context.Database.GetLevelById(level.LevelId);

0 commit comments

Comments
 (0)