1
Fork 0
mirror of https://github.com/Steffo99/patched-porobot.git synced 2024-10-16 17:47:29 +00:00

Add decks support (#1)

This commit is contained in:
Steffo 2022-08-22 00:21:41 +02:00 committed by GitHub
parent b3bb652607
commit 7b060feb9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1164 additions and 25 deletions

View file

@ -1,6 +1,37 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="HttpUrlsUsage" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredUrls">
<list>
<option value="http://localhost" />
<option value="http://127.0.0.1" />
<option value="http://0.0.0.0" />
<option value="http://www.w3.org/" />
<option value="http://json-schema.org/draft" />
<option value="http://java.sun.com/" />
<option value="http://xmlns.jcp.org/" />
<option value="http://javafx.com/javafx/" />
<option value="http://javafx.com/fxml" />
<option value="http://maven.apache.org/xsd/" />
<option value="http://maven.apache.org/POM/" />
<option value="http://www.springframework.org/schema/" />
<option value="http://www.springframework.org/tags" />
<option value="http://www.springframework.org/security/tags" />
<option value="http://www.thymeleaf.org" />
<option value="http://www.jboss.org/j2ee/schema/" />
<option value="http://www.jboss.com/xml/ns/" />
<option value="http://www.ibm.com/webservices/xsd" />
<option value="http://activemq.apache.org/schema/" />
<option value="http://schema.cloudfoundry.org/spring/" />
<option value="http://schemas.xmlsoap.org/" />
<option value="http://cxf.apache.org/schemas/" />
<option value="http://primefaces.org/ui" />
<option value="http://tiles.apache.org/" />
<option value="http://dd.b.pvp.net" />
</list>
</option>
</inspection_tool>
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false"> <inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" /> <option name="processCode" value="true" />

29
Cargo.lock generated
View file

@ -275,6 +275,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "data-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.17" version = "0.99.17"
@ -758,9 +764,9 @@ checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.131" version = "0.2.132"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04c3b4822ccebfa39c02fc03d1534441b22ead323fa0f48bb7ddd8e6ba076a40" checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
[[package]] [[package]]
name = "log" name = "log"
@ -814,6 +820,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]] [[package]]
name = "measure_time" name = "measure_time"
version = "0.8.2" version = "0.8.2"
@ -949,9 +961,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.13.0" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e"
[[package]] [[package]]
name = "oneshot" name = "oneshot"
@ -1029,10 +1041,12 @@ dependencies = [
name = "patched_porobot" name = "patched_porobot"
version = "0.5.3" version = "0.5.3"
dependencies = [ dependencies = [
"data-encoding",
"glob", "glob",
"itertools 0.10.3", "itertools 0.10.3",
"lazy_static", "lazy_static",
"log", "log",
"md5",
"pretty_env_logger", "pretty_env_logger",
"regex", "regex",
"reqwest", "reqwest",
@ -1041,6 +1055,7 @@ dependencies = [
"tantivy", "tantivy",
"teloxide", "teloxide",
"tokio", "tokio",
"varint-rs",
] ]
[[package]] [[package]]
@ -1959,6 +1974,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "varint-rs"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
version = "0.2.15" version = "0.2.15"

View file

@ -21,6 +21,8 @@ log = { version = "0.4.17" }
itertools = { version = "0.10.3" } itertools = { version = "0.10.3" }
regex = { version = "1.6.0" } regex = { version = "1.6.0" }
lazy_static = { version = "1.4.0" } lazy_static = { version = "1.4.0" }
data-encoding = { version = "2.3.2" }
varint-rs = { version = "2.2.0" }
# exec # exec
pretty_env_logger = { version = "0.4.0", optional = true } pretty_env_logger = { version = "0.4.0", optional = true }
glob = { version = "0.3.0", optional = true } glob = { version = "0.3.0", optional = true }
@ -33,6 +35,7 @@ tantivy = { version = "0.18.0", optional = true }
teloxide = { version = "0.10.1", optional = true } teloxide = { version = "0.10.1", optional = true }
reqwest = { version = "0.11.11", optional = true } reqwest = { version = "0.11.11", optional = true }
tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"], optional = true } tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"], optional = true }
md5 = { version = "0.7.0", optional = true }
# discord # discord
# matrix # matrix
@ -41,7 +44,7 @@ tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"], optiona
# data = [] # Always included # data = [] # Always included
exec = ["pretty_env_logger", "glob"] exec = ["pretty_env_logger", "glob"]
search = ["tantivy"] search = ["tantivy"]
telegram = ["exec", "search", "teloxide", "reqwest", "tokio"] telegram = ["exec", "search", "teloxide", "reqwest", "tokio", "md5"]
discord = ["exec", "search"] discord = ["exec", "search"]
matrix = ["exec", "search"] matrix = ["exec", "search"]

576
src/data/deckcode/deck.rs Normal file
View file

