diff --git a/Refresh.GameServer/Database/GameDatabaseContext.Photos.cs b/Refresh.GameServer/Database/GameDatabaseContext.Photos.cs index 3ec4b1a2..eb520306 100644 --- a/Refresh.GameServer/Database/GameDatabaseContext.Photos.cs +++ b/Refresh.GameServer/Database/GameDatabaseContext.Photos.cs @@ -11,8 +11,6 @@ public partial class GameDatabaseContext // Photos { public void UploadPhoto(SerializedPhoto photo, GameUser publisher) { - long firstValidTime = new DateTimeOffset(2007, 1, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeSeconds(); - GamePhoto newPhoto = new() { SmallHash = photo.SmallHash, @@ -25,7 +23,7 @@ public void UploadPhoto(SerializedPhoto photo, GameUser publisher) LevelType = photo.Level.Type, LevelId = photo.Level.LevelId, - TakenAt = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(photo.Timestamp, firstValidTime, this._time.TimestampSeconds)), + TakenAt = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(photo.Timestamp, this._time.EarliestDate, this._time.TimestampSeconds)), PublishedAt = this._time.Now, }; diff --git a/Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs b/Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs index 1995402a..59ea841c 100644 --- a/Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/CommentEndpoints.cs @@ -5,6 +5,7 @@ using NotEnoughLogs; using Refresh.GameServer.Database; using Refresh.GameServer.Extensions; +using Refresh.GameServer.Time; using Refresh.GameServer.Types.Comments; using Refresh.GameServer.Types.Levels; using Refresh.GameServer.Types.Lists; @@ -16,8 +17,13 @@ namespace Refresh.GameServer.Endpoints.Game; public class CommentEndpoints : EndpointGroup { [GameEndpoint("postUserComment/{username}", ContentType.Xml, Method.Post)] - public Response PostProfileComment(RequestContext context, GameDatabaseContext database, string username, GameComment body, GameUser user) + public Response PostProfileComment(RequestContext context, GameDatabaseContext database, string username, GameComment body, GameUser user, IDateTimeProvider timeProvider) { + if (body.Content.Length > 4096) + { + return BadRequest; + } + GameUser? profile = database.GetUserByUsername(username); if (profile == null) return NotFound; diff --git a/Refresh.GameServer/Endpoints/Game/DataTypes/Request/GameLevelRequest.cs b/Refresh.GameServer/Endpoints/Game/DataTypes/Request/GameLevelRequest.cs new file mode 100644 index 00000000..50586907 --- /dev/null +++ b/Refresh.GameServer/Endpoints/Game/DataTypes/Request/GameLevelRequest.cs @@ -0,0 +1,94 @@ +using System.Xml.Serialization; +using Refresh.GameServer.Authentication; +using Refresh.GameServer.Database; +using Refresh.GameServer.Endpoints.ApiV3.DataTypes; +using Refresh.GameServer.Endpoints.Game.DataTypes.Response; +using Refresh.GameServer.Services; +using Refresh.GameServer.Types; +using Refresh.GameServer.Types.Levels; +using Refresh.GameServer.Types.Levels.SkillRewards; +using Refresh.GameServer.Types.Matching; +using Refresh.GameServer.Types.Reviews; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Endpoints.Game.DataTypes.Request; + +[XmlRoot("slot")] +[XmlType("slot")] +public class GameLevelRequest : IDataConvertableFrom +{ + [XmlElement("id")] public required int LevelId { get; set; } + + [XmlElement("name")] public required string Title { get; set; } + [XmlElement("icon")] public required string IconHash { get; set; } + [XmlElement("description")] public required string Description { get; set; } + [XmlElement("location")] public required GameLocation Location { get; set; } + + [XmlElement("game")] public required int GameVersion { get; set; } + [XmlElement("rootLevel")] public required string RootResource { get; set; } + + [XmlElement("firstPublished")] public required long PublishDate { get; set; } // unix seconds + [XmlElement("lastUpdated")] public required long UpdateDate { get; set; } + + [XmlElement("minPlayers")] public required int MinPlayers { get; set; } + [XmlElement("maxPlayers")] public required int MaxPlayers { get; set; } + [XmlElement("enforceMinMaxPlayers")] public required bool EnforceMinMaxPlayers { get; set; } + + [XmlElement("sameScreenGame")] public required bool SameScreenGame { get; set; } + + [XmlAttribute("type")] public string Type { get; set; } = "user"; + + [XmlElement("npHandle")] public SerializedUserHandle Handle { get; set; } = null!; + + [XmlArray("customRewards")] + [XmlArrayItem("customReward")] + public required List SkillRewards { get; set; } + + [XmlElement("resource")] public List XmlResources { get; set; } = new(); + + public static GameLevelRequest? FromOld(GameLevel? old) + { + if (old == null) return null; + + GameLevelRequest request = new() + { + LevelId = old.LevelId, + Title = old.Title, + IconHash = old.IconHash, + Description = old.Description, + Location = old.Location, + GameVersion = old.GameVersion.ToSerializedGame(), + RootResource = old.RootResource, + PublishDate = old.PublishDate, + UpdateDate = old.UpdateDate, + MinPlayers = old.MinPlayers, + MaxPlayers = old.MaxPlayers, + EnforceMinMaxPlayers = old.EnforceMinMaxPlayers, + SameScreenGame = old.SameScreenGame, + SkillRewards = old.SkillRewards.ToList(), + }; + + return request; + } + + public GameLevel ToGameLevel(GameUser publisher) => + new() + { + LevelId = this.LevelId, + Title = this.Title, + IconHash = this.IconHash, + Description = this.Description, + Location = this.Location, + RootResource = this.RootResource, + PublishDate = this.PublishDate, + UpdateDate = this.UpdateDate, + MinPlayers = this.MinPlayers, + MaxPlayers = this.MaxPlayers, + EnforceMinMaxPlayers = this.EnforceMinMaxPlayers, + SameScreenGame = this.SameScreenGame, + SkillRewards = this.SkillRewards.ToArray(), + Publisher = publisher, + }; + + public static IEnumerable FromOldList(IEnumerable oldList) => oldList.Select(FromOld)!; +} \ No newline at end of file diff --git a/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs index d725b898..f035e049 100644 --- a/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs @@ -16,11 +16,9 @@ namespace Refresh.GameServer.Endpoints.Game.Levels; public class LeaderboardEndpoints : EndpointGroup { [GameEndpoint("play/user/{id}", ContentType.Xml, Method.Post)] - public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseContext database, int? id) + public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseContext database, int id) { - if (id == null) return BadRequest; - - GameLevel? level = database.GetLevelById(id.Value); + GameLevel? level = database.GetLevelById(id); if (level == null) return NotFound; database.PlayLevel(level, user); @@ -28,11 +26,9 @@ public Response PlayLevel(RequestContext context, GameUser user, GameDatabaseCon } [GameEndpoint("scoreboard/user/{id}", ContentType.Xml, Method.Post)] - public Response SubmitScore(RequestContext context, GameUser user, GameDatabaseContext database, int? id, SerializedScore body) + public Response SubmitScore(RequestContext context, GameUser user, GameDatabaseContext database, int id, SerializedScore body) { - if (id == null) return BadRequest; - - GameLevel? level = database.GetLevelById(id.Value); + GameLevel? level = database.GetLevelById(id); if (level == null) return NotFound; //Validate the score is a non-negative amount @@ -41,8 +37,7 @@ public Response SubmitScore(RequestContext context, GameUser user, GameDatabaseC return BadRequest; } - GameSubmittedScore? score = database.SubmitScore(body, user, level); - if (score == null) return Unauthorized; + GameSubmittedScore score = database.SubmitScore(body, user, level); IEnumerable? scores = database.GetRankedScoresAroundScore(score, 5); Debug.Assert(scores != null); @@ -52,12 +47,9 @@ public Response SubmitScore(RequestContext context, GameUser user, GameDatabaseC [GameEndpoint("topscores/user/{id}/{type}", ContentType.Xml)] [MinimumRole(GameUserRole.Restricted)] - public SerializedScoreList? GetTopScoresForLevel(RequestContext context, GameDatabaseContext database, int? id, int? type) + public SerializedScoreList? GetTopScoresForLevel(RequestContext context, GameDatabaseContext database, int id, int type) { - if (id == null) return null; - if (type == null) return null; - - GameLevel? level = database.GetLevelById(id.Value); + GameLevel? level = database.GetLevelById(id); if (level == null) return null; (int skip, int count) = context.GetPageData(); diff --git a/Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs index 1f9c8b55..07d097b2 100644 --- a/Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs @@ -3,8 +3,10 @@ using Bunkum.HttpServer.Endpoints; using Bunkum.HttpServer.Responses; using Bunkum.HttpServer.Storage; +using NotEnoughLogs; using Refresh.GameServer.Authentication; using Refresh.GameServer.Database; +using Refresh.GameServer.Endpoints.Game.DataTypes.Request; using Refresh.GameServer.Endpoints.Game.DataTypes.Response; using Refresh.GameServer.Types.Levels; using Refresh.GameServer.Types.UserData; @@ -13,10 +15,40 @@ namespace Refresh.GameServer.Endpoints.Game.Levels; public class PublishEndpoints : EndpointGroup { + /// + /// Does basic verification on a level + /// + /// The level to verify + /// The user that is attempting to upload + /// A logger instance + /// Whether or not validation succeeded + private static bool VerifyLevel(GameLevelRequest body, GameUser user, LoggerContainer logger) + { + if (body.Title.Length > 256) + { + return false; + } + + if (body.Description.Length > 4096) + { + return false; + } + + if (body.MaxPlayers > 4 || body.MinPlayers > 4) + { + return false; + } + + return true; + } + [GameEndpoint("startPublish", ContentType.Xml, Method.Post)] [NullStatusCode(BadRequest)] - public SerializedLevelResources? StartPublish(RequestContext context, GameDatabaseContext database, GameLevelResponse body, IDataStore dataStore) + public SerializedLevelResources? StartPublish(RequestContext context, GameUser user, GameDatabaseContext database, GameLevelRequest body, IDataStore dataStore, LoggerContainer logger) { + //If verifying the request fails, return null + if (!VerifyLevel(body, user, logger)) return null; + List hashes = new(); hashes.AddRange(body.XmlResources); hashes.Add(body.RootResource); @@ -33,8 +65,11 @@ public class PublishEndpoints : EndpointGroup } [GameEndpoint("publish", ContentType.Xml, Method.Post)] - public Response PublishLevel(RequestContext context, GameUser user, Token token, GameDatabaseContext database, GameLevelResponse body, IDataStore dataStore) + public Response PublishLevel(RequestContext context, GameUser user, Token token, GameDatabaseContext database, GameLevelRequest body, IDataStore dataStore, LoggerContainer logger) { + //If verifying the request fails, return null + if (!VerifyLevel(body, user, logger)) return BadRequest; + GameLevel level = body.ToGameLevel(user); level.GameVersion = token.TokenGame; @@ -67,12 +102,9 @@ public Response PublishLevel(RequestContext context, GameUser user, Token token, return new Response(GameLevelResponse.FromOld(level)!, ContentType.Xml); } - [GameEndpoint("unpublish/{idStr}", ContentType.Xml, Method.Post)] - public Response DeleteLevel(RequestContext context, GameUser user, GameDatabaseContext database, string idStr) + [GameEndpoint("unpublish/{id}", ContentType.Xml, Method.Post)] + public Response DeleteLevel(RequestContext context, GameUser user, GameDatabaseContext database, int id) { - int.TryParse(idStr, out int id); - if (id == default) return BadRequest; - GameLevel? level = database.GetLevelById(id); if (level == null) return NotFound; diff --git a/Refresh.GameServer/Endpoints/Game/RelationEndpoints.cs b/Refresh.GameServer/Endpoints/Game/RelationEndpoints.cs index d4a9a177..356428b5 100644 --- a/Refresh.GameServer/Endpoints/Game/RelationEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/RelationEndpoints.cs @@ -15,12 +15,9 @@ namespace Refresh.GameServer.Endpoints.Game; public class RelationEndpoints : EndpointGroup { - [GameEndpoint("favourite/slot/user/{idStr}", Method.Post)] - public Response FavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string idStr) + [GameEndpoint("favourite/slot/user/{id}", Method.Post)] + public Response FavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, int id) { - int.TryParse(idStr, out int id); - if (id == default) return BadRequest; - GameLevel? level = database.GetLevelById(id); if (level == null) return NotFound; @@ -30,12 +27,9 @@ public Response FavouriteLevel(RequestContext context, GameDatabaseContext datab return Unauthorized; } - [GameEndpoint("unfavourite/slot/user/{idStr}", Method.Post)] - public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, string idStr) + [GameEndpoint("unfavourite/slot/user/{id}", Method.Post)] + public Response UnfavouriteLevel(RequestContext context, GameDatabaseContext database, GameUser user, int id) { - int.TryParse(idStr, out int id); - if (id == default) return BadRequest; - GameLevel? level = database.GetLevelById(id); if (level == null) return NotFound; @@ -84,12 +78,9 @@ public Response UnfavouriteUser(RequestContext context, GameDatabaseContext data return new SerializedFavouriteUserList(GameUserResponse.FromOldListWithExtraData(users, token.TokenGame).ToList(), users.Count); } - [GameEndpoint("lolcatftw/add/user/{idStr}", Method.Post)] - public Response QueueLevel(RequestContext context, GameDatabaseContext database, GameUser user, string idStr) + [GameEndpoint("lolcatftw/add/user/{id}", Method.Post)] + public Response QueueLevel(RequestContext context, GameDatabaseContext database, GameUser user, int id) { - int.TryParse(idStr, out int id); - if (id == default) return BadRequest; - GameLevel? level = database.GetLevelById(id); if (level == null) return NotFound; @@ -99,12 +90,9 @@ public Response QueueLevel(RequestContext context, GameDatabaseContext database, return Unauthorized; } - [GameEndpoint("lolcatftw/remove/user/{idStr}", Method.Post)] - public Response DequeueLevel(RequestContext context, GameDatabaseContext database, GameUser user, string idStr) + [GameEndpoint("lolcatftw/remove/user/{id}", Method.Post)] + public Response DequeueLevel(RequestContext context, GameDatabaseContext database, GameUser user, int id) { - int.TryParse(idStr, out int id); - if (id == default) return BadRequest; - GameLevel? level = database.GetLevelById(id); if (level == null) return NotFound; diff --git a/Refresh.GameServer/Endpoints/Game/ReportingEndpoints.cs b/Refresh.GameServer/Endpoints/Game/ReportingEndpoints.cs index 373b8753..29028c45 100644 --- a/Refresh.GameServer/Endpoints/Game/ReportingEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/ReportingEndpoints.cs @@ -10,8 +10,13 @@ namespace Refresh.GameServer.Endpoints.Game; public class ReportingEndpoints : EndpointGroup { [GameEndpoint("grief", Method.Post, ContentType.Xml)] - public Response UploadReport(RequestContext context, GameDatabaseContext database, GameReport body) + 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) + { + return BadRequest; + } + database.AddGriefReport(body); return OK; diff --git a/Refresh.GameServer/Endpoints/Game/ResourceEndpoints.cs b/Refresh.GameServer/Endpoints/Game/ResourceEndpoints.cs index 84f5edd4..22759743 100644 --- a/Refresh.GameServer/Endpoints/Game/ResourceEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/ResourceEndpoints.cs @@ -9,6 +9,7 @@ using Refresh.GameServer.Configuration; using Refresh.GameServer.Database; using Refresh.GameServer.Importing; +using Refresh.GameServer.Time; using Refresh.GameServer.Types.Assets; using Refresh.GameServer.Types.Lists; using Refresh.GameServer.Types.Roles; @@ -23,7 +24,7 @@ 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) + GameDatabaseContext database, GameUser user, AssetImporter importer, GameServerConfig config, IDateTimeProvider timeProvider) { if (dataStore.ExistsInStore(hash)) return Conflict; @@ -32,6 +33,8 @@ public Response UploadAsset(RequestContext context, string hash, string type, by if (gameAsset == null) return BadRequest; + gameAsset.UploadDate = DateTimeOffset.FromUnixTimeSeconds(Math.Clamp(gameAsset.UploadDate.ToUnixTimeSeconds(), timeProvider.EarliestDate, timeProvider.TimestampSeconds)); + // 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) diff --git a/Refresh.GameServer/Endpoints/Game/UserEndpoints.cs b/Refresh.GameServer/Endpoints/Game/UserEndpoints.cs index e4f3705c..ebe2398d 100644 --- a/Refresh.GameServer/Endpoints/Game/UserEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/UserEndpoints.cs @@ -66,26 +66,19 @@ public SerializedUserList GetMultipleUsers(RequestContext context, GameDatabaseC // LBP is just fantastic man try { - XmlSerializer serializer = new(typeof(SerializedUpdateDataProfile)); - if (serializer.Deserialize(new StringReader(body)) is not SerializedUpdateDataProfile profileData) return null; + XmlSerializer profileSerializer = new(typeof(SerializedUpdateDataProfile)); + if (profileSerializer.Deserialize(new StringReader(body)) is not SerializedUpdateDataProfile profileData) return null; data = profileData; - } - catch - { - // ignored - } - - try - { - XmlSerializer serializer = new(typeof(SerializedUpdateDataPlanets)); - if (serializer.Deserialize(new StringReader(body)) is not SerializedUpdateDataPlanets planetsData) return null; + + XmlSerializer planetSerializer = new(typeof(SerializedUpdateDataPlanets)); + if (planetSerializer.Deserialize(new StringReader(body)) is not SerializedUpdateDataPlanets planetsData) return null; data = planetsData; } catch { // ignored } - + if (data == null) { database.AddErrorNotification("Profile update failed", "Your profile failed to update because the data could not be read.", user); @@ -104,6 +97,13 @@ public SerializedUserList GetMultipleUsers(RequestContext context, GameDatabaseC return null; } + const int maxDescriptionLength = 4096; + if (data.Description is { Length: > maxDescriptionLength }) + { + database.AddErrorNotification("Profile update failed", $"Your profile failed to update because the description was too long. The max length is {maxDescriptionLength}.", user); + return null; + } + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault switch (token.TokenGame) { @@ -140,7 +140,7 @@ public SerializedUserList GetMultipleUsers(RequestContext context, GameDatabaseC database.AddErrorNotification("Pin sync failed", "Your pins failed to update because the data could not be read.", user); return null; } - + //NOTE: the returned value in the packet capture has a few higher values than the ones sent in the request, // so im not sure what we are supposed to return here, so im just passing it through with `profile_pins` nulled out database.UpdateUserPins(user, updateUserPins); @@ -173,28 +173,10 @@ public string Filter(RequestContext context, string body, GameUser user) Debug.Assert(user != null); Debug.Assert(body != null); - string msg = $"<{user}>: {body}"; // For some reason, the logger breaks if we put this directly into the call - try - { - context.Logger.LogInfo(BunkumContext.Filter, msg); - } - catch(Exception e) - { - // FIXME: workaround heisenbug - // this shouldn't crash but does somehow - /* -[02/24/23 10:40:42] [Request:Trace] Handling request with UserEndpoints.Filter -[02/24/23 10:40:42] [Request:Error] <d__19:MoveNext> System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. - ---> System.NullReferenceException: Object reference not set to an instance of an object. - at NotEnoughLogs.TraceHelper.GetTrace(Int32 depth, Int32 extraTraceLines) - at NotEnoughLogs.LoggerContainer`1.LogInfo(TContext context, String message) - at Refresh.GameServer.Endpoints.Game.UserEndpoints.Filter(RequestContext context, String body, GameUser user) in /home/jvyden/Documents/Refresh/Refresh.GameServer/Endpoints/Game/UserEndpoints.cs:line 128 - at InvokeStub_UserEndpoints.Filter(Object, Object, IntPtr*) - at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr) - */ - Console.WriteLine("HIT FILTER BUG: " + e); - if(Debugger.IsAttached) Debugger.Break(); - } + context.Logger.LogInfo(BunkumContext.Filter, $"<{user}>: {body}"); + + //TODO: Add filtering + return body; } } \ No newline at end of file diff --git a/Refresh.GameServer/RefreshGameServer.cs b/Refresh.GameServer/RefreshGameServer.cs index 8cc50f5f..15b19b47 100644 --- a/Refresh.GameServer/RefreshGameServer.cs +++ b/Refresh.GameServer/RefreshGameServer.cs @@ -19,6 +19,7 @@ using Refresh.GameServer.Importing; using Refresh.GameServer.Middlewares; using Refresh.GameServer.Services; +using Refresh.GameServer.Time; using Refresh.GameServer.Types.Levels.Categories; using Refresh.GameServer.Types.Roles; using Refresh.GameServer.Types.UserData; @@ -113,7 +114,7 @@ protected virtual void SetupConfiguration() this._server.UseConfig(integrationConfig); this._server.UseJsonConfig("rpc.json"); } - + protected virtual void SetupServices() { this._server.AddRateLimitService(new RateLimitSettings(60, 400, 30, "global")); @@ -133,6 +134,7 @@ protected virtual void SetupServices() this._server.AddService(); this._server.AddService(); + this._server.AddService(this.GetTimeProvider()); if (this._config!.TrackRequestStatistics) this._server.AddService(); @@ -174,6 +176,11 @@ private GameDatabaseContext InitializeDatabase() this._databaseProvider.Initialize(); return this._databaseProvider.GetContext(); } + + protected virtual IDateTimeProvider GetTimeProvider() + { + return new SystemDateTimeProvider(); + } public void ImportAssets(bool force = false) { diff --git a/Refresh.GameServer/Services/MatchService.cs b/Refresh.GameServer/Services/MatchService.cs index d32c43bb..d68d3906 100644 --- a/Refresh.GameServer/Services/MatchService.cs +++ b/Refresh.GameServer/Services/MatchService.cs @@ -35,14 +35,14 @@ public IEnumerable Rooms public MatchService(LoggerContainer logger) : base(logger) {} - public GameRoom GetOrCreateRoomByPlayer(GameUser player, TokenPlatform platform, TokenGame game) + public GameRoom GetOrCreateRoomByPlayer(GameUser player, TokenPlatform platform, TokenGame game, NatType natType) { GameRoom? room = this.GetRoomByPlayer(player, platform, game); // ReSharper disable once InvertIf (happy path goes last) if (room == null) { - room = new GameRoom(player, platform, game); + room = new GameRoom(player, platform, game, natType); this._rooms.Add(room); } diff --git a/Refresh.GameServer/Services/TimeProviderService.cs b/Refresh.GameServer/Services/TimeProviderService.cs new file mode 100644 index 00000000..aa7cea63 --- /dev/null +++ b/Refresh.GameServer/Services/TimeProviderService.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using Bunkum.CustomHttpListener.Request; +using Bunkum.HttpServer; +using Bunkum.HttpServer.Database; +using Bunkum.HttpServer.Services; +using NotEnoughLogs; +using Refresh.GameServer.Time; + +namespace Refresh.GameServer.Services; + +public class TimeProviderService : Service +{ + private readonly IDateTimeProvider _timeProvider; + + internal TimeProviderService(LoggerContainer logger, IDateTimeProvider timeProvider) : base(logger) + { + this._timeProvider = timeProvider; + + } + + public override object? AddParameterToEndpoint(ListenerContext context, ParameterInfo paramInfo, Lazy database) + { + if (paramInfo.ParameterType == typeof(IDateTimeProvider)) + { + return this._timeProvider; + } + + return null; + } +} \ No newline at end of file diff --git a/Refresh.GameServer/Time/IDateTimeProvider.cs b/Refresh.GameServer/Time/IDateTimeProvider.cs index 6b9778d2..515bc206 100644 --- a/Refresh.GameServer/Time/IDateTimeProvider.cs +++ b/Refresh.GameServer/Time/IDateTimeProvider.cs @@ -4,5 +4,9 @@ public interface IDateTimeProvider { public long TimestampMilliseconds { get; } public long TimestampSeconds { get; } + /// + /// The earliest acceptable date, in unix seconds + /// + public long EarliestDate { get; } public DateTimeOffset Now { get; } } \ No newline at end of file diff --git a/Refresh.GameServer/Time/SystemDateTimeProvider.cs b/Refresh.GameServer/Time/SystemDateTimeProvider.cs index b1c1f7f3..a5d36a37 100644 --- a/Refresh.GameServer/Time/SystemDateTimeProvider.cs +++ b/Refresh.GameServer/Time/SystemDateTimeProvider.cs @@ -4,5 +4,8 @@ public class SystemDateTimeProvider : IDateTimeProvider { public long TimestampMilliseconds => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); public long TimestampSeconds => DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + + public long EarliestDate => new DateTimeOffset(2007, 1, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeSeconds(); + public DateTimeOffset Now => DateTimeOffset.Now; } \ No newline at end of file diff --git a/Refresh.GameServer/Types/Comments/GameComment.cs b/Refresh.GameServer/Types/Comments/GameComment.cs index 142df395..c9602e67 100644 --- a/Refresh.GameServer/Types/Comments/GameComment.cs +++ b/Refresh.GameServer/Types/Comments/GameComment.cs @@ -14,7 +14,10 @@ public partial class GameComment : IRealmObject, ISequentialId, INeedsPreparatio [XmlIgnore] public GameUser Author { get; set; } = null!; [XmlElement("message")] public string Content { get; set; } = string.Empty; - [XmlElement("timestamp")] public long Timestamp { get; set; } // Unix Milliseconds + /// + /// Timestamp in Unix miliseconds + /// + [XmlElement("timestamp")] public long Timestamp { get; set; } #region LBP Serialization Quirks diff --git a/Refresh.GameServer/Types/Matching/GameRoom.cs b/Refresh.GameServer/Types/Matching/GameRoom.cs index 24e03b56..71170717 100644 --- a/Refresh.GameServer/Types/Matching/GameRoom.cs +++ b/Refresh.GameServer/Types/Matching/GameRoom.cs @@ -8,11 +8,12 @@ namespace Refresh.GameServer.Types.Matching; public class GameRoom { - public GameRoom(GameUser host, TokenPlatform platform, TokenGame game) + public GameRoom(GameUser host, TokenPlatform platform, TokenGame game, NatType natType) { this.PlayerIds.Add(new GameRoomPlayer(host.Username, host.UserId)); this.Platform = platform; this.Game = game; + this.NatType = natType; } public readonly ObjectId RoomId = ObjectId.GenerateNewId(); @@ -23,6 +24,8 @@ public GameRoom(GameUser host, TokenPlatform platform, TokenGame game) public readonly TokenPlatform Platform; public readonly TokenGame Game; + public readonly NatType NatType; + public DateTimeOffset LastContact; public List GetPlayers(GameDatabaseContext database) => diff --git a/Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs b/Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs index 8dcccfd5..6ea7bf70 100644 --- a/Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs +++ b/Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs @@ -20,6 +20,11 @@ public Response Execute(MatchService service, LoggerContainer log Token token, SerializedRoomData body) { + if (body.NatType is not { Count: 1 }) + { + return BadRequest; + } + GameRoom? usersRoom = service.GetRoomByPlayer(user, token.TokenPlatform, token.TokenGame); if (usersRoom == null) return BadRequest; // user should already have a room. @@ -35,12 +40,20 @@ public Response Execute(MatchService service, LoggerContainer log levelId = body.Slots[1]; } + //TODO: add user option to filter rooms by language + List rooms = service.Rooms.Where(r => r.RoomId != usersRoom.RoomId && r.Platform == usersRoom.Platform && (levelId == null || r.LevelId == levelId)) .OrderByDescending(r => r.RoomMood) .ToList(); + //When a user is behind a Strict NAT layer, we can only connect them to players with Open NAT types + if (body.NatType[0] == NatType.Strict) + { + rooms = rooms.Where(r => r.NatType == NatType.Open).ToList(); + } + if (rooms.Count <= 0) { return NotFound; // TODO: update this response, shouldn't be 404 diff --git a/Refresh.GameServer/Types/Matching/MatchMethods/UpdatePlayersInRoomMethod.cs b/Refresh.GameServer/Types/Matching/MatchMethods/UpdatePlayersInRoomMethod.cs index cb742161..1eefae48 100644 --- a/Refresh.GameServer/Types/Matching/MatchMethods/UpdatePlayersInRoomMethod.cs +++ b/Refresh.GameServer/Types/Matching/MatchMethods/UpdatePlayersInRoomMethod.cs @@ -17,8 +17,13 @@ public Response Execute(MatchService service, LoggerContainer log Token token, SerializedRoomData body) { + if (body.NatType is not { Count: 1 }) + { + return BadRequest; + } + if (body.Players == null) return BadRequest; - GameRoom room = service.GetOrCreateRoomByPlayer(user, token.TokenPlatform, token.TokenGame); + GameRoom room = service.GetOrCreateRoomByPlayer(user, token.TokenPlatform, token.TokenGame, body.NatType[0]); room.LastContact = DateTimeOffset.Now; diff --git a/Refresh.GameServer/Types/Matching/MatchMethods/UpdateRoomDataMethod.cs b/Refresh.GameServer/Types/Matching/MatchMethods/UpdateRoomDataMethod.cs index 84f0faf8..2ae1c9b8 100644 --- a/Refresh.GameServer/Types/Matching/MatchMethods/UpdateRoomDataMethod.cs +++ b/Refresh.GameServer/Types/Matching/MatchMethods/UpdateRoomDataMethod.cs @@ -15,7 +15,12 @@ public class UpdateRoomDataMethod : IMatchMethod public Response Execute(MatchService service, LoggerContainer logger, GameDatabaseContext database, GameUser user, Token token, SerializedRoomData body) { - GameRoom room = service.GetOrCreateRoomByPlayer(user, token.TokenPlatform, token.TokenGame); + if (body.NatType is not { Count: 1 }) + { + return BadRequest; + } + + GameRoom room = service.GetOrCreateRoomByPlayer(user, token.TokenPlatform, token.TokenGame, body.NatType[0]); if (room.HostId.Id != user.UserId) return Unauthorized; room.LastContact = DateTimeOffset.Now; diff --git a/Refresh.GameServer/Types/Report/GameReport.cs b/Refresh.GameServer/Types/Report/GameReport.cs index 42a16b32..8a855632 100644 --- a/Refresh.GameServer/Types/Report/GameReport.cs +++ b/Refresh.GameServer/Types/Report/GameReport.cs @@ -20,6 +20,11 @@ public InfoBubble[] InfoBubble set { this.InternalInfoBubble.Clear(); + + if (value == null) + { + return; + } foreach (InfoBubble infoBubble in value) this.InternalInfoBubble.Add(infoBubble); @@ -64,6 +69,11 @@ public Player[] Players set { this.InternalPlayers.Clear(); + + if (value == null) + { + return; + } foreach (Player player in value) this.InternalPlayers.Add(player); diff --git a/Refresh.GameServer/Types/Report/ScreenElements.cs b/Refresh.GameServer/Types/Report/ScreenElements.cs index 7b55e5b2..cc18283d 100644 --- a/Refresh.GameServer/Types/Report/ScreenElements.cs +++ b/Refresh.GameServer/Types/Report/ScreenElements.cs @@ -19,6 +19,11 @@ public Slot[] Slot { this.InternalSlot.Clear(); + if (value == null) + { + return; + } + foreach (Slot slot in value) this.InternalSlot.Add(slot); } @@ -35,6 +40,11 @@ public Player[] Player { this.InternalPlayer.Clear(); + if (value == null) + { + return; + } + foreach (Player player in value) this.InternalPlayer.Add(player); } diff --git a/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs b/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs index f6539a1f..169797e9 100644 --- a/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs +++ b/RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs @@ -5,6 +5,8 @@ using Refresh.GameServer; using Refresh.GameServer.Configuration; using Refresh.GameServer.Database; +using Refresh.GameServer.Time; +using RefreshTests.GameServer.Time; namespace RefreshTests.GameServer.GameServer; @@ -29,6 +31,10 @@ public override void Start() // this._workerManager.Start(); } + public IDateTimeProvider DateTimeProvider { get; set; } = new MockDateTimeProvider(); + + protected override IDateTimeProvider GetTimeProvider() => this.DateTimeProvider; + protected override void SetupMiddlewares() { diff --git a/RefreshTests.GameServer/Tests/Matching/MatchingTests.cs b/RefreshTests.GameServer/Tests/Matching/MatchingTests.cs index 450d11cc..a1dea5c4 100644 --- a/RefreshTests.GameServer/Tests/Matching/MatchingTests.cs +++ b/RefreshTests.GameServer/Tests/Matching/MatchingTests.cs @@ -16,7 +16,13 @@ public void CreatesRooms() MatchService match = new(Logger); match.Initialize(); - SerializedRoomData roomData = new(); + SerializedRoomData roomData = new() + { + NatType = new List + { + NatType.Open, + }, + }; GameUser user1 = context.CreateUser(); GameUser user2 = context.CreateUser(); @@ -53,18 +59,113 @@ public void DoesntMatchIfNoRooms() MatchService match = new(Logger); match.Initialize(); - SerializedRoomData roomData = new(); + SerializedRoomData roomData = new() + { + NatType = new List + { + NatType.Open, + }, + }; // Setup room GameUser user1 = context.CreateUser(); Token token1 = context.CreateToken(user1); match.ExecuteMethod("UpdateMyPlayerData", roomData, context.Database, user1, token1); - + // Tell user1 to try to find a room - Response response = match.ExecuteMethod("FindBestRoom", new SerializedRoomData(), context.Database, user1, token1); + Response response = match.ExecuteMethod("FindBestRoom", new SerializedRoomData + { + NatType = new List + { + NatType.Open, + }, + }, context.Database, user1, token1); Assert.That(response.StatusCode, Is.EqualTo(NotFound)); } + [Test] + public void StrictNatCantJoinStrict() + { + using TestContext context = this.GetServer(false); + MatchService match = new(Logger); + match.Initialize(); + + SerializedRoomData roomData = new() + { + Mood = (byte)RoomMood.AllowingAll, // Tells their rooms that they can be matched with each other + NatType = new List + { + NatType.Strict, + }, + }; + + // Setup rooms + GameUser user1 = context.CreateUser(); + GameUser user2 = context.CreateUser(); + + Token token1 = context.CreateToken(user1); + Token token2 = context.CreateToken(user2); + + match.ExecuteMethod("UpdateMyPlayerData", roomData, context.Database, user1, token1); + match.ExecuteMethod("UpdateMyPlayerData", roomData, context.Database, user2, token2); + + // Tell user2 to try to find a room + Response response = match.ExecuteMethod("FindBestRoom", new SerializedRoomData + { + NatType = new List { + NatType.Strict, + }, + }, context.Database, user2, token2); + // File.WriteAllBytes("/tmp/matchresp.json", response.Data); + Assert.That(response.StatusCode, Is.EqualTo(NotFound)); + } + + [Test] + public void StrictNatCanJoinOpen() + { + using TestContext context = this.GetServer(false); + MatchService match = new(Logger); + match.Initialize(); + + SerializedRoomData roomData = new() + { + Mood = (byte)RoomMood.AllowingAll, // Tells their rooms that they can be matched with each other + NatType = new List + { + NatType.Open, + }, + }; + + SerializedRoomData roomData2 = new() + { + Mood = (byte)RoomMood.AllowingAll, // Tells their rooms that they can be matched with each other + NatType = new List + { + NatType.Strict, + }, + }; + + // Setup rooms + GameUser user1 = context.CreateUser(); + GameUser user2 = context.CreateUser(); + + Token token1 = context.CreateToken(user1); + Token token2 = context.CreateToken(user2); + + match.ExecuteMethod("UpdateMyPlayerData", roomData, context.Database, user1, token1); + match.ExecuteMethod("UpdateMyPlayerData", roomData2, context.Database, user2, token2); + + // Tell user2 to try to find a room + Response response = match.ExecuteMethod("FindBestRoom", new SerializedRoomData + { + NatType = new List { + NatType.Strict, + }, + }, context.Database, user2, token2); + // File.WriteAllBytes("/tmp/matchresp.json", response.Data); + Assert.That(response.StatusCode, Is.EqualTo(OK)); + } + [Test] public void MatchesPlayersTogether() { @@ -75,6 +176,10 @@ public void MatchesPlayersTogether() SerializedRoomData roomData = new() { Mood = (byte)RoomMood.AllowingAll, // Tells their rooms that they can be matched with each other + NatType = new List + { + NatType.Open, + }, }; // Setup rooms @@ -88,7 +193,12 @@ public void MatchesPlayersTogether() match.ExecuteMethod("UpdateMyPlayerData", roomData, context.Database, user2, token2); // Tell user2 to try to find a room - Response response = match.ExecuteMethod("FindBestRoom", new SerializedRoomData(), context.Database, user2, token2); + Response response = match.ExecuteMethod("FindBestRoom", new SerializedRoomData + { + NatType = new List { + NatType.Open, + }, + }, context.Database, user2, token2); // File.WriteAllBytes("/tmp/matchresp.json", response.Data); Assert.That(response.StatusCode, Is.EqualTo(OK)); } diff --git a/RefreshTests.GameServer/Time/MockDateTimeProvider.cs b/RefreshTests.GameServer/Time/MockDateTimeProvider.cs index 2bd9bcf1..70cdddfd 100644 --- a/RefreshTests.GameServer/Time/MockDateTimeProvider.cs +++ b/RefreshTests.GameServer/Time/MockDateTimeProvider.cs @@ -6,5 +6,6 @@ public class MockDateTimeProvider : IDateTimeProvider { public long TimestampMilliseconds { get; set; } public long TimestampSeconds => TimestampMilliseconds / 1000; + public long EarliestDate => new DateTimeOffset(2007, 1, 1, 0, 0, 0, TimeSpan.Zero).ToUnixTimeSeconds(); public DateTimeOffset Now => DateTimeOffset.FromUnixTimeMilliseconds(TimestampMilliseconds); } \ No newline at end of file