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