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