Trainer rankings work

This commit is contained in:
Greg Edwards
2022-03-19 01:03:56 -04:00
parent 598d246a0d
commit ec62d3bcd8
5 changed files with 564 additions and 10 deletions

View File

@@ -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

View File

@@ -2456,6 +2456,31 @@ namespace PkmnFoundations.Data
return WithTransaction(tran => BattleVideoCount4(tran));
}
public override bool TrainerRankingsPerformRollover()
{
throw new NotImplementedException();
}
public override IList<TrainerRankingsRecordTypes> 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

View File

@@ -145,6 +145,53 @@ namespace PkmnFoundations.Data
public abstract bool BattleVideoFlagSaved4(ulong serial);
public abstract ulong BattleVideoCount4();
/// <summary>
/// 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.
/// </summary>
/// <param name="date"></param>
/// <returns>True if it began a new leaderboard</returns>
public abstract bool TrainerRankingsPerformRollover();
/// <summary>
/// Gets the three record types being collected for the active leaderboard.
/// </summary>
/// <returns>RecordTypes</returns>
public abstract IList<TrainerRankingsRecordTypes> TrainerRankingsGetActiveRecordTypes();
/// <summary>
/// Submits trainer rankings data for one player and populates the active leaderboard with it.
/// </summary>
/// <param name="submission"></param>
/// <returns>True if the data was processed</returns>
public abstract bool TrainerRankingsSubmit(TrainerRankingsSubmission submission);
/// <summary>
/// Gets past reports falling within a specified date range.
/// </summary>
/// <param name="start">Datetime during which the oldest returned leaderboard was active</param>
/// <param name="end">Datetime during which the newest returned leaderboard was active</param>
/// <returns>Reports</returns>
public abstract TrainerRankingsReport[] TrainerRankingsGetReport(DateTime start, DateTime end);
/// <summary>
/// Gets the single report which was active during the specified date.
/// </summary>
/// <param name="during"></param>
/// <returns></returns>
public TrainerRankingsReport TrainerRankingsGetReport(DateTime during)
{
return TrainerRankingsGetReport(during, during).FirstOrDefault();
}
/// <summary>
/// Gets the currently active, incomplete report.
/// </summary>
/// <returns></returns>
public abstract TrainerRankingsReport TrainerRankingsGetPendingReport();
#endregion
#region Global Terminal 5

View File

@@ -113,6 +113,7 @@
<Compile Include="Structures\TrainerProfile5.cs" />
<Compile Include="Structures\TrainerProfileBase.cs" />
<Compile Include="Structures\TrainerProfilePlaza.cs" />
<Compile Include="Structures\TrainerRankings.cs" />
<Compile Include="Support\AssertHelper.cs" />
<Compile Include="Support\EncodedString4.cs" />
<Compile Include="Support\EncodedString5.cs" />

View File

@@ -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; }
}
}