diff --git a/Refresh.GameServer/Configuration/GameServerConfig.cs b/Refresh.GameServer/Configuration/GameServerConfig.cs index 0df8b4fa..d2d4bd21 100644 --- a/Refresh.GameServer/Configuration/GameServerConfig.cs +++ b/Refresh.GameServer/Configuration/GameServerConfig.cs @@ -9,7 +9,7 @@ namespace Refresh.GameServer.Configuration; [SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")] public class GameServerConfig : Config { - public override int CurrentConfigVersion => 12; + public override int CurrentConfigVersion => 13; public override int Version { get; set; } = 0; protected override void Migrate(int oldVer, dynamic oldConfig) {} @@ -28,6 +28,11 @@ protected override void Migrate(int oldVer, dynamic oldConfig) {} public bool RequireGameLoginToRegister { get; set; } = false; public bool TrackRequestStatistics { get; set; } = false; public string WebExternalUrl { get; set; } = "https://refresh.example.com"; + /// + /// The base URL that LBP3 uses to grab config files like `network_settings.nws`. + /// URL must point to a HTTPS capable server with TLSv1.2 connectivity, the game will automatically correct HTTP to HTTPS. + /// + public string GameConfigStorageUrl { get; set; } = "https://refresh.example.com/lbp"; public bool AllowInvalidTextureGuids { get; set; } = false; public bool BlockAssetUploads { get; set; } = false; /// diff --git a/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs index ab7604a1..1d0b83e5 100644 --- a/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Handshake/AuthenticationEndpoints.cs @@ -179,6 +179,7 @@ public class AuthenticationEndpoints : EndpointGroup { TokenData = "MM_AUTH=" + token.TokenData, ServerBrand = $"{config.InstanceName} (Refresh {VersionInformation.Version})", + TitleStorageUrl = config.GameConfigStorageUrl, }; } @@ -286,6 +287,9 @@ public struct FullLoginResponse [XmlElement("lbpEnvVer")] public string ServerBrand { get; set; } + + [XmlElement("titleStorageURL")] + public string TitleStorageUrl { get; set; } } [XmlRoot("authTicket")] diff --git a/Refresh.GameServer/Endpoints/Game/Handshake/MetadataEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Handshake/MetadataEndpoints.cs index 84f279fb..0a2f5540 100644 --- a/Refresh.GameServer/Endpoints/Game/Handshake/MetadataEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Handshake/MetadataEndpoints.cs @@ -1,12 +1,14 @@ using System.Xml.Serialization; using Bunkum.Core; using Bunkum.Core.Endpoints; +using Bunkum.Core.Endpoints.Debugging; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.GameServer.Database; -using Refresh.GameServer.Services; +using Refresh.GameServer.Time; using Refresh.GameServer.Types; +using Refresh.GameServer.Types.Challenges; using Refresh.GameServer.Types.Roles; using Refresh.GameServer.Types.UserData; @@ -70,9 +72,12 @@ public string NetworkSettings(RequestContext context) // Only log this warning once if(!created && networkSettings == null) context.Logger.LogWarning(BunkumCategory.Request, "network_settings.nws file is missing! " + - "LBP will work without it, but it may be relevant to you if you are an advanced user."); + "We've defaulted to one with sane defaults, but it may be relevant to write your own if you are an advanced user. " + + "If everything works the way you like, you can safely ignore this warning."); - networkSettings ??= "ShowLevelBoos true\nAllowOnlineCreate true"; + // EnableHackChecks being false fixes the "There was a problem with the level you were playing on that forced a return to your Pod." error that LBP3 tends to show in the pod. + // EnableDiveIn being true enables dive in for LBP3 + networkSettings ??= "ShowLevelBoos true\nAllowOnlineCreate true\nEnableDiveIn true\nEnableHackChecks false\n"; return networkSettings; } @@ -126,4 +131,67 @@ private static readonly Lazy PromotionsFile return promotions; } + + [GameEndpoint("farc_hashes")] + [MinimumRole(GameUserRole.Restricted)] + //Stubbed to return a 410 Gone, so LBP3 doesn't spam us. + //The game doesn't actually use this information for anything, so we don't allow server owners to replace this. + public Response FarcHashes(RequestContext context) => Gone; + + //TODO: In the future this should allow you to have separate files per language since the game sends the language through the `language` query parameter. + private static readonly Lazy DeveloperVideosFile + = new(() => + { + string path = Path.Combine(Environment.CurrentDirectory, "developer_videos.xml"); + + return File.Exists(path) ? File.ReadAllText(path) : null; + }); + + [GameEndpoint("developer_videos")] + [MinimumRole(GameUserRole.Restricted)] + [NullStatusCode(OK)] + public string? DeveloperVideos(RequestContext context) + { + bool created = DeveloperVideosFile.IsValueCreated; + + string? developerVideos = DeveloperVideosFile.Value; + + // Only log this warning once + if(!created && developerVideos == null) + context.Logger.LogWarning(BunkumCategory.Request, "developer_videos.xml file is missing! " + + "LBP will work without it, but it may be relevant to you if you are an advanced user."); + + return developerVideos; + } + + [GameEndpoint("gameState", ContentType.Plaintext, HttpMethods.Post)] + [MinimumRole(GameUserRole.Restricted)] + // It's unknown what an "invalid" result/state would be. + // Since it sends information like the current create mode tool in use, + // maybe it was used as a server-side anti-cheat to detect hacks/cheats? + // The packet captures show `VALID` being returned, so we stub this method to that. + // + // Example request bodies: + // {"currentLevel": ["pod", 0],"participants": ["turecross321","","",""]} + // {"currentLevel": ["user_local", 59],"inCreateMode": true,"participants": ["turecross321","","",""],"selectedCreateTool": ""} + // {"currentLevel": ["developer_adventure_planet", 349],"inStore": true,"participants": ["turecross321","","",""]} + // {"highlightedSearchResult": ["level",811],"currentLevel": ["pod", 0],"inStore": true,"participants": ["turecross321","","",""]} + public string GameState(RequestContext context) => "VALID"; + + [GameEndpoint("ChallengeConfig.xml", ContentType.Xml)] + public SerializedGameChallengeList ChallengeConfig(RequestContext context, IDateTimeProvider timeProvider) + { + //TODO: allow this to be controlled by the server owner, right now lets just send the game 0 challenges, + // so nothing appears in the challenges menu + return new SerializedGameChallengeList + { + TotalChallenges = 0, + EndTime = (ulong)(timeProvider.Now.ToUnixTimeMilliseconds() * 1000), + BronzeRankPercentage = 0, + SilverRankPercentage = 0, + GoldRankPercentage = 0, + CycleTime = 0, + Challenges = [], + }; + } } \ No newline at end of file diff --git a/Refresh.GameServer/Services/MatchService.cs b/Refresh.GameServer/Services/MatchService.cs index 2036f703..61d433ec 100644 --- a/Refresh.GameServer/Services/MatchService.cs +++ b/Refresh.GameServer/Services/MatchService.cs @@ -4,11 +4,12 @@ using NotEnoughLogs; using System.Reflection; using Bunkum.Core.Responses; -using MongoDB.Bson; +using Bunkum.Listener.Protocol; using Refresh.GameServer.Authentication; using Refresh.GameServer.Database; using Refresh.GameServer.Types.Matching; using Refresh.GameServer.Types.Matching.MatchMethods; +using Refresh.GameServer.Types.Matching.Responses; using Refresh.GameServer.Types.Matching.RoomAccessors; using Refresh.GameServer.Types.UserData; @@ -116,7 +117,20 @@ public Response ExecuteMethod(string methodStr, SerializedRoomData roomData, Gam { IMatchMethod? method = this.TryGetMatchMethod(methodStr); if (method == null) return BadRequest; - - return method.Execute(this, this.Logger, database, user, token, roomData); + + Response response = method.Execute(this, this.Logger, database, user, token, roomData); + + // If there's a response data specified, then there's nothing more we need to do + if (response.Data.Length != 0) + return response; + + // If there's no response body, then we need to make our own using the status code + SerializedStatusCodeMatchResponse status = new((int)response.StatusCode); + + List responseData = [status]; + + response = new Response(responseData, ContentType.Json, response.StatusCode); + + return response; } } \ No newline at end of file diff --git a/Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs b/Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs new file mode 100644 index 00000000..0f65b4b4 --- /dev/null +++ b/Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs @@ -0,0 +1,107 @@ +using System.Xml.Serialization; + +namespace Refresh.GameServer.Types.Challenges; + +[XmlRoot("Challenge_header")] +public class SerializedGameChallengeList +{ + [XmlElement("Total_challenges")] + public int TotalChallenges { get; set; } + + /// + /// Timestamp is stored as a unix epoch in microseconds, is equal to the very last challenge's end date + /// + [XmlElement("Challenge_End_Date")] + public ulong EndTime { get; set; } + + /// + /// Percentage required to get bronze, stored as a float 0-1 + /// + [XmlElement("Challenge_Top_Rank_Bronze_Range")] + public float BronzeRankPercentage { get; set; } + + /// + /// Percentage required to get silver, stored as a float 0-1 + /// + [XmlElement("Challenge_Top_Rank_Silver_Range")] + public float SilverRankPercentage { get; set; } + + /// + /// Percentage required to get gold, stored as a float 0-1 + /// + [XmlElement("Challenge_Top_Rank_Gold_Range")] + public float GoldRankPercentage { get; set; } + + /// + /// Cycle time stored as a unix epoch in microseconds + /// + [XmlElement("Challenge_CycleTime")] + public ulong CycleTime { get; set; } + + // ReSharper disable once IdentifierTypo + [XmlElement("item_data")] + public List Challenges { get; set; } +} + +public class SerializedGameChallenge +{ + /// + /// A sequential ID from 0 of which challenge this is + /// + [XmlAttribute("Challenge_ID")] + public int Id { get; set; } + + /// + /// A unix epoch timestamp in microseconds for when this challenge starts + /// + [XmlAttribute("Challenge_active_date_starts")] + public ulong StartTime { get; set; } + + /// + /// A unix epoct timestamp in microseconds for when this challenge ends + /// + [XmlAttribute("Challenge_active_date_ends")] + public ulong EndTime { get; set; } + + /// + /// The LAMS description ID for this challenge's description + /// + [XmlAttribute("Challenge_LAMSDescription_Id")] + public string LamsDescriptionId { get; set; } + + /// + /// The LAMS description ID for this challenge's title + /// + [XmlAttribute("Challenge_LAMSTitle_Id")] + public string LamsTitleId { get; set; } + + /// + /// The ID of the pin you receive for completing this challenge + /// + [XmlAttribute("Challenge_PinId")] + public ulong PinId { get; set; } + + [XmlAttribute("Challenge_RankPin")] + public ulong RankPin { get; set; } + + /// + /// A PSN DLC id for the DLC associated with this challenge + /// + [XmlAttribute("Challenge_Content")] + public string Content { get; set; } + + /// + /// The LAMS translation ID for this challenge's content + /// + [XmlAttribute("Challenge_Content_name")] + public string ContentName { get; set; } + + [XmlAttribute("Challenge_Planet_User")] + public string PlanetUser { get; set; } + + [XmlAttribute("Challenge_planetId")] + public ulong PlanetId { get; set; } + + [XmlAttribute("Challenge_photo_1")] + public ulong PhotoId { get; set; } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs b/Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs index bc36e87b..f3619fbe 100644 --- a/Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs +++ b/Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs @@ -124,11 +124,11 @@ public Response Execute(MatchService service, Logger logger, GameDatabaseContext SerializedStatusCodeMatchResponse status = new(200); - List response = new(2) - { + List response = + [ status, roomMatch, - }; + ]; return new Response(response, ContentType.Json); }