@ -0,0 +1,576 @@
//! Module defining the [`Deck`] struct and its serialization methods and results.
use std::collections::HashMap;
use std::io::{Cursor, Read, Write};
use itertools::Itertools;
use varint_rs::{VarintReader, VarintWriter};
use crate::data::deckcode::version::{DeckCodeVersion, DeckCodeVersioned};
use crate::data::setbundle::code::CardCode;
use crate::data::setbundle::region::CardRegion;
use crate::data::setbundle::set::CardSet;
use super::format::DeckCodeFormat;
/// A unshuffled Legends of Runeterra card deck.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Deck {
/// The contents of the deck, represented as a [`HashMap`] mapping [`CardCode`]s to the number of inserted cards.
pub contents: HashMap<CardCode, u32>,
}
impl Deck {
/// Decode a deck code into a [`Vec`] of [bytes](u8).
fn decode_code(code: &str) -> DeckDecodingResult<Vec<u8>> {
data_encoding::BASE32_NOPAD
.decode(code.as_bytes())
.map_err(DeckDecodingError::Base32Encoding)
}
/// Encode a slice of [bytes](u8) into a deck code.
fn encode_code(bytes: &[u8]) -> String {
data_encoding::BASE32_NOPAD
.encode(&bytes)
}
/// [Read] the header byte into a [format](DeckCodeFormat) and [version](DeckCodeVersion) tuple.
fn read_header<R: Read>(reader: &mut R) -> DeckDecodingResult<(DeckCodeFormat, DeckCodeVersion)> {
let mut format_version: [u8; 1] = [0; 1];
reader.read_exact(&mut format_version).map_err(DeckDecodingError::Read)?;
let format = DeckCodeFormat::try_from(format_version[0] >> 4).map_err(|_| DeckDecodingError::UnknownFormat)?;
let version = DeckCodeVersion::try_from(format_version[0] & 0xF).map_err(|_| DeckDecodingError::UnknownVersion)?;
Ok((format, version))
}
/// [Write] the header byte with the given [format](DeckCodeFormat) and [version](DeckCodeVersion).
fn write_header<W: Write>(writer: &mut W, format: DeckCodeFormat, version: DeckCodeVersion) -> DeckEncodingResult<()> {
let format: u8 = format.into();
let version: u8 = version.into();
let byte: u8 = (format << 4) | (version & 0xF);
writer.write(&[byte]).map_err(DeckEncodingError::Write)?;
Ok(())
}
/// [Read] a [`Deck`] using the [`F1`](DeckCodeFormat::F1) format.
fn read_f1_body<R: Read>(reader: &mut R) -> DeckDecodingResult<Self> {
// Create the deck's cards container
let mut contents = HashMap::<CardCode, u32>::new();
// Read the standard body
for quantity in (1..=3).rev() {
Self::read_f1_supergroup(reader, &mut contents, quantity)?;
}
// Read the extra body
Self::read_f1_extra(reader, &mut contents)?;
// Create and return the deck
Ok(Deck { contents })
}
/// [Write] this [`Deck`] using the [`F1`](DeckCodeFormat::F1) format.
fn write_f1_body<W: Write>(&self, writer: &mut W) -> DeckEncodingResult<()> {
// Create four vecs of cards, for 3×, 2×, 1× and any× respectively.
let mut triplets = Vec::<&CardCode>::new();
let mut twins = Vec::<&CardCode>::new();
let mut singletons = Vec::<&CardCode>::new();
let mut extra = Vec::<(&CardCode, u32)>::new();
// Fill the previous vecs
for (code, quantity) in self.contents.iter() {
match quantity {
3 => triplets.push(code),
2 => twins.push(code),
1 => singletons.push(code),
_ => extra.push((code, *quantity)),
}
}
// Write the standard body
Self::write_f1_supergroup(writer, &triplets)?;
Self::write_f1_supergroup(writer, &twins)?;
Self::write_f1_supergroup(writer, &singletons)?;
// Write the extra body
Self::write_f1_extra(writer, extra)?;
Ok(())
}
/// [Read] the **groups** of a single supergroup.
fn read_f1_supergroup<R: Read>(reader: &mut R, contents: &mut HashMap<CardCode, u32>, quantity: u32) -> DeckDecodingResult<()> {
// Read the number of groups in the supergroup
let len = reader.read_u32_varint().map_err(DeckDecodingError::Read)?;
// Read all groups
for _ in 0..len {
Self::read_f1_group(reader, contents, quantity)?;
}
Ok(())
}
/// Given a slice of [`CardCode`]s, group them by set and region.
fn f1_group_cards<'cc>(codes: &[&'cc CardCode]) -> HashMap<(&'cc str, &'cc str), Vec<&'cc CardCode>> {
// Create the hashmap accumulating groups of cards
// It has the tuple (set, region) as key
let mut groups = HashMap::new();
// Insert all cards into the various groups
for card in codes {
// Determine the key tuple
let key = (card.set(), card.region());
// Create or insert groups
match groups.get(&key) {
None => {
let mut group = Vec::new();
group.push(*card);
groups.insert(key, group);
}
Some(_) => {
// FIXME: there's a better version for sure
let mut group = groups.remove(&key).unwrap();
group.push(*card);
groups.insert(key, group);
}
}
}
groups
}
/// [Write] the **groups** of a single supergroup.
fn write_f1_supergroup<W: Write>(writer: &mut W, supergroup: &[&CardCode]) -> DeckEncodingResult<()> {
// Arrange cards into groups
let groups = Self::f1_group_cards(supergroup);
// Determine the number of groups in the supergroup
let len: u32 = groups.len().try_into().expect("groups length to be smaller than usize");
writer.write_u32_varint(len).map_err(DeckEncodingError::Write)?;
// Sort first by ascending group length, then by key
let groups = groups
.into_iter()
.sorted_by(|(a_key, a_group), (b_key, b_group)|
a_group.len().cmp(&b_group.len())
.then(
a_key.cmp(&b_key))
);
// Write all groups
for ((set, region), group) in groups {
Self::write_f1_group(writer, &group, set, region)?;
}
Ok(())
}
/// [Read] the **cards** of a single group.
fn read_f1_group<R: Read>(reader: &mut R, contents: &mut HashMap<CardCode, u32>, quantity: u32) -> DeckDecodingResult<()> {
let card_count = reader.read_u32_varint().map_err(DeckDecodingError::Read)?;
let set = reader.read_u32_varint().map_err(DeckDecodingError::Read)?;
let set = CardSet::from(set).to_code().ok_or(DeckDecodingError::UnknownSet)?;
let region = reader.read_u32_varint().map_err(DeckDecodingError::Read)?;
let region = CardRegion::from(region).to_code().ok_or(DeckDecodingError::UnknownRegion)?;
for _card in 0..card_count {
Self::read_f1_standard_card(reader, contents, quantity, &set, &region)?;
}
Ok(())
}
/// [Write] the **cards** of a single group.
fn write_f1_group<W: Write>(writer: &mut W, group: &[&CardCode], set: &str, region: &str) -> DeckEncodingResult<()> {
let len: u32 = group.len().try_into().expect("cards length to be smaller than usize");
writer.write_u32_varint(len).map_err(DeckEncodingError::Write)?;
let set: u32 = CardSet::from_code(set).try_into().map_err(|_| DeckEncodingError::UnknownSet)?;
writer.write_u32_varint(set).map_err(DeckEncodingError::Write)?;
let region: u32 = CardRegion::from_code(region).try_into().map_err(|_| DeckEncodingError::UnknownRegion)?;
writer.write_u32_varint(region).map_err(DeckEncodingError::Write)?;
for card in group.iter().sorted() {
Self::write_f1_standard_card(writer, card.card())?;
}
Ok(())
}
/// [Read] **a single card**.
fn read_f1_standard_card<R: Read>(reader: &mut R, contents: &mut HashMap<CardCode, u32>, quantity: u32, set: &str, region: &str) -> DeckDecodingResult<()> {
let card = reader.read_u32_varint().map_err(DeckDecodingError::Read)?;
let code = CardCode::from_s_r_c(set, region, card);
contents.insert(code, quantity);
Ok(())
}
/// [Write] **a single card**.
fn write_f1_standard_card<W: Write>(writer: &mut W, card: &str) -> DeckEncodingResult<()> {
let card = card.parse::<u32>().map_err(DeckEncodingError::InvalidCardNumber)?;
writer.write_u32_varint(card).map_err(DeckEncodingError::Write)?;
Ok(())
}
/// [Read] the **extra segment** of the deck code.
fn read_f1_extra<R: Read>(reader: &mut R, contents: &mut HashMap<CardCode, u32>) -> DeckDecodingResult<()> {
// While the cursor has still some bytes left...
while let Ok(_) = Self::read_f1_extra_card(reader, contents) {}
Ok(())
}
/// [Write] the **extra segment** of the deck code.
fn write_f1_extra<W: Write>(writer: &mut W, codes: Vec<(&CardCode, u32)>) -> DeckEncodingResult<()> {
for (code, quantity) in codes.iter().sorted() {
Self::write_f1_extra_card(writer, code, *quantity)?;
}
Ok(())
}
/// [Read] **a single card** with a **non-standard quantity**.
fn read_f1_extra_card<R: Read>(reader: &mut R, contents: &mut HashMap<CardCode, u32>) -> DeckDecodingResult<()> {
let quantity = reader.read_u32_varint().map_err(DeckDecodingError::Read)?;
let set = reader.read_u32_varint().map_err(DeckDecodingError::Read)?;
let set = CardSet::from(set).to_code().ok_or(DeckDecodingError::UnknownSet)?;
let region = reader.read_u32_varint().map_err(DeckDecodingError::Read)?;
let region = CardRegion::from(region).to_code().ok_or(DeckDecodingError::UnknownRegion)?;
let card = reader.read_u32_varint().map_err(DeckDecodingError::Read)?;
let code = CardCode::from_s_r_c(&set, &region, card);
contents.insert(code, quantity);
Ok(())
}
/// [Write] **a single card** with a **non-standard quantity**.
fn write_f1_extra_card<W: Write>(writer: &mut W, code: &CardCode, quantity: u32) -> DeckEncodingResult<()> {
writer.write_u32_varint(quantity).map_err(DeckEncodingError::Write)?;
let set = CardSet::from_code(code.set());
let set: u32 = set.try_into().map_err(|_| DeckEncodingError::UnknownSet)?;
writer.write_u32_varint(set).map_err(DeckEncodingError::Write)?;
let region = CardRegion::from_code(code.region());
let region: u32 = region.try_into().map_err(|_| DeckEncodingError::UnknownRegion)?;
writer.write_u32_varint(region).map_err(DeckEncodingError::Write)?;
let card = code.card().parse::<u32>().map_err(DeckEncodingError::InvalidCardNumber)?;
writer.write_u32_varint(card).map_err(DeckEncodingError::Write)?;
Ok(())
}
/// Deserialize a deck code into a [`Deck`].
///
/// # Example
///
/// ```rust
/// use patched_porobot::data::deckcode::deck::Deck;
///
/// Deck::from_code("CQBQCBAJBUCAKCRYHKTADNIBAYBQSDQ2DQ3FEWACAECQVNQBAIBQSOK5AEAQGCIV")
/// .expect("deck to be deserialized successfully");
/// ```
pub fn from_code(code: &str) -> DeckDecodingResult<Deck> {
let mut cursor = Cursor::new(Self::decode_code(&code)?);
let (format, _version) = Self::read_header(&mut cursor)?;
match format {
DeckCodeFormat::F1 => Self::read_f1_body(&mut cursor)
}
}
/// Serialize the [`Deck`] into a deck code of the given [format](DeckCodeFormat).
///
/// # Example
///
/// ```rust
/// use patched_porobot::deck;
/// use patched_porobot::data::deckcode::deck::Deck;
/// use patched_porobot::data::deckcode::format::DeckCodeFormat;
///
/// let d: Deck = deck![
/// "01DE002": 40,
/// ];
///
/// d.to_code(DeckCodeFormat::F1).expect("deck to be serialized successfully");
/// ```
pub fn to_code(&self, format: DeckCodeFormat) -> DeckEncodingResult<String> {
let mut cursor = Cursor::new(Vec::new());
let version = self.min_deckcode_version().ok_or(DeckEncodingError::UnknownVersion)?;
Self::write_header(&mut cursor, format, version)?;
match format {
DeckCodeFormat::F1 => self.write_f1_body(&mut cursor)?,
}
Ok(Self::encode_code(&cursor.into_inner()))
}
}
/// An error occoured while decoding a [`Deck`] from a code.
#[derive(Debug)]
pub enum DeckDecodingError {
/// The provided string was not a valid base-32 string.
Base32Encoding(data_encoding::DecodeError),
/// The decoder cursor was not able to read data from the byte buffer.
Read(std::io::Error),
/// The deck code format of the provided string was unknown.
UnknownFormat,
/// The deck code version of the provided string was unknown.
UnknownVersion,
/// The deck code contains a set with an unknown short code.
UnknownSet,
/// The deck code contains a region with an unknown short code.
UnknownRegion,
}
/// An error occoured while encoding a [`Deck`] into a code.
#[derive(Debug)]
pub enum DeckEncodingError {
/// The encoder cursor was not able to write data into the byte buffer.
Write(std::io::Error),
/// The deck code version of the deck could not be determined.
UnknownVersion,
/// A card in the deck belongs to a set with an unknown internal id.
UnknownSet,
/// A card in the deck belongs to a region with an unknown internal id.
UnknownRegion,
/// A card in the deck has a invalid card number segment in the card code.
InvalidCardNumber(std::num::ParseIntError),
}
/// The [`Result`] of a [`Deck`] **decoding** operation, for example [`Deck::from_code`].
pub type DeckDecodingResult<T> = Result<T, DeckDecodingError>;
/// The [`Result`] of a [`Deck`] **encoding** operation, for example [`Deck::to_code`].
pub type DeckEncodingResult<T> = Result<T, DeckEncodingError>;
/// Macro to build a deck from card code strings and quantities.
///
/// # Example
///
/// ```rust
/// use patched_porobot::deck;
///
/// let _my_deck = deck![
/// "01DE002": 3,
/// "01DE003": 3,
/// "01DE004": 3,
/// "01DE005": 3,
/// "01DE006": 3,
/// "01DE007": 3,
/// "01DE008": 3,
/// "01DE009": 3,
/// "01DE010": 3,
/// "01DE011": 3,
/// "01DE012": 3,
/// "01DE013": 3,
/// "01DE014": 3,
/// "01DE015": 3,
/// "01DE016": 3,
/// "01DE017": 3,
/// "01DE018": 3,
/// "01DE019": 3,
/// "01DE020": 3,
/// "01DE021": 3,
/// ];
/// ```
#[macro_export]
macro_rules! deck {
[$($cd:literal: $qty:literal),* $(,)?] => {
patched_porobot::data::deckcode::deck::Deck {
contents: std::collections::HashMap::from([
$((patched_porobot::data::setbundle::code::CardCode { full: $cd.to_string() }, $qty),)*
])
}
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_de_ser {
( $id:ident, $src:literal ) => {
#[test]
fn $id() {
let deck = Deck::from_code($src).expect("deck to deserialize successfully");
let code = deck.to_code(DeckCodeFormat::F1).expect("deck to serialize successfully");
assert_eq!(code, $src);
}
};
( $id:ident, $src:literal, $($tag:meta),* $(,)?) => {
#[test]
$(#[$tag])*
fn $id() {
let deck = Deck::from_code($src).expect("deck to deserialize successfully");
let code = deck.to_code(DeckCodeFormat::F1).expect("deck to serialize successfully");
assert_eq!(code, $src);
}
};
}
// riot's code is perfect and their examples always work correctly
test_de_ser!(test_de_ser_riotexample, "CEAAECABAQJRWHBIFU2DOOYIAEBAMCIMCINCILJZAICACBANE4VCYBABAILR2HRL", ignore);
test_de_ser!(test_de_ser_yordlestar, "CQBQCBAJBUCAKCRYHKTADNIBAYBQSDQ2DQ3FEWACAECQVNQBAIBQSOK5AEAQGCIV");
test_de_ser!(test_de_ser_gimboclown, "CICACAYGBAAQIBIBAMAQKKZPGEDQEBQEBEGBEFA2EYAQCAYGCABACAIFGUAQGBIH");
test_de_ser!(test_de_ser_paltrisundisc, "CMCACAQGAMAQKBYOAEDAOMAEAQDSOPSCKMBAEBIHA4FQMBAHAEGA2HBMJQAQGBAHEVHWQ");
test_de_ser!(test_de_ser_nomoretf, "CMBQCBABCEBACAIXEYCQIBZWINQWO3IFAEBACBABAQAQUAIGAEOQEAIBBMVAEBAHHNCQEAIEA4TACBIHCY");
test_de_ser!(test_de_ser_lonelyporo3, "CEAQCAIBBAAAA");
test_de_ser!(test_de_ser_lonelyporo2, "CEAACAIBAEEAA");
test_de_ser!(test_de_ser_lonelyporo1, "CEAAAAIBAEAQQ");
test_de_ser!(test_de_ser_someporos1, "CQAAABIBAEAQQAIBAMRACAIECQAQGBATAECQVIAB");
test_de_ser!(test_de_ser_steffonoxus, "CECQCAQCA4AQIAYKAIAQGLRWAQAQECAPEUXAIAQDAEBQOCIBAIAQEMJYAA");
test_de_ser!(test_de_ser_rampanivia, "CECACBAAAUBQCAACA4VAGAYBBIJRMBIBAEGBQIJSGQAQCAIBGEBACAIAEAAQCAIU");
test_de_ser!(test_de_ser_poromegamix, "CQDACAQBAMAQIAIPAECQVIABAIAQIFAXAIBQIEQTAMAQCCAQGUCACAYBAIAQGBADAECQVDABAIAQCKZZAA");
test_de_ser!(test_de_ser_hecapony, "CMBAMAIFAQDRKJRKGEDQIBYDGNFWGZ3SPEAACAIEA45Q");
test_de_ser!(test_de_ser_paltriunholy, "CQCQCAYBAIAQIAIPAECQVIABAEDAOCIFAEAQGCAJCA2QIAICAEBQCBABBIAQMBYOAMCAOO2MNUAQCBQHBM");
// test_de_ser!(test_de_ser_, "");
macro_rules! test_ser_de {
( $id:ident, $deck:expr ) => {
#[test]
fn $id() {
use patched_porobot::data::deckcode::deck::Deck;
let deck1 = $deck;
let code = deck1.to_code(DeckCodeFormat::F1).expect("deck to serialize successfully");
println!("Serialized deck code (for science, obviously): {}", &code);
let deck2 = Deck::from_code(&code).expect("deck to deserialize successfully");
assert_eq!(deck1, deck2);
}
}
}
// Some tests from https://github.com/RiotGames/LoRDeckCodes/blob/main/LoRDeckCodes_Tests/UnitTest1.cs
test_ser_de!(test_ser_de_smalldeck, deck![
"01DE002": 1,
]);
test_ser_de!(test_ser_de_largedeck, deck![
"01DE002": 3,
"01DE003": 3,
"01DE004": 3,
"01DE005": 3,
"01DE006": 3,
"01DE007": 3,
"01DE008": 3,
"01DE009": 3,
"01DE010": 3,
"01DE011": 3,
"01DE012": 3,
"01DE013": 3,
"01DE014": 3,
"01DE015": 3,
"01DE016": 3,
"01DE017": 3,
"01DE018": 3,
"01DE019": 3,
"01DE020": 3,
"01DE021": 3,
]);
test_ser_de!(test_ser_de_smallmorethan3, deck![
"01DE002": 4,
]);
test_ser_de!(test_ser_de_largemorethan3, deck![
"01DE002": 3,
"01DE003": 3,
"01DE004": 3,
"01DE005": 3,
"01DE006": 4,
"01DE007": 5,
"01DE008": 6,
"01DE009": 7,
"01DE010": 8,
"01DE011": 9,
"01DE012": 3,
"01DE013": 3,
"01DE014": 3,
"01DE015": 3,
"01DE016": 3,
"01DE017": 3,
"01DE018": 3,
"01DE019": 3,
"01DE020": 3,
"01DE021": 3,
]);
test_ser_de!(test_ser_de_single40, deck![
"01DE002": 40,
]);
test_ser_de!(test_ser_de_worstcaselength, deck![
"01DE002": 4,
"01DE003": 4,
"01DE004": 4,
"01DE005": 4,
"01DE006": 4,
"01DE007": 5,
"01DE008": 6,
"01DE009": 7,
"01DE010": 8,
"01DE011": 9,
"01DE012": 4,
"01DE013": 4,
"01DE014": 4,
"01DE015": 4,
"01DE016": 4,
"01DE017": 4,
"01DE018": 4,
"01DE019": 4,
"01DE020": 4,
"01DE021": 4,
]);
test_ser_de!(test_ser_de_bilgewater, deck![
"01DE002": 4,
"02BW003": 2,
"02BW010": 3,
"01DE004": 5,
]);
test_ser_de!(test_ser_de_shurima, deck![
"01DE002": 4,
"02BW003": 2,
"02BW010": 3,
"04SH047": 5,
]);
test_ser_de!(test_ser_de_targon, deck![
"01DE002": 4,
"03MT003": 2,
"03MT010": 3,
"02BW004": 5,
]);
test_ser_de!(test_ser_de_runeterra, deck![
"01DE002": 4,
"03MT003": 2,
"03MT010": 3,
"01RU001": 5,
]);
test_ser_de!(test_ser_de_morepowder, deck![
"02BW012": 69,
]);
//test_ser_de!(test_ser_de_, deck![]);
}

View file

@ -0,0 +1,30 @@
//! Module defining [`DeckCodeFormat`].
/// The format of a deck code.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DeckCodeFormat {
/// The only format specified so far.
F1,
}
impl TryFrom<u8> for DeckCodeFormat {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::F1),
_ => Err(())
}
}
}
impl From<DeckCodeFormat> for u8 {
fn from(value: DeckCodeFormat) -> Self {
match value {
DeckCodeFormat::F1 => 1,
}
}
}

