#steam #currencies #team #fortress #tf2 #weapon #pricing

tf2-price

Utilities for Team Fortress 2 item pricing

20 releases (12 breaking)

0.13.1 Apr 6, 2024
0.12.0 Jan 26, 2024
0.11.0 Feb 18, 2023
0.8.0 Dec 31, 2022
0.5.3 Mar 18, 2022

#798 in Games

Download history 7/week @ 2024-01-20 1/week @ 2024-01-27 78/week @ 2024-02-24 3/week @ 2024-03-02 2/week @ 2024-03-09 7/week @ 2024-03-30 245/week @ 2024-04-06

254 downloads per month

MIT license

90KB
2K SLoC

tf2-price

Utilities for Team Fortress 2 item pricing. Lightweight with only one required dependency.

Fractional currencies pose arithmetic challenges due to the inherent imprecision of floating-point numbers. A solution is to handle currency in its smallest unit (e.g., cents for US currency, or weapons in Team Fortress 2), stored as integers. This allows precise calculations without cumbersome conversions, ensuring predictable outcomes.

Installation

With Serde

tf2-price = { version = "0.13.1", features = ["serde"] }

Without Serde

tf2-price = "0.13.1"

Usage

Basic Usage

use tf2_price::{Currencies, metal, ONE_REF, ONE_REC};

let currencies = Currencies {
    keys: 5,
    weapons: metal!(1.33), // 24 weapons.
};

// 1.33 refined or 24 weapons.
assert_eq!(currencies.weapons, 24);
assert_eq!(currencies.weapons, ONE_REF + ONE_REC);
assert_eq!(currencies.weapons, metal!(1.33));
// String conversions.
assert_eq!(
    format!("Selling for {currencies}."),
    "Selling for 5 keys, 1.33 ref.",
);
assert_eq!(
    "5 keys, 1.33 ref".parse::<Currencies>().unwrap(),
    currencies,
);

// Key price stored as weapons.
let key_price_weapons = metal!(50);
// Conversion to a single total.
let total = currencies.to_weapons(key_price_weapons);

// 5 (keys) * 50 (key price) * 18 (one ref) = 4500
// 4500 + 24 (weapons) = 4524
assert_eq!(total, 4524);
assert_eq!(
    // Convert total back into keys + weapons.
    Currencies::from_weapons(total, key_price_weapons),
    currencies,
);

Arithmetic

use tf2_price::{Currencies, Currency};

let golden_pan = Currencies {
    keys: 3000,
    weapons: 0,
};

assert_eq!(
    // Multiply by an integer.
    golden_pan * 2,
    Currencies {
        keys: 6000,
        weapons: 0,
    },
);
assert_eq!(
    // Multiply by a floating point number.
    golden_pan * 2.5,
    Currencies {
        keys: 7500,
        weapons: 0,
    },
);
assert_eq!(
    // Add another currencies.
    golden_pan + Currencies {
        keys: 0,
        weapons: 2,
    },
    Currencies {
        keys: 3000,
        weapons: 2,
    },
);

In release builds in Rust, integers pose the risk of overflowing. While, this behaviour is not considered unsafe, it is problematic. This crate uses saturating arithmetic for integer arithmetic and also provides methods for checking for overflow.

Due to the vast size of 64-bit integers, worries about reaching their bounds are generally unnecessary. To give a practical example:

use tf2_price::{Currencies, Currency, refined};

// Let's say, hypothetically, a Golden Pan is selling for 3000 keys.
let golden_pan = Currencies {
    keys: 3000,
    weapons: 0,
};
// And the price of a key is 50 refined.
let key_price_weapons = refined!(50);
// And we convert the price of the Golden Pan into weapons.
let golden_pan_weapons = golden_pan.to_weapons(key_price_weapons);
// And take how many times this number fits into the max value.
let num_golden_pans = Currency::MAX / golden_pan_weapons;

// It would be enough to buy over 3 trillion Golden Pans in weapons.
assert_eq!(num_golden_pans, 3_416_063_717_353);

let currencies = Currencies {
    keys: 0,
    weapons: golden_pan_weapons,
};

// We can multiply the number of Golden Pans by its price in weapons.
assert!(currencies.checked_mul(num_golden_pans).is_some());
// But if we try to multiply by one more, it will overflow.
assert!(currencies.checked_mul(num_golden_pans + 1).is_none());
// Now we do the same multiplication but without checking.
let currencies = currencies * (num_golden_pans + 1);
// The result will be the max value rather than wrapping around.
assert_eq!(currencies.weapons, Currency::MAX);

Floating Point Precision

To store floating point numbers from responses, use FloatCurrencies as a container. However, it's advised not to use it for calculations or comparisons. This crate provides utilities for converting floats to integers based on use-case (saturating, checked).

use tf2_price::{Currencies, FloatCurrencies, Currency};

// To preserve original values, use FloatCurrencies.
let float_currencies = FloatCurrencies {
    keys: 1.0,
    // Unlike Currencies, metal is not counted in weapons.
    // 1.33 means 1.33 refined.
    metal: 1.33,
};
// Converting to Currencies (checks for safe conversion).
let currencies = Currencies::try_from(float_currencies).unwrap();

assert_eq!(
    currencies,
    Currencies {
        keys: 1,
        metal: 24,
    },
);
// Fails if the key value holds a fractional number.
assert!(Currencies::try_from(FloatCurrencies {
    keys: 1.5,
    metal: 0.0,
}).is_err());
// Fails if a value is outside of integer bounds.
assert!(Currencies::try_from(FloatCurrencies {
    keys: Currency::MAX as f32 * 2.0,
    metal: 0.0,
}).is_err());

Serialization

use tf2_price::{Currencies, metal};
    
let json = r#"{"keys":5,"metal":2.33}"#;
let currencies: Currencies = serde_json::from_str(json).unwrap();

assert_eq!(
    currencies,
    Currencies {
        keys: 5,
        weapons: metal!(2.33),
    },
);
assert_eq!(
    json,
    serde_json::to_string(&currencies).unwrap(),
);

License

MIT

Dependencies

~205KB