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