diff --git a/GlobalTerminalService/GTServer4.cs b/GlobalTerminalService/GTServer4.cs index fb4e1fbc..d1458677 100644 --- a/GlobalTerminalService/GTServer4.cs +++ b/GlobalTerminalService/GTServer4.cs @@ -337,9 +337,6 @@ namespace PkmnFoundations.GlobalTerminalService #region Trainer Rankings case RequestTypes4.TrainerRankingsHead: { -#if !DEBUG - goto default; -#endif if (data.Length != 0x140) { logEntry.AppendLine("Length did not validate."); @@ -348,8 +345,10 @@ namespace PkmnFoundations.GlobalTerminalService break; } - logEntry.AppendLine("Replayed response."); - response.Write(new byte[] { 0x00, 0x00 }, 0, 2); + // AdmiralCurtiss's request: completely blank! (just the 0x140 byte header) + + // AdmiralCurtiss's response: + // 0000: 0c000000f0550000 0128431c // Response format seems to be a list of the three record categories currently being collected. // 0x01: Hall of fame entries @@ -357,8 +356,28 @@ namespace PkmnFoundations.GlobalTerminalService // 0x43: Facilities challenged at battle frontier // The purpose of 0x1c is unclear to me. - // todo: See if we can ask for more than 3 different records. Would be neat to have all of them on the web! - response.Write(new byte[] { 0x01, 0x28, 0x43, 0x1c }, 0, 4); + if (Database.Instance.TrainerRankingsPerformRollover()) + { + logEntry.AppendLine("Leaderboard rollover."); + } + var recordTypes = Database.Instance.TrainerRankingsGetActiveRecordTypes(); + + response.Write(new byte[] { 0x00, 0x00 }, 0, 2); + + // The game will give error 10609 if the response is longer than 4 bytes. + // The game will bluescreen if the response is shorter than 3 bytes. + // The 4th byte is optional and I don't know what, if anything, it does. + int i; + for (i = 0; i < 3 && i < recordTypes.Count; i++) + { + response.WriteByte((byte)recordTypes[i]); + } + for (; i < 3; i++) + { + // Must be valid RecordTypes. If we pad with 0x00, it causes a bluescreen. + response.WriteByte((byte)0x01); + } + response.WriteByte(0x1c); // todo: Find out the meaning of this 0x1c. } break; @@ -385,7 +404,7 @@ namespace PkmnFoundations.GlobalTerminalService // 0160: 00000000 // Hikari's request: - // Platinum EN July [I forget] Gallade + // Platinum EN July AceTrainerF Gallade // 0140: 0c02070bdb010000 e200350f01000000 // 0150: 0300000028000000 0800000043000000 // 0160: 28000000 @@ -402,10 +421,20 @@ namespace PkmnFoundations.GlobalTerminalService // The response is biig and contains all the records for both this week and last week. // This week lacks numbers because they're still growing and to make it a surprise. + // Including more than 3 RecordTypes in the response will give error 10609. + + var submission = new TrainerRankingsSubmission(pid, data, 0x140); + if (!Database.Instance.TrainerRankingsSubmit(submission)) + { + logEntry.AppendLine("Submission error on database end."); + type = EventLogEntryType.Error; + response.Write(new byte[] { 0x02, 0x00 }, 0, 2); + break; + } - logEntry.AppendLine("Replayed response."); response.Write(new byte[] { 0x00, 0x00 }, 0, 2); + /* response.WriteBytes(new byte[] { // Last week comes first: 0x1c, 0x00, 0x00, 0x00, // 0x1c is a record type here. @@ -615,7 +644,79 @@ namespace PkmnFoundations.GlobalTerminalService 0x9a, 0x00, 0x8f, 0x00, 0x9d, 0x00, 0xe8, 0x01, 0x8e, 0x00, 0x7c, 0x01, 0x7d, 0x01, 0xe5, 0x00, 0x88, 0x01, 0x1a, 0x00, 0xfe, 0x00, 0xf9, 0x00 - }); + });*/ + + // fake replay data: + TrainerRankingsReport lastWeek = GenerateFakeReport( + new DateTime(2014, 04, 27), + new TrainerRankingsRecordTypes[] + { + TrainerRankingsRecordTypes.TimesBattledWildPokemon, + TrainerRankingsRecordTypes.BattlesTiedOverNintendoWiFiConnection, + TrainerRankingsRecordTypes.TotalCastlePointsEarnedAtTheBattleCastle + }); + TrainerRankingsReport thisWeek = GenerateFakeReport( + new DateTime(2014, 05, 04), + new TrainerRankingsRecordTypes[] + { + TrainerRankingsRecordTypes.HallOfFameEntries, + TrainerRankingsRecordTypes.CompletedGtsPokemonTrades, + TrainerRankingsRecordTypes.FacilitiesChallengedAtTheBattleFrontier + }); + + // Last week: + foreach (var lbg in lastWeek.Leaderboards) + { + response.Write(BitConverter.GetBytes((int)lbg.RecordType), 0, 4); + + foreach (var lbe in lbg.LeaderboardTrainerClass.Entries) + { + response.WriteByte((byte)lbe.Team); + } + foreach (var lbe in lbg.LeaderboardTrainerClass.Entries) + { + response.Write(BitConverter.GetBytes(lbe.Score), 0, 8); + } + + foreach (var lbe in lbg.LeaderboardBirthMonth.Entries) + { + response.WriteByte((byte)lbe.Team); + } + foreach (var lbe in lbg.LeaderboardBirthMonth.Entries) + { + response.Write(BitConverter.GetBytes(lbe.Score), 0, 8); + } + + foreach (var lbe in lbg.LeaderboardFavouritePokemon.Entries) + { + response.Write(BitConverter.GetBytes((short)lbe.Team), 0, 2); + } + foreach (var lbe in lbg.LeaderboardFavouritePokemon.Entries) + { + response.Write(BitConverter.GetBytes(lbe.Score), 0, 8); + } + } + + // This week: + foreach (var lbg in thisWeek.Leaderboards) + { + response.Write(BitConverter.GetBytes((int)lbg.RecordType), 0, 4); + + foreach (var lbe in lbg.LeaderboardTrainerClass.Entries) + { + response.WriteByte((byte)lbe.Team); + } + + foreach (var lbe in lbg.LeaderboardBirthMonth.Entries) + { + response.WriteByte((byte)lbe.Team); + } + + foreach (var lbe in lbg.LeaderboardFavouritePokemon.Entries) + { + response.Write(BitConverter.GetBytes((short)lbe.Team), 0, 2); + } + } } break; #endregion @@ -662,6 +763,23 @@ namespace PkmnFoundations.GlobalTerminalService } private byte[] m_pad; + + private static TrainerRankingsLeaderboardEntry[] GenerateFakeData(int entryCount, int minTeam, int teamCount, int maxScore) + { + Random scoreRandomizer = new Random(); + return Enumerable.Range(minTeam, teamCount).Select(i => new TrainerRankingsLeaderboardEntry(i, scoreRandomizer.Next(maxScore))) + .OrderBy(e => -e.Score).Take(entryCount).ToArray(); + } + + private static TrainerRankingsReport GenerateFakeReport(DateTime startDate, TrainerRankingsRecordTypes[] recordTypes) + { + var leaderboards = recordTypes.Select(r => new TrainerRankingsLeaderboardGroup(r, + new TrainerRankingsLeaderboard(TrainerRankingsTeamCategories.BirthMonth, GenerateFakeData(12, 1, 12, 100000)), + new TrainerRankingsLeaderboard(TrainerRankingsTeamCategories.TrainerClass, GenerateFakeData(16, 0, 16, 100000)), + new TrainerRankingsLeaderboard(TrainerRankingsTeamCategories.FavouritePokemon, GenerateFakeData(20, 1, 493, 100000)))).ToArray(); + + return new TrainerRankingsReport(startDate, startDate.AddDays(7), leaderboards); + } } internal enum RequestTypes4 : byte diff --git a/library/Data/DataMysql.cs b/library/Data/DataMysql.cs index ae277ef7..e7fdc1b4 100644 --- a/library/Data/DataMysql.cs +++ b/library/Data/DataMysql.cs @@ -2456,6 +2456,31 @@ namespace PkmnFoundations.Data return WithTransaction(tran => BattleVideoCount4(tran)); } + public override bool TrainerRankingsPerformRollover() + { + throw new NotImplementedException(); + } + + public override IList TrainerRankingsGetActiveRecordTypes() + { + throw new NotImplementedException(); + } + + public override bool TrainerRankingsSubmit(TrainerRankingsSubmission submission) + { + throw new NotImplementedException(); + } + + public override TrainerRankingsReport[] TrainerRankingsGetReport(DateTime start, DateTime end) + { + throw new NotImplementedException(); + } + + public override TrainerRankingsReport TrainerRankingsGetPendingReport() + { + throw new NotImplementedException(); + } + #endregion #region Global Terminal 5 diff --git a/library/Data/Database.cs b/library/Data/Database.cs index 671f5047..293c6c29 100644 --- a/library/Data/Database.cs +++ b/library/Data/Database.cs @@ -145,6 +145,53 @@ namespace PkmnFoundations.Data public abstract bool BattleVideoFlagSaved4(ulong serial); public abstract ulong BattleVideoCount4(); + + /// + /// Instructs the database that the provided datetime is now active. + /// If the new datetime is outside the active leaderboard's datetime + /// range, a new leaderboard should be initialized. + /// + /// + /// True if it began a new leaderboard + public abstract bool TrainerRankingsPerformRollover(); + + /// + /// Gets the three record types being collected for the active leaderboard. + /// + /// RecordTypes + public abstract IList TrainerRankingsGetActiveRecordTypes(); + + /// + /// Submits trainer rankings data for one player and populates the active leaderboard with it. + /// + /// + /// True if the data was processed + public abstract bool TrainerRankingsSubmit(TrainerRankingsSubmission submission); + + /// + /// Gets past reports falling within a specified date range. + /// + /// Datetime during which the oldest returned leaderboard was active + /// Datetime during which the newest returned leaderboard was active + /// Reports + public abstract TrainerRankingsReport[] TrainerRankingsGetReport(DateTime start, DateTime end); + + /// + /// Gets the single report which was active during the specified date. + /// + /// + /// + public TrainerRankingsReport TrainerRankingsGetReport(DateTime during) + { + return TrainerRankingsGetReport(during, during).FirstOrDefault(); + } + + /// + /// Gets the currently active, incomplete report. + /// + /// + public abstract TrainerRankingsReport TrainerRankingsGetPendingReport(); + #endregion #region Global Terminal 5 diff --git a/library/Library.csproj b/library/Library.csproj index b19564f8..b0828463 100644 --- a/library/Library.csproj +++ b/library/Library.csproj @@ -113,6 +113,7 @@ + diff --git a/library/Structures/TrainerRankings.cs b/library/Structures/TrainerRankings.cs new file mode 100644 index 00000000..d2fd99d5 --- /dev/null +++ b/library/Structures/TrainerRankings.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace PkmnFoundations.Structures +{ + public class TrainerRankingsReport + { + public TrainerRankingsReport(DateTime start_date, DateTime end_date, TrainerRankingsLeaderboardGroup[] leaderboards) + { + StartDate = start_date; + EndDate = end_date; + Leaderboards = leaderboards; + } + + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + + public TrainerRankingsLeaderboardGroup[] Leaderboards { get; set; } + } + + public class TrainerRankingsLeaderboardGroup + { + public TrainerRankingsLeaderboardGroup(TrainerRankingsRecordTypes record_type, TrainerRankingsLeaderboard leaderboard_birth_month, + TrainerRankingsLeaderboard leaderboard_trainer_class, TrainerRankingsLeaderboard leaderboard_favourite_pokemon) + { + RecordType = record_type; + LeaderboardBirthMonth = leaderboard_birth_month; + LeaderboardTrainerClass = leaderboard_trainer_class; + LeaderboardFavouritePokemon = leaderboard_favourite_pokemon; + } + + public TrainerRankingsRecordTypes RecordType { get; set; } + + public TrainerRankingsLeaderboard LeaderboardBirthMonth { get; set; } + public TrainerRankingsLeaderboard LeaderboardTrainerClass { get; set; } + public TrainerRankingsLeaderboard LeaderboardFavouritePokemon { get; set; } + } + + public class TrainerRankingsLeaderboard + { + public TrainerRankingsLeaderboard(TrainerRankingsTeamCategories team_category, TrainerRankingsLeaderboardEntry[] entries) + { + TeamCategory = team_category; + Entries = entries; + } + + public TrainerRankingsTeamCategories TeamCategory { get; set; } + + public TrainerRankingsLeaderboardEntry[] Entries { get; set; } + } + + public class TrainerRankingsLeaderboardEntry + { + public TrainerRankingsLeaderboardEntry(int team, long score) + { + Team = team; + Score = score; + } + + public int Team { get; set; } + public long Score { get; set; } + } + + public enum TrainerRankingsRecordTypes : int + { + // todo: document these + //Invalid = 0x00, // Seems to trigger a 'there is no data' if used. + HallOfFameEntries = 0x01, + //Blank02 = 0x02, + //Blank03 = 0x03, + //Blank04 = 0x04, + BattleTowerSingleBattleWinStreak = 0x05, + //Blank06 = 0x06, + BattleTowerDoubleBattleWinStreak = 0x07, + //Blank08 = 0x08, + BattleTowerMultiBattleWinStreak = 0x09, + //Blank0a = 0x0a, + BattleTowerPartneredMultiBattleWinStreak = 0x0b, + //Blank0c = 0x0c, + WiFiBattleTowerWinStreak = 0x0d, + //Blank0e = 0x0e, + ContestsEnteredAlone = 0x0f, + ContestsEnteredWithFriends = 0x10, + ContestsEnteredAloneAndWon = 0x11, + ContestsEnteredWithFriendsAndWon = 0x12, + //Blank13 = 0x13, + //Blank14 = 0x14, + //Blank15 = 0x15, + //Blank16 = 0x16, + //Blank17 = 0x17, + TimesWildPokemonFled = 0x18, + //Blank19 = 0x19, + BerriesPlanted = 0x1a, + StepsWalked = 0x1b, + TimesBattledWildPokemon = 0x1c, + TrainerBattlesExcludingUnionRoomAndFrontier = 0x1d, + PokemonCaught = 0x1e, + PokemonCaughtFishing = 0x1f, + EggsHatched = 0x20, + TimesOwnPokemonEvolved = 0x21, + GameCornerSlotJackpots = 0x22, + BattleFrontierChallenges = 0x23, + //Blank24 = 0x24, + //Blank25 = 0x25, + //Blank26 = 0x26, + //Blank27 = 0x27, + CompletedGtsPokemonTrades = 0x28, + BattlesWonOverNintendoWiFiConnection = 0x29, + //Blank2a = 0x2a, + BattlesTiedOverNintendoWiFiConnection = 0x2b, + BattlesWonAtBattleTower = 0x2c, + TotalMoneySpendShopping = 0x2d, + PokemonLeftWithDayCare = 0x2e, + PokemonDefeated = 0x2f, + PokemonOfferedOnGts = 0x30, + TimesPokemonWereTradedWithFriendsAtWiFiClub = 0x31, + TimesYouSignedYourTrainerCard = 0x32, + PokemonExtractedFromFossils = 0x33, + TimesFootprintsWereCheckedByDrFootstep = 0x34, + TimesMailWasSent = 0x35, + WildPokemonLuredUsingHoney = 0x36, + TimesYouTalkedToSomeoneInWiFiPlaza = 0x37, + SpheresBuriedInUnderground = 0x38, + TimesWatchedTv = 0x39, + TimesPokemonWereGivenNicknames = 0x3a, + PremierBallsReceived = 0x3b, + ItemsFoundByPokemonInAmitySquare = 0x3c, + PoffinsCookedAlone = 0x3d, + PoffinsCookedWithFriends = 0x3e, + PokemonDressUpDataPhotosTaken = 0x3f, + BouldersPushedUsingTheHiddenMoveStrength = 0x40, + TimesMiredInASwamp = 0x41, + MatchesAgainstYourFrivalAndGymLeadersAtTheBattleground = 0x42, + FacilitiesChallengedAtTheBattleFrontier = 0x43, + TimesYouMetTheFrontierBrains = 0x44, + WinsAtTheBattleFactory = 0x45, + WinsAtTheBattleCastle = 0x46, + WinsAtTheBattleHall = 0x47, + WinsAtTheBattleArcade = 0x48, + PokemonTradesAtTheBattleFactory = 0x49, + TotalCastlePointsEarnedAtTheBattleCastle = 0x4a, + WinsOverRank10PokemonAtTheBattleHall = 0x4b, + BattlePointsEarnedFromTheBattleArcadeGameBoard = 0x4c, + TotalBattlePointsWon = 0x4d, + TotalBattlePointsSpent = 0x4e, + WiFiPlazaGamesPlayed = 0x4f, + EggsTradedUsingSpinTrade = 0x50, + StarPiecesTradedAtFuegoIronworks = 0x51, + //BattlePointsEarnedFromTheBattleArcadeGameBoard52 = 0x52, + ItemsAndBerriesEarnedFromTheBattleArcadeGameBoard = 0x53, + //TotalBattlePointsWon54 = 0x54, + //TotalBattlePointsSpent55 = 0x55, + WinsInWiFiPlazaGames = 0x56, + //EggsTradedUsingSpinTrade57 = 0x57, + //Blank58 = 0x58, + //Blank59 = 0x59, + //Blank5a = 0x5a, + //BlueScreen = 0x5b + } + + public enum TrainerClass : int + { + SchoolKidMale = 0x00, + BugCatcher = 0x01, + AceTrainerMale = 0x02, + Roughneck = 0x03, + RuinManiac = 0x04, + BlackBelt = 0x05, + RichBoy = 0x06, + PsychicMale = 0x07, + Lass = 0x08, + BattleGirl = 0x09, + Beauty = 0x0a, + AceTrainerFemale = 0x0b, + Idol = 0x0c, + Socialite = 0x0d, + Cowgirl = 0x0e, + Lady = 0x0f, + // Plato = 0x10, // ??? o_O + // 0x11 and up blue screen + } + + public enum TrainerRankingsTeamCategories + { + BirthMonth, TrainerClass, FavouritePokemon + } + + public class TrainerRankingsSubmission : BinarySerializableBase + { + public TrainerRankingsSubmission(int pid, Versions version, + Languages language, byte birth_month, byte trainer_class, + ushort favourite_pokemon, ushort unknown1, ushort unknown2, + ushort unknown3, TrainerRankingsSubmissionEntry[] entries) + { + PID = pid; + Version = version; + Language = language; + BirthMonth = birth_month; + TrainerClass = trainer_class; + FavouritePokemon = favourite_pokemon; + Unknown1 = unknown1; + Unknown2 = unknown2; + Unknown3 = unknown3; + Entries = entries; + } + + public TrainerRankingsSubmission(int pid, byte[] data, int offset) + { + PID = pid; + Load(data, offset); + } + + public TrainerRankingsSubmission(int pid, byte[] data) + { + PID = pid; + Load(data); + } + + public TrainerRankingsSubmission(int pid, BinaryReader reader) + { + PID = pid; + Load(reader); + } + + protected override void Load(BinaryReader reader) + { + // AdmiralCurtiss's request: + // 0140: 0c02050900000000 1800303b01000000 + // 0150: 0100000028000000 0000000043000000 + // 0160: 00000000 + + // Hikari's request: + // Platinum EN July [I forget] Gallade + // 0140: 0c02070bdb010000 e200350f01000000 + // 0150: 0300000028000000 0800000043000000 + // 0160: 28000000 + + // 140: Version + // 141: Language + // 142: Birth Month + // 143: Trainer Class + // 144-145: Favourite Pokémon + // 146-14b: Unknown + // 14c-163: Three record records + + // Record record contains 4 bytes of category and 4 bytes of my score in the category. + + Version = (Versions)reader.ReadByte(); // 00-00 + Language = (Languages)reader.ReadByte(); // 01-01 + BirthMonth = reader.ReadByte(); // 02-02 + TrainerClass = reader.ReadByte(); // 03-03 + FavouritePokemon = reader.ReadUInt16(); // 04-05 + Unknown1 = reader.ReadUInt16(); // 06-07 + Unknown2 = reader.ReadUInt16(); // 08-09 + Unknown3 = reader.ReadUInt16(); // 0a-0b + var entries = new TrainerRankingsSubmissionEntry[3]; + entries[0] = new TrainerRankingsSubmissionEntry(reader); // 0c-13 + entries[1] = new TrainerRankingsSubmissionEntry(reader); // 14-1b + entries[2] = new TrainerRankingsSubmissionEntry(reader); // 1c-23 + Entries = entries; + } + + protected override void Save(BinaryWriter writer) + { + writer.Write((byte)Version); // 00-00 + writer.Write((byte)Language); // 01-01 + writer.Write(BirthMonth); // 02-02 + writer.Write(TrainerClass); // 03-03 + writer.Write(FavouritePokemon); // 04-05 + writer.Write(Unknown1); // 06-07 + writer.Write(Unknown2); // 08-09 + writer.Write(Unknown3); // 0a-0b + writer.Write(Entries[0].Save()); // 0c-13 + writer.Write(Entries[1].Save()); // 14-1b + writer.Write(Entries[2].Save()); // 1c-23 + } + + public override int Size + { + get + { + return 36; + } + } + + public int PID { get; set; } + + public Versions Version { get; set; } + public Languages Language { get; set; } + public byte BirthMonth { get; set; } + public byte TrainerClass { get; set; } + public ushort FavouritePokemon { get; set; } + + public ushort Unknown1 { get; set; } + public ushort Unknown2 { get; set; } + public ushort Unknown3 { get; set; } + + private TrainerRankingsSubmissionEntry[] m_entries; + public TrainerRankingsSubmissionEntry[] Entries + { + get + { + return m_entries; + } + set + { + if (value == null) throw new ArgumentNullException(); + if (value.Length != 3) throw new ArgumentException("TrainerRankingsSubmission must have exactly 3 entries."); + m_entries = value; + } + } + } + + public class TrainerRankingsSubmissionEntry : BinarySerializableBase + { + public TrainerRankingsSubmissionEntry(TrainerRankingsRecordTypes record_type, uint score) + { + RecordType = record_type; + Score = score; + } + + public TrainerRankingsSubmissionEntry(byte[] data, int offset) + { + Load(data, offset); + } + + public TrainerRankingsSubmissionEntry(byte[] data) + { + Load(data); + } + + public TrainerRankingsSubmissionEntry(BinaryReader reader) + { + Load(reader); + } + + protected override void Load(BinaryReader reader) + { + RecordType = (TrainerRankingsRecordTypes)reader.ReadInt32(); // 00 + Score = reader.ReadUInt32(); // 04 + } + + protected override void Save(BinaryWriter writer) + { + writer.Write((int)RecordType); // 00 + writer.Write(Score); // 04 + } + + public override int Size + { + get + { + return 8; + } + } + + public TrainerRankingsRecordTypes RecordType { get; set; } + public uint Score { get; set; } + } +}