diff --git a/HearthDb.Tests/CardDefsLoadTest.cs b/HearthDb.Tests/CardDefsLoadTest.cs new file mode 100644 index 00000000..f6724ee4 --- /dev/null +++ b/HearthDb.Tests/CardDefsLoadTest.cs @@ -0,0 +1,40 @@ +using System.IO; +using HearthDb.Enums; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace HearthDb.Tests +{ + [TestClass] + public class CardDefsLoadTest + { + [TestInitialize] + public void Init() + { + Cards.ClearData(); + } + + [TestCleanup] + public void Cleanup() + { + Cards.LoadBaseData(); + } + + [TestMethod] + public void LoadCards_CorrectlyAssemblesData() + { + using var fsBase = File.OpenRead("Data/TestCardDefs.base.xml"); + Cards.LoadBaseData(fsBase); + + Assert.AreEqual("Nutthapon Petchthai", Cards.All["AT_001"].ArtistName); + Assert.AreEqual("Flame Lance", Cards.All["AT_001"].GetLocName(Locale.enUS)); + Assert.AreEqual(null, Cards.All["AT_001"].GetLocName(Locale.deDE)); + + using var fsLocDe = File.OpenRead("Data/TestCardDefs.deDE.xml"); + Cards.LoadLocaleData(fsLocDe, Locale.deDE); + + Assert.AreEqual("Nutthapon Petchthai", Cards.All["AT_001"].ArtistName); + Assert.AreEqual("Flame Lance", Cards.All["AT_001"].GetLocName(Locale.enUS)); + Assert.AreEqual("Flammenlanze", Cards.All["AT_001"].GetLocName(Locale.deDE)); + } + } +} \ No newline at end of file diff --git a/HearthDb.Tests/Data/TestCardDefs.base.xml b/HearthDb.Tests/Data/TestCardDefs.base.xml new file mode 100644 index 00000000..8ad641a1 --- /dev/null +++ b/HearthDb.Tests/Data/TestCardDefs.base.xml @@ -0,0 +1,23 @@ + + + + + Flame Lance + + + Deal $25 damage + to a minion. + + + It's on the rack next to ice lance, acid lance, and English muffin lance. + + Nutthapon Petchthai + + + + + + + + + \ No newline at end of file diff --git a/HearthDb.Tests/Data/TestCardDefs.deDE.xml b/HearthDb.Tests/Data/TestCardDefs.deDE.xml new file mode 100644 index 00000000..d52cf312 --- /dev/null +++ b/HearthDb.Tests/Data/TestCardDefs.deDE.xml @@ -0,0 +1,14 @@ + + + + + Flammenlanze + + + Fügt einem Diener $25 Schaden zu. + + + Die Sommerversion der Eislanze. Gut gegen chronisch kalte Hände. + + + \ No newline at end of file diff --git a/HearthDb.Tests/HearthDb.Tests.csproj b/HearthDb.Tests/HearthDb.Tests.csproj index 9ea1105b..e5f7c962 100644 --- a/HearthDb.Tests/HearthDb.Tests.csproj +++ b/HearthDb.Tests/HearthDb.Tests.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/HearthDb.Tests/UnitTest1.cs b/HearthDb.Tests/UnitTest1.cs index 9d4c1506..c9497e9d 100644 --- a/HearthDb.Tests/UnitTest1.cs +++ b/HearthDb.Tests/UnitTest1.cs @@ -11,7 +11,8 @@ public class UnitTest1 public void BasicTest() { Assert.AreEqual("Flame Lance", Cards.All["AT_001"].Name); - Assert.AreEqual("Flammenlanze", Cards.All["AT_001"].GetLocName(Locale.deDE)); + // non-enUS no longer included by default. CardDefsLoadTest verifies this works. + // Assert.AreEqual("Flammenlanze", Cards.All["AT_001"].GetLocName(Locale.deDE)); Assert.AreEqual("Nutthapon Petchthai", Cards.All["AT_001"].ArtistName); Assert.AreEqual(CardSet.TGT, Cards.All["AT_001"].Set); Assert.AreEqual(true, Cards.All["AT_001"].Collectible); @@ -80,7 +81,7 @@ public void TestCardText() Assert.IsTrue(cramSession.Text.Contains("improved by")); var flameLance = Cards.All[CardIds.Collectible.Mage.FlameLanceTGT]; - Assert.IsTrue(flameLance.GetLocText(Locale.frFR).Contains("$25")); + Assert.IsTrue(flameLance.GetLocText(Locale.enUS).Contains("$25")); var elvenArcher = Cards.All[CardIds.Collectible.Neutral.ElvenArcherVanilla]; Assert.IsTrue(elvenArcher.Text.Contains("Deal 1 damage")); diff --git a/HearthDb/CardDefs/CardDefs.cs b/HearthDb/CardDefs/CardDefs.cs index a0c1fc70..17637ab3 100644 --- a/HearthDb/CardDefs/CardDefs.cs +++ b/HearthDb/CardDefs/CardDefs.cs @@ -12,5 +12,9 @@ public class CardDefs { [XmlElement("Entity")] public List Entites { get; set; } + + + [XmlAttribute("build")] + public string Build { get; set; } } } \ No newline at end of file diff --git a/HearthDb/Cards.cs b/HearthDb/Cards.cs index 6406f2bb..50e546dd 100644 --- a/HearthDb/Cards.cs +++ b/HearthDb/Cards.cs @@ -37,66 +37,164 @@ public static class Cards CardIds.NonCollectible.Neutral.SeabreakerGoliathGILNEAS }; - static Cards() + private static readonly XmlSerializer CardDefsSerializer = new XmlSerializer(typeof(CardDefs.CardDefs)); + + public static string Build { get; private set; } + + internal static void ClearData() + { + All.Clear(); + AllByDbfId.Clear(); + Collectible.Clear(); + CollectibleByDbfId.Clear(); + BaconPoolMinions.Clear(); + BaconPoolMinionsByDbfId.Clear(); + NormalToTripleCardIds.Clear(); + TripleToNormalCardIds.Clear(); + NormalToTripleDbfIds.Clear(); + TripleToNormalCardIds.Clear(); + } + + public static CardDefs.CardDefs ParseCardDefs(Stream cardDefsData) { - var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("HearthDb.CardDefs.xml"); - if(stream == null) - return; - using(TextReader tr = new StreamReader(stream)) + return (CardDefs.CardDefs)CardDefsSerializer.Deserialize(cardDefsData); + } + + public static void LoadBaseData() + { + var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("HearthDb.CardDefs.base.xml"); + if(stream != null) + LoadBaseData(stream); + } + + public static void LoadBaseData(Stream cardDefsData) => LoadBaseData(ParseCardDefs(cardDefsData)); + + public static void LoadBaseData(CardDefs.CardDefs cardDefs) + { + ClearData(); + Build = cardDefs.Build; + + var baconTriples = new List<(Card, int)>(); + var nonBaconTriples = new List<(Card, int)>(); + foreach (var entity in cardDefs.Entites) { - var xml = new XmlSerializer(typeof(CardDefs.CardDefs)); - var cardDefs = (CardDefs.CardDefs)xml.Deserialize(tr); - var baconTriples = new List<(Card, int)>(); - var nonBaconTriples = new List<(Card, int)>(); - foreach(var entity in cardDefs.Entites) + // For some reason Deflect-o-bot is missing divine shield + if (IsDeflectOBot(entity) && !entity.Tags.Any(x => x.EnumId == (int)GameTag.DIVINE_SHIELD)) + entity.Tags.Add(new Tag { EnumId = (int)GameTag.DIVINE_SHIELD, Value = 1 }); + + var card = new Card(entity); + All[entity.CardId] = card; + AllByDbfId[entity.DbfId] = card; + if (card.Collectible && (card.Type != CardType.HERO || card.Set != CardSet.HERO_SKINS)) { - // For some reason Deflect-o-bot is missing divine shield - if (IsDeflectOBot(entity) && !entity.Tags.Any(x => x.EnumId == (int)GameTag.DIVINE_SHIELD)) - entity.Tags.Add(new Tag { EnumId = (int)GameTag.DIVINE_SHIELD, Value = 1 }); - - var card = new Card(entity); - All.Add(entity.CardId, card); - AllByDbfId.Add(entity.DbfId, card); - if (card.Collectible && (card.Type != CardType.HERO || card.Set != CardSet.HERO_SKINS)) - { - Collectible.Add(entity.CardId, card); - CollectibleByDbfId.Add(entity.DbfId, card); - } + Collectible[entity.CardId] = card; + CollectibleByDbfId[entity.DbfId] = card; + } - if (card.IsBaconPoolMinion) + if (card.IsBaconPoolMinion) + { + BaconPoolMinions[entity.CardId] = card; + BaconPoolMinionsByDbfId[entity.DbfId] = card; + } + + if (!IgnoreTripleIds.Contains(entity.CardId)) + { + var tripleDbfId = card.Entity.Tags.FirstOrDefault(x => x.EnumId == 1429); + if (tripleDbfId != null) { - BaconPoolMinions.Add(entity.CardId, card); - BaconPoolMinionsByDbfId.Add(entity.DbfId, card); + if (card.IsBaconPoolMinion) + baconTriples.Add((card, tripleDbfId.Value)); + else + nonBaconTriples.Add((card, tripleDbfId.Value)); } + } + } + + // Triples have to be resolved after the first loop since we need to look up the triple card from the id + // Loop over non-bacon first in case both contain a mapping to the same card. + // We want to use the bacon one in that case. + foreach (var (card, tripleDbfId) in nonBaconTriples.Concat(baconTriples)) + { + if (!AllByDbfId.TryGetValue(tripleDbfId, out var triple)) + continue; + NormalToTripleCardIds[card.Id] = triple.Id; + NormalToTripleDbfIds[card.DbfId] = triple.DbfId; + TripleToNormalCardIds[triple.Id] = card.Id; + TripleToNormalDbfIds[triple.DbfId] = card.DbfId; + } + } + + public static void LoadLocaleData(Stream cardDefsData, Locale locale) => LoadLocaleData(ParseCardDefs(cardDefsData), locale); - if (!IgnoreTripleIds.Contains(entity.CardId)) + public static void LoadLocaleData(CardDefs.CardDefs cardDefs, Locale locale) + { + foreach (var entity in cardDefs.Entites) + { + if (!All.TryGetValue(entity.CardId, out var curr)) + continue; + foreach (var tag in entity.Tags) + { + var currTag = curr.Entity.Tags.FirstOrDefault(x => x.EnumId == tag.EnumId); + if (currTag == null) + curr.Entity.Tags.Add(tag); + else { - var tripleDbfId = card.Entity.Tags.FirstOrDefault(x => x.EnumId == 1429); - if (tripleDbfId != null) + switch (locale) { - if(card.IsBaconPoolMinion) - baconTriples.Add((card, tripleDbfId.Value)); - else - nonBaconTriples.Add((card, tripleDbfId.Value)); + case Locale.deDE: + currTag.LocStringDeDe = tag.LocStringDeDe; + break; + case Locale.enUS: + currTag.LocStringEnUs = tag.LocStringEnUs; + break; + case Locale.esES: + currTag.LocStringEsEs = tag.LocStringEsEs; + break; + case Locale.esMX: + currTag.LocStringEsMx = tag.LocStringEsMx; + break; + case Locale.frFR: + currTag.LocStringFrFr = tag.LocStringFrFr; + break; + case Locale.itIT: + currTag.LocStringItIt = tag.LocStringItIt; + break; + case Locale.jaJP: + currTag.LocStringJaJp = tag.LocStringJaJp; + break; + case Locale.koKR: + currTag.LocStringKoKr = tag.LocStringKoKr; + break; + case Locale.plPL: + currTag.LocStringPlPl = tag.LocStringPlPl; + break; + case Locale.ptBR: + currTag.LocStringPtBr = tag.LocStringPtBr; + break; + case Locale.ruRU: + currTag.LocStringRuRu = tag.LocStringRuRu; + break; + case Locale.zhCN: + currTag.LocStringZhCn = tag.LocStringZhCn; + break; + case Locale.zhTW: + currTag.LocStringZhTw = tag.LocStringZhTw; + break; + case Locale.thTH: + currTag.LocStringThTh = tag.LocStringThTh; + break; } } } - - // Triples have to be resolved after the first loop since we need to look up the triple card from the id - // Loop over non-bacon first in case both contain a mapping to the same card. - // We want to use the bacon one in that case. - foreach (var (card, tripleDbfId) in nonBaconTriples.Concat(baconTriples)) - { - if (!AllByDbfId.TryGetValue(tripleDbfId, out var triple)) - continue; - NormalToTripleCardIds[card.Id] = triple.Id; - NormalToTripleDbfIds[card.DbfId] = triple.DbfId; - TripleToNormalCardIds[triple.Id] = card.Id; - TripleToNormalDbfIds[triple.DbfId] = card.DbfId; - } } } + static Cards() + { + if(Config.AutoLoadCardDefs) + LoadBaseData(); + } + /// /// Will try to return the card ID of the triple. Returns the normal card id if it cannot be found. /// diff --git a/HearthDb/Config.cs b/HearthDb/Config.cs new file mode 100644 index 00000000..c9f72244 --- /dev/null +++ b/HearthDb/Config.cs @@ -0,0 +1,7 @@ +namespace HearthDb +{ + public static class Config + { + public static bool AutoLoadCardDefs { get; set; } = true; + } +} \ No newline at end of file diff --git a/HearthDb/HearthDb.csproj b/HearthDb/HearthDb.csproj index 5bfb2d56..cd82c23e 100644 --- a/HearthDb/HearthDb.csproj +++ b/HearthDb/HearthDb.csproj @@ -17,22 +17,27 @@ - - $(RootNamespace).CardDefs.xml + + $(RootNamespace).CardDefs.base.xml - - - - - + + + <_Parameter1>HearthDb.Tests + + + + + + + https://api.hearthstonejson.com/v1/latest/CardDefs.base.xml + - - - - - + + + + diff --git a/README.md b/README.md index cf867fd2..f4182c73 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,15 @@ Full deck strings documentation can be found here: https://hearthsim.info/docs/d See [here](https://github.com/HearthSim/HearthDb/blob/master/HearthDb.Tests/UnitTest1.cs#L14-L25) for example usage. +### Localized strings +By default, HearthDb only loads locale data for enUS and zhCN. Additional language +data can be downloaded from `api.hearthstonejson.com`, e.g. `https://api.hearthstonejson.com/v1/latest/CardDefs.deDE.xml` and loaded at runtime via +`HearthDb.Cards.LoadLocaleData(...)`. + +If desired, all language data can be included by default by replacing the +`` in `HearthDb.csproj` with `https://github. +com/HearthSim/hsdata/blob/master/CardDefs.xml`. + ## CardIDs `HearthDb.CardIds` contains properly named constant for all cardIds existing in Hearthstone.