From addcd6f044e56cd7cf47cdaf23b2be1bd56f6573 Mon Sep 17 00:00:00 2001 From: Cynthia Date: Sat, 8 Jul 2023 23:19:47 -0600 Subject: [PATCH] fully support pokemon wifi-plaza's fixes #7 - checkProfile.asp parseable for all the important stuff. - getSchedule.asp , schedule generation and more added in. - getVIP.asp still works. - Questionnaries , now working and documented. I've tested all the bits individually (and that they match what my rust code does which is fully confirmed working). However, since there's no linux support I can't test this web server with the rest of my stack easily. Everything should all work and I've double checked each class individually on my side, but we should still probably get a double check from mm :) --- gts/pokemondpds_web.ashx.cs | 243 +++------------ library/Library.csproj | 3 + library/Support/AliasTable.cs | 109 +++++++ library/Wfc/PlazaQuestionnaire.cs | 310 ++++++++++++++++++ library/Wfc/PlazaSchedule.cs | 485 +++++++++++++++++++++++++++++ library/Wfc/TrainerProfilePlaza.cs | 255 ++++++++++----- 6 files changed, 1138 insertions(+), 267 deletions(-) create mode 100644 library/Support/AliasTable.cs create mode 100644 library/Wfc/PlazaQuestionnaire.cs create mode 100644 library/Wfc/PlazaSchedule.cs diff --git a/gts/pokemondpds_web.ashx.cs b/gts/pokemondpds_web.ashx.cs index 56f6f235..868fdecf 100644 --- a/gts/pokemondpds_web.ashx.cs +++ b/gts/pokemondpds_web.ashx.cs @@ -41,228 +41,83 @@ namespace PkmnFoundations.GTS return; } - // I am going to guess that the PID provided second is the - // one whose data should appear in the response. - int requestedPid = BitConverter.ToInt32(request, 0); - byte[] requestDataPrefix = new byte[12]; - byte[] requestData = new byte[152]; + byte[] gamestatsHeader = new byte[20]; + byte[] requestData = new byte[148]; - Array.Copy(request, 4, requestDataPrefix, 0, 12); - Array.Copy(request, 16, requestData, 0, 152); + Array.Copy(request, 0, gamestatsHeader, 0, 20); + Array.Copy(request, 20, requestData, 0, 148); - TrainerProfilePlaza requestProfile = new TrainerProfilePlaza(pid, requestDataPrefix, requestData); + TrainerProfilePlaza requestProfile = new TrainerProfilePlaza(gamestatsHeader, requestData); Database.Instance.PlazaSetProfile(requestProfile); - TrainerProfilePlaza responseProfile = Database.Instance.PlazaGetProfile(requestedPid); + TrainerProfilePlaza responseProfile = Database.Instance.PlazaGetProfile(requestProfile.PID); response.Write(responseProfile.Data, 0, 152); } break; case "/pokemondpds/web/enc/lobby/getSchedule.asp": { - // This is a replayed response from a game I had with Pipian. - // It appears to be 49 ints. - // todo(mythra): A real implementation - // - we can generate events manually now, but we have a few - // missing fields, so more research will need to be done before - // that implementation. - - // note(mythra): this response is usually overwritten by the - // peerchat server (through GETCHANKEY `b_lib_c_lobby`). - // this is only taken if that channel key returns an - // "empty" response. - Random room_choice = new Random(); - if (room_choice.Next() % 2 == 0) { - // Mew Room w/ Arceus Footprint. - response.Write(new byte[] - { - 0x00, 0x00, 0x00, 0x00, 0xb0, 0x04, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x04, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0x0c, 0x03, 0x00, 0x00, - 0x08, 0x00, 0x00, 0x00, 0x48, 0x03, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x48, 0x03, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, - 0x03, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, - 0x0a, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, - 0x0c, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, - 0x04, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, - 0x09, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, - 0x0f, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, - 0x05, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, - 0x0e, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, - 0x10, 0x00, 0x00, 0x00, 0x33, 0x04, 0x00, 0x00, - 0x12, 0x00, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, - 0x06, 0x00, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, - 0x0d, 0x00, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, - 0x11, 0x00, 0x00, 0x00, 0x74, 0x04, 0x00, 0x00, - 0x0b, 0x00, 0x00, 0x00, 0xb0, 0x04, 0x00, 0x00, - 0x13, 0x00, 0x00, 0x00 - }, 0, 196); - } else { - // Grass Room without Arceus Footprint. - response.Write(new byte[] - { - 0x00, 0x00, 0x00, 0x00, - 0xb0, 0x04, 0x00, 0x00, // Duration the room remains open for (seconds) - 0x9e, 0xc4, 0x70, 0xa7, // Unknown, Mythra thinks it may be a random seed - 0x00, 0x00, 0x00, 0x00, // Arceus footprint flag. 0 for disabled, 1 for enabled. - 0x03, // Room type (0x03 = grass) - 0x00, // "Season" tbd - 0x16, 0x00, // Number of timed events (22) - // List of 22 events. - // Each event has an int for time and an int for what to do. - // Events are sorted according to time. - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, - 0x0c, 0x03, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, - 0x48, 0x03, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, - 0x48, 0x03, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0x84, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x84, 0x03, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, - 0x84, 0x03, 0x00, 0x00, 0x0c, 0x00, 0x00, 0x00, - 0xc0, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, - 0xc0, 0x03, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, - 0xc0, 0x03, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0xc0, 0x03, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, - 0xfc, 0x03, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, - 0xfc, 0x03, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, - 0xfc, 0x03, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x33, 0x04, 0x00, 0x00, 0x12, 0x00, 0x00, 0x00, - 0x38, 0x04, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, - 0x38, 0x04, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, - 0x38, 0x04, 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, - 0x74, 0x04, 0x00, 0x00, 0x0b, 0x00, 0x00, 0x00, - 0xb0, 0x04, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00 - }, 0, 196); - } - + // note(mythra): this response CAN be overwritten by the PEERCHAT server. + // + // Effectively the client fetches what it needs if it wants to create a new room, + // joins or creates a channel, and checks the `b_lib_c_lobby` channel key has been set. + // if it has, it loads that room data. If not it loads this response. + byte[] serializedSchedule = PlazaSchedule.Generate().Save(); + // The 'status code' if we succeeded. We just always write success. + response.Write(new byte[] { 0x0, 0x0, 0x0, 0x0 }, 0, 4); + response.Write(serializedSchedule, 0, serializedSchedule.Length); } break; case "/pokemondpds/web/enc/lobby/getVIP.asp": { + // Status Code. response.Write(new byte[] { 0x00, 0x00, 0x00, 0x00 }, 0, 4); - - foreach (var i in new[] { 600403373, 601315647, 601988829 }) - { - response.Write(BitConverter.GetBytes(i), 0, 4); - response.Write(new byte[] { 0x00, 0x00, 0x00, 0x00 }, 0, 4); - } - + // VIPs. + foreach (var id in VIPIds) + { + response.Write(BitConverter.GetBytes(id), 0, 4); + response.Write(new byte[] { 0x00, 0x00, 0x00, 0x00 }, 0, 4); } + } break; case "/pokemondpds/web/enc/lobby/getQuestionnaire.asp": { - response.Write(new byte[]{ - - 0x00, 0x00, 0x00, 0x00, 0x2a, 0x01, 0x00, - 0x00, 0x2d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, - 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x01, 0x00, - 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, - 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, - 0x00, 0x46, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, - 0x00, 0x64, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, - 0x00, 0x83, 0x00, 0x00, 0x00 - }, 0, 732); + response.Write(new byte[] { 0x0, 0x0, 0x0, 0x0 }, 0, 4); + response.Write(staticQuestionnaire, 0, staticQuestionnaire.Length); } break; case "/pokemondpds/web/enc/lobby/submitQuestionnaire.asp": { - // literally 'thx' in ascii... lol - response.Write(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x74, 0x68, 0x78, 0x00 }, 0, 8); + // One day we could parse as 'SubmittedQuestionnaire', and save in a DB somewhere. + // that'd be cool! + // + // literally 'thx' in ascii... lol + response.Write(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x74, 0x68, 0x78, 0x00 }, 0, 8); } break; } } + + /// + /// The list of "VIPs". + /// + /// Being a VIP in a lobby just gives you a golden trainer card, and upgrades your 'Touch Toy' to the highest + /// level right away. So far we've just been giving this to the developers who signed up for it. + /// + private static int[] VIPIds = new int[] { 600403373, 601315647, 601988829 }; + + /// + /// A static questionnaire, who's id is not above 1k so it doesn't load the custom question text. + /// + /// The last weeks results is still taken. + /// And the footer of unknown data is copied from static responses. + /// + private static byte[] staticQuestionnaire = new PlazaQuestionnaire( + new PlazaQuestion(730, "Not used", new string[] { "N/A", "N/A", "N/A" }, new byte[12], false), + new PlazaQuestion(729, "Not used", new string[] { "N/A", "N/A", "N/A" }, new byte[12], false), + new int[] { 69, 420, 100 }, + new byte[] { 0x64, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00 }).Save(); } } diff --git a/library/Library.csproj b/library/Library.csproj index bb2b1584..d24c174d 100644 --- a/library/Library.csproj +++ b/library/Library.csproj @@ -75,6 +75,7 @@ + @@ -112,6 +113,8 @@ + + diff --git a/library/Support/AliasTable.cs b/library/Support/AliasTable.cs new file mode 100644 index 00000000..3abf522b --- /dev/null +++ b/library/Support/AliasTable.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; + +namespace PkmnFoundations.Support +{ + /// + /// Vose's implementation of the Alias method for choosing weighted randomly from a set. + /// + /// See: https://www.keithschwarz.com/darts-dice-coins/ + /// + public class AliasTable + { + public static AliasTable NewWithWeights(Dictionary typesWithProbabilities) + { + List elements = new List(); + foreach (var pair in typesWithProbabilities) + { + elements.Add(pair.Key); + } + Dictionary table = new Dictionary(); + Dictionary probs = new Dictionary(); + + double size = (double)typesWithProbabilities.Count; + Stack smallTypes = new Stack(); + Stack largeTypes = new Stack(); + Dictionary scaledProbabilityMap = new Dictionary(); + + foreach (var pair in typesWithProbabilities) + { + double scaledProbability = pair.Value * size; + scaledProbabilityMap[pair.Key] = scaledProbability; + + if (scaledProbability < 1.0) + { + smallTypes.Push(pair.Key); + } + else + { + largeTypes.Push(pair.Key); + } + } + + while (smallTypes.Count != 0 && largeTypes.Count != 0) + { + Type smallElement = smallTypes.Pop(); + Type largeElement = largeTypes.Pop(); + table[smallElement] = largeElement; + + double scaledSmall = scaledProbabilityMap[smallElement]; + double scaledLarge = scaledProbabilityMap[largeElement]; + probs[smallElement] = scaledSmall; + double newLarge = (scaledLarge + scaledSmall) - 1.0; + probs[largeElement] = newLarge; + + if (newLarge < 1.0) + { + smallTypes.Push(largeElement); + } + else + { + largeTypes.Push(largeElement); + } + } + + while (largeTypes.Count != 0) + { + Type largeElement = largeTypes.Pop(); + probs[largeElement] = 1.0; + } + + while (smallTypes.Count != 0) + { + Type smallElement = smallTypes.Pop(); + probs[smallElement] = 1.0; + } + + return new AliasTable(table, elements, probs); + } + + public Type Sample() + { + Type element = elements[rng.Next(0, elements.Count)]; + int number = rng.Next(0, 101); + + double probability = probabilities[element]; + if (number <= (probability * 100)) + { + return element; + } + else + { + return underlyingTable[element]; + } + } + + private AliasTable(Dictionary table, List elem, Dictionary probs) + { + underlyingTable = table; + elements = elem; + probabilities = probs; + rng = new Random(); + } + + private Dictionary underlyingTable; + private List elements; + private Dictionary probabilities; + private Random rng; + } +} diff --git a/library/Wfc/PlazaQuestionnaire.cs b/library/Wfc/PlazaQuestionnaire.cs new file mode 100644 index 00000000..9217e878 --- /dev/null +++ b/library/Wfc/PlazaQuestionnaire.cs @@ -0,0 +1,310 @@ +using System; +using System.IO; + +namespace PkmnFoundations.Wfc +{ + public class PlazaQuestionnaire + { + public PlazaQuestion CurrentQuestion; + public PlazaQuestion LastWeeksQuestion; + private int[] lastWeeksResults; + public byte[] Unk; + + public PlazaQuestionnaire(PlazaQuestion currentQuestion, PlazaQuestion lastQuestion, int[] results, byte[] unk) + { + CurrentQuestion = currentQuestion; + LastWeeksQuestion = lastQuestion; + LastWeeksResults = results; + Unk = unk; + } + + public int[] LastWeeksResults + { + get + { + return lastWeeksResults; + } + + set + { + if (value.Length != 3) + { + throw new ArgumentException("Results must be 3 integers! which represent the total count of each answer!"); + } + lastWeeksResults = value; + } + } + + public byte[] Save() + { + byte[] serialized = new byte[728]; + MemoryStream ms = new MemoryStream(serialized); + // BinaryWriter uses little endian which is what we want. + BinaryWriter writer = new BinaryWriter(ms); + + writer.Write(CurrentQuestion.Save()); + writer.Write(LastWeeksQuestion.Save()); + foreach (int answer in LastWeeksResults) + { + writer.Write(answer); + } + writer.Write(Unk); + + writer.Flush(); + ms.Flush(); + return serialized; + } + + public static PlazaQuestionnaire Load(byte[] data, int start) + { + PlazaQuestion question = PlazaQuestion.Load(data, start); + PlazaQuestion lastWeeksQuestion = PlazaQuestion.Load(data, start + 352); + int[] lastResults = new int[3]; + + int dataIdx = 704; + for (byte idx = 0; idx < 3; ++idx) + { + lastResults[idx] = BitConverter.ToInt32(data, start + dataIdx); + dataIdx += 4; + } + + return new PlazaQuestionnaire( + question, + lastWeeksQuestion, + lastResults, + new byte[] { + data[start + dataIdx], + data[start + dataIdx + 1], + data[start + dataIdx + 2], + data[start + dataIdx + 3], + data[start + dataIdx + 4], + data[start + dataIdx + 5], + data[start + dataIdx + 6], + data[start + dataIdx + 7], + data[start + dataIdx + 8], + data[start + dataIdx + 9], + data[start + dataIdx + 10], + data[start + dataIdx + 11], + }); + } + } + + /// + /// A question that can be sent to a client for answering within the Wifi-Plaza. + /// + public class PlazaQuestion + { + /// + /// Seems to be an internal ID as it's much higher than the week number should too the device + /// from the responses we have captured. + /// + /// For us we just keep the IDs the same. + /// + /// The ID needs to be bigger than 1000 to show 'custom user text'. + /// Otherwise, it overwrites the answers + /// + public int ID; + /// The public ID, or week number shown to devices. + public int PublicID; + /// + /// The sentence of the question, to actually show. + /// + /// Although the final question can't be more than 220 bytes encoded (110 characters since it's UTF-16). + /// Although in reality, each line can only be 35 characters before needing a 'new line', spanned across + /// two lines. + /// + /// Extra newlines are ignored. + /// + private string QuestionSentence; + /// + /// The three answers to the question. + /// + /// Each answer should be 36 bytes encoded (18 characters since it's UTF-16). + /// If there is an unprintable character, they repeat the last printable character. + /// Answers should not have newlines otherwise they can overwrite other lines. + /// + private string[] QuestionAnswers; + /// A series of unknown bytes. + public byte[] Unk; + /// If the question is a 'special' question, and the man in the plaza will say so. + public bool IsSpecial; + + public PlazaQuestion(int id, string sentence, string[] answers, byte[] unk, bool isSpecial) + { + ID = id; + PublicID = id; + Unk = unk; + Sentence = sentence; + Answers = answers; + IsSpecial = isSpecial; + } + + private PlazaQuestion(int id, int publicID, string sentence, string[] answers, byte[] unk, bool isSpecial) + { + ID = id; + PublicID = publicID; + Unk = unk; + QuestionSentence = sentence; + QuestionAnswers = answers; + IsSpecial = isSpecial; + } + + public string Sentence + { + get + { + return QuestionSentence; + } + + set + { + // TODO add some validation on max length here. + QuestionSentence = value; + } + } + + public string[] Answers + { + get + { + return QuestionAnswers; + } + + set + { + if (value.Length != 3) + { + throw new ArgumentException("You MUST supply 3 answers for a particular question!"); + } + // TODO validate encoded size + QuestionAnswers = value; + } + } + + public byte[] Save() + { + byte[] serialized = new byte[352]; + MemoryStream ms = new MemoryStream(serialized); + // BinaryWriter uses little endian which is what we want. + BinaryWriter writer = new BinaryWriter(ms); + + writer.Write(ID); + writer.Write(PublicID); + + byte[] encodedQuestion = Support.EncodedString4.EncodeString_impl(QuestionSentence, 220); + writer.Write(encodedQuestion); + foreach (string answer in QuestionAnswers) + { + byte[] encodedAnswer = Support.EncodedString4.EncodeString_impl(answer, 36); + writer.Write(encodedAnswer); + } + writer.Write(Unk); + writer.Write((int)(IsSpecial ? 1 : 0)); + + writer.Flush(); + ms.Flush(); + return serialized; + } + + public static PlazaQuestion Load(byte[] data, int start) + { + int internalID = BitConverter.ToInt32(data, start); + int publicID = BitConverter.ToInt32(data, start + 4); + + byte[] questionBytes = new byte[220]; + Array.Copy(data, 8 + start, questionBytes, 0, 220); + string question = Support.EncodedString4.DecodeString_impl(questionBytes); + + string[] answers = new string[3]; + int dataIdx = 228 + start; + for (byte idx = 0; idx < 3; idx++) + { + byte[] answerBytes = new byte[36]; + Array.Copy(data, dataIdx, answerBytes, 0, 36); + answers[idx] = Support.EncodedString4.DecodeString_impl(answerBytes); + dataIdx += 36; + } + + byte[] unk = new byte[] { + data[start + 336], data[start + 337], data[start + 338], data[start + 339], data[start + 340], + data[start + 341], data[start + 342], data[start + 343], data[start + 344], data[start + 345], + data[start + 346], data[start + 347], + }; + bool isSpecial = BitConverter.ToInt32(data, start + 348) != 0; + + return new PlazaQuestion(internalID, publicID, question, answers, unk, isSpecial); + } + } + + public class SubmittedQuestionnaire + { + public int ID; + public int PublicID; + private int answerNo; + public uint OT; + public Structures.TrainerGenders TrainerGender; + public uint Country; + public uint Region; + + public SubmittedQuestionnaire(int id, int publicID, int answerNumber, uint ot, Structures.TrainerGenders gender, uint country, uint region) + { + ID = id; + PublicID = publicID; + AnswerNumber = answerNumber; + OT = ot; + TrainerGender = gender; + Country = country; + Region = region; + } + + public int AnswerNumber + { + get + { + return answerNo; + } + + set + { + if (value > 3 || value < 0) + { + throw new ArgumentException("Answer can only be 0-3!"); + } + answerNo = value; + } + } + + public byte[] Save() + { + byte[] serialized = new byte[24]; + MemoryStream ms = new MemoryStream(serialized); + // BinaryWriter uses little endian which is what we want. + BinaryWriter writer = new BinaryWriter(ms); + + writer.Write(ID); + writer.Write(PublicID); + writer.Write(answerNo); + writer.Write(OT); + writer.Write((int)TrainerGender); + writer.Write(Country); + writer.Write(Region); + + writer.Flush(); + ms.Flush(); + return serialized; + } + + public static SubmittedQuestionnaire Load(byte[] data, int start) + { + int id = BitConverter.ToInt32(data, start); + int publicId = BitConverter.ToInt32(data, start + 4); + int answerNo = BitConverter.ToInt32(data, start + 8); + uint ot = BitConverter.ToUInt32(data, start + 12); + int genderNum = BitConverter.ToInt32(data, start + 16); + ushort country = BitConverter.ToUInt16(data, start + 20); + ushort region = BitConverter.ToUInt16(data, start + 22); + + return new SubmittedQuestionnaire(id, publicId, answerNo, ot, (Structures.TrainerGenders)genderNum, country, region); + } + } +} diff --git a/library/Wfc/PlazaSchedule.cs b/library/Wfc/PlazaSchedule.cs new file mode 100644 index 00000000..580b02c9 --- /dev/null +++ b/library/Wfc/PlazaSchedule.cs @@ -0,0 +1,485 @@ +using System; +using System.IO; +using System.Collections.Generic; + +namespace PkmnFoundations.Wfc +{ + /// The schedule that a plaza will follow. + public class PlazaSchedule + { + public PlazaSchedule(uint lock_after_seconds, byte[] unk, uint bitflags, PlazaRoomType room_type, PlazaRoomSeason season, PlazaEventAndTime[] schedule) + { + this.LockAfterSeconds = lock_after_seconds; + this.Unk = unk; + this.BitFlags = bitflags; + this.RoomType = room_type; + this.Season = season; + this.Schedule = schedule; + } + + /// + /// When to close the room and force people from joining. Usually near the + /// end of the room. + /// + public uint LockAfterSeconds; + /// + /// An unknown series of 4 bytes. TODO: figure out + /// + public byte[] Unk; + /// + /// A series of bit flags that change behavior. + /// + /// The only one identified is: 0x1 -- which in pokemon heart gold causes arceus to have a real footprint. + /// + public uint BitFlags; + /// + /// The type of room this schedule is for. + /// + public PlazaRoomType RoomType; + /// + /// What seasonal styling should be applied to this room. + /// + public PlazaRoomSeason Season; + /// + /// The actual underlying schedule. + /// + public PlazaEventAndTime[] Schedule; + + public byte[] Save() + { + byte[] serialized = new byte[16 + (8 * Schedule.Length)]; + MemoryStream ms = new MemoryStream(serialized); + // BinaryWriter uses little endian which is what we want. + BinaryWriter writer = new BinaryWriter(ms); + + writer.Write(LockAfterSeconds); + writer.Write(Unk); + writer.Write(BitFlags); + writer.Write((byte)RoomType); + writer.Write((byte)Season); + writer.Write((ushort)Schedule.Length); + foreach (PlazaEventAndTime eventAndTime in Schedule) + { + writer.Write(eventAndTime.AfterSeconds); + writer.Write((int)eventAndTime.Event); + } + + writer.Flush(); + ms.Flush(); + return serialized; + } + + public static PlazaSchedule Load(byte[] data, int start) + { + uint lockAfter = BitConverter.ToUInt32(data, start); + byte[] unk = new byte[] { data[start + 4], data[start + 5], data[start + 6], data[start + 7] }; + uint bitflags = BitConverter.ToUInt32(data, start + 8); + PlazaRoomType roomType = (PlazaRoomType)data[start + 12]; + PlazaRoomSeason season = (PlazaRoomSeason)data[start + 13]; + + ushort scheduleLength = BitConverter.ToUInt16(data, start + 14); + ushort dataIdx = 16; + List schedule = new List(); + for (ushort eventIdx = 0; eventIdx < scheduleLength; ++eventIdx) + { + schedule.Add(new PlazaEventAndTime( + BitConverter.ToInt32(data, dataIdx), + BitConverter.ToInt32(data, dataIdx + 4) + )); + dataIdx += 8; + } + return new PlazaSchedule(lockAfter, unk, bitflags, roomType, season, schedule.ToArray()); + } + + public static PlazaSchedule Generate() + { + Random rng = new Random(); + + PlazaRoomType room = RoomSampler.Sample(); + uint arceusFlag = 0x0; + // love flipping coins. + if (rng.Next(0, 2) == 1) + { + arceusFlag = 0x1; + } + PlazaRoomSeason season = PlazaRoomSeason.None; + // Let's flip a coin again to see if should do any seasons at all. + if (rng.Next(0, 2) == 1) + { + // We give our current season a 62.5% chance of being selected, everything else a 12.5%. + double[] seasonWeights = new double[] { 0.125, 0.125, 0.125, 0.125 }; + int dayOfYear = DateTime.Now.DayOfYear; + if (dayOfYear >= 80 && dayOfYear < 172) + { + seasonWeights[0] = 0.625; + } + else if (dayOfYear >= 172 && dayOfYear < 264) + { + seasonWeights[1] = 0.625; + } + else if (dayOfYear >= 264 && dayOfYear < 355) + { + seasonWeights[2] = 0.625; + } + else + { + seasonWeights[3] = 0.625; + } + Support.AliasTable seasonPicker = Support.AliasTable.NewWithWeights(new Dictionary + { + [PlazaRoomSeason.Spring] = seasonWeights[0], + [PlazaRoomSeason.Summer] = seasonWeights[1], + [PlazaRoomSeason.Fall] = seasonWeights[2], + [PlazaRoomSeason.Winter] = seasonWeights[3] + }); + season = seasonPicker.Sample(); + } + + PlazaEventAndTime[] schedule = ScheduleSampler.Sample(); + return new PlazaSchedule( + (uint)schedule[schedule.Length - 1].AfterSeconds, + new byte[] { 0, 0, 0, 0 }, + arceusFlag, + room, + season, + schedule + ); + } + + /// + /// The random choices for generating a room, and their associated weights. + /// + /// These weights are roughly based off of what the PEERCHAT server which is written in python + /// does. The weights in that project were chosen incredibly roughly. Basically each base room + /// has little bit less than 1/4th of a chance of appearing. With the Mew room having a 2% chance. + /// + private static Support.AliasTable RoomSampler = Support.AliasTable.NewWithWeights(new Dictionary + { + [PlazaRoomType.Fire] = 0.244, + [PlazaRoomType.Water] = 0.244, + [PlazaRoomType.Grass] = 0.244, + [PlazaRoomType.Electric] = 0.244, + [PlazaRoomType.Mew] = 0.024, + }); + /// + /// Random choices for generating a schedule. All are around ~33%, with a slight preference to the actually captured short 20 minutes so we add up to 100%. + /// + private static Support.AliasTable ScheduleSampler = Support.AliasTable.NewWithWeights(new Dictionary + { + [RawTimeTables[0]] = 0.34, + [RawTimeTables[1]] = 0.33, + [RawTimeTables[2]] = 0.33, + }); + /// + /// A list of time tables to choose from when generating a schedule. + /// + /// We've only gotten a confirmed capture from a 20 minute time schedule which was + /// the lowest time ever reported. So we've created two other schedules at 25, and + /// 30 minutes where we just offset the 20 minute schedule so it still hopefully + /// feels real? + /// + /// Maybe someday we should create our own time tables. + /// + private static PlazaEventAndTime[][] RawTimeTables = new PlazaEventAndTime[][] { + // Straight from a real schedule -- 20 minutes. + new PlazaEventAndTime[] { + new PlazaEventAndTime(0, PlazaEvent.OverheadLightingBase), + new PlazaEventAndTime(0, PlazaEvent.StatueLightingBase), + new PlazaEventAndTime(0, PlazaEvent.SpotlightLightingBase), + new PlazaEventAndTime(780, PlazaEvent.StatueEndingPhaseOne), + new PlazaEventAndTime(840, PlazaEvent.OverheadEndingPhaseOne), + new PlazaEventAndTime(840, PlazaEvent.StatueEndingPhaseTwo), + new PlazaEventAndTime(900, PlazaEvent.OverheadEndingPhaseTwo), + new PlazaEventAndTime(900, PlazaEvent.OverheadEndingPhaseThree), + new PlazaEventAndTime(900, PlazaEvent.SpotlightEndingPhaseOne), + new PlazaEventAndTime(960, PlazaEvent.OverheadEndingPhaseThree), + new PlazaEventAndTime(960, PlazaEvent.StatueEndingPhaseTwo), + new PlazaEventAndTime(960, PlazaEvent.SpotlightEndingPhaseTwo), + new PlazaEventAndTime(960, PlazaEvent.EndAllMinigames), + new PlazaEventAndTime(1020, PlazaEvent.OverheadEndingPhaseFour), + new PlazaEventAndTime(1020, PlazaEvent.SpotlightEndingPhaseThree), + new PlazaEventAndTime(1020, PlazaEvent.StartFireworks), + new PlazaEventAndTime(1075, PlazaEvent.CreateParade), + new PlazaEventAndTime(1080, PlazaEvent.OverheadEndingPhaseFive), + new PlazaEventAndTime(1080, PlazaEvent.SpotlightEndingPhaseTwo), + new PlazaEventAndTime(1080, PlazaEvent.EndFireworks), + new PlazaEventAndTime(1140, PlazaEvent.SpotlightLightingBase), + new PlazaEventAndTime(1200, PlazaEvent.ClosePlaza) + }, + // 25 minute 'schedule' is just the 20 minute schedule that's been offset by 5 minutes. + new PlazaEventAndTime[] { + new PlazaEventAndTime(0, PlazaEvent.OverheadLightingBase), + new PlazaEventAndTime(0, PlazaEvent.StatueLightingBase), + new PlazaEventAndTime(0, PlazaEvent.SpotlightLightingBase), + new PlazaEventAndTime(1080, PlazaEvent.StatueEndingPhaseOne), + new PlazaEventAndTime(1140, PlazaEvent.OverheadEndingPhaseOne), + new PlazaEventAndTime(1140, PlazaEvent.StatueEndingPhaseTwo), + new PlazaEventAndTime(1200, PlazaEvent.OverheadEndingPhaseTwo), + new PlazaEventAndTime(1200, PlazaEvent.OverheadEndingPhaseThree), + new PlazaEventAndTime(1200, PlazaEvent.SpotlightEndingPhaseOne), + new PlazaEventAndTime(1260, PlazaEvent.OverheadEndingPhaseThree), + new PlazaEventAndTime(1260, PlazaEvent.StatueEndingPhaseTwo), + new PlazaEventAndTime(1260, PlazaEvent.SpotlightEndingPhaseTwo), + new PlazaEventAndTime(1260, PlazaEvent.EndAllMinigames), + new PlazaEventAndTime(1320, PlazaEvent.OverheadEndingPhaseFour), + new PlazaEventAndTime(1320, PlazaEvent.SpotlightEndingPhaseThree), + new PlazaEventAndTime(1320, PlazaEvent.StartFireworks), + new PlazaEventAndTime(1375, PlazaEvent.CreateParade), + new PlazaEventAndTime(1380, PlazaEvent.OverheadEndingPhaseFive), + new PlazaEventAndTime(1380, PlazaEvent.SpotlightEndingPhaseTwo), + new PlazaEventAndTime(1380, PlazaEvent.EndFireworks), + new PlazaEventAndTime(1440, PlazaEvent.SpotlightLightingBase), + new PlazaEventAndTime(1500, PlazaEvent.ClosePlaza) + }, + // 30 minute 'schedule' is just the 20 minute schedule that's been offset by 10 minutes. + new PlazaEventAndTime[] { + new PlazaEventAndTime(0, PlazaEvent.OverheadLightingBase), + new PlazaEventAndTime(0, PlazaEvent.StatueLightingBase), + new PlazaEventAndTime(0, PlazaEvent.SpotlightLightingBase), + new PlazaEventAndTime(1380, PlazaEvent.StatueEndingPhaseOne), + new PlazaEventAndTime(1440, PlazaEvent.OverheadEndingPhaseOne), + new PlazaEventAndTime(1440, PlazaEvent.StatueEndingPhaseTwo), + new PlazaEventAndTime(1500, PlazaEvent.OverheadEndingPhaseTwo), + new PlazaEventAndTime(1500, PlazaEvent.OverheadEndingPhaseThree), + new PlazaEventAndTime(1500, PlazaEvent.SpotlightEndingPhaseOne), + new PlazaEventAndTime(1560, PlazaEvent.OverheadEndingPhaseThree), + new PlazaEventAndTime(1560, PlazaEvent.StatueEndingPhaseTwo), + new PlazaEventAndTime(1560, PlazaEvent.SpotlightEndingPhaseTwo), + new PlazaEventAndTime(1560, PlazaEvent.EndAllMinigames), + new PlazaEventAndTime(1620, PlazaEvent.OverheadEndingPhaseFour), + new PlazaEventAndTime(1620, PlazaEvent.SpotlightEndingPhaseThree), + new PlazaEventAndTime(1620, PlazaEvent.StartFireworks), + new PlazaEventAndTime(1675, PlazaEvent.CreateParade), + new PlazaEventAndTime(1680, PlazaEvent.OverheadEndingPhaseFive), + new PlazaEventAndTime(1680, PlazaEvent.SpotlightEndingPhaseTwo), + new PlazaEventAndTime(1680, PlazaEvent.EndFireworks), + new PlazaEventAndTime(1740, PlazaEvent.SpotlightLightingBase), + new PlazaEventAndTime(1800, PlazaEvent.ClosePlaza) + } + }; + } + + public class PlazaEventAndTime + { + public PlazaEventAndTime(int afterSeconds, int plazaEvent) + { + this.AfterSeconds = afterSeconds; + this.Event = (PlazaEvent)plazaEvent; + } + + public PlazaEventAndTime(int afterSeconds, PlazaEvent plazaEvent) + { + this.AfterSeconds = afterSeconds; + this.Event = plazaEvent; + } + + /// How many seconds after the room opens, this event should happen at. + public int AfterSeconds; + /// The event that should actually happen. + public PlazaEvent Event; + } + + /// + /// The "type" of room that gets loaded, this basically just changes what color the room is, + /// what standees are being used, and what the center standee is. + /// + public enum PlazaRoomType + { + /// + /// Fire type room, so the base room theme is red, with standees of fire type starters. + /// + Fire = 0, + /// + /// Water type room, so the base room theme is blue, with standees of water type starters. + /// + Water = 1, + /// + /// Electric type room, so the base room theme is yellow, with standees of the electric mouse baby pokemon + /// (specifically Pichu, Plusle, Minun, and Pachirisu). + /// + Electric = 2, + /// + /// Grass type room, so the base room theme is green, with standees of grass type starters. + /// + Grass = 3, + /// + /// The 'special', or 'rare' mew themed room. All standees are replaced with lamps, the center display + /// is replaced with a giant statue of Mew, and this room CANNOT be themed with seasons. + /// + Mew = 4 + } + + /// + /// A season which basically overlays an existing rooms floors, and trees. Changing it to look like a specific + /// season. It has no functional differences. + /// + public enum PlazaRoomSeason + { + /// Load just the base room theme. No season styling. + None = 0, + Spring = 1, + Summer = 2, + Fall = 3, + Winter = 4 + } + + /// + /// An 'event' or cause a predetermined event to happen within a WiFi-Plaza. + /// + /// NOTE: many of these events intrinsically _imply_ that another event also happens. If you send one event, + /// the games own internal logic may choose to apply yet another event. + /// + public enum PlazaEvent + { + /// Lock the room, preventing new visitors from entering. + LockRoom = 0, + /// + /// Initialize the overhead lighting as it's "base" color and lighting + /// values. + /// + /// This is always implied, though the game should still be sent this event + /// at second 0. + /// + OverheadLightingBase = 1, + /// + /// Start the 'ending' lighting sequence of the room. + /// + /// If this is sent, the game will also force starting the spotlights. Though + /// again the game should be sent this event at the same time as the + /// spotlight starting. + /// + /// the questionnaire person will also start hopping. + /// + /// This slightly darkens the room, spotlights start, and the man starts + /// jumping. + /// + OverheadEndingPhaseOne = 2, + /// + /// Phase 2 of the ending lighting sequence of the room. + /// + /// If this is sent, the game will also force starting the spotlights. Though + /// again the game should have already sent the event with the spotlights + /// starting. + /// + /// the questionnair person will also start hopping. + /// + /// Compared to phase 1, the room just darkens more, but is not dark enough + /// for it to be fully 'dark'. Just like an afternoon shade. + /// + OverheadEndingPhaseTwo = 3, + /// + /// Phase 3 of the ending lighting sequence of the room. + /// + /// If this is sent, the game will also force starting the spotlights. Though + /// again the game should have already sent the event with the spotlights + /// starting. + /// + /// the questionnair person will also start hopping. + /// + /// This makes the room actually 'dark' compared to phase two. + /// + OverheadEndingPhaseThree = 4, + /// + /// Phase 4 of the ending lighting sequence of the room. + /// + /// If this is sent, the game will also force starting the spotlights. Though + /// again the game should have already sent the event with the spotlights + /// starting. + /// + /// the questionnair person will also start hopping. + /// + /// Compared to phase three, there are significantly more sparkles in the + /// room at this point. + /// + OverheadEndingPhaseFour = 5, + /// + /// Phase 5 of the ending lighting sequence of the room. + /// + /// If this is sent, the game will also force starting the spotlights. Though + /// again the game should have already sent the event with the spotlights + /// starting. + /// + /// the questionnair person will also start hopping. + /// + /// Compared to phase four, the room darkens once more and is at it's darkest + /// point. + /// + OverheadEndingPhaseFive = 6, + /// + /// Initialize the statue lighting as it's "base" color and lighting + /// values. + /// + /// This is always implied, though the game should still be sent this event + /// at second 0. + /// + StatueLightingBase = 7, + /// + /// Phase one of the statue ending lighting. + /// + /// Should be sent at the same time as overhead ending phase one most of the + /// time. + /// + /// Compared to the base lighting the statue starts having the back of the + /// statue very slightly darkened. + /// + StatueEndingPhaseOne = 8, + /// + /// Phase two of the statue ending lighting. + /// + /// Should be sent at the same time as overhead ending phase three most of + /// the time. + /// + /// Compared to phase one the statue front now has a light in front of the + /// statue to make the contrast of the statue higher, so it's pops out more. + /// + StatueEndingPhaseTwo = 9, + /// + /// Phase three of the statue ending lighting. + /// + /// Should be sent at the same time as overhead ending phase five most of + /// the time. + /// + /// Compared to phase two the light on the front of the statue goes away, and + /// it gets dark all the way around. + /// + StatueEndingPhaseThree = 10, + /// + /// Initialize the spotlight lighting as it's "base" color and lighting + /// values. + /// + /// This is always implied, though the game should still be sent this event + /// at second 0. + /// + SpotlightLightingBase = 11, + /// + /// Phase one of the spotlights when the room is ending. + /// + /// This should be sent at the same time as overhead ending phase one, and + /// also causes the spotlights to start, and the question man to start + /// hopping. + /// + SpotlightEndingPhaseOne = 12, + /// Phase two of the spotlights when the room is ending. + SpotlightEndingPhaseTwo = 13, + /// Phase three of the spotlights when the room is ending. + SpotlightEndingPhaseThree = 14, + /// Force end all the minigames, and don't let people play anymore. + EndAllMinigames = 15, + /// + /// Start the fireworks in the lobby. + /// + /// This happens usually about ~3 minutes before room end. + /// + StartFireworks = 16, + /// Stop the fireworks in the lobby + EndFireworks = 17, + /// + /// Create the parade, and get the floats to start going through. + /// + /// This normally happens about a minute after the fireworks start. + /// + CreateParade = 18, + /// This forcefully closes the plaza. + ClosePlaza = 19 + } +} diff --git a/library/Wfc/TrainerProfilePlaza.cs b/library/Wfc/TrainerProfilePlaza.cs index 832fb64b..292e00c0 100644 --- a/library/Wfc/TrainerProfilePlaza.cs +++ b/library/Wfc/TrainerProfilePlaza.cs @@ -1,86 +1,195 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.NetworkInformation; using System.Text; using PkmnFoundations.Structures; using PkmnFoundations.Support; namespace PkmnFoundations.Wfc { - public class TrainerProfilePlaza + public class TrainerProfilePlaza + { + public TrainerProfilePlaza() { - public TrainerProfilePlaza() - { - } - - public TrainerProfilePlaza(int pid, byte[] data_prefix, byte[] data) - { - if (data.Length != 152) throw new ArgumentException("Profile data must be 152 bytes."); - - PID = pid; - DataPrefix = data_prefix; - Data = data; - } - - // todo: encapsulate these so calculated fields are always correct - public int PID; - public byte[] DataPrefix; // 12 bytes - public byte[] Data; // 152 bytes - - // todo: These 4 values are basically big guesses. Fact check. - // todo: Add more fields - public Versions Version - { - get - { - return (Versions)DataPrefix[0x02]; - } - } - - public Languages Language - { - get - { - return (Languages)DataPrefix[0x03]; - } - } - - public byte Country - { - get - { - return Data[0x40]; - } - } - - public byte Region - { - get - { - return Data[0x42]; - } - } - - public uint OT - { - get - { - return BitConverter.ToUInt32(Data, 8); - } - } - - public EncodedString4 Name - { - get - { - return new EncodedString4(Data, 12, 16); - } - } - - public TrainerProfilePlaza Clone() - { - return new TrainerProfilePlaza(PID, DataPrefix.ToArray(), Data.ToArray()); - } } + + public TrainerProfilePlaza(byte[] gamestats_header, byte[] data) + { + if (gamestats_header.Length != 20) throw new ArgumentException("Gamestats header must be 20 bytes."); + if (data.Length != 148) throw new ArgumentException("Profile data must be 148 bytes."); + + GamestatsHeader = gamestats_header; + Data = data; + } + + public byte[] GamestatsHeader; // 20 bytes + public byte[] Data; // 148 bytes + + public int PID + { + get + { + return BitConverter.ToInt32(GamestatsHeader, 0); + } + } + + // Index 4 & 5 is unknown from `GameStatsHeader` + + public Versions Version + { + get + { + return (Versions)GamestatsHeader[6]; + } + } + + public Languages Language + { + get + { + return (Languages)GamestatsHeader[7]; + } + } + + public PhysicalAddress MacAddres + { + get + { + return new PhysicalAddress(new byte[] { + GamestatsHeader[8], GamestatsHeader[9], GamestatsHeader[10], + GamestatsHeader[11], GamestatsHeader[12], GamestatsHeader[13], + }); + } + } + + // Index 14-19 is unknown from `GameStatsHeader` + // Index 0-3 is unknown from `Data` + + public uint OT + { + get + { + return BitConverter.ToUInt32(Data, 4); + } + } + + public EncodedString4 Name + { + get + { + return new EncodedString4(Data, 8, 16); + } + } + + // Index 24-31 is unknown from `Data` + + public PlazaFavoritePokemon[] FavoritePokemon + { + get + { + List favorites = new List(); + + int form_idx = 44; + int egg_idx = 50; + foreach (var species_start in new int[] { 32, 34, 36, 38, 40, 42 }) + { + favorites.Add(new PlazaFavoritePokemon( + BitConverter.ToUInt16(Data, species_start), + Data[form_idx], + Data[egg_idx] + )); + form_idx += 1; + egg_idx += 1; + } + + return favorites.ToArray(); + } + } + + public TrainerGenders TrainerGender + { + get + { + return (TrainerGenders)Data[56]; + } + } + + public byte TrainerRegion + { + get + { + return Data[57]; + } + } + + public ushort TrainerModel + { + get + { + return BitConverter.ToUInt16(Data, 58); + } + } + + // Bytes 60 - 61 - 62 are unknown for `Data` + + public bool PokedexComplete + { + get + { + return Data[63] == 1; + } + } + + public bool GameCleared + { + get + { + return Data[64] == 1; + } + } + + // Byte 65 is unknown for `Data` + + public Versions VersionInsideData + { + get + { + return (Versions)Data[66]; + } + } + + // Bytes 67-136 are unknown for `Data` + + public ushort[] FavoriteMoveTypeIDs + { + get + { + return new ushort[] { + BitConverter.ToUInt16(Data, 136), + BitConverter.ToUInt16(Data, 138) + }; + } + } + + public TrainerProfilePlaza Clone() + { + return new TrainerProfilePlaza(GamestatsHeader.ToArray(), Data.ToArray()); + } + } + + public class PlazaFavoritePokemon + { + public PlazaFavoritePokemon(ushort species_number, byte form, byte was_egg) + { + this.SpeciesNumber = species_number; + this.Form = form; + this.WasEgg = was_egg == 1; + } + + public ushort SpeciesNumber; + public byte Form; + public bool WasEgg; + } }