Skip to content

Commit

Permalink
Add initial UWC support
Browse files Browse the repository at this point in the history
  • Loading branch information
madelson committed Jun 3, 2024
1 parent a65a382 commit 86297b1
Show file tree
Hide file tree
Showing 18 changed files with 184 additions and 36 deletions.
12 changes: 8 additions & 4 deletions TabletopMtgImporter.Blazor/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public static class Application

public static string GetDeckName(string deckText)
{
var input = new StringDeckInput(deckText) { Name = string.Empty };
var input = new StringDeckInput(deckText, useUwcCards: false) { Name = string.Empty };
using var reader = input.OpenReader();

int commanderCount;
Expand Down Expand Up @@ -53,23 +53,27 @@ public static Task<bool> ImportAsync(
ILogger logger,
IJSRuntime jsRuntime,
string deckName,
string deckText)
string deckText,
bool useUwcCards)
{
var importer = new Importer(logger, new LocalStorageCache(jsRuntime), new DownloadSaver(jsRuntime, logger));
return importer.TryImportAsync(new StringDeckInput(deckText) { Name = deckName });
return importer.TryImportAsync(new StringDeckInput(deckText, useUwcCards) { Name = deckName });
}

private class StringDeckInput : IDeckInput
{
private readonly string _deckText;

public StringDeckInput(string deckText)
public StringDeckInput(string deckText, bool useUwcCards)
{
this._deckText = deckText;
this.UwcSetRegex = new(useUwcCards ? "." : "$^");
}

public string Name { get; set; } = default!;

public Regex UwcSetRegex { get; }

public TextReader OpenReader() => new StringReader(this._deckText);
}
}
Expand Down
16 changes: 12 additions & 4 deletions TabletopMtgImporter.Blazor/Pages/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
@if (this.ShowDeckListHelpText)
{
<div class="my-1" style="font-size: 0.7rem; color:gray">
This tool is easiest to use with <a href="https://archidekt.com">Architedekt</a>.
This tool is easiest to use with <a href="https://archidekt.com">Architedekt</a>.
<br /><br />
When viewing your deck, click the "Import" button on the left sidebar, then copy the contents into the text box below.
<br /><br />
Expand Down Expand Up @@ -41,6 +41,13 @@
class="btn btn-primary w-25">
Import
</button>
<label>
<input type="checkbox"
@bind="this.UseUwcCards"
disabled="@(this.Status == ImportStatus.InProgress)"
title="Use UWC cards"
typeof="checkbox" /> Use <a href="https://madelson.github.io/universes-within-collection/">UWC</a> cards
</label>
@{
var statusMessageColor = this.Status switch
{
Expand All @@ -61,8 +68,7 @@
font-weight: bold;
font-family: consolas;
overflow-wrap: break-word;
overflow-y: auto;
">
overflow-y: auto;">
@foreach (var entry in this.LogEntries)
{
var entryColor = entry.Type switch
Expand All @@ -88,6 +94,8 @@

public string DeckText { get; set; } = string.Empty;

public bool UseUwcCards { get; set; } = true;

public string DefaultDeckName => Application.GetDeckName(this.DeckText);

public ImportStatus Status { get; set; }
Expand All @@ -114,7 +122,7 @@
});

var deckName = string.IsNullOrWhiteSpace(this.DeckName) ? this.DefaultDeckName : this.DeckName;
if (await Application.ImportAsync(logger, this.JSRuntime, deckName: deckName, deckText: this.DeckText))
if (await Application.ImportAsync(logger, this.JSRuntime, deckName: deckName, deckText: this.DeckText, useUwcCards: this.UseUwcCards))
{
this.Status = ImportStatus.Succeeded;
this.StatusMessage = "Downloaded!";
Expand Down
10 changes: 9 additions & 1 deletion TabletopMtgImporter.Console/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ public static async Task<int> Main(string[] args)
Debugger.Launch();
}

bool useUwcCards;
if (args.Contains("--uwc"))
{
useUwcCards = true;
args = args.Where(a => a != "--uwc").ToArray();
}
else { useUwcCards = false; }

if (args.Length != 1)
{
Console.Error.WriteLine($"Usage {typeof(Program).Assembly.GetName().Name} <deckFile>");
Expand All @@ -40,7 +48,7 @@ public static async Task<int> Main(string[] args)
}

var logger = new ConsoleLogger();
return await new Importer(logger, new DiskCache(), new DiskSaver(logger)).TryImportAsync(new DeckFileInput(deckFile)) ? 0 : 3;
return await new Importer(logger, new DiskCache(), new DiskSaver(logger)).TryImportAsync(new DeckFileInput(deckFile, useUwcCards)) ? 0 : 3;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<TargetFramework>net461</TargetFramework>
<LangVersion>8.0</LangVersion>
<AssemblyName>TabletopMTGImporter</AssemblyName>
<AssemblyVersion>0.6.1.0</AssemblyVersion>
<FileVersion>0.6.1.0</FileVersion>
<AssemblyVersion>0.7.0.0</AssemblyVersion>
<FileVersion>0.7.0.0</FileVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

Expand Down
3 changes: 2 additions & 1 deletion TabletopMtgImporter.Core/DeckCard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public DeckCard(string name, string? set, string? collectorNumber, bool isComman
}

this.Name = name;
this.Set = set;
// MA: for a while list cards would get various different set codes from Archidekt, but now Scryfall wants them as "plst"
this.Set = set is "mb1" or "fmb1" or "phed" or "plist" ? "plst" : set;
this.CollectorNumber = collectorNumber;
this.IsCommander = isCommander;
}
Expand Down
9 changes: 7 additions & 2 deletions TabletopMtgImporter.Core/DeckFileInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,25 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace TabletopMtgImporter
{
public sealed class DeckFileInput : IDeckInput
{
private readonly string _path;

public DeckFileInput(string path)
public DeckFileInput(string path, bool useUwcCards)
{
this._path = Path.GetFullPath(path ?? throw new ArgumentNullException(nameof(path)));
this.UwcSetRegex = new(useUwcCards ? "." : "$^", RegexOptions.IgnoreCase);
}

public string Name => Path.GetFileNameWithoutExtension(this._path);

public Regex UwcSetRegex { get; }

public TextReader OpenReader() => new StreamReader(this._path);
}
}
2 changes: 2 additions & 0 deletions TabletopMtgImporter.Core/IDeckInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace TabletopMtgImporter
{
public interface IDeckInput
{
string Name { get; }
Regex UwcSetRegex { get; }
TextReader OpenReader();
}
}
3 changes: 3 additions & 0 deletions TabletopMtgImporter.Core/Importer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ async Task<bool> TryImportAsync()

if (info != null)
{
await UwcProvider.UpdateAsync(info, deckInput);

cardInfo[card] = info;
foreach (var relatedCard in (info.RelatedCards ?? Enumerable.Empty<ScryfallCard.RelatedCard>())
.Where(rc => rc.Component != "combo_piece" || NonStandardCardTypesRegex.IsMatch(rc.TypeLine)))
Expand All @@ -96,6 +98,7 @@ async Task<bool> TryImportAsync()
try
{
var relatedInfo = await scryfallClient.GetJsonAsync<ScryfallCard>(relatedCard.Uri.AbsoluteUri).ConfigureAwait(false);
await UwcProvider.UpdateAsync(relatedInfo, deckInput);
cardInfo[new DeckCard(relatedInfo.Name, set: relatedInfo.Set, collectorNumber: relatedInfo.CollectorNumber, isCommander: false)] = relatedInfo;
}
catch (Exception ex)
Expand Down
1 change: 1 addition & 0 deletions TabletopMtgImporter.Core/ScryfallCard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal class ScryfallCard

public class Face
{
public string Name = default!;
[JsonProperty("image_uris")]
public Dictionary<string, Uri> ImageUris = default!;
}
Expand Down
4 changes: 2 additions & 2 deletions TabletopMtgImporter.Core/ScryfallClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace TabletopMtgImporter
{
internal class ScryfallClient
{
private readonly HttpClient _httpClient = new HttpClient { BaseAddress = new Uri("https://api.scryfall.com/") };
private static readonly HttpClient HttpClient = new() { BaseAddress = new Uri("https://api.scryfall.com/") };
private readonly ILogger _logger;
private readonly ICache _cache;

Expand All @@ -37,7 +37,7 @@ public async Task<T> GetJsonAsync<T>(string url)
// rate-limiting requested by scryfall
await Task.Delay(TimeSpan.FromMilliseconds(50)).ConfigureAwait(false);

using var response = await this._httpClient.GetAsync(url).ConfigureAwait(false);
using var response = await HttpClient.GetAsync(url).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var body = await (response.Content?.ReadAsStringAsync() ?? Task.FromResult("n/a")).ConfigureAwait(false);
Expand Down
10 changes: 7 additions & 3 deletions TabletopMtgImporter.Core/TabletopDeckCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public static TabletopDeckObject CreateDeck(IReadOnlyList<DeckCard> cards, IRead
.ToArray();
var tokens = cardsAndRelatedCards.Keys.Except(cards)
.ToArray();
var doubleFacedCards = cardsAndRelatedCards.Values.Where(c => c.Layout == "transform" || c.Layout == "modal_dfc")
var doubleFacedCards = cardsAndRelatedCards.Values.Where(IsDoubleFaced)
.ToArray();

var deck = new TabletopDeckObject
Expand All @@ -29,7 +29,8 @@ public static TabletopDeckObject CreateDeck(IReadOnlyList<DeckCard> cards, IRead
{
CardId = ToId(index),
Name = "Card",
Nickname = c.Name,
// Use name from info to get UWC name if available
Nickname = cardsAndRelatedCards[c].Name,
})
.ToList(),
DeckIds = Enumerable.Range(0, mainDeckCards.Length).Select(ToId).ToList(),
Expand All @@ -52,7 +53,8 @@ public static TabletopDeckObject CreateDeck(IReadOnlyList<DeckCard> cards, IRead
{
CardId = ToId(index),
Name = "Card",
Nickname = c.Name,
// Use name from info to get UWC name if available
Nickname = cardsAndRelatedCards[c].Name,
})
.ToList(),
DeckIds = Enumerable.Range(0, tokens.Length).Select(ToId).ToList(),
Expand Down Expand Up @@ -99,5 +101,7 @@ public static TabletopDeckObject CreateDeck(IReadOnlyList<DeckCard> cards, IRead
}

static int ToId(int index) => 100 * (index + 1);

public static bool IsDoubleFaced(ScryfallCard card) => card.Layout == "transform" || card.Layout == "modal_dfc";
}
}
63 changes: 63 additions & 0 deletions TabletopMtgImporter.Core/UwcProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace TabletopMtgImporter
{
internal static class UwcProvider
{
private const string BaseUrl = "https://madelson.github.io/universes-within-collection/cards";

private static readonly Lazy<Task<IReadOnlyDictionary<string, CardInfo>>> CardNames = new(DownloadUwcCards);

public static async Task UpdateAsync(ScryfallCard card, IDeckInput input)
{
if (!input.UwcSetRegex.IsMatch(card.Set)
|| !(await CardNames.Value).TryGetValue(card.Name, out var cardInfo))
{
return;
}

if (TabletopDeckCreator.IsDoubleFaced(card))
{
foreach (var face in card.Faces!)
{
face.ImageUris["large"] = new($"{BaseUrl}/{UrlEncodeCardName(face.Name)}.png");
}
}
else
{
card.ImageUris!["large"] = new($"{BaseUrl}/{UrlEncodeCardName(card.Name)}.png");
}

if (cardInfo.Nickname != null)
{
card.Name = cardInfo.Nickname;
}
}

private static async Task<IReadOnlyDictionary<string, CardInfo>> DownloadUwcCards()
{
using HttpClient client = new();
using var cardsResponse = await client.GetAsync($"{BaseUrl}/cards.json");
cardsResponse.EnsureSuccessStatusCode();
var cards = JsonConvert.DeserializeObject<Dictionary<string, CardInfo>>(await cardsResponse.Content.ReadAsStringAsync());
return cards!.Values.ToDictionary(c => c.Name);
}

// github pages hosts the urls with %20 instead of + for spaces.
private static string UrlEncodeCardName(string name) => WebUtility.UrlEncode(name).Replace("+", "%20");

private class CardInfo
{
[JsonProperty(Required = Required.Always)]
public string Name { get; set; } = default!;
public string? Nickname { get; set; } = default!;
}
}
}
Loading

0 comments on commit 86297b1

Please sign in to comment.