7
src/data/deckcode/mod.rs Normal file
View file

@ -0,0 +1,7 @@
//! Module defining the types used to deserialize deck codes.
//!
//! Adapted from [RiotGames/LoRDeckCodes](https://github.com/RiotGames/LoRDeckCodes) and from [iulianR/lordeckcodes-rs](https://github.com/iulianR/lordeckcodes-rs).
pub mod deck;
pub mod version;
pub mod format;

View file

@ -0,0 +1,120 @@
//! Module defining the [`DeckCodeVersion`] enum and [`DeckCodeVersioned`] trait.
use crate::data::deckcode::deck::Deck;
use crate::data::setbundle::code::CardCode;
use crate::data::setbundle::region::CardRegion;
/// The version of a deck code.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum DeckCodeVersion {
/// > Closed alpha. Supports original set.
V1,
/// > Launch. Supports second set with the Bilgewater faction.
///
/// > Supports third set with the Targon faction.
V2,
/// > Supports Empires of the Ascended expansion with Shurima faction.
V3,
/// > Supports Beyond the Bandlewood expansion with Bandle City faction and an update to the deck code library which will create the lowest version code required based on the cards in the deck.
V4,
/// > Supports Worldwalker expansion with Runeterra faction.
V5,
}
impl TryFrom<u8> for DeckCodeVersion {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::V1),
2 => Ok(Self::V2),
3 => Ok(Self::V3),
4 => Ok(Self::V4),
5 => Ok(Self::V5),
_ => Err(())
}
}
}
impl From<DeckCodeVersion> for u8 {
fn from(value: DeckCodeVersion) -> Self {
match value {
DeckCodeVersion::V1 => 1,
DeckCodeVersion::V2 => 2,
DeckCodeVersion::V3 => 3,
DeckCodeVersion::V4 => 4,
DeckCodeVersion::V5 => 5,
}
}
}
/// Indicates that a given type is versioned in the [specs for deck codes](https://github.com/RiotGames/LoRDeckCodes).
pub trait DeckCodeVersioned {
/// Get the minimum deck version required to encode the deck in "code" format.
fn min_deckcode_version(&self) -> Option<DeckCodeVersion>;
}
impl DeckCodeVersioned for CardRegion {
fn min_deckcode_version(&self) -> Option<DeckCodeVersion> {
match self {
CardRegion::Noxus => Some(DeckCodeVersion::V1),
CardRegion::Demacia => Some(DeckCodeVersion::V1),
CardRegion::Freljord => Some(DeckCodeVersion::V1),
CardRegion::ShadowIsles => Some(DeckCodeVersion::V1),
CardRegion::PiltoverZaun => Some(DeckCodeVersion::V1),
CardRegion::Ionia => Some(DeckCodeVersion::V1),
CardRegion::Targon => Some(DeckCodeVersion::V2),
CardRegion::Bilgewater => Some(DeckCodeVersion::V2),
CardRegion::Shurima => Some(DeckCodeVersion::V3),
CardRegion::BandleCity => Some(DeckCodeVersion::V4),
CardRegion::Runeterra => Some(DeckCodeVersion::V5),
CardRegion::Jhin => Some(DeckCodeVersion::V5),
CardRegion::Evelynn => Some(DeckCodeVersion::V5),
CardRegion::Bard => Some(DeckCodeVersion::V5),
_ => None,
}
}
}
/// [`CardCode`]'s version is the maximum version of its components.
impl DeckCodeVersioned for CardCode {
fn min_deckcode_version(&self) -> Option<DeckCodeVersion> {
CardRegion::from_code(self.region()).min_deckcode_version()
}
}
/// [`Deck`]'s version is the maximum version of all its [`CardCode`]s.
impl DeckCodeVersioned for Deck {
fn min_deckcode_version(&self) -> Option<DeckCodeVersion> {
let codes = self.contents.keys();
let versions = codes.map(|cc| cc.min_deckcode_version());
// TODO: I'm almost sure this can be optimized, but it's 5 AM
for version in versions.clone() {
if version.is_none() {
return None;
}
}
let versions = versions.map(|v| v.unwrap());
versions.max()
}
}

