diff --git a/.gitignore b/.gitignore index d01bd1a..ff1c9f8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk @@ -18,4 +17,6 @@ Cargo.lock # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +.idea/ + +releases \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c14de90 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,108 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "idlib" +version = "0.1.0" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "newestidmangler" +version = "0.1.0" +dependencies = [ + "base64", + "idlib", + "serde", + "serde_json", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2ecae40 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "newestidmangler" +version = "0.1.0" +edition = "2021" + +[dependencies] +base64 = "0.22.1" +idlib = { path = "./idlib" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/config.md b/config.md new file mode 100644 index 0000000..f417244 --- /dev/null +++ b/config.md @@ -0,0 +1,59 @@ +# Values Guide +## Name +**Name** is stored as a String. It must be a valid Wynncraft item for it to display as intended. +## Shiny ID +not yet implemented +## Powders + +### Powder Limit +Powder limit cannot be increased beyond 255. This is because the powder count is stored as a byte. +Potential value range: 0<->255. + +### Format +Inside the "powders":[ ] array, set it up in the following structure. +**{ "type":"$TYPE", "tier":$TIER, "amount": $AMOUNT }** +**TYPE** is stored as a single character. +Potential Values: "E" "T" "W" "F" "A" . These represent Earth, Thunder, Wind, Fire, Air. +**TIER** is stored as a single digit integer. +Potential value range: 1<->6. +**AMOUNT** is stored as an integer. It is optional. If not provided it falls back to 1. +Potential value range: 1<->255. +#### Other things about powder format: +Each value in the array must have a comma at the end except the last. +The use of spaces is optional, as well as letter case for the $TYPE value. +The keys ("type" "tier" "amount") must all be lowercase. +If type is invalid, it will default back to being Thunder powder. +#### Powders Example +```js +"powders": [ + { "type":"T", "tier":6, "amount":5 }, + {"type":"e","tier":1,"amount":5}, + {"type":"F", "tier":3,"amount":1}, + { "type" : "w" , "tier":6 } +], +``` +Note that the last powder block in array has no comma at the end. +### Which items can have powders? +Powders can only be encoded on an item that originally supported powders in the first place. +Unfortunately you can't add powders to an item that didn't originally have them. +e.g. can't put powder on depressing shears, as depressing weapons don't have powder slot. + +## Identifications +### Format +Inside the "ids":[] array, set it up in the following structure. +**{"id": "$ID","base": $BASE,"roll": $ROLL}** +**ID** is stored as a string corresponding to the Wynntils internal ID string of any roll. See here for a list: https://raw.githubusercontent.com/Wynntils/Static-Storage/main/Reference/id_keys.json . +**BASE** is the base roll. The default base data is defined in (WARNING: EXTREMELY MASSIVE TEXT FILE) https://raw.githubusercontent.com/Wynntils/Static-Storage/main/Reference/gear.json . +Beautify it then look for `ITEMNAME > base > IDENTIFICATION > raw` for default value. This base value defines the listed ID ranges. +**ROLL** defines the actual rolled value for the Identification. The formula is `BASE * ROLL / 100`. Most values are in the range 30-130 due to how Wynncraft handles many Identifications as 30-130 percent of a base stat. Thus, if you are trying to find the ROLL value, try your desired roll **(NOT the roll percentage 1-100)** divided by the BASE then round it to the nearest integer. This value is optional, and you should only exclude it when it is a fixed value e.g. Skill Points. + +## Rerolls +Optional single value, i8. Stores number of rerolls. If missing or is 0, rerolls are not encoded. +Potential range: 0<->255. + +### Other things about this +The format obeys the same json rules as the powders. +If you are trying to get the 100% or 0% roll and the value you calculated is close to 130 or 30, change it to 130 or 30 because that is the real value. + +# Current issues +Crafteds are not implemented \ No newline at end of file diff --git a/id_keys.json b/id_keys.json new file mode 100644 index 0000000..440542c --- /dev/null +++ b/id_keys.json @@ -0,0 +1,115 @@ +{ + "1stSpellCost": 0, + "2ndSpellCost": 1, + "3rdSpellCost": 2, + "4thSpellCost": 3, + "airDamage": 4, + "airDefence": 5, + "airMainAttackDamage": 6, + "airSpellDamage": 7, + "damageFromMobs": 8, + "earthDamage": 9, + "earthDefence": 10, + "earthMainAttackDamage": 11, + "earthSpellDamage": 12, + "elementalDamage": 13, + "elementalDefence": 14, + "elementalDefense": 15, + "elementalSpellDamage": 16, + "exploding": 17, + "fireDamage": 18, + "fireDefence": 19, + "fireSpellDamage": 20, + "gatherSpeed": 21, + "gatherXpBonus": 22, + "healingEfficiency": 23, + "healthRegen": 24, + "healthRegenRaw": 25, + "jumpHeight": 26, + "knockback": 27, + "leveledLootBonus": 28, + "leveledXpBonus": 29, + "lifeSteal": 30, + "lootBonus": 31, + "lootQuality": 32, + "mainAttackDamage": 33, + "manaRegen": 34, + "manaSteal": 35, + "poison": 36, + "raw1stSpellCost": 37, + "raw2ndSpellCost": 38, + "raw3rdSpellCost": 39, + "raw4thSpellCost": 40, + "rawAgility": 41, + "rawAirDamage": 42, + "rawAirMainAttackDamage": 43, + "rawAirSpellDamage": 44, + "rawAttackSpeed": 45, + "rawDefence": 46, + "rawDexterity": 47, + "rawEarthDamage": 48, + "rawEarthSpellDamage": 49, + "rawElementalDamage": 50, + "rawElementalMainAttackDamage": 51, + "rawElementalSpellDamage": 52, + "rawFireDamage": 53, + "rawFireMainAttackDamage": 54, + "rawFireSpellDamage": 55, + "rawHealth": 56, + "rawIntelligence": 57, + "rawMainAttackDamage": 58, + "rawNeutralDamage": 59, + "rawNeutralSpellDamage": 60, + "rawSpellDamage": 61, + "rawStrength": 62, + "rawThunderDamage": 63, + "rawThunderMainAttackDamage": 64, + "rawThunderSpellDamage": 65, + "rawWaterDamage": 66, + "rawWaterMainAttackDamage": 67, + "rawWaterSpellDamage": 68, + "reflection": 69, + "slowEnemy": 70, + "soulPointRegen": 71, + "spellDamage": 72, + "sprint": 73, + "sprintRegen": 74, + "stealing": 75, + "thorns": 76, + "thunderDamage": 77, + "thunderDefence": 78, + "thunderMainAttackDamage": 79, + "thunderSpellDamage": 80, + "walkSpeed": 81, + "waterDamage": 82, + "waterDefence": 83, + "waterSpellDamage": 84, + "weakenEnemy": 85, + "xpBonus": 86, + "healing": 87, + "rawEarthMainAttackDamage": 88, + "agility": 89, + "baseAirDamage": 90, + "baseAirDefence": 91, + "baseDamage": 92, + "baseEarthDamage": 93, + "baseEarthDefence": 94, + "baseFireDamage": 95, + "baseFireDefence": 96, + "baseHealth": 97, + "baseThunderDamage": 98, + "baseThunderDefence": 99, + "baseWaterDamage": 100, + "baseWaterDefence": 101, + "damage": 102, + "defence": 103, + "dexterity": 104, + "elementalMainAttackDamage": 105, + "fireMainAttackDamage": 106, + "intelligence": 107, + "neutralDamage": 108, + "neutralMainAttackDamage": 109, + "rawDamage": 110, + "rawNeutralMainAttackDamage": 111, + "strength": 112 +} diff --git a/idlib/Cargo.toml b/idlib/Cargo.toml new file mode 100644 index 0000000..d64b589 --- /dev/null +++ b/idlib/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "idlib" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/idlib/src/data_transformer/enddata.rs b/idlib/src/data_transformer/enddata.rs new file mode 100644 index 0000000..4013742 --- /dev/null +++ b/idlib/src/data_transformer/enddata.rs @@ -0,0 +1,36 @@ +use crate::types::transform::TransformVersion; + +use super::{DataDecoder, DataEncoder, DataTransformerTypes, TransformId}; + +#[derive(Debug, Clone)] +pub struct EndData; + +impl TransformId for EndData { + fn get_id() -> u8 { + DataTransformerTypes::EndDataTransformer as u8 + } +} + +impl DataEncoder for EndData { + fn encode_data( + &self, + _ver: TransformVersion, + _out: &mut Vec, + ) -> Result<(), super::DataTransformError> { + // end data is always empty + return Ok(()); + } +} + +impl> DataDecoder for EndData { + fn decode_data( + _bytes: &mut B, + _ver: TransformVersion, + ) -> Result + where + Self: Sized, + { + // end data is always empty + return Ok(Self); + } +} diff --git a/idlib/src/data_transformer/identdata.rs b/idlib/src/data_transformer/identdata.rs new file mode 100644 index 0000000..ffd83c3 --- /dev/null +++ b/idlib/src/data_transformer/identdata.rs @@ -0,0 +1,175 @@ +use crate::{ + data_transformer::DataTransformError, + encoding::{decode_varint, encode_varint}, + types::{ + stat::{RollType, Stat, StatId}, + transform::TransformVersion, + }, +}; + +use super::{DataDecoder, DataEncoder, DataTransformerTypes, TransformId}; + +#[derive(Debug, Clone)] +pub struct IdentificationData { + pub identifications: Vec, + pub extended_encoding: bool, +} + +impl TransformId for IdentificationData { + fn get_id() -> u8 { + DataTransformerTypes::IdentificationDataTransformer as u8 + } +} + +impl DataEncoder for IdentificationData { + fn encode_data( + &self, + ver: TransformVersion, + out: &mut Vec, + ) -> Result<(), super::DataTransformError> { + match ver { + TransformVersion::Version1 => { + if self.identifications.len() > 255 { + return Err(DataTransformError::TooManyIdentifications); + } + + let encoded_id_count: u8 = self + .identifications + .iter() + .filter(|id| !id.pre_identified()) + .count() as u8; + + out.push(encoded_id_count); + out.push(self.extended_encoding as u8); + + self.encode_individual_idents(out)?; + + return Ok(()); + } + } + } + + fn should_encode_data(&self, ver: TransformVersion) -> bool { + match ver { + TransformVersion::Version1 => { + if self.extended_encoding { + return self.identifications.len() != 0; + } else { + return self + .identifications + .iter() + .any(|id: &Stat| !id.pre_identified()); + } + } + } + } +} + +impl IdentificationData { + fn encode_individual_idents(&self, bytes: &mut Vec) -> Result<(), DataTransformError> { + // encode the static values if extended encoding is used + if self.extended_encoding { + let preid_stats: Vec<_> = self + .identifications + .iter() + .filter(|id| id.pre_identified()) + .collect(); + + bytes.push(preid_stats.len() as u8); + + for stat in preid_stats { + // first add the id of the ident + bytes.push(stat.kind.0); + + // then add the basevalue + bytes.append(&mut encode_varint( + stat.base.ok_or(DataTransformError::NoBasevalueForIdent)? as i64, + )); + } + } + + for ident in self.identifications.iter() { + // only handle non preids since preids are encoded using the earlier system + if let RollType::Value(roll_val) = ident.roll { + // add id of the ident + bytes.push(ident.kind.0); + + if self.extended_encoding { + // push the baseval + bytes.append(&mut encode_varint( + ident.base.ok_or(DataTransformError::NoBasevalueForIdent)? as i64, + )); + } + + bytes.push(roll_val); + } else { + continue; + } + } + + Ok(()) + } +} + +impl> DataDecoder for IdentificationData { + fn decode_data(bytes: &mut B, ver: TransformVersion) -> Result + where + Self: Sized, + { + match ver { + TransformVersion::Version1 => { + let mut idents = Vec::new(); + + // first byte is the number of identifications + let ident_count = bytes.next().unwrap(); + + // second byte is whether or not extended coding is used + let extended_encoding = bytes.next().unwrap() == 1; + + let mut preid_count = 0; + if extended_encoding { + // count of preid idents + preid_count = bytes.next().unwrap(); + } + + for i in 0..(ident_count + preid_count) { + // id of the ident + let id = bytes.next().unwrap(); + + let preid = i < preid_count; + + // decode the possible baseval if using extended coding + let baseval = if extended_encoding { + Some(decode_varint(bytes) as i32) + } else { + None + }; + + // if preid skip decoding the value + if preid { + idents.push(Stat { + kind: StatId(id), + base: baseval, + roll: RollType::PreIdentified, + }); + continue; + } else { + // decode the roll + let introll = bytes.next().unwrap(); + + idents.push(Stat { + kind: StatId(id), + base: baseval, + roll: RollType::Value(introll), + }) + } + } + + Ok(Self { + identifications: idents, + extended_encoding, + }) + } + } + } +} diff --git a/idlib/src/data_transformer/mod.rs b/idlib/src/data_transformer/mod.rs new file mode 100644 index 0000000..aa8018e --- /dev/null +++ b/idlib/src/data_transformer/mod.rs @@ -0,0 +1,130 @@ +use enddata::EndData; +use identdata::IdentificationData; +use namedata::NameData; +use powderdata::PowderData; +use rerolldata::RerollData; +use startdata::StartData; +use typedata::TypeData; + +use crate::types::transform::TransformVersion; + +pub mod enddata; +pub mod identdata; +pub mod namedata; +pub mod powderdata; +pub mod rerolldata; +pub mod shinydata; +pub mod startdata; +pub mod typedata; + +pub trait TransformId { + fn get_id() -> u8; +} + +pub trait DataEncoder: TransformId { + fn encode(&self, ver: TransformVersion, out: &mut Vec) -> Result<(), DataTransformError> { + // skip encoding data which should not be encoded + if !self.should_encode_data(ver) { + return Ok(()); + } + + // encode the id + out.push(Self::get_id()); + + // encode the data + self.encode_data(ver, out)?; + + Ok(()) + } + + fn encode_data( + &self, + ver: TransformVersion, + out: &mut Vec, + ) -> Result<(), DataTransformError>; + + fn should_encode_data(&self, _ver: TransformVersion) -> bool { + true + } +} + +pub trait DataDecoder>: TransformId { + fn decode_data(bytes: &mut B, ver: TransformVersion) -> Result + where + Self: Sized; +} + +pub fn decode>(bytes: &mut B) -> Result, DataTransformError> { + let mut out = Vec::new(); + + // decode the start byte and version + let ver = StartData::decode_start_bytes(bytes)?; + + while let Some(id) = bytes.next() { + match id { + 0 => return Err(DataTransformError::StartReparse), + 1 => out.push(AnyData::TypeData(TypeData::decode_data(bytes, ver)?)), + 2 => out.push(AnyData::NameData(NameData::decode_data(bytes, ver)?)), + 3 => out.push(AnyData::IdentificationData( + IdentificationData::decode_data(bytes, ver)?, + )), + // TODO + 255 => out.push(AnyData::EndData(EndData::decode_data(bytes, ver)?)), + _ => return Err(DataTransformError::UnknownTransformer(id)), + } + } + + Ok(out) +} + +#[derive(Debug)] +pub enum DataTransformError { + NoStartBlock, + UnknownVersion(u8), + /// Attempt to parse start data. Start data is specially handled. + StartReparse, + + InvalidTypeError, + + BadString, + + TooManyIdentifications, + NoBasevalueForIdent, + NoPotentialValuesForIdent, + InvalidIntRoll, + + UnexpectedEndOfBytes, + UnknownTransformer(u8), +} + +pub enum DataTransformerTypes { + StartDataTransformer = 0, + TypeDataTransformer = 1, + NameDataTransformer = 2, + IdentificationDataTransformer = 3, + PowderDataTransformer = 4, + RerollDataTransformer = 5, + ShinyDataTransformer = 6, + CustomGearTypeTransformer = 7, + DurabilityDataTransformer = 8, + RequirementsDataTransformer = 9, + DamageDataTransformer = 10, + DefenseDataTransformer = 11, + CustomIdentificationDataTransformer = 12, + CustomConsumableTypeDataTransformer = 13, + UsesDataTransformer = 14, + EffectsDataTransformer = 15, + EndDataTransformer = 255, +} + +#[derive(Debug)] +pub enum AnyData { + StartData(StartData), + TypeData(TypeData), + NameData(NameData), + IdentificationData(IdentificationData), + PowderData(PowderData), + RerollData(RerollData), + // TODO + EndData(EndData), +} diff --git a/idlib/src/data_transformer/namedata.rs b/idlib/src/data_transformer/namedata.rs new file mode 100644 index 0000000..252bf8c --- /dev/null +++ b/idlib/src/data_transformer/namedata.rs @@ -0,0 +1,54 @@ +use crate::{data_transformer::DataTransformError, types::transform::TransformVersion}; + +use super::{DataDecoder, DataEncoder, DataTransformerTypes, TransformId}; + +#[derive(Debug, Clone)] +pub struct NameData(pub String); + +impl TransformId for NameData { + fn get_id() -> u8 { + DataTransformerTypes::NameDataTransformer as u8 + } +} + +impl DataEncoder for NameData { + fn encode_data( + &self, + ver: TransformVersion, + out: &mut Vec, + ) -> Result<(), super::DataTransformError> { + match ver { + TransformVersion::Version1 => { + // check that the string is valid ascii + if self.0.chars().any(|c| !c.is_ascii()) { + return Err(DataTransformError::BadString); + } + + // push the bytes + out.extend_from_slice(self.0.as_bytes()); + // push the null terminator + out.push(0); + } + } + + Ok(()) + } +} + +impl> DataDecoder for NameData { + fn decode_data(bytes: &mut B, ver: TransformVersion) -> Result + where + Self: Sized, + { + match ver { + TransformVersion::Version1 => { + let b: Vec = bytes.take_while(|b| *b != 0).collect(); + + // UTF-8 and ASCII share the same set of characters + Ok(NameData( + String::from_utf8(b).map_err(|_| DataTransformError::BadString)?, + )) + } + } + } +} diff --git a/idlib/src/data_transformer/powderdata.rs b/idlib/src/data_transformer/powderdata.rs new file mode 100644 index 0000000..bc2d90f --- /dev/null +++ b/idlib/src/data_transformer/powderdata.rs @@ -0,0 +1,59 @@ +use crate::types::{powder::Powders, transform::TransformVersion}; + +use super::{DataEncoder, DataTransformerTypes, TransformId}; + +#[derive(Debug, Clone)] +pub struct PowderData { + pub powder_slots: u8, + pub powders: Vec<(Powders, u8)>, +} + +impl TransformId for PowderData { + fn get_id() -> u8 { + DataTransformerTypes::PowderDataTransformer as u8 + } +} + +impl DataEncoder for PowderData { + fn encode_data( + &self, + ver: TransformVersion, + out: &mut Vec, + ) -> Result<(), super::DataTransformError> { + match ver { + TransformVersion::Version1 => { + let bits_needed = self.powders.len() * 5; + let total_bits = (bits_needed + 7) / 8; + + let mut powder_data = vec![0u8; total_bits]; + + for (i, pow) in self.powders.iter().enumerate() { + let elem = pow.0 as u8; + // TODO: figure out if wynntils fixes this and make the tier be encoded correctly + let tier = 0; //pow.1; + + // calculate the 5 bit powder value + let powder_num = (elem * 6 + tier) & 0b00011111; + + // bit position where this specific powder starts + let powder_idx = i * 5; + + // set the values + for j in 0..5 { + // calculate the bit position of this bit + let idx = powder_idx + j; + let bit = (powder_num >> (4 - j)) & 0b1; + powder_data[idx / 8] |= bit << (7 - (idx % 8)); + } + } + + out.push(self.powder_slots); + out.push(self.powders.len() as u8); + + out.append(&mut powder_data); + } + } + + Ok(()) + } +} diff --git a/idlib/src/data_transformer/rerolldata.rs b/idlib/src/data_transformer/rerolldata.rs new file mode 100644 index 0000000..617eabf --- /dev/null +++ b/idlib/src/data_transformer/rerolldata.rs @@ -0,0 +1,41 @@ +use crate::types::transform::TransformVersion; + +use super::{DataDecoder, DataEncoder, DataTransformError, DataTransformerTypes, TransformId}; + +#[derive(Debug, Clone)] +pub struct RerollData(pub u8); + +impl TransformId for RerollData { + fn get_id() -> u8 { + DataTransformerTypes::RerollDataTransformer as u8 + } +} + +impl DataEncoder for RerollData { + fn encode_data( + &self, + ver: crate::types::transform::TransformVersion, + out: &mut Vec, + ) -> Result<(), super::DataTransformError> { + match ver { + TransformVersion::Version1 => out.push(self.0), + } + + Ok(()) + } +} + +impl> DataDecoder for RerollData { + fn decode_data(bytes: &mut B, ver: TransformVersion) -> Result + where + Self: Sized, + { + match ver { + TransformVersion::Version1 => Ok(Self( + bytes + .next() + .ok_or(DataTransformError::UnexpectedEndOfBytes)?, + )), + } + } +} diff --git a/idlib/src/data_transformer/shinydata.rs b/idlib/src/data_transformer/shinydata.rs new file mode 100644 index 0000000..2e30df7 --- /dev/null +++ b/idlib/src/data_transformer/shinydata.rs @@ -0,0 +1,31 @@ +use crate::{encoding::encode_varint, types::transform::TransformVersion}; + +use super::{DataEncoder, DataTransformerTypes, TransformId}; + +pub struct ShinyData { + pub id: u8, + pub val: i64, +} + +impl TransformId for ShinyData { + fn get_id() -> u8 { + DataTransformerTypes::ShinyDataTransformer as u8 + } +} + +impl DataEncoder for ShinyData { + fn encode_data( + &self, + ver: crate::types::transform::TransformVersion, + out: &mut Vec, + ) -> Result<(), super::DataTransformError> { + match ver { + TransformVersion::Version1 => { + out.push(self.id); + out.append(&mut encode_varint(self.val)); + } + } + + Ok(()) + } +} diff --git a/idlib/src/data_transformer/startdata.rs b/idlib/src/data_transformer/startdata.rs new file mode 100644 index 0000000..7c85d94 --- /dev/null +++ b/idlib/src/data_transformer/startdata.rs @@ -0,0 +1,42 @@ +use crate::types::transform::TransformVersion; + +use super::{DataEncoder, DataTransformError, DataTransformerTypes, TransformId}; + +#[derive(Debug, Clone)] +pub struct StartData(pub TransformVersion); + +impl TransformId for StartData { + fn get_id() -> u8 { + DataTransformerTypes::StartDataTransformer as u8 + } +} + +impl DataEncoder for StartData { + fn encode_data( + &self, + ver: TransformVersion, + out: &mut Vec, + ) -> Result<(), DataTransformError> { + match ver { + TransformVersion::Version1 => out.push(self.0.version()), + } + + Ok(()) + } +} + +impl StartData { + /// Special case function for parsing the start bytes + pub(crate) fn decode_start_bytes>( + bytes: &mut B, + ) -> Result { + let idbyte = bytes.next().unwrap(); + if idbyte != DataTransformerTypes::StartDataTransformer as u8 { + return Err(DataTransformError::NoStartBlock); + } + + let verbyte = bytes.next().unwrap(); + + TransformVersion::from_u8(verbyte).map_err(|_| DataTransformError::UnknownVersion(verbyte)) + } +} diff --git a/idlib/src/data_transformer/typedata.rs b/idlib/src/data_transformer/typedata.rs new file mode 100644 index 0000000..0042743 --- /dev/null +++ b/idlib/src/data_transformer/typedata.rs @@ -0,0 +1,43 @@ +use crate::types::{itemtype::ItemType, transform::TransformVersion}; + +use super::{DataDecoder, DataEncoder, DataTransformError, DataTransformerTypes, TransformId}; + +#[derive(Debug, Clone)] +pub struct TypeData(pub ItemType); + +impl TransformId for TypeData { + fn get_id() -> u8 { + DataTransformerTypes::TypeDataTransformer as u8 + } +} + +impl DataEncoder for TypeData { + fn encode_data( + &self, + ver: TransformVersion, + out: &mut Vec, + ) -> Result<(), super::DataTransformError> { + match ver { + TransformVersion::Version1 => out.push(self.0.into()), + } + + Ok(()) + } +} + +impl> DataDecoder for TypeData { + fn decode_data(bytes: &mut B, ver: TransformVersion) -> Result + where + Self: Sized, + { + match ver { + TransformVersion::Version1 => { + let b = bytes.next().unwrap(); + + Ok(Self( + ItemType::try_from(b).map_err(|_| DataTransformError::InvalidTypeError)?, + )) + } + } + } +} diff --git a/idlib/src/encoding.rs b/idlib/src/encoding.rs new file mode 100644 index 0000000..9a1e0d9 --- /dev/null +++ b/idlib/src/encoding.rs @@ -0,0 +1,112 @@ +/// Encode bytes into a string using the wynntils byte encoding scheme +/// +/// https://github.com/Wynntils/Wynntils/blob/main/common/src/main/java/com/wynntils/utils/EncodedByteBuffer.java#L87 +pub fn encode_string(data: &[u8]) -> String { + let mut out = String::new(); + + for d in data.chunks(2) { + if d.len() == 2 { + if d[0] == 255 && d[1] >= 254 { + out.push(char::from_u32(0x100000 + ((d[1] - 254) as u32)).unwrap()); + } else { + out.push(char::from_u32(0xF0000 + ((d[0] as u32) << 8) + d[1] as u32).unwrap()); + } + } else { + // encode leftover singular bits with the seperate encoding + out.push(char::from_u32(0x100000 + ((d[0] as u32) << 8) + 0xEE).unwrap()); + } + } + + out +} + +/// Decodes the bytes of a wynntils private area encoded string +/// +/// This function does not check whether or not the encoded data is valid +/// +/// https://github.com/Wynntils/Wynntils/blob/main/common/src/main/java/com/wynntils/utils/EncodedByteBuffer.java#L33 +pub fn decode_string(data: &str) -> Vec { + let mut out = Vec::new(); + + for c in data.chars() { + let n: u32 = c.into(); + + // special case Private use area B + if n > 0x100000 { + // single byte + if n & 0xFF == 0xEE { + out.push(((n & 0xFF00) >> 8) as u8); + + assert!(((n & 0xFF00) >> 8) <= 255, "Invalid codepoint: {n:06X}"); + continue; + } + + // two bytes + + out.push(255); + out.push((254 + (n & 0xFF)) as u8); + + // Only 0x100000-0x100001 are used + assert!(n < 0x100002, "Invalid codepoint: {n:06X}"); + continue; + } + + out.push(((n & 0xFF00) >> 8) as u8); + out.push((n & 0x00FF) as u8); + } + + out +} + +pub fn encode_varint(value: i64) -> Vec { + // zigzag encoding magic + // removes sign bit so values are only positive + let value = ((value << 1) ^ (value >> 63)) as u64; + + // 7 bits per byte + // highest bit is used to indicate end of encoding + + // calulate number of bytes needed + let mut numofbytes = 1; + let mut temp = value >> 7; + while temp != 0 { + // println!("{temp}"); + numofbytes += 1; + temp >>= 7; + } + + let mut outbytes = Vec::new(); + for i in 0..numofbytes { + let mut next = (value >> (7 * i)) as u8 & 0x7F; + + // indicate that we are **not** done by setting the highest bit + if i < numofbytes - 1 { + next |= 0b10000000; + } + + outbytes.push(next); + } + + outbytes +} + +pub fn decode_varint>(bytes: &mut B) -> i64 { + let mut value = 0; + + let mut data = Vec::new(); + loop { + let b = bytes.next().unwrap(); + + data.push(b); + + if (b & 0b10000000) == 0 { + break; + } + } + + for (i, n) in data.into_iter().enumerate() { + value |= ((n & 0b01111111) as i64) << (7 * i); + } + + return (value >> 1) ^ -(value & 1); +} diff --git a/idlib/src/lib.rs b/idlib/src/lib.rs new file mode 100644 index 0000000..b2c34cf --- /dev/null +++ b/idlib/src/lib.rs @@ -0,0 +1,3 @@ +pub mod data_transformer; +pub mod encoding; +pub mod types; diff --git a/idlib/src/types/itemtype.rs b/idlib/src/types/itemtype.rs new file mode 100644 index 0000000..ba2a37e --- /dev/null +++ b/idlib/src/types/itemtype.rs @@ -0,0 +1,31 @@ +#[repr(u8)] +#[derive(Clone, Copy, Debug)] +pub enum ItemType { + Gear = 0, + Tome = 1, + Charm = 2, + CraftedGear = 3, + CraftedConsu = 4, +} + +impl Into for ItemType { + fn into(self) -> u8 { + self as u8 + } +} + +impl TryFrom for ItemType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Gear), + 1 => Ok(Self::Tome), + 2 => Ok(Self::Charm), + 3 => Ok(Self::CraftedGear), + 4 => Ok(Self::CraftedConsu), + + _ => Err(()), + } + } +} diff --git a/idlib/src/types/mod.rs b/idlib/src/types/mod.rs new file mode 100644 index 0000000..429f629 --- /dev/null +++ b/idlib/src/types/mod.rs @@ -0,0 +1,4 @@ +pub mod itemtype; +pub mod powder; +pub mod stat; +pub mod transform; diff --git a/idlib/src/types/powder.rs b/idlib/src/types/powder.rs new file mode 100644 index 0000000..ad2ceea --- /dev/null +++ b/idlib/src/types/powder.rs @@ -0,0 +1,9 @@ +/// Powder types +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Powders { + EARTH = 1, + THUNDER = 2, + WATER = 3, + FIRE = 4, + AIR = 5, +} diff --git a/idlib/src/types/stat.rs b/idlib/src/types/stat.rs new file mode 100644 index 0000000..59c8d43 --- /dev/null +++ b/idlib/src/types/stat.rs @@ -0,0 +1,24 @@ +#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)] +pub struct StatId(pub u8); + +#[derive(Debug, Clone)] +pub struct Stat { + pub kind: StatId, + pub base: Option, + pub roll: RollType, +} + +#[derive(Debug, Clone)] +pub enum RollType { + Value(u8), + PreIdentified, +} + +impl Stat { + pub fn pre_identified(&self) -> bool { + match self.roll { + RollType::Value(_) => false, + RollType::PreIdentified => true, + } + } +} diff --git a/idlib/src/types/transform.rs b/idlib/src/types/transform.rs new file mode 100644 index 0000000..42dd200 --- /dev/null +++ b/idlib/src/types/transform.rs @@ -0,0 +1,17 @@ +#[derive(Clone, Copy, Debug)] +pub enum TransformVersion { + Version1 = 0, +} + +impl TransformVersion { + pub fn version(&self) -> u8 { + *self as u8 + } + + pub fn from_u8(byte: u8) -> Result { + match byte { + 0 => Ok(Self::Version1), + _ => Err(()), + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..96f4859 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,269 @@ +use idlib::{ + data_transformer::{ + decode, enddata::EndData, identdata::IdentificationData, namedata::NameData, + powderdata::PowderData, rerolldata::RerollData, shinydata::ShinyData, startdata::StartData, + typedata::TypeData, DataEncoder, + }, + encoding::{decode_string, encode_string}, + types::{ + itemtype::ItemType, + powder::Powders, + stat::{RollType, Stat, StatId}, + transform::TransformVersion, + }, +}; + +use std::collections::HashMap; +use std::fs; +use std::panic; +use std::env; +use serde_json; +use serde::Deserialize; +use base64::engine::{general_purpose, Engine}; + +// structs +#[derive(Deserialize)] +struct Powder { + r#type: char, + tier: u8, + amount: Option +} +#[derive(Deserialize)] +struct Identificationer { + id: String, + base: i32, + roll: Option +} +#[derive(Deserialize)] +struct jsonconfig { + name: String, + ids: Vec, + powder_limit: u8, + powders: Vec, + rerolls:Option +} + +fn main() { + // enable fancypanic when building for release + fancypanic(); + + // newest json reading code + let json_config: jsonconfig = serde_json::from_reader( + fs::File::open("values.json").expect(ERROR[1])) + .expect(ERROR[2]); + let idsmap: HashMap = serde_json::from_reader(fs::File::open("id_keys.json").expect(ERROR[3])) + .expect(ERROR[4]); + // println!("{:?}",idsmap.get("airDamage")); + // below is no longer needed as ive merged it + //let imported2: jsoned = serde_json::from_reader(importedjson) + // .expect("this json sucks"); + + // read the file and stuff + // thanks to https://stackoverflow.com/a/52964674 + // obselete do not use + //let file = fs::File::open("values.json") + // .expect("where file?"); + //let thejson: serde_json::Value = serde_json::from_reader(file) + // .expect("where proper json format?"); + //let powders = thejson.get("powders").expect("e").get("a"); + //let powders2 = serde_json::json!(powders); + //println!("powders: {:?}",powders); + //println!("powders2: {}",powders2); + //println!("name is {}", thejson); + + // let fuy = thejson.get("a"); + // println!("{:#?}",fuy); + + + + let mut out = Vec::new(); + let ver = TransformVersion::Version1; + + StartData(ver).encode(ver, &mut out).unwrap(); + + TypeData(ItemType::Gear).encode(ver, &mut out).unwrap(); + + NameData(String::from(format!("{}", json_config.name.trim()) )) + .encode(ver, &mut out) + .unwrap(); + + let w1 = "aWRtYW5nbGVyLXJld3JpdGUgUHJlLVJlbGVhc2UgdjE="; + let w2 = "KEMpIFpBVFpPVSBhbmQgRU5ERVJOT04gMjAyNA=="; + + let l1 = String::from_utf8(general_purpose::STANDARD.decode(w1).unwrap()).unwrap(); + let l2 = String::from_utf8(general_purpose::STANDARD.decode(w2).unwrap()).unwrap(); + println!("{l1}"); + println!("{l2}"); + + // json identification data handling + let mut idvec = Vec::new(); + for eachid in json_config.ids { + let id_id = idsmap.get(eachid.id.trim()); + let id_base = eachid.base as i32; + let id_roll = eachid.roll; + + idvec.push( + ( + Stat { + kind: StatId(match id_id { + Some(ide) => *ide, + None => panic!("There is a mismatched ID, and this message has replaced where the line is meant to be") + }), + base: Some(id_base), + roll: match id_roll{ + Some(rolle) => RollType::Value(rolle), + None => RollType::PreIdentified + } + } + ) + ); + + // println!("{:?} {:?} {:?}",id_id,id_base,id_roll) + } + + IdentificationData { + identifications: idvec, + extended_encoding: true, + } + .encode(ver, &mut out) + .unwrap(); + + + + + + // json powder data handling + let mut powdervec = Vec::new(); + for eachpowder in json_config.powders { + let powdertier = eachpowder.tier; // get the powder tier + let powderamount:u8 = match eachpowder.amount { // get amount of powder if exists, otherwise 1 + Some(amount) => { + amount + },// good, + None => { + 1 + }// bad, + }; + // match for the powder type + // no need to return to variable or i'll need to rematch AGAIN + match eachpowder.r#type { + 'E' | 'e' => { + for i in 0..powderamount { + powdervec.push((Powders::EARTH,powdertier)) + } + }, + 'T' | 't' => { + for i in 0..powderamount { + powdervec.push((Powders::THUNDER,powdertier)) + } + }, + 'W' | 'w' => { + for i in 0..powderamount { + powdervec.push((Powders::WATER,powdertier)) + } + }, + 'F' | 'f' => { + for i in 0..powderamount { + powdervec.push((Powders::FIRE,powdertier)) + } + }, + 'A' | 'a' => { + for i in 0..powderamount { + powdervec.push((Powders::AIR,powdertier)) + } + }, + _ => { + for i in 0..powderamount { + powdervec.push((Powders::THUNDER,powdertier)) + } + } + }; + + // println!("tier {}",powdertier); + // println!("amount {}",powderamount); + } + // println!("{:?}",powdervec); + + + + + // old powder data encode kinda, takes data from new encode + PowderData { + powder_slots: json_config.powder_limit, + powders: powdervec, + } + .encode(ver, &mut out) + .unwrap(); + + + match json_config.rerolls { + Some(i) => { + if i != 0 { + RerollData(i).encode(ver, &mut out).unwrap(); + } + }, + None => pass() + } + + + + ShinyData { + id: 2, + val: i64::MAX as i64, //- 0b0100_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000, + // u16::MAX is the max value of unsigned 16bit value + } + .encode(ver, &mut out) + .unwrap(); + + + // prints (Water,6) 255 times + // println!("{:?}",vec![(Powders::WATER, 6); 255]); + + EndData.encode(ver, &mut out).unwrap(); + + + + // final string print + println!("{}", encode_string(&out)); + + // I don't even know what the fuck this does + //for b in out { + // print!("{:02X}", b); + //} + + // println!(); + + // decode test + let input = "󰀀󰄀󰉁󶹴󶡲󶅣󶥴󶔠󴉡󶱬󶥳󷑡󰀃󰠁󰀞󾠇󵠑󳱩󳢠󱽴󴠧󷄡󱹵󳫠󰢂󱌨󵴅󲠞􏿮"; + let bytes = decode_string(&input); + let mut bytes_iter = bytes.into_iter(); + + let out = decode(&mut bytes_iter).unwrap(); + + // println!("{:#?}", out); +} + + +fn fancypanic() { + panic::set_hook(Box::new(|panic_info| { + let panic_msg = format!("{panic_info}"); + println!("{}", panic_msg.lines().skip(1).next().unwrap_or("HOW DID YOU BREAK THE PANIC HANDLER???")); + })); +} + +fn pass() { + +} + +const ERROR: [&'static str; 5] = [ + "Error 0: what did you even do to get this? ", + "Error 1: json config file is missing, reobtain it from the values.json I have sent you. ", + "Error 2: json config is broken. Reread the example data or reobtain it from the values.json I have sent you. ", + "Error 3: Identifications hashmap not found. Get it from https://raw.githubusercontent.com/Wynntils/Static-Storage/main/Reference/id_keys.json and move it to this directory.", + "Error 4: Identifications hashhmap is corrupt. Reobtain it from https://raw.githubusercontent.com/Wynntils/Static-Storage/main/Reference/id_keys.json and move it to this directory." +]; +const _BOIL: [&'static str; 3] = [ + "0", + "reobtain it from the values.json I have sent you. ", + "Get it from https://raw.githubusercontent.com/Wynntils/Static-Storage/main/Reference/id_keys.json and move it to this directory." +]; \ No newline at end of file diff --git a/values.json b/values.json new file mode 100644 index 0000000..c45180f --- /dev/null +++ b/values.json @@ -0,0 +1,26 @@ +{ + "READMEFIRST": [ + "Refer to config.md to understand how to apply the data shown here.", + "There are also some values you cannot increase beyond a limit." + ], + "name":"Singularity", + + "ids": [ + {"id": "mainAttackDamage","base": 320,"roll": 69}, + {"id": "healthRegenRaw", "base":250 , "roll":130 }, + {"id": "rawDexterity", "base":35 }, + {"id": "walkSpeed", "base":-40 , "roll":69}, + {"id": "mainAttackDamage", "base":15, "roll":130 }, + {"id": "rawMainAttackDamage", "base":444 , "roll":130 }, + {"id": "rawSpellDamage", "base":222 , "roll":130 }, + {"id": "spellDamage", "base":10 , "roll":130 } + ], + "powder_limit": 255, + "powders": [ + { "type":"T", "tier":6, "amount":5 }, + {"type":"e","tier":6,"amount":5}, + {"type":"f", "tier":6,"amount":1}, + { "type" : "f" , "tier":6 } + ], + "rerolls": 0 +} \ No newline at end of file