mirror of
https://github.com/mm201/pkmn-classic-framework.git
synced 2026-04-23 09:07:16 -05:00
Added Generation V support.
This commit is contained in:
parent
62145e89dd
commit
6983ff8340
|
|
@ -34,6 +34,24 @@ namespace PkmnFoundations.GTS
|
|||
builder.Append("<br /><br />");
|
||||
}
|
||||
|
||||
builder.Append("Active GenV sessions (");
|
||||
builder.Append(manager.Sessions5.Count);
|
||||
builder.Append("):<br />");
|
||||
foreach (KeyValuePair<String, GtsSession5> session in manager.Sessions5)
|
||||
{
|
||||
builder.Append("PID: ");
|
||||
builder.Append(session.Value.PID);
|
||||
builder.Append("<br />Token: ");
|
||||
builder.Append(session.Value.Token);
|
||||
builder.Append("<br />Hash: ");
|
||||
builder.Append(session.Value.Hash);
|
||||
builder.Append("<br />URL: ");
|
||||
builder.Append(session.Value.URL);
|
||||
builder.Append("<br />Expires: ");
|
||||
builder.Append(session.Value.ExpiryDate);
|
||||
builder.Append("<br /><br />");
|
||||
}
|
||||
|
||||
if (Request.QueryString["data"] != null)
|
||||
{
|
||||
byte[] data = GtsSession4.DecryptData(Request.QueryString["data"]);
|
||||
|
|
@ -41,6 +59,13 @@ namespace PkmnFoundations.GTS
|
|||
builder.Append(RenderHex(data.ToHexStringLower()));
|
||||
builder.Append("<br />");
|
||||
}
|
||||
if (Request.QueryString["data5"] != null)
|
||||
{
|
||||
byte[] data = GtsSession5.DecryptData(Request.QueryString["data5"]);
|
||||
builder.Append("Data:<br />");
|
||||
builder.Append(RenderHex(data.ToHexStringLower()));
|
||||
builder.Append("<br />");
|
||||
}
|
||||
|
||||
litDebug.Text = builder.ToString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
<%@ Application Codebehind="Global.asax.cs" Inherits="gts.Global" Language="C#" %>
|
||||
<%@ Application Codebehind="Global.asax.cs" Inherits="PkmnFoundations.GTS.Global" Language="C#" %>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using System.Linq;
|
|||
using System.Web;
|
||||
using System.Web.Security;
|
||||
using System.Web.SessionState;
|
||||
using PkmnFoundations.GTS;
|
||||
|
||||
namespace PkmnFoundations.GTS
|
||||
{
|
||||
|
|
@ -86,6 +85,11 @@ namespace PkmnFoundations.GTS
|
|||
pathInfo = "/" + String.Join("/", split, 2, split.Length - 2);
|
||||
return VirtualPathUtility.ToAbsolute("~/pokemondpds.ashx");
|
||||
}
|
||||
else if (split.Length > 2 && split[1] == "syachi2ds" && split[2] == "web")
|
||||
{
|
||||
pathInfo = "/" + String.Join("/", split, 3, split.Length - 3);
|
||||
return VirtualPathUtility.ToAbsolute("~/syachi2ds.ashx");
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,5 +50,6 @@
|
|||
|
||||
<system.webServer>
|
||||
<modules runAllManagedModulesForAllRequests="true"/>
|
||||
<httpErrors existingResponse="PassThrough"></httpErrors>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@
|
|||
<Compile Include="BoxUploadDecode.aspx.designer.cs">
|
||||
<DependentUpon>BoxUploadDecode.aspx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="syachi2ds.ashx.cs">
|
||||
<DependentUpon>syachi2ds.ashx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="DatabaseTest.aspx.cs">
|
||||
<DependentUpon>DatabaseTest.aspx</DependentUpon>
|
||||
<SubType>ASPXCodeBehind</SubType>
|
||||
|
|
@ -127,6 +130,9 @@
|
|||
<Name>Library</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="syachi2ds.ashx" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" />
|
||||
<ProjectExtensions>
|
||||
|
|
|
|||
77
gts/src/GtsSession5.cs
Normal file
77
gts/src/GtsSession5.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using System.Security.Cryptography;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
|
||||
namespace PkmnFoundations.GTS
|
||||
{
|
||||
public class GtsSession5 : GtsSessionBase
|
||||
{
|
||||
public GtsSession5(int pid, String url) : base(pid, url)
|
||||
{
|
||||
Hash = ComputeHash(Token);
|
||||
}
|
||||
|
||||
public static String ComputeHash(String token)
|
||||
{
|
||||
// todo: add unit tests with some wiresharked examples.
|
||||
|
||||
if (m_sha1 == null) m_sha1 = SHA1.Create();
|
||||
|
||||
String longToken = "HZEdGCzcGGLvguqUEKQN" + token;
|
||||
|
||||
byte[] data = new byte[longToken.Length];
|
||||
MemoryStream stream = new MemoryStream(data);
|
||||
StreamWriter writer = new StreamWriter(stream);
|
||||
writer.Write(longToken); // fixme: this throws an OutOfBoundsException if the passed token contains non-ascii.
|
||||
writer.Flush();
|
||||
|
||||
return m_sha1.ComputeHash(data).ToHexStringLower();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts the NDS &data= querystring into readable binary data.
|
||||
/// The PID (little endian) is left at the start of the output
|
||||
/// but the (unencrypted) checksum is removed.
|
||||
/// </summary>
|
||||
public static byte[] DecryptData(String data)
|
||||
{
|
||||
byte[] data2 = FromUrlSafeBase64String(data);
|
||||
if (data2.Length < 12) throw new FormatException("Data must contain at least 12 bytes.");
|
||||
|
||||
byte[] data3 = new byte[data2.Length - 4];
|
||||
int checksum = BitConverter.ToInt32(data2, 0);
|
||||
checksum = IPAddress.NetworkToHostOrder(checksum); // endian flip
|
||||
checksum ^= 0x2db842b2;
|
||||
|
||||
int length = BitConverter.ToInt32(data2, 8);
|
||||
|
||||
// prune the checksum but keep pid and length.
|
||||
// this maximizes similarity with genIV's and allows
|
||||
// the ashx to check for pids to match.
|
||||
// todo: prune first 12 bytes, pass pid to this function to validate
|
||||
Array.Copy(data2, 4, data3, 0, data2.Length - 4);
|
||||
// todo: validate checksum and length
|
||||
|
||||
return data3;
|
||||
}
|
||||
|
||||
public static String ResponseChecksum(byte[] responseArray)
|
||||
{
|
||||
if (m_sha1 == null) m_sha1 = SHA1.Create();
|
||||
|
||||
String toCheck = "HZEdGCzcGGLvguqUEKQN" + ToUrlSafeBase64String(responseArray) + "HZEdGCzcGGLvguqUEKQN";
|
||||
|
||||
byte[] data = new byte[toCheck.Length];
|
||||
MemoryStream stream = new MemoryStream(data);
|
||||
StreamWriter writer = new StreamWriter(stream);
|
||||
writer.Write(toCheck);
|
||||
writer.Flush();
|
||||
|
||||
return m_sha1.ComputeHash(data).ToHexStringLower();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,15 +12,21 @@ namespace PkmnFoundations.GTS
|
|||
public class GtsSessionManager
|
||||
{
|
||||
public readonly Dictionary<String, GtsSession4> Sessions4;
|
||||
//public readonly Dictionary<String, GtsSession5> Sessions5;
|
||||
public readonly Dictionary<String, GtsSession5> Sessions5;
|
||||
|
||||
public GtsSessionManager()
|
||||
{
|
||||
Sessions4 = new Dictionary<String, GtsSession4>();
|
||||
//Sessions5 = new Dictionary<String, GtsSession5>();
|
||||
Sessions5 = new Dictionary<String, GtsSession5>();
|
||||
}
|
||||
|
||||
public void PruneSessions()
|
||||
{
|
||||
PruneSessions4();
|
||||
PruneSessions5();
|
||||
}
|
||||
|
||||
public void PruneSessions4()
|
||||
{
|
||||
Dictionary<String, GtsSession4> sessions = Sessions4;
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
|
@ -39,16 +45,45 @@ namespace PkmnFoundations.GTS
|
|||
}
|
||||
}
|
||||
|
||||
public void PruneSessions5()
|
||||
{
|
||||
Dictionary<String, GtsSession5> sessions = Sessions5;
|
||||
DateTime now = DateTime.UtcNow;
|
||||
|
||||
lock (sessions)
|
||||
{
|
||||
Queue<String> toRemove = new Queue<String>();
|
||||
foreach (KeyValuePair<String, GtsSession5> session in sessions)
|
||||
{
|
||||
if (session.Value.ExpiryDate < now) toRemove.Enqueue(session.Key);
|
||||
}
|
||||
while (toRemove.Count > 0)
|
||||
{
|
||||
sessions.Remove(toRemove.Dequeue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(GtsSession4 session)
|
||||
{
|
||||
Sessions4.Add(session.Hash, session);
|
||||
}
|
||||
|
||||
public void Add(GtsSession5 session)
|
||||
{
|
||||
Sessions5.Add(session.Hash, session);
|
||||
}
|
||||
|
||||
public void Remove(GtsSession4 session)
|
||||
{
|
||||
Sessions4.Remove(session.Hash);
|
||||
}
|
||||
|
||||
public void Remove(GtsSession5 session)
|
||||
{
|
||||
Sessions5.Remove(session.Hash);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the GtsSessionManager from the context's application state.
|
||||
/// </summary>
|
||||
|
|
@ -68,6 +103,7 @@ namespace PkmnFoundations.GTS
|
|||
|
||||
public GtsSession4 FindSession4(int pid, String url)
|
||||
{
|
||||
// todo: keep a hash table of pids that maps them onto session objects
|
||||
GtsSession4 result = null;
|
||||
|
||||
foreach (GtsSession4 sess in Sessions4.Values)
|
||||
|
|
@ -85,5 +121,25 @@ namespace PkmnFoundations.GTS
|
|||
return result;
|
||||
}
|
||||
|
||||
public GtsSession5 FindSession5(int pid, String url)
|
||||
{
|
||||
// todo: keep a hash table of pids that maps them onto session objects
|
||||
GtsSession5 result = null;
|
||||
|
||||
foreach (GtsSession5 sess in Sessions5.Values)
|
||||
{
|
||||
if (sess.PID == pid && sess.URL == url)
|
||||
{
|
||||
if (result != null)
|
||||
{
|
||||
// todo: there's more than one matching session... delete them all.
|
||||
}
|
||||
return sess; // temp until I get it to cleanup old sessions
|
||||
result = sess;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
1
gts/syachi2ds.ashx
Normal file
1
gts/syachi2ds.ashx
Normal file
|
|
@ -0,0 +1 @@
|
|||
<%@ WebHandler Language="C#" CodeBehind="syachi2ds.ashx.cs" Class="PkmnFoundations.GTS.syachi2ds" %>
|
||||
465
gts/syachi2ds.ashx.cs
Normal file
465
gts/syachi2ds.ashx.cs
Normal file
|
|
@ -0,0 +1,465 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
using System.Web.SessionState;
|
||||
using PkmnFoundations.Data;
|
||||
using PkmnFoundations.Structures;
|
||||
using PkmnFoundations.Support;
|
||||
using System.IO;
|
||||
|
||||
namespace PkmnFoundations.GTS
|
||||
{
|
||||
/// <summary>
|
||||
/// Summary description for pokemondpds
|
||||
/// </summary>
|
||||
public class syachi2ds : IHttpHandler, IRequiresSessionState
|
||||
{
|
||||
public void ProcessRequest(HttpContext context)
|
||||
{
|
||||
int pid;
|
||||
|
||||
if (context.Request.QueryString["pid"] == null ||
|
||||
!Int32.TryParse(context.Request.QueryString["pid"], out pid))
|
||||
{
|
||||
// pid missing or bad format
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.Request.QueryString.Count == 1)
|
||||
{
|
||||
// this is a new session request
|
||||
GtsSession5 session = new GtsSession5(pid, context.Request.PathInfo);
|
||||
GtsSessionManager manager = GtsSessionManager.FromContext(context);
|
||||
manager.Add(session);
|
||||
|
||||
context.Response.Write(session.Token);
|
||||
return;
|
||||
}
|
||||
else if (context.Request.QueryString.Count == 3)
|
||||
{
|
||||
// this is a main request
|
||||
if (context.Request.QueryString["hash"] == null ||
|
||||
context.Request.QueryString["data"] == null ||
|
||||
context.Request.QueryString["data"].Length < 16)
|
||||
{
|
||||
// arguments missing, partial check for data length.
|
||||
// (Here, we require data to hold at least 10 bytes.
|
||||
// In reality, it must hold at least 12, which is checked
|
||||
// for below after decoding)
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
GtsSessionManager manager = GtsSessionManager.FromContext(context);
|
||||
if (!manager.Sessions5.ContainsKey(context.Request.QueryString["hash"]))
|
||||
{
|
||||
// session hash not matched
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
GtsSession5 session = manager.Sessions5[context.Request.QueryString["hash"]];
|
||||
byte[] data;
|
||||
try
|
||||
{
|
||||
data = GtsSession5.DecryptData(context.Request.QueryString["data"]);
|
||||
if (data.Length < 8)
|
||||
{
|
||||
// data too short to contain a pid and length.
|
||||
// We check for 8 bytes, not 12, since the hash
|
||||
// isn't included in DecryptData's result.
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
// data too short to contain a checksum,
|
||||
// base64 format errors
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
int pid2 = BitConverter.ToInt32(data, 0);
|
||||
if (pid2 != pid)
|
||||
{
|
||||
// packed pid doesn't match ?pid=
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
int length = BitConverter.ToInt32(data, 4);
|
||||
if (length + 8 != data.Length)
|
||||
{
|
||||
// message length is incorrect
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
MemoryStream response = new MemoryStream();
|
||||
|
||||
switch (session.URL)
|
||||
{
|
||||
default:
|
||||
manager.Remove(session);
|
||||
|
||||
// unrecognized page url
|
||||
// should be error 404 once we're done debugging
|
||||
context.Response.Write("Almost there. Your path is:\n");
|
||||
context.Response.Write(session.URL);
|
||||
return;
|
||||
|
||||
// Called during startup. Unknown purpose.
|
||||
case "/worldexchange/info.asp":
|
||||
manager.Remove(session);
|
||||
|
||||
// todo: find out the meaning of this request.
|
||||
// is it simply done to check whether the GTS is online?
|
||||
response.Write(new byte[] { 0x01, 0x00 }, 0, 2);
|
||||
break;
|
||||
|
||||
// Called during startup. Seems to contain trainer profile stats.
|
||||
case "/common/setProfile.asp":
|
||||
manager.Remove(session);
|
||||
|
||||
// todo: Figure out what fun stuff is contained in this blob!
|
||||
|
||||
response.Write(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
|
||||
0, 8);
|
||||
break;
|
||||
|
||||
// Called during startup and when you check your pokemon's status.
|
||||
case "/worldexchange/result.asp":
|
||||
{
|
||||
manager.Remove(session);
|
||||
|
||||
// todo: more fun stuff is contained in this blob on genV.
|
||||
// my guess is that it's trainer profile info like setProfile.asp
|
||||
// There's a long string of 0s which could be a trainer card signature raster
|
||||
|
||||
GtsRecord5 record = DataAbstract.Instance.GtsDataForUser5(pid);
|
||||
|
||||
if (record == null)
|
||||
{
|
||||
// No pokemon in the system
|
||||
response.Write(new byte[] { 0x05, 0x00 }, 0, 2);
|
||||
}
|
||||
else if (record.IsExchanged > 0)
|
||||
{
|
||||
// traded pokemon arriving!!!
|
||||
response.Write(record.Save(), 0, 296);
|
||||
}
|
||||
else
|
||||
{
|
||||
// my existing pokemon is in the system, untraded
|
||||
response.Write(new byte[] { 0x04, 0x00 }, 0, 2);
|
||||
}
|
||||
|
||||
} break;
|
||||
|
||||
// Called after result.asp returns 4 when you check your pokemon's status
|
||||
case "/worldexchange/get.asp":
|
||||
{
|
||||
manager.Remove(session);
|
||||
|
||||
// this is only called if result.asp returned 4.
|
||||
// todo: what does this do if the contained pokemon is traded??
|
||||
// todo: the same big blob of stuff from result.asp is sent here too.
|
||||
|
||||
GtsRecord5 record = DataAbstract.Instance.GtsDataForUser5(pid);
|
||||
|
||||
if (record == null)
|
||||
{
|
||||
// No pokemon in the system
|
||||
// what do here?
|
||||
}
|
||||
else
|
||||
{
|
||||
// just write the record whether traded or not...
|
||||
response.Write(record.Save(), 0, 296);
|
||||
}
|
||||
} break;
|
||||
|
||||
// Called after result.asp returns an inbound pokemon record to delete it
|
||||
case "/worldexchange/delete.asp":
|
||||
{
|
||||
manager.Remove(session);
|
||||
|
||||
// todo: the same big blob of stuff from result.asp is sent here too.
|
||||
|
||||
GtsRecord5 record = DataAbstract.Instance.GtsDataForUser5(pid);
|
||||
if (record == null)
|
||||
{
|
||||
response.Write(new byte[] { 0x00, 0x00 }, 0, 2);
|
||||
}
|
||||
else if (record.IsExchanged > 0)
|
||||
{
|
||||
// delete the arrived pokemon from the system
|
||||
// todo: add transactions
|
||||
// todo: log the successful trade?
|
||||
// (either here or when the trade is done)
|
||||
bool success = DataAbstract.Instance.GtsDeletePokemon5(pid);
|
||||
if (success)
|
||||
{
|
||||
response.Write(new byte[] { 0x01, 0x00 }, 0, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
response.Write(new byte[] { 0x00, 0x00 }, 0, 2);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// own pokemon is there, fail. Use return.asp instead.
|
||||
response.Write(new byte[] { 0x00, 0x00 }, 0, 2);
|
||||
}
|
||||
} break;
|
||||
|
||||
// called to delete your own pokemon after taking it back
|
||||
case "/worldexchange/return.asp":
|
||||
{
|
||||
manager.Remove(session);
|
||||
|
||||
GtsRecord5 record = DataAbstract.Instance.GtsDataForUser5(pid);
|
||||
if (record == null)
|
||||
{
|
||||
response.Write(new byte[] { 0x00, 0x00 }, 0, 2);
|
||||
}
|
||||
else if (record.IsExchanged > 0)
|
||||
{
|
||||
// a traded pokemon is there, fail. Use delete.asp instead.
|
||||
response.Write(new byte[] { 0x00, 0x00 }, 0, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// delete own pokemon
|
||||
// todo: add transactions
|
||||
bool success = DataAbstract.Instance.GtsDeletePokemon5(pid);
|
||||
if (success)
|
||||
{
|
||||
response.Write(new byte[] { 0x01, 0x00 }, 0, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
response.Write(new byte[] { 0x00, 0x00 }, 0, 2);
|
||||
}
|
||||
}
|
||||
} break;
|
||||
|
||||
// Called when you deposit a pokemon into the system.
|
||||
case "/worldexchange/post.asp":
|
||||
{
|
||||
if (data.Length != 440)
|
||||
{
|
||||
manager.Remove(session);
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// todo: add transaction
|
||||
if (DataAbstract.Instance.GtsDataForUser5(pid) != null)
|
||||
{
|
||||
// there's already a pokemon inside
|
||||
manager.Remove(session);
|
||||
response.Write(new byte[] { 0x00, 0x00 }, 0, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
// keep the record in memory while we wait for post_finish.asp request
|
||||
byte[] recordBinary = new byte[296];
|
||||
Array.Copy(data, 8, recordBinary, 0, 296);
|
||||
GtsRecord5 record = new GtsRecord5(recordBinary);
|
||||
|
||||
// todo: figure out what bytes 304-439 do:
|
||||
// appears to be 4 bytes of 00, 128 bytes of stuff, 4 bytes of 80 00 00 00
|
||||
// projectpokemon says it's a "signature of pokémon struct."
|
||||
// does this mean sha1 with a secret const?
|
||||
// if it's just a checksum, we can probably ignore it for now.
|
||||
|
||||
if (!record.Validate())
|
||||
{
|
||||
// hack check failed
|
||||
manager.Remove(session);
|
||||
response.Write(new byte[] { 0x00, 0x00 }, 0, 2);
|
||||
break;
|
||||
}
|
||||
|
||||
// the following two fields are blank in the uploaded record.
|
||||
// The server must provide them instead.
|
||||
record.TimeDeposited = DateTime.UtcNow;
|
||||
record.PID = pid;
|
||||
|
||||
session.Tag = record;
|
||||
// todo: delete any other post.asp sessions registered under this PID
|
||||
|
||||
response.Write(new byte[] { 0x01, 0x00 }, 0, 2);
|
||||
|
||||
} break;
|
||||
|
||||
case "/worldexchange/post_finish.asp":
|
||||
{
|
||||
manager.Remove(session);
|
||||
|
||||
if (data.Length != 16)
|
||||
{
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// find a matching session which contains our record
|
||||
GtsSession5 prevSession = manager.FindSession5(pid, "/worldexchange/post.asp");
|
||||
|
||||
manager.Remove(prevSession);
|
||||
AssertHelper.Assert(prevSession.Tag is GtsRecord5);
|
||||
GtsRecord5 record = (GtsRecord5)prevSession.Tag;
|
||||
|
||||
if (DataAbstract.Instance.GtsDepositPokemon5(record))
|
||||
response.Write(new byte[] { 0x01, 0x00 }, 0, 2);
|
||||
else
|
||||
response.Write(new byte[] { 0x00, 0x00 }, 0, 2);
|
||||
|
||||
} break;
|
||||
|
||||
// the search request has a funny bit string request of search terms
|
||||
// and just returns a chunk of records end to end.
|
||||
case "/worldexchange/search.asp":
|
||||
{
|
||||
manager.Remove(session);
|
||||
|
||||
if (data.Length < 15 || data.Length > 16)
|
||||
{
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
int resultsCount = (int)data[14];
|
||||
|
||||
response.Write(new byte[] { 0x01, 0x00 }, 0, 2);
|
||||
|
||||
if (resultsCount < 1) break; // optimize away requests for no rows
|
||||
|
||||
ushort species = BitConverter.ToUInt16(data, 8);
|
||||
Genders gender = (Genders)data[10];
|
||||
byte minLevel = data[11];
|
||||
byte maxLevel = data[12];
|
||||
// byte 13 unknown
|
||||
byte country = 0;
|
||||
if (data.Length > 15) country = data[15];
|
||||
|
||||
if (resultsCount > 7) resultsCount = 7; // stop DDOS
|
||||
GtsRecord5[] records = DataAbstract.Instance.GtsSearch5(pid, species, gender, minLevel, maxLevel, country, resultsCount);
|
||||
foreach (GtsRecord5 record in records)
|
||||
{
|
||||
response.Write(record.Save(), 0, 296);
|
||||
}
|
||||
|
||||
} break;
|
||||
|
||||
// the exchange request uploads a record of the exchangee pokemon
|
||||
// plus the desired PID to trade for at the very end.
|
||||
case "/worldexchange/exchange.asp":
|
||||
{
|
||||
if (data.Length != 440)
|
||||
{
|
||||
manager.Remove(session);
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] uploadData = new byte[296];
|
||||
Array.Copy(data, 8, uploadData, 0, 296);
|
||||
GtsRecord5 upload = new GtsRecord5(uploadData);
|
||||
int targetPid = BitConverter.ToInt32(data, 304);
|
||||
GtsRecord5 result = DataAbstract.Instance.GtsDataForUser5(targetPid);
|
||||
|
||||
// enforce request requirements server side
|
||||
if (result == null || !upload.Validate() || !upload.CanTrade(result))
|
||||
{
|
||||
manager.Remove(session);
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
object[] tag = new GtsRecord5[2];
|
||||
tag[0] = upload;
|
||||
tag[1] = result;
|
||||
session.Tag = tag;
|
||||
|
||||
GtsRecord5 tradedResult = result.Clone();
|
||||
tradedResult.FlagTraded(upload); // only real purpose is to generate a proper response
|
||||
|
||||
// todo: we need a mechanism to "reserve" a pokemon being traded at this
|
||||
// point in the process, but be able to relinquish it if exchange_finish
|
||||
// never happens.
|
||||
// Currently, if two people try to take the same pokemon, it will appear
|
||||
// to work for both but then fail for the second after they've saved
|
||||
// their game. This causes a hard crash and a "save file is corrupt,
|
||||
// "previous will be loaded" error when restarting.
|
||||
// the reservation can be done in application state and has no reason
|
||||
// to touch the database. (exchange_finish won't work anyway if application
|
||||
// state is lost.)
|
||||
|
||||
response.Write(result.Save(), 0, 296);
|
||||
|
||||
} break;
|
||||
|
||||
case "/worldexchange/exchange_finish.asp":
|
||||
{
|
||||
manager.Remove(session);
|
||||
|
||||
if (data.Length != 16)
|
||||
{
|
||||
Error400(context);
|
||||
return;
|
||||
}
|
||||
|
||||
// find a matching session which contains our record
|
||||
GtsSession5 prevSession = manager.FindSession5(pid, "/worldexchange/exchange.asp");
|
||||
|
||||
manager.Remove(prevSession);
|
||||
AssertHelper.Assert(prevSession.Tag is GtsRecord5[]);
|
||||
GtsRecord5[] tag = (GtsRecord5[])prevSession.Tag;
|
||||
AssertHelper.Assert(tag.Length == 2);
|
||||
|
||||
GtsRecord5 upload = (GtsRecord5)tag[0];
|
||||
GtsRecord5 result = (GtsRecord5)tag[1];
|
||||
|
||||
if (DataAbstract.Instance.GtsTradePokemon5(upload, result))
|
||||
response.Write(new byte[] { 0x01, 0x00 }, 0, 2);
|
||||
else
|
||||
response.Write(new byte[] { 0x00, 0x00 }, 0, 2);
|
||||
|
||||
} break;
|
||||
}
|
||||
|
||||
response.Flush();
|
||||
byte[] responseArray = response.ToArray();
|
||||
response.Dispose();
|
||||
response = null;
|
||||
|
||||
// write GenV response checksum
|
||||
context.Response.OutputStream.Write(responseArray, 0, responseArray.Length);
|
||||
context.Response.Write(GtsSession5.ResponseChecksum(responseArray));
|
||||
}
|
||||
else
|
||||
// wrong number of querystring arguments
|
||||
Error400(context);
|
||||
}
|
||||
|
||||
private void Error400(HttpContext context)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
context.Response.Write("Bad request");
|
||||
}
|
||||
|
||||
public bool IsReusable
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,17 @@ namespace PkmnFoundations.Data
|
|||
|
||||
public abstract GtsRecord4[] GtsSearch4(int pid, ushort species, Genders gender, byte minLevel, byte maxLevel, byte country, int count);
|
||||
|
||||
public abstract GtsRecord5 GtsDataForUser5(int pid);
|
||||
|
||||
public abstract bool GtsDepositPokemon5(GtsRecord5 record);
|
||||
|
||||
public abstract bool GtsDeletePokemon5(int pid);
|
||||
|
||||
public abstract bool GtsTradePokemon5(int pidSrc, int pidDest);
|
||||
public abstract bool GtsTradePokemon5(GtsRecord5 upload, GtsRecord5 result);
|
||||
|
||||
public abstract GtsRecord5[] GtsSearch5(int pid, ushort species, Genders gender, byte minLevel, byte maxLevel, byte country, int count);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ namespace PkmnFoundations.Data
|
|||
}
|
||||
#endregion
|
||||
|
||||
#region GTS
|
||||
#region GTS 4
|
||||
public GtsRecord4 GtsDataForUser4(MySqlTransaction tran, int pid)
|
||||
{
|
||||
MySqlDataReader reader = (MySqlDataReader)tran.ExecuteReader("SELECT Data, Species, Gender, Level, " +
|
||||
|
|
@ -125,7 +125,7 @@ namespace PkmnFoundations.Data
|
|||
}
|
||||
}
|
||||
|
||||
public int GtsGetDepositId(int pid, MySqlTransaction tran)
|
||||
public int GtsGetDepositId4(int pid, MySqlTransaction tran)
|
||||
{
|
||||
object o = tran.ExecuteScalar("SELECT id FROM GtsPokemon4 WHERE pid = @pid " +
|
||||
"ORDER BY IsExchanged DESC, TimeWithdrawn, TimeDeposited LIMIT 1",
|
||||
|
|
@ -136,7 +136,7 @@ namespace PkmnFoundations.Data
|
|||
|
||||
public bool GtsDeletePokemon4(MySqlTransaction tran, int pid)
|
||||
{
|
||||
int pkmnId = GtsGetDepositId(pid, tran);
|
||||
int pkmnId = GtsGetDepositId4(pid, tran);
|
||||
if (pkmnId == 0) return false;
|
||||
|
||||
tran.ExecuteNonQuery("DELETE FROM GtsPokemon4 WHERE id = @id",
|
||||
|
|
@ -334,6 +334,325 @@ namespace PkmnFoundations.Data
|
|||
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region GTS 5
|
||||
|
||||
public GtsRecord5 GtsDataForUser5(MySqlTransaction tran, int pid)
|
||||
{
|
||||
MySqlDataReader reader = (MySqlDataReader)tran.ExecuteReader("SELECT Data, Unknown0, " +
|
||||
"Species, Gender, Level, " +
|
||||
"RequestedSpecies, RequestedGender, RequestedMinLevel, RequestedMaxLevel, " +
|
||||
"Unknown1, TrainerGender, Unknown2, TimeDeposited, TimeWithdrawn, pid, " +
|
||||
"TrainerOT, TrainerName, TrainerCountry, TrainerRegion, TrainerClass, " +
|
||||
"IsExchanged, TrainerVersion, TrainerLanguage, TrainerBadges, TrainerUnityTower " +
|
||||
"FROM GtsPokemon5 WHERE pid = @pid",
|
||||
new MySqlParameter("@pid", pid));
|
||||
|
||||
if (!reader.Read())
|
||||
{
|
||||
reader.Close();
|
||||
return null;
|
||||
}
|
||||
GtsRecord5 result = Record5FromReader(reader);
|
||||
#if DEBUG
|
||||
AssertHelper.Equals(result.PID, pid);
|
||||
#endif
|
||||
reader.Close();
|
||||
return result;
|
||||
}
|
||||
|
||||
public override GtsRecord5 GtsDataForUser5(int pid)
|
||||
{
|
||||
using (MySqlConnection db = CreateConnection())
|
||||
{
|
||||
db.Open();
|
||||
MySqlTransaction tran = db.BeginTransaction();
|
||||
GtsRecord5 result = GtsDataForUser5(tran, pid);
|
||||
tran.Commit();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public bool GtsDepositPokemon5(MySqlTransaction tran, GtsRecord5 record)
|
||||
{
|
||||
if (record.Data.Length != 220) throw new FormatException("pkm data must be 220 bytes.");
|
||||
if (record.Unknown0.Length != 16) throw new FormatException("pkm padding must be 16 bytes.");
|
||||
if (record.TrainerName.Length != 16) throw new FormatException("Trainer name must be 16 bytes.");
|
||||
// note that IsTraded being true in the record is not an error condition
|
||||
// since it might have use later on. You should check for this in the upload handler.
|
||||
|
||||
long count = (long)tran.ExecuteScalar("SELECT Count(*) FROM GtsPokemon5 WHERE pid = @pid",
|
||||
new MySqlParameter("@pid", record.PID));
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
// This player already has a pokemon in the system.
|
||||
// we can possibly allow multiples under some future conditions
|
||||
return false;
|
||||
}
|
||||
|
||||
tran.ExecuteNonQuery("INSERT INTO GtsPokemon5 " +
|
||||
"(Data, Unknown0, Species, Gender, Level, RequestedSpecies, RequestedGender, " +
|
||||
"RequestedMinLevel, RequestedMaxLevel, Unknown1, TrainerGender, " +
|
||||
"Unknown2, TimeDeposited, TimeWithdrawn, pid, TrainerOT, TrainerName, " +
|
||||
"TrainerCountry, TrainerRegion, TrainerClass, IsExchanged, TrainerVersion, " +
|
||||
"TrainerLanguage, TrainerBadges, TrainerUnityTower) " +
|
||||
"VALUES (@Data, @Unknown0, @Species, @Gender, @Level, @RequestedSpecies, " +
|
||||
"@RequestedGender, @RequestedMinLevel, @RequestedMaxLevel, @Unknown1, " +
|
||||
"@TrainerGender, @Unknown2, @TimeDeposited, @TimeWithdrawn, @pid, " +
|
||||
"@TrainerOT, @TrainerName, @TrainerCountry, @TrainerRegion, @TrainerClass, " +
|
||||
"@IsExchanged, @TrainerVersion, @TrainerLanguage, @TrainerBadges, @TrainerUnityTower)",
|
||||
ParamsFromRecord5(record));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool GtsDepositPokemon5(GtsRecord5 record)
|
||||
{
|
||||
if (record.Data.Length != 220) throw new FormatException("pkm data must be 220 bytes.");
|
||||
if (record.Unknown0.Length != 16) throw new FormatException("pkm padding must be 16 bytes.");
|
||||
if (record.TrainerName.Length != 16) throw new FormatException("Trainer name must be 16 bytes.");
|
||||
// note that IsTraded being true in the record is not an error condition
|
||||
// since it might have use later on. You should check for this in the upload handler.
|
||||
|
||||
using (MySqlConnection db = CreateConnection())
|
||||
{
|
||||
db.Open();
|
||||
MySqlTransaction tran = db.BeginTransaction();
|
||||
|
||||
if (!GtsDepositPokemon5(tran, record))
|
||||
{
|
||||
tran.Rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
tran.Commit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public int GtsGetDepositId5(int pid, MySqlTransaction tran)
|
||||
{
|
||||
object o = tran.ExecuteScalar("SELECT id FROM GtsPokemon5 WHERE pid = @pid " +
|
||||
"ORDER BY IsExchanged DESC, TimeWithdrawn, TimeDeposited LIMIT 1",
|
||||
new MySqlParameter("@pid", pid));
|
||||
if (o == null || o == DBNull.Value) return 0;
|
||||
return (int)((uint)o);
|
||||
}
|
||||
|
||||
public bool GtsDeletePokemon5(MySqlTransaction tran, int pid)
|
||||
{
|
||||
int pkmnId = GtsGetDepositId5(pid, tran);
|
||||
if (pkmnId == 0) return false;
|
||||
|
||||
tran.ExecuteNonQuery("DELETE FROM GtsPokemon5 WHERE id = @id",
|
||||
new MySqlParameter("@id", pkmnId));
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool GtsDeletePokemon5(int pid)
|
||||
{
|
||||
using (MySqlConnection db = CreateConnection())
|
||||
{
|
||||
db.Open();
|
||||
MySqlTransaction tran = db.BeginTransaction();
|
||||
|
||||
if (!GtsDeletePokemon5(tran, pid))
|
||||
{
|
||||
tran.Rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
tran.Commit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool GtsTradePokemon5(int pidSrc, int pidDest)
|
||||
{
|
||||
// not needed yet.
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool GtsTradePokemon5(GtsRecord5 upload, GtsRecord5 result)
|
||||
{
|
||||
GtsRecord5 traded = upload.Clone();
|
||||
traded.FlagTraded(result);
|
||||
|
||||
using (MySqlConnection db = CreateConnection())
|
||||
{
|
||||
db.Open();
|
||||
MySqlTransaction tran = db.BeginTransaction();
|
||||
|
||||
GtsRecord5 resultOrig = GtsDataForUser5(tran, result.PID);
|
||||
if (resultOrig == null || resultOrig != result)
|
||||
{
|
||||
// looks like the pokemon was ninja'd between the Exchange and Exchange_finish
|
||||
tran.Rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!GtsDeletePokemon5(tran, result.PID))
|
||||
{
|
||||
tran.Rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!GtsDepositPokemon5(tran, traded))
|
||||
{
|
||||
tran.Rollback();
|
||||
return false;
|
||||
}
|
||||
|
||||
tran.Commit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public override GtsRecord5[] GtsSearch5(int pid, ushort species, Genders gender, byte minLevel, byte maxLevel, byte country, int count)
|
||||
{
|
||||
using (MySqlConnection db = CreateConnection())
|
||||
{
|
||||
List<MySqlParameter> _params = new List<MySqlParameter>();
|
||||
String where = "WHERE pid != @pid AND Species = @species";
|
||||
_params.Add(new MySqlParameter("@pid", pid));
|
||||
_params.Add(new MySqlParameter("@species", species));
|
||||
|
||||
if (gender != Genders.Either)
|
||||
{
|
||||
where += " AND Gender = @gender";
|
||||
_params.Add(new MySqlParameter("@gender", (byte)gender));
|
||||
}
|
||||
|
||||
if (minLevel > 0 && maxLevel > 0)
|
||||
{
|
||||
where += " AND Level BETWEEN @min_level AND @max_level";
|
||||
_params.Add(new MySqlParameter("@min_level", minLevel));
|
||||
_params.Add(new MySqlParameter("@max_level", maxLevel));
|
||||
}
|
||||
else if (minLevel > 0)
|
||||
{
|
||||
where += " AND Level >= @min_level";
|
||||
_params.Add(new MySqlParameter("@min_level", minLevel));
|
||||
}
|
||||
else if (maxLevel > 0)
|
||||
{
|
||||
where += " AND Level <= @max_level";
|
||||
_params.Add(new MySqlParameter("@max_level", maxLevel));
|
||||
}
|
||||
|
||||
if (country > 0)
|
||||
{
|
||||
where += " AND TrainerCountry = @country";
|
||||
_params.Add(new MySqlParameter("@country", country));
|
||||
}
|
||||
|
||||
_params.Add(new MySqlParameter("@count", count));
|
||||
|
||||
db.Open();
|
||||
// todo: sort me in creative ways
|
||||
MySqlDataReader reader = (MySqlDataReader)db.ExecuteReader("SELECT Data, Unknown0, " +
|
||||
"Species, Gender, Level, " +
|
||||
"RequestedSpecies, RequestedGender, RequestedMinLevel, RequestedMaxLevel, " +
|
||||
"Unknown1, TrainerGender, Unknown2, TimeDeposited, TimeWithdrawn, pid, " +
|
||||
"TrainerOT, TrainerName, TrainerCountry, TrainerRegion, TrainerClass, " +
|
||||
"IsExchanged, TrainerVersion, TrainerLanguage, TrainerBadges, TrainerUnityTower " +
|
||||
"FROM GtsPokemon5 " + where +
|
||||
" ORDER BY TimeDeposited DESC LIMIT @count",
|
||||
_params.ToArray());
|
||||
|
||||
List<GtsRecord5> records = new List<GtsRecord5>(count);
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
records.Add(Record5FromReader(reader));
|
||||
}
|
||||
|
||||
return records.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private static GtsRecord5 Record5FromReader(MySqlDataReader reader)
|
||||
{
|
||||
GtsRecord5 result = new GtsRecord5();
|
||||
|
||||
byte[] data = new byte[220];
|
||||
reader.GetBytes(0, 0, data, 0, 220);
|
||||
result.Data = data;
|
||||
data = null;
|
||||
|
||||
data = new byte[16];
|
||||
reader.GetBytes(1, 0, data, 0, 16);
|
||||
result.Unknown0 = data;
|
||||
data = null;
|
||||
|
||||
result.Species = reader.GetUInt16(2);
|
||||
result.Gender = (Genders)reader.GetByte(3);
|
||||
result.Level = reader.GetByte(4);
|
||||
result.RequestedSpecies = reader.GetUInt16(5);
|
||||
result.RequestedGender = (Genders)reader.GetByte(6);
|
||||
result.RequestedMinLevel = reader.GetByte(7);
|
||||
result.RequestedMaxLevel = reader.GetByte(8);
|
||||
result.Unknown1 = reader.GetByte(9);
|
||||
result.TrainerGender = (GtsTrainerGenders)reader.GetByte(10);
|
||||
result.Unknown2 = reader.GetByte(11);
|
||||
if (reader.IsDBNull(12)) result.TimeDeposited = null;
|
||||
else result.TimeDeposited = reader.GetDateTime(12);
|
||||
if (reader.IsDBNull(13)) result.TimeWithdrawn = null;
|
||||
else result.TimeWithdrawn = reader.GetDateTime(13);
|
||||
result.PID = reader.GetInt32(14);
|
||||
result.TrainerOT = reader.GetUInt32(15);
|
||||
|
||||
data = new byte[16];
|
||||
reader.GetBytes(16, 0, data, 0, 16);
|
||||
result.TrainerName = data;
|
||||
data = null;
|
||||
|
||||
result.TrainerCountry = reader.GetByte(17);
|
||||
result.TrainerRegion = reader.GetByte(18);
|
||||
result.TrainerClass = reader.GetByte(19);
|
||||
result.IsExchanged = reader.GetByte(20);
|
||||
result.TrainerVersion = reader.GetByte(21);
|
||||
result.TrainerLanguage = reader.GetByte(22);
|
||||
result.TrainerBadges = reader.GetByte(23);
|
||||
result.TrainerUnityTower = reader.GetByte(24);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static MySqlParameter[] ParamsFromRecord5(GtsRecord5 record)
|
||||
{
|
||||
MySqlParameter[] result = new MySqlParameter[25];
|
||||
|
||||
result[0] = new MySqlParameter("@Data", record.Data);
|
||||
result[1] = new MySqlParameter("@Unknown0", record.Unknown0);
|
||||
result[2] = new MySqlParameter("@Species", record.Species);
|
||||
result[3] = new MySqlParameter("@Gender", (byte)record.Gender);
|
||||
result[4] = new MySqlParameter("@Level", record.Level);
|
||||
result[5] = new MySqlParameter("@RequestedSpecies", record.RequestedSpecies);
|
||||
result[6] = new MySqlParameter("@RequestedGender", (byte)record.RequestedGender);
|
||||
result[7] = new MySqlParameter("@RequestedMinLevel", record.RequestedMinLevel);
|
||||
result[8] = new MySqlParameter("@RequestedMaxLevel", record.RequestedMaxLevel);
|
||||
result[9] = new MySqlParameter("@Unknown1", record.Unknown1);
|
||||
result[10] = new MySqlParameter("@TrainerGender", (byte)record.TrainerGender);
|
||||
result[11] = new MySqlParameter("@Unknown2", record.Unknown2);
|
||||
result[12] = new MySqlParameter("@TimeDeposited", record.TimeDeposited);
|
||||
result[13] = new MySqlParameter("@TimeWithdrawn", record.TimeWithdrawn);
|
||||
result[14] = new MySqlParameter("@pid", record.PID);
|
||||
result[15] = new MySqlParameter("@TrainerOT", record.TrainerOT);
|
||||
result[16] = new MySqlParameter("@TrainerName", record.TrainerName);
|
||||
result[17] = new MySqlParameter("@TrainerCountry", record.TrainerCountry);
|
||||
result[18] = new MySqlParameter("@TrainerRegion", record.TrainerRegion);
|
||||
result[19] = new MySqlParameter("@TrainerClass", record.TrainerClass);
|
||||
result[20] = new MySqlParameter("@IsExchanged", record.IsExchanged);
|
||||
result[21] = new MySqlParameter("@TrainerVersion", record.TrainerVersion);
|
||||
result[22] = new MySqlParameter("@TrainerLanguage", record.TrainerLanguage);
|
||||
result[23] = new MySqlParameter("@TrainerBadges", record.TrainerBadges);
|
||||
result[24] = new MySqlParameter("@TrainerUnityTower", record.TrainerUnityTower);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@
|
|||
<Compile Include="Data\DataMysql.cs" />
|
||||
<Compile Include="Data\DataSqlite.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Structures\GtsRecord5.cs" />
|
||||
<Compile Include="Structures\Enums.cs" />
|
||||
<Compile Include="Structures\GtsRecord4.cs" />
|
||||
<Compile Include="Support\AssertHelper.cs" />
|
||||
|
|
|
|||
250
library/Structures/GtsRecord5.cs
Normal file
250
library/Structures/GtsRecord5.cs
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Runtime.Serialization;
|
||||
using System.IO;
|
||||
|
||||
namespace PkmnFoundations.Structures
|
||||
{
|
||||
/// <summary>
|
||||
/// Structure used to represent Pokémon on the GTS in Generation V.
|
||||
/// Includes a Pokémon box structure and metadata related to the trainer
|
||||
/// and request.
|
||||
/// </summary>
|
||||
[Serializable()]
|
||||
public class GtsRecord5
|
||||
{
|
||||
public GtsRecord5()
|
||||
{
|
||||
}
|
||||
|
||||
public GtsRecord5(byte[] data)
|
||||
{
|
||||
Load(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obfuscated Pokémon (pkm) data. 220 bytes
|
||||
/// </summary>
|
||||
public byte[] Data;
|
||||
|
||||
/// <summary>
|
||||
/// Unknown padding between pkm and rest of data. 16 bytes.
|
||||
/// </summary>
|
||||
public byte[] Unknown0;
|
||||
|
||||
/// <summary>
|
||||
/// National Dex species number
|
||||
/// </summary>
|
||||
public ushort Species;
|
||||
|
||||
/// <summary>
|
||||
/// Pokémon gender
|
||||
/// </summary>
|
||||
public Genders Gender;
|
||||
|
||||
/// <summary>
|
||||
/// Pokémon level
|
||||
/// </summary>
|
||||
public byte Level;
|
||||
|
||||
/// <summary>
|
||||
/// Requested National Dex species number
|
||||
/// </summary>
|
||||
public ushort RequestedSpecies;
|
||||
|
||||
public Genders RequestedGender;
|
||||
|
||||
public byte RequestedMinLevel;
|
||||
public byte RequestedMaxLevel;
|
||||
public byte Unknown1;
|
||||
public GtsTrainerGenders TrainerGender;
|
||||
public byte Unknown2;
|
||||
|
||||
public DateTime ? TimeDeposited;
|
||||
public DateTime ? TimeWithdrawn;
|
||||
|
||||
/// <summary>
|
||||
/// User ID of the player (not Personality Value)
|
||||
/// </summary>
|
||||
public int PID;
|
||||
|
||||
public uint TrainerOT;
|
||||
|
||||
/// <summary>
|
||||
/// 16 bytes
|
||||
/// </summary>
|
||||
public byte[] TrainerName;
|
||||
|
||||
public byte TrainerCountry;
|
||||
public byte TrainerRegion;
|
||||
public byte TrainerClass;
|
||||
|
||||
public byte IsExchanged;
|
||||
|
||||
public byte TrainerVersion;
|
||||
public byte TrainerLanguage;
|
||||
|
||||
public byte TrainerBadges; // speculative. Usually 8.
|
||||
public byte TrainerUnityTower;
|
||||
|
||||
public byte[] Save()
|
||||
{
|
||||
// todo: enclose in properties and validate these when assigning.
|
||||
if (Data.Length != 0xDC) throw new FormatException("PKM length is incorrect");
|
||||
if (TrainerName.Length != 0x10) throw new FormatException("Trainer name length is incorrect");
|
||||
byte[] data = new byte[296];
|
||||
MemoryStream s = new MemoryStream(data);
|
||||
s.Write(Data, 0, 0xDC);
|
||||
s.Write(Unknown0, 0, 0x10);
|
||||
s.Write(BitConverter.GetBytes(Species), 0, 2);
|
||||
s.WriteByte((byte)Gender);
|
||||
s.WriteByte(Level);
|
||||
s.Write(BitConverter.GetBytes(RequestedSpecies), 0, 2);
|
||||
s.WriteByte((byte)RequestedGender);
|
||||
s.WriteByte(RequestedMinLevel);
|
||||
s.WriteByte(RequestedMaxLevel);
|
||||
s.WriteByte(Unknown1);
|
||||
s.WriteByte((byte)TrainerGender);
|
||||
s.WriteByte(Unknown2);
|
||||
s.Write(BitConverter.GetBytes(DateToTimestamp(TimeDeposited)), 0, 8);
|
||||
s.Write(BitConverter.GetBytes(DateToTimestamp(TimeWithdrawn)), 0, 8);
|
||||
s.Write(BitConverter.GetBytes(PID), 0, 4);
|
||||
s.Write(BitConverter.GetBytes(TrainerOT), 0, 4);
|
||||
s.Write(TrainerName, 0, 0x10);
|
||||
s.WriteByte(TrainerCountry);
|
||||
s.WriteByte(TrainerRegion);
|
||||
s.WriteByte(TrainerClass);
|
||||
s.WriteByte(IsExchanged);
|
||||
s.WriteByte(TrainerVersion);
|
||||
s.WriteByte(TrainerLanguage);
|
||||
s.WriteByte(TrainerBadges);
|
||||
s.WriteByte(TrainerUnityTower);
|
||||
s.Close();
|
||||
return data;
|
||||
}
|
||||
|
||||
public void Load(byte[] data)
|
||||
{
|
||||
if (data.Length != 296) throw new FormatException("GTS record length is incorrect.");
|
||||
|
||||
Data = new byte[0xDC];
|
||||
Array.Copy(data, 0, Data, 0, 0xDC);
|
||||
Unknown0 = new byte[0x10];
|
||||
Array.Copy(data, 0xDC, Unknown0, 0, 0x10);
|
||||
Species = BitConverter.ToUInt16(data, 0xEC);
|
||||
Gender = (Genders)data[0xEE];
|
||||
Level = data[0xEF];
|
||||
RequestedSpecies = BitConverter.ToUInt16(data, 0xF0);
|
||||
RequestedGender = (Genders)data[0xF2];
|
||||
RequestedMinLevel = data[0xF3];
|
||||
RequestedMaxLevel = data[0xF4];
|
||||
Unknown1 = data[0xF5];
|
||||
TrainerGender = (GtsTrainerGenders)data[0xF6];
|
||||
Unknown2 = data[0xF7];
|
||||
TimeDeposited = TimestampToDate(BitConverter.ToUInt64(data, 0xF8));
|
||||
TimeWithdrawn = TimestampToDate(BitConverter.ToUInt64(data, 0x100));
|
||||
PID = BitConverter.ToInt32(data, 0x108);
|
||||
TrainerOT = BitConverter.ToUInt32(data, 0x10C);
|
||||
TrainerName = new byte[0x10];
|
||||
Array.Copy(data, 0x110, TrainerName, 0, 0x10);
|
||||
TrainerCountry = data[0x120];
|
||||
TrainerRegion = data[0x121];
|
||||
TrainerClass = data[0x122];
|
||||
IsExchanged = data[0x123];
|
||||
TrainerVersion = data[0x124];
|
||||
TrainerLanguage = data[0x125];
|
||||
TrainerBadges = data[0x126];
|
||||
TrainerUnityTower = data[0x127];
|
||||
}
|
||||
|
||||
public GtsRecord5 Clone()
|
||||
{
|
||||
// todo: I am not very efficient
|
||||
return new GtsRecord5(Save());
|
||||
}
|
||||
|
||||
public bool Validate()
|
||||
{
|
||||
// todo: a. legitimacy check, and b. check that pkm data matches metadata
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CanTrade(GtsRecord5 other)
|
||||
{
|
||||
if (Species != other.RequestedSpecies) return false;
|
||||
if (other.RequestedGender != Genders.Either && Gender != other.RequestedGender) return false;
|
||||
if (!CheckLevels(other.RequestedMinLevel, other.RequestedMaxLevel, Level)) return false;
|
||||
|
||||
if (RequestedSpecies != other.Species) return false;
|
||||
if (RequestedGender != Genders.Either && other.Gender != RequestedGender) return false;
|
||||
if (!CheckLevels(RequestedMinLevel, RequestedMaxLevel, other.Level)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FlagTraded(GtsRecord5 other)
|
||||
{
|
||||
Species = other.Species;
|
||||
Gender = other.Gender;
|
||||
Level = other.Level;
|
||||
RequestedSpecies = other.RequestedSpecies;
|
||||
RequestedGender = other.RequestedGender;
|
||||
RequestedMinLevel = other.RequestedMinLevel;
|
||||
RequestedMaxLevel = other.RequestedMaxLevel;
|
||||
TimeDeposited = other.TimeDeposited;
|
||||
TimeWithdrawn = DateTime.UtcNow;
|
||||
PID = other.PID;
|
||||
IsExchanged = 0x01;
|
||||
}
|
||||
|
||||
public static bool CheckLevels(byte min, byte max, byte other)
|
||||
{
|
||||
if (max == 0) max = 255;
|
||||
return other >= min && other <= max;
|
||||
}
|
||||
|
||||
public static DateTime ? TimestampToDate(ulong timestamp)
|
||||
{
|
||||
if (timestamp == 0) return null;
|
||||
|
||||
ushort year = (ushort)((timestamp >> 0x30) & 0xffff);
|
||||
byte month = (byte)((timestamp >> 0x28) & 0xff);
|
||||
byte day = (byte)((timestamp >> 0x20) & 0xff);
|
||||
byte hour = (byte)((timestamp >> 0x18) & 0xff);
|
||||
byte minute = (byte)((timestamp >> 0x10) & 0xff);
|
||||
byte second = (byte)((timestamp >> 0x08) & 0xff);
|
||||
//byte fractional = (byte)(timestamp & 0xff); // always 0
|
||||
|
||||
// allow ArgumentOutOfRangeExceptions to escape
|
||||
return new DateTime(year, month, day, hour, minute, second);
|
||||
}
|
||||
|
||||
public static ulong DateToTimestamp(DateTime ? date)
|
||||
{
|
||||
if (date == null) return 0;
|
||||
DateTime date2 = (DateTime)date;
|
||||
|
||||
return (ulong)(date2.Year & 0xffff) << 0x30
|
||||
| (ulong)(date2.Month & 0xff) << 0x28
|
||||
| (ulong)(date2.Day & 0xff) << 0x20
|
||||
| (ulong)(date2.Hour & 0xff) << 0x18
|
||||
| (ulong)(date2.Minute & 0xff) << 0x10
|
||||
| (ulong)(date2.Second & 0xff) << 0x08;
|
||||
}
|
||||
|
||||
public static bool operator ==(GtsRecord5 a, GtsRecord5 b)
|
||||
{
|
||||
if ((object)a == null && (object)b == null) return true;
|
||||
if ((object)a == null || (object)b == null) return false;
|
||||
// todo: optimize me
|
||||
return a.Save().SequenceEqual(b.Save());
|
||||
}
|
||||
|
||||
public static bool operator !=(GtsRecord5 a, GtsRecord5 b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user