View file

@ -5,3 +5,4 @@
pub mod anybundle; pub mod anybundle;
pub mod corebundle; pub mod corebundle;
pub mod setbundle; pub mod setbundle;
pub mod deckcode;

View file

@ -1,6 +1,7 @@
//! Module defining [Card]. //! Module defining [Card].
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use crate::data::setbundle::subtype::CardSubtype; use crate::data::setbundle::subtype::CardSubtype;
use crate::data::setbundle::supertype::CardSupertype; use crate::data::setbundle::supertype::CardSupertype;
use super::r#type::CardType; use super::r#type::CardType;
@ -10,15 +11,16 @@ use super::rarity::CardRarity;
use super::region::CardRegion; use super::region::CardRegion;
use super::speed::SpellSpeed; use super::speed::SpellSpeed;
use super::set::CardSet; use super::set::CardSet;
use super::code::CardCode;
/// A single Legends of Runeterra card, as represented in a `set*.json` file. /// A single Legends of Runeterra card, as represented in a `set*.json` file.
/// ///
/// The data is available in a developer-friendly interface, but nevertheless it can be serialized and deserialized via [serde] in the exact same format used in the `set*.json` files. /// The data is available in a developer-friendly interface, but nevertheless it can be serialized and deserialized via [serde] in the exact same format used in the `set*.json` files.
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Card { pub struct Card {
/// Unique seven-character identifier of the card. /// Unique seven-character identifier of the card.
#[serde(rename = "cardCode")] #[serde(rename = "cardCode")]
pub code: String, pub code: CardCode,
/// Localized name of the card. /// Localized name of the card.
pub name: String, pub name: String,
@ -119,7 +121,7 @@ pub struct Card {
/// ///
/// To access references to the cards themselves, use [Card::associated_cards]. /// To access references to the cards themselves, use [Card::associated_cards].
#[serde(rename = "associatedCardRefs")] #[serde(rename = "associatedCardRefs")]
pub associated_card_codes: Vec<String>, pub associated_card_codes: Vec<CardCode>,
/// [Vec] with [Card::name]s of other cards associated with this one. /// [Vec] with [Card::name]s of other cards associated with this one.
/// ///
@ -160,8 +162,16 @@ impl Card {
} }
/// An index of [Card]s, with [Card::code]s as keys. /// Card [`Hash`]es are equal to hashes of their [`Card::code`].
pub type CardIndex = HashMap<String, Card>; impl Hash for Card {
fn hash<H: Hasher>(&self, state: &mut H) {
self.code.hash(state)
}
}
/// An index of [Card]s, with [CardCode]s as keys.
pub type CardIndex = HashMap<CardCode, Card>;
#[cfg(test)] #[cfg(test)]
@ -217,7 +227,7 @@ mod tests {
} }
"#).unwrap(), "#).unwrap(),
Card { Card {
code: String::from("06RU025"), code: CardCode::from("06RU025".to_string()),
name: String::from("Evelynn"), name: String::from("Evelynn"),
r#type: CardType::Unit, r#type: CardType::Unit,
set: CardSet::Worldwalker, set: CardSet::Worldwalker,
@ -247,9 +257,9 @@ mod tests {
localized_levelup_xml: String::from("When you or an ally kill an allied Husk, give me its positive keywords this round and I level up."), localized_levelup_xml: String::from("When you or an ally kill an allied Husk, give me its positive keywords this round and I level up."),
localized_levelup_text: String::from("When you or an ally kill an allied Husk, give me its positive keywords this round and I level up."), localized_levelup_text: String::from("When you or an ally kill an allied Husk, give me its positive keywords this round and I level up."),
associated_card_codes: vec![ associated_card_codes: vec![
String::from("06RU025T14"), CardCode::from("06RU025T14".to_string()),
String::from("06RU025T6"), CardCode::from("06RU025T6".to_string()),
String::from("06RU025T5"), CardCode::from("06RU025T5".to_string()),
], ],
associated_card_names_localized: vec![], associated_card_names_localized: vec![],
localized_flavor_text: String::from("The priestess' pupils were blown wide, and her hand trembled with nervous excitement. She was ready. This was the single moment Evelynn craved more than any other. She grinned, and slowly shed her visage. Then, as always, the screaming began."), localized_flavor_text: String::from("The priestess' pupils were blown wide, and her hand trembled with nervous excitement. She was ready. This was the single moment Evelynn craved more than any other. She grinned, and slowly shed her visage. Then, as always, the screaming began."),

104
src/data/setbundle/code.rs Normal file
View file

@ -0,0 +1,104 @@
//! Module defining [CardCode].
use crate::data::setbundle::card::{Card, CardIndex};
/// The internal code of a [Card](super::card::Card).
///
/// It is a ASCII string composed of the following segments:
/// - `0..2`: set;
/// - `2..4`: region;
/// - `4..7`: card;
/// - `7..9`: token, never present if the card is [collectible](super::card::Card::collectible).
///
/// # Example
///
/// ```rust
/// use patched_porobot::data::setbundle::code::CardCode;
///
/// CardCode { full: "06RU025".to_string() };
/// ```
///
/// # Warning
///
/// The way this is built is pretty... unsafe, so beware to not construct this with invalid codes.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct CardCode {
/// The card code as a [`String`].
pub full: String
}
impl CardCode {
/// Determines whether the card code is valid or not by checking the following:
///
/// - that the full string [is ascii](str::is_ascii);
/// - that it is either 7 or 9 characters long.
pub fn is_valid(&self) -> bool {
let is_ascii = self.full.is_ascii();
let is_long = match self.full.len() {
7 => true,
9 => true,
_ => false,
};
is_ascii && is_long
}
/// The set segment of the code.
///
/// In valid codes, it is always 2-ASCII-characters long.
pub fn set(&self) -> &str {
&self.full[0..2]
}
/// The region segment of the code.
///
/// In valid codes, it is always 2-ASCII-characters long.
pub fn region(&self) -> &str {
&self.full[2..4]
}
/// The card segment of the code.
///
/// In valid codes, it is always 3-ASCII-characters long.
pub fn card(&self) -> &str {
&self.full[4..7]
}
/// The token segment of the code.
///
/// In valid codes, it may either be an empty string, or 2-ASCII-characters long.
pub fn token(&self) -> &str {
&self.full[7..9]
}
/// Create a new card code given the set and region strings and the card number.
///
/// Note: Does not perform any kind of check on the `set` and `region` parameters, and may cause the creation of invalid [`CardCode`]s if misused.
pub fn from_s_r_c(set: &str, region: &str, card: u32) -> Self {
CardCode::from(format!("{:02}{}{:03}", &set, &region, &card))
}
/// Find, in a [`CardIndex`], the [`Card`] this code belongs to.
pub fn to_card<'c>(&self, cards: &'c CardIndex) -> Option<&'c Card> {
cards.get(&self)
}
}
impl From<CardCode> for String {
fn from(cc: CardCode) -> Self {
cc.full
}
}
/// Create a new card code given the full card code string.
///
/// Note: Does not perform any kind of check on the given string, and may cause the creation of invalid [`CardCode`]s if misused.
impl From<String> for CardCode {
fn from(full: String) -> Self {
CardCode { full }
}
}

View file

@ -18,6 +18,7 @@ pub mod speed;
pub mod subtype; pub mod subtype;
pub mod supertype; pub mod supertype;
pub mod r#type; pub mod r#type;
pub mod code;
/// A parsed [Data Dragon] [Set Bundle]. /// A parsed [Data Dragon] [Set Bundle].
/// ///

View file

@ -53,6 +53,92 @@ impl CardRegion {
pub fn localized<'hm>(&self, hm: &'hm LocalizedCardRegionIndex) -> Option<&'hm LocalizedCardRegion> { pub fn localized<'hm>(&self, hm: &'hm LocalizedCardRegionIndex) -> Option<&'hm LocalizedCardRegion> {
hm.get(self) hm.get(self)
} }
/// Get the [`CardRegion`] from its short code.
///
/// If no region has the specified short code, this will return [`CardRegion::Unsupported`].
pub fn from_code(value: &str) -> Self {
match value {
"DE" => Self::Demacia,
"FR" => Self::Freljord,
"IO" => Self::Ionia,
"NX" => Self::Noxus,
"PZ" => Self::PiltoverZaun,
"SI" => Self::ShadowIsles,
"BW" => Self::Bilgewater,
"SH" => Self::Shurima,
"MT" => Self::Targon,
"BC" => Self::BandleCity,
"RU" => Self::Runeterra,
_ => Self::Unsupported,
}
}
/// Get the short code of this [`CardRegion`].
///
/// If the region has no short code, it will return [`Option::None`].
pub fn to_code(&self) -> Option<String> {
match self {
Self::Demacia => Some("DE".to_string()),
Self::Freljord => Some("FR".to_string()),
Self::Ionia => Some("IO".to_string()),
Self::Noxus => Some("NX".to_string()),
Self::PiltoverZaun => Some("PZ".to_string()),
Self::ShadowIsles => Some("SI".to_string()),
Self::Bilgewater => Some("BW".to_string()),
Self::Shurima => Some("SH".to_string()),
Self::Targon => Some("MT".to_string()),
Self::BandleCity => Some("BC".to_string()),
Self::Runeterra => Some("RU".to_string()),
_ => None,
}
}
}
/// Get the [`CardRegion`] from its internal id.
///
/// If no region has the specified id, this will return [`CardRegion::Unsupported`].
impl From<u32> for CardRegion {
fn from(value: u32) -> Self {
match value {
0 => CardRegion::Demacia,
1 => CardRegion::Freljord,
2 => CardRegion::Ionia,
3 => CardRegion::Noxus,
4 => CardRegion::PiltoverZaun,
5 => CardRegion::ShadowIsles,
6 => CardRegion::Bilgewater,
7 => CardRegion::Shurima,
9 => CardRegion::Targon,
10 => CardRegion::BandleCity,
12 => CardRegion::Runeterra,
_ => CardRegion::Unsupported,
}
}
}
/// Get the internal id of this [`CardRegion`].
///
/// If the region has no internal id, it will return [`Result::Err`].
impl TryFrom<CardRegion> for u32 {
type Error = ();
fn try_from(value: CardRegion) -> Result<Self, Self::Error> {
match value {
CardRegion::Demacia => Ok(0),
CardRegion::Freljord => Ok(1),
CardRegion::Ionia => Ok(2),
CardRegion::Noxus => Ok(3),
CardRegion::PiltoverZaun => Ok(4),
CardRegion::ShadowIsles => Ok(5),
CardRegion::Bilgewater => Ok(6),
CardRegion::Shurima => Ok(7),
CardRegion::Targon => Ok(9),
CardRegion::BandleCity => Ok(10),
CardRegion::Runeterra => Ok(12),
_ => Err(()),
}
}
} }

