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.