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