View file

@ -50,8 +50,80 @@ impl CardSet {
pub fn localized<'hm>(&self, hm: &'hm LocalizedCardSetIndex) -> Option<&'hm LocalizedCardSet> { pub fn localized<'hm>(&self, hm: &'hm LocalizedCardSetIndex) -> Option<&'hm LocalizedCardSet> {
hm.get(self) hm.get(self)
} }
/// Get the [`CardSet`] from its short code, **assuming it is not an [`CardSet::Events`] card**.
///
/// [`CardSet::Events`] cards have the short code of the set they were released in, so it is impossible to determine if a card belongs to that set from its short code.
pub fn from_code(value: &str) -> Self {
match value {
"01" => Self::Foundations,
"02" => Self::RisingTides,
"03" => Self::CallOfTheMountain,
"04" => Self::EmpiresOfTheAscended,
"05" => Self::BeyondTheBandlewood,
"06" => Self::Worldwalker,
_ => Self::Unsupported,
}
}
/// Get the short code of this [`CardSet`].
///
/// [`CardSet::Events`] cards have the short code of the set they were released in, so this method will return [`Option::None`] for them.
///
/// If the set has no short code, it will also return [`Option::None`].
pub fn to_code(&self) -> Option<String> {
match self {
Self::Foundations => Some("01".to_string()),
Self::RisingTides => Some("02".to_string()),
Self::CallOfTheMountain => Some("03".to_string()),
Self::EmpiresOfTheAscended => Some("04".to_string()),
Self::BeyondTheBandlewood => Some("05".to_string()),
Self::Worldwalker => Some("06".to_string()),
_ => None,
}
}
} }
/// Get the [`CardSet`] from its internal id.
///
/// [`CardSet::Events`] cards have the id of the set they were released in, so it is impossible to determine if a card belongs to that set from its id.
impl From<u32> for CardSet {
fn from(value: u32) -> Self {
match value {
1 => CardSet::Foundations,
2 => CardSet::RisingTides,
3 => CardSet::CallOfTheMountain,
4 => CardSet::EmpiresOfTheAscended,
5 => CardSet::BeyondTheBandlewood,
6 => CardSet::Worldwalker,
_ => CardSet::Unsupported,
}
}
}
/// Get the internal id of this [`CardSet`].
///
/// If the set has no associated internal id, it will return [`Result::Err`].
impl TryFrom<CardSet> for u32 {
type Error = ();
fn try_from(value: CardSet) -> Result<Self, Self::Error> {
match value {
CardSet::Foundations => Ok(1),
CardSet::RisingTides => Ok(2),
CardSet::CallOfTheMountain => Ok(3),
CardSet::EmpiresOfTheAscended => Ok(4),
CardSet::BeyondTheBandlewood => Ok(5),
CardSet::Worldwalker => Ok(6),
_ => Err(()),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::CardSet; use super::CardSet;

View file

@ -8,6 +8,7 @@ use tantivy::query::{QueryParser, QueryParserError};
use tantivy::schema::{Field, NumericOptions, Schema, TextOptions}; use tantivy::schema::{Field, NumericOptions, Schema, TextOptions};
use tantivy::tokenizer::TextAnalyzer; use tantivy::tokenizer::TextAnalyzer;
use tantivy::{Document, Index, IndexReader, IndexWriter}; use tantivy::{Document, Index, IndexReader, IndexWriter};
use crate::data::setbundle::code::CardCode;
/// The search engine. /// The search engine.
/// ///
@ -249,7 +250,7 @@ impl CardSearchEngine {
use tantivy::doc; use tantivy::doc;
doc!( doc!(
fields.code => card.code, fields.code => card.code.full,
fields.name => card.name, fields.name => card.name,
fields.r#type => String::from(&card.r#type), fields.r#type => String::from(&card.r#type),
fields.set => card.set fields.set => card.set
@ -364,7 +365,7 @@ impl CardSearchEngine {
.filter_map(|(_score, address)| searcher.doc(address.to_owned()).ok()) .filter_map(|(_score, address)| searcher.doc(address.to_owned()).ok())
.filter_map(|doc| doc.get_first(f_code).cloned()) .filter_map(|doc| doc.get_first(f_code).cloned())
.filter_map(|field| field.as_text().map(String::from)) .filter_map(|field| field.as_text().map(String::from))
.filter_map(|code| self.cards.get(&*code)) .filter_map(|code| self.cards.get(&CardCode::from(code)))
.collect_vec(); .collect_vec();
Ok(results) Ok(results)

View file

@ -6,15 +6,17 @@ use crate::data::corebundle::globals::LocalizedGlobalsIndexes;
use crate::data::corebundle::keyword::LocalizedCardKeywordIndex; use crate::data::corebundle::keyword::LocalizedCardKeywordIndex;
use crate::data::corebundle::region::LocalizedCardRegionIndex; use crate::data::corebundle::region::LocalizedCardRegionIndex;
use crate::data::corebundle::set::LocalizedCardSetIndex; use crate::data::corebundle::set::LocalizedCardSetIndex;
use crate::data::setbundle::card::Card; use crate::data::setbundle::card::{Card, CardIndex};
use crate::data::setbundle::keyword::CardKeyword; use crate::data::setbundle::keyword::CardKeyword;
use crate::data::setbundle::r#type::CardType; use crate::data::setbundle::r#type::CardType;
use crate::data::setbundle::region::CardRegion; use crate::data::setbundle::region::CardRegion;
use crate::data::setbundle::set::CardSet; use crate::data::setbundle::set::CardSet;
use crate::data::setbundle::subtype::CardSubtype; use crate::data::setbundle::subtype::CardSubtype;
use crate::data::setbundle::supertype::CardSupertype; use crate::data::setbundle::supertype::CardSupertype;
use crate::data::deckcode::deck::Deck;
use itertools::Itertools; use itertools::Itertools;
use teloxide::utils::html::escape; use teloxide::utils::html::escape;
use crate::data::deckcode::format::DeckCodeFormat;
/// Render a [Card] in [Telegram Bot HTML]. /// Render a [Card] in [Telegram Bot HTML].
/// ///
@ -151,3 +153,32 @@ fn display_levelup(levelup: &String) -> String {
format!("<u>Level up</u>: {}\n\n", escape(&levelup)) format!("<u>Level up</u>: {}\n\n", escape(&levelup))
} }
} }
/// Render a [Deck] in [Telegram Bot HTML].
///
/// [Telegram Bot HTML]: https://core.telegram.org/bots/api#html-style
pub fn display_deck(index: &CardIndex, deck: &Deck, code: String) -> String {
// TODO: optimize this
let cards = deck.contents
.keys()
.sorted_by(|a, b| {
let card_a = index.get(a).expect("card to exist in the index");
let card_b = index.get(b).expect("card to exist in the index");
card_a.cost.cmp(&card_b.cost).then(card_a.name.cmp(&card_b.name))
})
.map(|k| {
let card = index.get(k).expect("card to exist in the index");
let quantity = deck.contents.get(k).unwrap();
if card.supertype == "Champion" {
format!("<b>{}×</b> <u>{}</u>", &quantity, &card.name)
}
else {
format!("<b>{}×</b> {}", &quantity, &card.name)
}
})
.join("\n");
format!("<code>{}</code>\n\n{}", &code, &cards)
}

View file

@ -1,7 +1,7 @@
//! Module providing handlers for @patchedporobot on Telegram. //! Module providing handlers for @patchedporobot on Telegram.
use crate::search::cardsearch::CardSearchEngine; use crate::search::cardsearch::CardSearchEngine;
use crate::telegram::inline::card_to_inlinequeryresult; use crate::telegram::inline::{card_to_inlinequeryresult, deck_to_inlinequeryresult};
use itertools::Itertools; use itertools::Itertools;
use log::*; use log::*;
use teloxide::dispatching::DpHandlerDescription; use teloxide::dispatching::DpHandlerDescription;
@ -9,6 +9,7 @@ use teloxide::payloads::{AnswerInlineQuery, SendMessage};
use teloxide::prelude::*; use teloxide::prelude::*;
use teloxide::requests::{JsonRequest, ResponseResult}; use teloxide::requests::{JsonRequest, ResponseResult};
use teloxide::types::{ParseMode, Recipient}; use teloxide::types::{ParseMode, Recipient};
use crate::data::deckcode::deck::Deck;
/// Handle inline queries by searching cards on the [CardSearchEngine]. /// Handle inline queries by searching cards on the [CardSearchEngine].
pub fn inline_query_handler( pub fn inline_query_handler(
@ -26,11 +27,25 @@ pub fn inline_query_handler(
cache_time: None, cache_time: None,
is_personal: Some(false), is_personal: Some(false),
next_offset: None, next_offset: None,
switch_pm_text: Some("How to search cards".to_string()), switch_pm_text: Some("How to use the bot".to_string()),
switch_pm_parameter: Some("err-no-query".to_string()), switch_pm_parameter: Some("err-no-query".to_string()),
}; };
} }
if let Ok(deck) = Deck::from_code(&query.query.to_ascii_uppercase()) {
break AnswerInlineQuery {
inline_query_id: query.id.clone(),
results: vec![
deck_to_inlinequeryresult(&engine.cards, &deck)
],
cache_time: None,
is_personal: Some(false),
next_offset: None,
switch_pm_text: None,
switch_pm_parameter: None,
}
}
debug!("Querying the search engine..."); debug!("Querying the search engine...");
let results = engine.query(&query.query, 50); let results = engine.query(&query.query, 50);

View file

@ -2,10 +2,15 @@
//! //!
//! [inline mode]: https://core.telegram.org/bots/api#inline-mode //! [inline mode]: https://core.telegram.org/bots/api#inline-mode
use std::collections::hash_map::DefaultHasher;
use std::hash::Hash;
use std::ptr::hash;
use crate::data::corebundle::globals::LocalizedGlobalsIndexes; use crate::data::corebundle::globals::LocalizedGlobalsIndexes;
use crate::data::setbundle::card::Card; use crate::data::setbundle::card::{Card, CardIndex};
use crate::telegram::display::display_card; use crate::telegram::display::{display_card, display_deck};
use teloxide::types::{InlineQueryResult, InlineQueryResultPhoto, ParseMode}; use teloxide::types::{InlineQueryResult, InlineQueryResultArticle, InlineQueryResultPhoto, InputMessageContent, InputMessageContentText, ParseMode};
use crate::data::deckcode::deck::Deck;
use crate::data::deckcode::format::DeckCodeFormat;
/// Converts a [Card] into a [InlineQueryResult]. /// Converts a [Card] into a [InlineQueryResult].
pub fn card_to_inlinequeryresult( pub fn card_to_inlinequeryresult(
@ -13,7 +18,7 @@ pub fn card_to_inlinequeryresult(
card: &Card, card: &Card,
) -> InlineQueryResult { ) -> InlineQueryResult {
InlineQueryResult::Photo(InlineQueryResultPhoto { InlineQueryResult::Photo(InlineQueryResultPhoto {
id: card.code.to_owned(), id: card.code.full.to_owned(),
title: Some(card.name.to_owned()), title: Some(card.name.to_owned()),
caption: Some(display_card(&globals, &card)), caption: Some(display_card(&globals, &card)),
parse_mode: Some(ParseMode::Html), parse_mode: Some(ParseMode::Html),
@ -37,3 +42,26 @@ pub fn card_to_inlinequeryresult(
input_message_content: None, input_message_content: None,
}) })
} }
pub fn deck_to_inlinequeryresult(index: &CardIndex, deck: &Deck) -> InlineQueryResult {
let code = deck.to_code(DeckCodeFormat::F1).expect("serialized deck to deserialize properly");
InlineQueryResult::Article(InlineQueryResultArticle {
id: format!("{:x}", md5::compute(&code)),
title: format!("Deck with {} cards", deck.contents.len()),
input_message_content: InputMessageContent::Text(InputMessageContentText {
message_text: display_deck(index, deck, code),
parse_mode: Some(ParseMode::Html),
entities: None,
disable_web_page_preview: Some(true)
}),
reply_markup: None,
url: None,
hide_url: None,
description: None,
thumb_url: None,
thumb_width: None,
thumb_height: None
})
}

View file

@ -10,7 +10,7 @@ use log::*;
use std::path::PathBuf; use std::path::PathBuf;
use teloxide::prelude::*; use teloxide::prelude::*;
/// The main function that [`patched_porobot_telegram`] should run when it's started. /// The main function that `patched_porobot_telegram` should run when it's started.
pub async fn main() { pub async fn main() {
pretty_env_logger::init(); pretty_env_logger::init();
debug!("Logger initialized successfully!"); debug!("Logger initialized successfully!");
@ -30,6 +30,8 @@ pub async fn main() {
.expect("to be able to load `set5-en_us` bundle"); .expect("to be able to load `set5-en_us` bundle");
let set6 = SetBundle::load(&*PathBuf::from("./data/set6-en_us")) let set6 = SetBundle::load(&*PathBuf::from("./data/set6-en_us"))
.expect("to be able to load `set6-en_us` bundle"); .expect("to be able to load `set6-en_us` bundle");
let set6cde = SetBundle::load(&*PathBuf::from("./data/set6cde-en_us"))
.expect("to be able to load `set6cde-en_us` bundle");
debug!("Loaded all bundles!"); debug!("Loaded all bundles!");
debug!("Indexing globals..."); debug!("Indexing globals...");
@ -38,7 +40,7 @@ pub async fn main() {
debug!("Indexing cards..."); debug!("Indexing cards...");
let cards: Vec<Card> = [ let cards: Vec<Card> = [
set1.cards, set2.cards, set3.cards, set4.cards, set5.cards, set6.cards, set1.cards, set2.cards, set3.cards, set4.cards, set5.cards, set6.cards, set6cde.cards,
] ]
.concat(); .concat();