diff --git a/Refresh.Database/Models/Pins/ServerPins.cs b/Refresh.Database/Models/Pins/ServerPins.cs
index 3a6624fb0..b820509dd 100644
--- a/Refresh.Database/Models/Pins/ServerPins.cs
+++ b/Refresh.Database/Models/Pins/ServerPins.cs
@@ -1,7 +1,8 @@
namespace Refresh.Database.Models.Pins;
///
-/// The progress types of pins which have to be awarded manually by the server.
+/// The progress types of pins which have to either be awarded manually by the server,
+/// or be returned/used elsewhere (e.g. LBP3 challenges)
///
public enum ServerPins : long
{
@@ -15,4 +16,47 @@ public enum ServerPins : long
SignIntoWebsite = 2691148325,
HeartPlayerOnWebsite = 1965011384,
QueueLevelOnWebsite = 2833810997,
+
+ // LBP3 challenges
+ OverLineLbp3ChallengeMedal = 3003874881,
+ OverLineLbp3ChallengeRanking = 2922567456,
+
+ PixelPaceLbp3ChallengeMedal = 282407472,
+ PixelPaceLbp3ChallengeRanking = 3340696069,
+
+ RabbitBoxingLbp3ChallengeMedal = 2529088759,
+ RabbitBoxingLbp3ChallengeRanking = 958144818,
+
+ FloatyFluidLbp3ChallengeMedal = 183892581,
+ FloatyFluidLbp3ChallengeRanking = 3442917932,
+
+ ToggleIslandLbp3ChallengeMedal = 315245769,
+ ToggleIslandLbp3ChallengeRanking = 443310584,
+
+ SpaceDodgeballLbp3ChallengeMedal = 144212050,
+ SpaceDodgeballLbp3ChallengeRanking = 2123417147,
+
+ InvisibleMazeLbp3ChallengeMedal = 249569175,
+ InvisibleMazeLbp3ChallengeRanking = 1943114258,
+
+ HoverboardLbp3ChallengeMedal = 3478661003,
+ HoverboardLbp3ChallengeRanking = 592022798,
+
+ WhoopTowerLbp3ChallengeMedal = 216730878,
+ WhoopTowerLbp3ChallengeRanking = 545532447,
+
+ SwoopPanelsLbp3ChallengeMedal = 2054302637,
+ SwoopPanelsLbp3ChallengeRanking = 3288689476,
+
+ PinballLbp3ChallengeMedal = 618998172,
+ PinballLbp3ChallengeRanking = 4087839785,
+
+ TieSkipLbp3ChallengeMedal = 3953447125,
+ TieSkipLbp3ChallengeRanking = 2556445436,
+
+ JokerLbp3ChallengeMedal = 1093784294,
+ JokerLbp3ChallengeRanking = 1757295127,
+
+ CherryShooterLbp3ChallengeMedal = 1568570416,
+ CherryShooterLbp3ChallengeRanking = 3721717765,
}
\ No newline at end of file
diff --git a/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs
index b211203be..483a02e44 100644
--- a/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs
+++ b/Refresh.Interfaces.Game/Endpoints/Handshake/MetadataEndpoints.cs
@@ -1,7 +1,9 @@
+using System.Xml.Serialization;
using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Bunkum.Core.Endpoints.Debugging;
using Bunkum.Core.Responses;
+using Bunkum.Core.Responses.Serialization;
using Bunkum.Listener.Protocol;
using Bunkum.Protocols.Http;
using Refresh.Common.Time;
@@ -191,23 +193,48 @@ private static readonly Lazy DeveloperVideosFile
// {"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";
+
+ private static readonly Lazy ChallengeConfigFile
+ = new(() =>
+ {
+ string path = Path.Combine(Environment.CurrentDirectory, "ChallengeConfig.xml");
+
+ return File.Exists(path) ? File.ReadAllText(path) : null;
+ });
[GameEndpoint("ChallengeConfig.xml", ContentType.Xml)]
[MinimumRole(GameUserRole.Restricted)]
- public SerializedLbp3ChallengeList ChallengeConfig(RequestContext context, IDateTimeProvider timeProvider)
+ public string ChallengeConfig(RequestContext context)
{
- //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 SerializedLbp3ChallengeList
+ bool created = ChallengeConfigFile.IsValueCreated;
+ string? challengeConfig = ChallengeConfigFile.Value;
+
+ // If file was read, return its contents. Else serialize and return a hard-coded default list of challenges.
+ if (challengeConfig != null)
{
- TotalChallenges = 0,
- EndTime = (ulong)(timeProvider.Now.ToUnixTimeMilliseconds() * 1000),
- BronzeRankPercentage = 0,
- SilverRankPercentage = 0,
- GoldRankPercentage = 0,
- CycleTime = 0,
- Challenges = [],
- };
+ return challengeConfig;
+ }
+ else
+ {
+ // Only log this warning once
+ if (!created) context.Logger.LogWarning(BunkumCategory.Request,
+ "ChallengeConfig.xml file is missing! We've defaulted to one which is loosely based off of the official server's config, "+
+ "but it might be relevant to you if you are an advanced user.");
+
+ using MemoryStream ms = new();
+ using BunkumXmlTextWriter bunkumXmlTextWriter = new(ms);
+
+ XmlSerializerNamespaces namespaces = new();
+ namespaces.Add("", "");
+
+ XmlSerializer serializer = new(typeof(SerializedLbp3ChallengeList));
+ serializer.Serialize(bunkumXmlTextWriter, SerializedLbp3ChallengeList.Default, namespaces);
+
+ ms.Seek(0, SeekOrigin.Begin);
+ using StreamReader reader = new(ms);
+
+ return reader.ReadToEnd();
+ }
}
[GameEndpoint("tags")]
diff --git a/Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLevelChallenge.cs b/Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLbp3Challenge.cs
similarity index 73%
rename from Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLevelChallenge.cs
rename to Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLbp3Challenge.cs
index 05b5ad58e..5f7ef66fa 100644
--- a/Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLevelChallenge.cs
+++ b/Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLbp3Challenge.cs
@@ -37,13 +37,20 @@ public class SerializedLbp3Challenge
public string LamsTitleId { get; set; }
///
- /// The ID of the pin you receive for completing this challenge
+ /// The progress type of the pin indicating whether the user's highscore is bronze, silver or gold (progress value is 1-3 accordingly)
///
[XmlAttribute("Challenge_PinId")]
- public ulong PinId { get; set; }
+ public ulong ScoreMedalPinProgressType { get; set; }
+ ///
+ /// The progress type of the Pin indicating whether the user is in the top 50%, 25% or 10% of the leaderboard
+ ///
+ // TODO: As soon as score submission for adventures/LBP3 challenges is implemented,
+ // find out whether these need to be awarded by the server, and also find out
+ // whether these pins have a descending progress and special case them
+ // in the progress update method if they do.
[XmlAttribute("Challenge_RankPin")]
- public ulong RankPin { get; set; }
+ public ulong ScoreRankingPinProgressType { get; set; }
///
/// A PSN DLC id for the DLC associated with this challenge
diff --git a/Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLbp3ChallengeList.cs b/Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLbp3ChallengeList.cs
new file mode 100644
index 000000000..345b15bd9
--- /dev/null
+++ b/Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLbp3ChallengeList.cs
@@ -0,0 +1,271 @@
+using System.Xml.Serialization;
+using Refresh.Database.Models.Pins;
+
+namespace Refresh.Interfaces.Game.Types.Challenges.Lbp3;
+
+#nullable disable
+
+[XmlRoot("Challenge_header")]
+public class SerializedLbp3ChallengeList
+{
+ [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; }
+
+ [XmlIgnore] private const ulong FromSecondsFactor = 1_000_000; // Factor to multiply with to convert a unix timestamp from seconds to microseconds
+ [XmlIgnore] private const ulong StartTimestamp = 1762872698 * FromSecondsFactor; // 11/11/2025 3:51:38 PM
+ [XmlIgnore] private const ulong Duration = 259200 * FromSecondsFactor; // 3 days
+
+ ///
+ /// A config which is loosely based off of the official server's config (more accurately,
+ /// https://github.com/LBPUnion/ProjectLighthouse/blob/e593d5c9570bc5b65ccf49ce9158d95a13fb70f4/ProjectLighthouse/Types/Serialization/GameChallenge.cs/#L72),
+ /// but every challenge has a duration/period of 3 days.
+ /// LBP3 doesn't care if any timestamps are in the past, and simply wraps the challenge periods in that case.
+ ///
+ public static SerializedLbp3ChallengeList Default
+ => new()
+ {
+ TotalChallenges = 14,
+ EndTime = StartTimestamp + 14 * Duration,
+ BronzeRankPercentage = 0.51f,
+ SilverRankPercentage = 0.26f,
+ GoldRankPercentage = 0.11f,
+ CycleTime = Duration,
+ Challenges =
+ [
+ new()
+ {
+ Id = 0,
+ StartTime = StartTimestamp + 0 * Duration,
+ EndTime = StartTimestamp + 1 * Duration,
+ LamsDescriptionId = "CHALLENGE_NEWTONBOUNCE_DESC",
+ LamsTitleId = "CHALLENGE_NEWTONBOUNCE_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.OverLineLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.OverLineLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet3",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1085260,
+ PhotoId = 1112639,
+ },
+ new()
+ {
+ Id = 1,
+ StartTime = StartTimestamp + 1 * Duration,
+ EndTime = StartTimestamp + 2 * Duration,
+ LamsDescriptionId = "CHALLENGE_SCREENCHASE_DESC",
+ LamsTitleId = "CHALLENGE_SCREENCHASE_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.PixelPaceLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.PixelPaceLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet2",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1102387,
+ PhotoId = 1112651,
+ },
+ new()
+ {
+ Id = 2,
+ StartTime = StartTimestamp + 2 * Duration,
+ EndTime = StartTimestamp + 3 * Duration,
+ LamsDescriptionId = "CHALLENGE_RABBITBOXING_DESC",
+ LamsTitleId = "CHALLENGE_RABBITBOXING_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.RabbitBoxingLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.RabbitBoxingLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet2",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1085264,
+ PhotoId = 1112627,
+ },
+ new()
+ {
+ Id = 3,
+ StartTime = StartTimestamp + 3 * Duration,
+ EndTime = StartTimestamp + 4 * Duration,
+ LamsDescriptionId = "CHALLENGE_FLOATYFLUID_DESC",
+ LamsTitleId = "CHALLENGE_FLOATYFLUID_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.FloatyFluidLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.FloatyFluidLbp3ChallengeRanking,
+ Content = "LBPDLCNISBLK0001",
+ ContentName = "SBSP_THEME_PACK_NAME",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1095449,
+ PhotoId = 1112619,
+ },
+ new()
+ {
+ Id = 4,
+ StartTime = StartTimestamp + 4 * Duration,
+ EndTime = StartTimestamp + 5 * Duration,
+ LamsDescriptionId = "CHALLENGE_ISLANDRACE_DESC",
+ LamsTitleId = "CHALLENGE_ISLANDRACE_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.ToggleIslandLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.ToggleIslandLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1102858,
+ PhotoId = 1112655,
+ },
+ new()
+ {
+ Id = 5,
+ StartTime = StartTimestamp + 5 * Duration,
+ EndTime = StartTimestamp + 6 * Duration,
+ LamsDescriptionId = "CHALLENGE_SPACEDODGING_DESC",
+ LamsTitleId = "CHALLENGE_SPACEDODGING_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.SpaceDodgeballLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.SpaceDodgeballLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet3",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1085266,
+ PhotoId = 1112667,
+ },
+ new()
+ {
+ Id = 6,
+ StartTime = StartTimestamp + 6 * Duration,
+ EndTime = StartTimestamp + 7 * Duration,
+ LamsDescriptionId = "CHALLENGE_INVISIBLECIRCUIT_DESC",
+ LamsTitleId = "CHALLENGE_INVISIBLECIRCUIT_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.InvisibleMazeLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.InvisibleMazeLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1096814,
+ PhotoId = 1112635,
+ },
+ new()
+ {
+ Id = 7,
+ StartTime = StartTimestamp + 7 * Duration,
+ EndTime = StartTimestamp + 8 * Duration,
+ LamsDescriptionId = "CHALLENGE_HOVERBOARDRAILS_DESC",
+ LamsTitleId = "CHALLENGE_HOVERBOARDRAILS_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.HoverboardLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.HoverboardLbp3ChallengeRanking,
+ Content = "LBPDLCBTTFLK0001",
+ ContentName = "BTTF_LEVEL_KIT_NAME",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1085256,
+ PhotoId = 1112623,
+ },
+ new()
+ {
+ Id = 8,
+ StartTime = StartTimestamp + 8 * Duration,
+ EndTime = StartTimestamp + 9 * Duration,
+ LamsDescriptionId = "CHALLENGE_TOWERBOOST_DESC",
+ LamsTitleId = "CHALLENGE_TOWERBOOST_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.WhoopTowerLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.WhoopTowerLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet2",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1092504,
+ PhotoId = 1112671,
+ },
+ new()
+ {
+ Id = 9,
+ StartTime = StartTimestamp + 9 * Duration,
+ EndTime = StartTimestamp + 10 * Duration,
+ LamsDescriptionId = "CHALLENGE_SWOOPPANELS_DESC",
+ LamsTitleId = "CHALLENGE_SWOOPPANELS_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.SwoopPanelsLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.SwoopPanelsLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet2",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1085268,
+ PhotoId = 1112643,
+ },
+ new()
+ {
+ Id = 10,
+ StartTime = StartTimestamp + 10 * Duration,
+ EndTime = StartTimestamp + 11 * Duration,
+ LamsDescriptionId = "CHALLENGE_PINBALLCRYPTS_DESC",
+ LamsTitleId = "CHALLENGE_PINBALLCRYPTS_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.PinballLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.PinballLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet3",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1085262,
+ PhotoId = 1112647,
+ },
+ new()
+ {
+ Id = 11,
+ StartTime = StartTimestamp + 11 * Duration,
+ EndTime = StartTimestamp + 12 * Duration,
+ LamsDescriptionId = "CHALLENGE_TIEHOP_DESC",
+ LamsTitleId = "CHALLENGE_TIEHOP_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.TieSkipLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.TieSkipLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1092367,
+ PhotoId = 1112659,
+ },
+ new()
+ {
+ Id = 12,
+ StartTime = StartTimestamp + 12 * Duration,
+ EndTime = StartTimestamp + 13 * Duration,
+ LamsDescriptionId = "CHALLENGE_JOKERFUNHOUSE_DESC",
+ LamsTitleId = "CHALLENGE_JOKERFUNHOUSE_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.JokerLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.JokerLbp3ChallengeRanking,
+ Content = "LBPDLCWBDCLK0001",
+ ContentName = "DCCOMICS_THEME_NAME",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1085258,
+ PhotoId = 1112631,
+ },
+ new()
+ {
+ Id = 13,
+ StartTime = StartTimestamp + 13 * Duration,
+ EndTime = StartTimestamp + 14 * Duration,
+ LamsDescriptionId = "CHALLENGE_DINERSHOOTING_DESC",
+ LamsTitleId = "CHALLENGE_DINERSHOOTING_NAME",
+ ScoreMedalPinProgressType = (long)ServerPins.CherryShooterLbp3ChallengeMedal,
+ ScoreRankingPinProgressType = (long)ServerPins.CherryShooterLbp3ChallengeRanking,
+ ContentName = "TG_LittleBigPlanet3",
+ PlanetUser = "qd3c781a5a6-GBen",
+ PlanetId = 1085254,
+ PhotoId = 1112663,
+ },
+ ],
+ };
+
+}
\ No newline at end of file
diff --git a/Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLevelChallengeList.cs b/Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLevelChallengeList.cs
deleted file mode 100644
index c7a26505c..000000000
--- a/Refresh.Interfaces.Game/Types/Challenges/Lbp3/SerializedLevelChallengeList.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.Xml.Serialization;
-
-namespace Refresh.Interfaces.Game.Types.Challenges.Lbp3;
-
-#nullable disable
-
-[XmlRoot("Challenge_header")]
-public class SerializedLbp3ChallengeList
-{
- [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; }
-}
\ No newline at end of file