Skip to content

Commit

Permalink
LBP3 batch fixes (#450)
Browse files Browse the repository at this point in the history
Closes #440 
Closes #439
Closes #428
Closes #427 
Closes #398 

#426 stays open until we allow users to specify challenges (ideally
through the database and a site UI)
  • Loading branch information
jvyden authored Apr 30, 2024
2 parents 36b5b7c + 95b924f commit dd81834
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 10 deletions.
7 changes: 6 additions & 1 deletion Refresh.GameServer/Configuration/GameServerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand All @@ -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";
/// <summary>
/// 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.
/// </summary>
public string GameConfigStorageUrl { get; set; } = "https://refresh.example.com/lbp";
public bool AllowInvalidTextureGuids { get; set; } = false;
public bool BlockAssetUploads { get; set; } = false;
/// <seealso cref="GameUserRole.Trusted"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ public class AuthenticationEndpoints : EndpointGroup
{
TokenData = "MM_AUTH=" + token.TokenData,
ServerBrand = $"{config.InstanceName} (Refresh {VersionInformation.Version})",
TitleStorageUrl = config.GameConfigStorageUrl,
};
}

Expand Down Expand Up @@ -286,6 +287,9 @@ public struct FullLoginResponse

[XmlElement("lbpEnvVer")]
public string ServerBrand { get; set; }

[XmlElement("titleStorageURL")]
public string TitleStorageUrl { get; set; }
}

[XmlRoot("authTicket")]
Expand Down
74 changes: 71 additions & 3 deletions Refresh.GameServer/Endpoints/Game/Handshake/MetadataEndpoints.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -126,4 +131,67 @@ private static readonly Lazy<string?> 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<string?> 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 = [],
};
}
}
20 changes: 17 additions & 3 deletions Refresh.GameServer/Services/MatchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<object> responseData = [status];

response = new Response(responseData, ContentType.Json, response.StatusCode);

return response;
}
}
107 changes: 107 additions & 0 deletions Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs
Original file line number Diff line number Diff line change
@@ -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; }

/// <summary>
/// Timestamp is stored as a unix epoch in microseconds, is equal to the very last challenge's end date
/// </summary>
[XmlElement("Challenge_End_Date")]
public ulong EndTime { get; set; }

/// <summary>
/// Percentage required to get bronze, stored as a float 0-1
/// </summary>
[XmlElement("Challenge_Top_Rank_Bronze_Range")]
public float BronzeRankPercentage { get; set; }

/// <summary>
/// Percentage required to get silver, stored as a float 0-1
/// </summary>
[XmlElement("Challenge_Top_Rank_Silver_Range")]
public float SilverRankPercentage { get; set; }

/// <summary>
/// Percentage required to get gold, stored as a float 0-1
/// </summary>
[XmlElement("Challenge_Top_Rank_Gold_Range")]
public float GoldRankPercentage { get; set; }

/// <summary>
/// Cycle time stored as a unix epoch in microseconds
/// </summary>
[XmlElement("Challenge_CycleTime")]
public ulong CycleTime { get; set; }

// ReSharper disable once IdentifierTypo
[XmlElement("item_data")]
public List<SerializedGameChallenge> Challenges { get; set; }

Check warning on line 43 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'Challenges' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 43 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'Challenges' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
}

public class SerializedGameChallenge
{
/// <summary>
/// A sequential ID from 0 of which challenge this is
/// </summary>
[XmlAttribute("Challenge_ID")]
public int Id { get; set; }

/// <summary>
/// A unix epoch timestamp in microseconds for when this challenge starts
/// </summary>
[XmlAttribute("Challenge_active_date_starts")]
public ulong StartTime { get; set; }

/// <summary>
/// A unix epoct timestamp in microseconds for when this challenge ends
/// </summary>
[XmlAttribute("Challenge_active_date_ends")]
public ulong EndTime { get; set; }

/// <summary>
/// The LAMS description ID for this challenge's description
/// </summary>
[XmlAttribute("Challenge_LAMSDescription_Id")]
public string LamsDescriptionId { get; set; }

Check warning on line 70 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'LamsDescriptionId' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 70 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Release Built Artifacts

Non-nullable property 'LamsDescriptionId' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 70 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'LamsDescriptionId' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

/// <summary>
/// The LAMS description ID for this challenge's title
/// </summary>
[XmlAttribute("Challenge_LAMSTitle_Id")]
public string LamsTitleId { get; set; }

Check warning on line 76 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'LamsTitleId' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 76 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Release Built Artifacts

Non-nullable property 'LamsTitleId' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 76 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'LamsTitleId' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

/// <summary>
/// The ID of the pin you receive for completing this challenge
/// </summary>
[XmlAttribute("Challenge_PinId")]
public ulong PinId { get; set; }

[XmlAttribute("Challenge_RankPin")]
public ulong RankPin { get; set; }

/// <summary>
/// A PSN DLC id for the DLC associated with this challenge
/// </summary>
[XmlAttribute("Challenge_Content")]
public string Content { get; set; }

Check warning on line 91 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'Content' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 91 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'Content' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

/// <summary>
/// The LAMS translation ID for this challenge's content
/// </summary>
[XmlAttribute("Challenge_Content_name")]
public string ContentName { get; set; }

Check warning on line 97 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'ContentName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 97 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'ContentName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

[XmlAttribute("Challenge_Planet_User")]
public string PlanetUser { get; set; }

Check warning on line 100 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'PlanetUser' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 100 in Refresh.GameServer/Types/Challenges/SerializedGameChallengeList.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'PlanetUser' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

[XmlAttribute("Challenge_planetId")]
public ulong PlanetId { get; set; }

[XmlAttribute("Challenge_photo_1")]
public ulong PhotoId { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ public Response Execute(MatchService service, Logger logger, GameDatabaseContext

SerializedStatusCodeMatchResponse status = new(200);

List<object> response = new(2)
{
List<object> response =
[
status,
roomMatch,
};
];

return new Response(response, ContentType.Json);
}
Expand Down

0 comments on commit dd81834

Please sign in to comment.