Added Generation V support.

This commit is contained in:
Greg Edwards 2014-05-04 20:03:25 -04:00
parent 62145e89dd
commit 6983ff8340
13 changed files with 1223 additions and 7 deletions

View File

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

View File

@ -1 +1 @@
<%@ Application Codebehind="Global.asax.cs" Inherits="gts.Global" Language="C#" %>
<%@ Application Codebehind="Global.asax.cs" Inherits="PkmnFoundations.GTS.Global" Language="C#" %>

View File

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

View File

@ -50,5 +50,6 @@
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
<httpErrors existingResponse="PassThrough"></httpErrors>
</system.webServer>
</configuration>

View File

@ -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
View 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();
}
}
}

View File

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

@ -0,0 +1 @@
<%@ WebHandler Language="C#" CodeBehind="syachi2ds.ashx.cs" Class="PkmnFoundations.GTS.syachi2ds" %>

465
gts/syachi2ds.ashx.cs Normal file
View 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;
}
}
}
}

View File

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

View File

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

View File

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

View 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);
}
}
}