diff --git a/HearthDb.CardDefsDownloader/HearthDb.CardDefsDownloader.csproj b/HearthDb.CardDefsDownloader/HearthDb.CardDefsDownloader.csproj
new file mode 100644
index 00000000..918adb65
--- /dev/null
+++ b/HearthDb.CardDefsDownloader/HearthDb.CardDefsDownloader.csproj
@@ -0,0 +1,9 @@
+
+
+
+ netcoreapp3.1
+ 8
+ Exe
+
+
+
diff --git a/HearthDb.CardDefsDownloader/Program.cs b/HearthDb.CardDefsDownloader/Program.cs
new file mode 100644
index 00000000..00ebf4c3
--- /dev/null
+++ b/HearthDb.CardDefsDownloader/Program.cs
@@ -0,0 +1,31 @@
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+
+namespace HearthDb.CardDefsDownloader
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ var outdir = args[0];
+ Directory.CreateDirectory(outdir);
+ var httpClient = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate });
+
+ // CardDefs.base.xml contains non localized tags and enUS tag
+ // Other languages can be found under e.g. CardDefs.deDE.xml
+ using var request = new HttpRequestMessage(HttpMethod.Get, "https://api.hearthstonejson.com/v1/latest/CardDefs.base.xml");
+ request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
+ request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("deflate"));
+
+ var response = httpClient.SendAsync(request).Result;
+ var data = response.Content.ReadAsStringAsync().Result;
+ File.WriteAllText(Path.Combine(outdir, "CardDefs.base.xml"), data);
+
+ var etag = response.Headers.ETag.Tag;
+ var lastModified = response.Content.Headers.LastModified;
+ File.WriteAllText(Path.Combine(outdir, "CardDefs.base.etag"), $"{etag}\n{lastModified.ToString()}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/HearthDb.Tests/CardDefsLoadTest.cs b/HearthDb.Tests/CardDefsLoadTest.cs
new file mode 100644
index 00000000..2805ce3d
--- /dev/null
+++ b/HearthDb.Tests/CardDefsLoadTest.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using HearthDb.CardDefs;
+using HearthDb.Enums;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace HearthDb.Tests
+{
+ [TestClass]
+ public class CardDefsLoadTest
+ {
+ [TestInitialize]
+ public void Init()
+ {
+ Cards.LoadBaseData(new CardDefs.CardDefs { Entites = new List() });
+ }
+
+ [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));
+ }
+
+ [TestMethod]
+ public void HasValidETagData()
+ {
+ var bundled = Cards.GetBundledCardDefsETag();
+ Assert.IsNotNull(bundled.ETag);
+ Assert.IsTrue(DateTime.TryParse(bundled.LastModified, out _));
+ }
+ }
+}
\ 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.sln b/HearthDb.sln
index 5773565f..286e4c35 100644
--- a/HearthDb.sln
+++ b/HearthDb.sln
@@ -11,6 +11,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HearthDb.EnumsGenerator", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HearthDb.Tests", "HearthDb.Tests\HearthDb.Tests.csproj", "{875316D1-C4C3-48BD-BDAE-3727269B2A67}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HearthDb.CardDefsDownloader", "HearthDb.CardDefsDownloader\HearthDb.CardDefsDownloader.csproj", "{92DBA109-DF71-4874-827A-20B00EABB57D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -51,6 +53,14 @@ Global
{875316D1-C4C3-48BD-BDAE-3727269B2A67}.Release|Any CPU.Build.0 = Release|Any CPU
{875316D1-C4C3-48BD-BDAE-3727269B2A67}.Release|x86.ActiveCfg = Release|Any CPU
{875316D1-C4C3-48BD-BDAE-3727269B2A67}.Release|x86.Build.0 = Release|Any CPU
+ {92DBA109-DF71-4874-827A-20B00EABB57D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {92DBA109-DF71-4874-827A-20B00EABB57D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {92DBA109-DF71-4874-827A-20B00EABB57D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {92DBA109-DF71-4874-827A-20B00EABB57D}.Debug|x86.Build.0 = Debug|Any CPU
+ {92DBA109-DF71-4874-827A-20B00EABB57D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {92DBA109-DF71-4874-827A-20B00EABB57D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {92DBA109-DF71-4874-827A-20B00EABB57D}.Release|x86.ActiveCfg = Release|Any CPU
+ {92DBA109-DF71-4874-827A-20B00EABB57D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
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..b7115cd0 100644
--- a/HearthDb/Cards.cs
+++ b/HearthDb/Cards.cs
@@ -15,20 +15,20 @@ namespace HearthDb
{
public static class Cards
{
- public static readonly Dictionary All = new Dictionary();
- public static readonly Dictionary AllByDbfId = new Dictionary();
+ public static Dictionary All { get; private set; } = new Dictionary();
+ public static Dictionary AllByDbfId { get; private set; } = new Dictionary();
- public static readonly Dictionary Collectible = new Dictionary();
- public static readonly Dictionary CollectibleByDbfId = new Dictionary();
+ public static Dictionary Collectible { get; private set; } = new Dictionary();
+ public static Dictionary CollectibleByDbfId { get; private set; } = new Dictionary();
- public static readonly Dictionary BaconPoolMinions = new Dictionary();
- public static readonly Dictionary BaconPoolMinionsByDbfId = new Dictionary();
+ public static Dictionary BaconPoolMinions { get; private set; } = new Dictionary();
+ public static Dictionary BaconPoolMinionsByDbfId { get; private set; } = new Dictionary();
- public static readonly Dictionary NormalToTripleCardIds = new Dictionary();
- public static readonly Dictionary TripleToNormalCardIds = new Dictionary();
+ public static Dictionary NormalToTripleCardIds { get; private set; } = new Dictionary();
+ public static Dictionary TripleToNormalCardIds { get; private set; } = new Dictionary();
- public static readonly Dictionary NormalToTripleDbfIds = new Dictionary();
- public static readonly Dictionary TripleToNormalDbfIds = new Dictionary();
+ public static Dictionary NormalToTripleDbfIds { get; private set; } = new Dictionary();
+ public static Dictionary TripleToNormalDbfIds { get; private set; } = new Dictionary();
private static readonly HashSet IgnoreTripleIds = new HashSet
{
@@ -37,63 +37,216 @@ public static class Cards
CardIds.NonCollectible.Neutral.SeabreakerGoliathGILNEAS
};
+ private static readonly XmlSerializer CardDefsSerializer = new XmlSerializer(typeof(CardDefs.CardDefs));
+
+ public static string Build { get; private set; }
+
+ private static (string ETag, string LastModified)? _bundledCardDefsETag;
+ public static (string ETag, string LastModified) GetBundledCardDefsETag()
+ {
+ if(_bundledCardDefsETag == null)
+ {
+ using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("HearthDb.CardDefs.base.etag")!;
+ using var reader = new StreamReader(stream);
+ var text = reader.ReadToEnd().Split('\n');
+ _bundledCardDefsETag = (text[0], text[1]);
+ }
+ return _bundledCardDefsETag.Value;
+ }
+
+ public static CardDefs.CardDefs GetBundledBaseData()
+ {
+ var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("HearthDb.CardDefs.base.xml")!;
+ return ParseCardDefs(stream);
+ }
+
static Cards()
{
- var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("HearthDb.CardDefs.xml");
- if(stream == null)
- return;
- using(TextReader tr = new StreamReader(stream))
+ if(Config.AutoLoadCardDefs)
+ LoadBaseData();
+ }
+
+ public static CardDefs.CardDefs ParseCardDefs(Stream cardDefsData)
+ {
+ return (CardDefs.CardDefs)CardDefsSerializer.Deserialize(cardDefsData);
+ }
+
+ ///
+ /// Load base card data (non-localized card metadata, as well as
+ /// localized strings in enUS and zhCN) bundled with HearthDb.
+ ///
+ public static void LoadBaseData() => LoadBaseData(GetBundledBaseData());
+
+ ///
+ /// Load base card data, this should usually be a
+ /// CardDefs.base.xml file, that at least includes all
+ /// non-localized metadata.
+ ///
+ /// This will clear any previously loaded data.
+ ///
+ public static void LoadBaseData(Stream cardDefsData) => LoadBaseData(ParseCardDefs(cardDefsData));
+
+ ///
+ /// Load base card data, this should usually be a
+ /// CardDefs.base.xml file, that at least includes all
+ /// non-localized metadata.
+ ///
+ /// This will clear any previously loaded data.
+ ///
+ public static void LoadBaseData(CardDefs.CardDefs cardDefs)
+ {
+ // Instantiate new dictionaries here and re-assign once complete to avoid modifying collections.
+ // This may be called in a thread.
+
+ var all = new Dictionary();
+ var allByDbfId = new Dictionary();
+ var collectible = new Dictionary();
+ var collectibleByDbfId = new Dictionary();
+ var baconPoolMinions = new Dictionary();
+ var baconPoolMinionsByDbfId = new Dictionary();
+
+ 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));
}
+ }
+ }
+
+ All = all;
+ AllByDbfId = allByDbfId;
+ Collectible = collectible;
+ CollectibleByDbfId = collectibleByDbfId;
+ BaconPoolMinions = baconPoolMinions;
+ BaconPoolMinionsByDbfId = baconPoolMinionsByDbfId;
+
+ var normalToTripleCardIds = new Dictionary();
+ var tripleToNormalCardIds = new Dictionary();
+ var normalToTripleDbfIds = new Dictionary();
+ var tripleToNormalDbfIds = new Dictionary();
+
+ // 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;
+ }
- if (!IgnoreTripleIds.Contains(entity.CardId))
+ NormalToTripleCardIds = normalToTripleCardIds;
+ TripleToNormalCardIds = tripleToNormalCardIds;
+ NormalToTripleDbfIds = normalToTripleDbfIds;
+ TripleToNormalDbfIds = tripleToNormalDbfIds;
+
+ }
+
+ ///
+ /// Load additional locale specific card data (names, text, ...).
+ /// LoadBaseData() must have been previously called if
+ /// Config.AutoLoadCardDefs (enabled by default) was disabled.
+ ///
+ public static void LoadLocaleData(Stream cardDefsData, Locale locale) => LoadLocaleData(ParseCardDefs(cardDefsData), locale);
+
+ ///
+ /// Load additional locale specific card data (names, text, ...).
+ /// LoadBaseData() must have been previously called if
+ /// Config.AutoLoadCardDefs (enabled by default) was disabled.
+ ///
+ 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;
- }
}
}
diff --git a/HearthDb/Config.cs b/HearthDb/Config.cs
new file mode 100644
index 00000000..d7b92edc
--- /dev/null
+++ b/HearthDb/Config.cs
@@ -0,0 +1,12 @@
+namespace HearthDb
+{
+ public static class Config
+ {
+ ///
+ /// Automatically and synchronously load card data included with HearthDB.
+ /// By default, this will be all non-localized card metadata, as well as localized strings in enUS and zhCN.
+ /// If disabled, card data needs to be explicitly loaded via HearthDb.Cards.LoadBaseData
+ ///
+ 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..adf72afe 100644
--- a/HearthDb/HearthDb.csproj
+++ b/HearthDb/HearthDb.csproj
@@ -1,4 +1,4 @@
-
+
netstandard2.0
@@ -17,22 +17,28 @@
-
- $(RootNamespace).CardDefs.xml
+
+ $(RootNamespace).CardDefs.base.xml
+
+ $(RootNamespace).CardDefs.base.etag
+
+
+
+
+
+ <_Parameter1>HearthDb.Tests
+
-
-
-
-
-
+
+ $(SolutionDir)HearthDb.CardDefsDownloader/bin/Release/netcoreapp3.1/
+
-
-
-
-
-
-
+
+
+
+
+
diff --git a/README.md b/README.md
index cf867fd2..d594ba9d 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,13 @@ 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 url in `HearthDb.CardDefsDownloader` with `https://github.com/HearthSim/hsdata/blob/master/CardDefs.xml`.
+
## CardIDs
`HearthDb.CardIds` contains properly named constant for all cardIds existing in Hearthstone.