essences/
currency.rs

1use crate::prelude::*;
2use event_system::script::types::ESCurrencyUnit;
3
4#[declare]
5pub type CurrencyId = uuid::Uuid;
6
7// Wire-compat note: the variant order is part of the wire format because
8// `serde` encodes unit variants as their declaration index, and the wire
9// codec (postcard) preserves that index as a varint discriminant.
10// Pre-existing variants keep their original positions; all new variants are
11// appended after.
12//
13// Known violation of the append rule: `ProgressPassClaim` was inserted
14// before `Cheat`, shifting `Cheat` from declaration index 17 to 18. The
15// `Serialize` impl below follows the *declaration* indices (`Cheat` = 18),
16// so the wire stays self-consistent with the derived `Deserialize`; the
17// `Cheat` index correction shipped as a coupled backend + client release.
18// Do not reorder these variants — and never insert in the middle again.
19//
20// The `Serialize` impl below masquerades new variants as `BundleClaim` for
21// serde channels (postcard wire + JSON state patches) so a backend release
22// can ship without re-releasing the Unity/Python client. Server-side
23// analytics uses `Debug`, not `Serialize`, so the canonical variant still
24// reaches the dashboard. Once all clients have shipped knowledge of the new
25// variants, the custom impl can be deleted and `Serialize` re-added to the
26// derive list.
27#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
28#[tsify(from_wasm_abi, into_wasm_abi)]
29pub enum CurrencySource {
30    EntityDeath,
31    ItemSell,
32    AutoItemSell,
33    QuestClaim,
34    QuestsTrackReward,
35    VassalTaskCompletion,
36    ReferralLvlUp,
37    ReferralDailyReward,
38    GiftClaim,
39    ArenaTicketBuy,
40    ClaimVassalReward,
41    ClaimSuzerainReward,
42    ArenaMatchmakingRefresh,
43    GachaLevelUp,
44    PetCaseLevelUp,
45    // BundleClaim stays at its original index — also serves as the
46    // `#[serde(default)]` fallback for legacy serialized state without a
47    // recorded bundle origin.
48    #[default]
49    BundleClaim,
50    AdReward,
51    ProgressPassClaim,
52    Cheat,
53    // Appended in this PR. Bundles are containers; the analytics signal lives
54    // in the source that decided to grant the bundle, not in the claim
55    // mechanism. Always append new variants — never insert in the middle.
56    MailReward,
57    DungeonReward,
58    OfferBuy,
59    ChapterReward,
60    NewUserGrant,
61    PvpArenaReward,
62    PvpVassalReward,
63    AfkReward,
64    DailyReset,
65    RatingReward,
66    CoreReset,
67}
68
69impl serde::Serialize for CurrencySource {
70    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
71    where
72        S: serde::Serializer,
73    {
74        // Wire-compat shim. Real wire variants serialize as themselves
75        // (matching what `#[derive(Serialize)]` would have produced — same
76        // declaration-index, same name), so the derived `Deserialize` decodes
77        // them back to the same variant. Backend-only variants masquerade as
78        // `BundleClaim` so older clients can still postcard-decode the
79        // message. Server-side analytics uses `Debug`, which keeps the
80        // canonical name. The indices here MUST equal the declaration indices
81        // above — a mismatch makes the postcard round-trip decode to a
82        // different variant (see `wire_compat.rs`).
83        let (idx, name) = match self {
84            Self::EntityDeath => (0u32, "EntityDeath"),
85            Self::ItemSell => (1, "ItemSell"),
86            Self::AutoItemSell => (2, "AutoItemSell"),
87            Self::QuestClaim => (3, "QuestClaim"),
88            Self::QuestsTrackReward => (4, "QuestsTrackReward"),
89            Self::VassalTaskCompletion => (5, "VassalTaskCompletion"),
90            Self::ReferralLvlUp => (6, "ReferralLvlUp"),
91            Self::ReferralDailyReward => (7, "ReferralDailyReward"),
92            Self::GiftClaim => (8, "GiftClaim"),
93            Self::ArenaTicketBuy => (9, "ArenaTicketBuy"),
94            Self::ClaimVassalReward => (10, "ClaimVassalReward"),
95            Self::ClaimSuzerainReward => (11, "ClaimSuzerainReward"),
96            Self::ArenaMatchmakingRefresh => (12, "ArenaMatchmakingRefresh"),
97            Self::GachaLevelUp => (13, "GachaLevelUp"),
98            Self::PetCaseLevelUp => (14, "PetCaseLevelUp"),
99            Self::BundleClaim => (15, "BundleClaim"),
100            Self::AdReward => (16, "AdReward"),
101            // Declaration index 18 — `ProgressPassClaim` sits at 17. Emitting
102            // 17 here would make clients decode Cheat as ProgressPassClaim.
103            Self::Cheat => (18, "Cheat"),
104            // Backend-only variants — collapse to BundleClaim on the wire.
105            Self::MailReward
106            | Self::DungeonReward
107            | Self::OfferBuy
108            | Self::ChapterReward
109            | Self::NewUserGrant
110            | Self::PvpArenaReward
111            | Self::PvpVassalReward
112            | Self::AfkReward
113            | Self::DailyReset
114            | Self::RatingReward
115            | Self::CoreReset
116            | Self::ProgressPassClaim => (15, "BundleClaim"),
117        };
118        serializer.serialize_unit_variant("CurrencySource", idx, name)
119    }
120}
121
122#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
123#[tsify(from_wasm_abi, into_wasm_abi)]
124pub enum CurrencyConsumer {
125    AbilityCaseOpen,
126    AbilityCaseSlotUpgrade,
127    ItemCaseOpen,
128    GiftSend,
129    DungeonRaid,
130    DungeonFightEnd,
131    ClassLevelUp,
132    OfferBuy,
133    ItemCaseUpgrade,
134    CaseUpgradeSpeedUp,
135    CaseUpgradeSkip,
136    ArenaFight,
137    ArenaMatchmakingRefresh,
138    ArenaTicketBuy,
139    SkinBuy,
140    AfkInstantReward,
141    StatueRoll,
142    TalentUpgrade,
143    TalentUpgradeSkip,
144    PetCaseOpen,
145    // OVT-2517: twin-core level-up. Appended — never insert in the middle.
146    CoreUpgrade,
147}
148
149#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Tsify)]
150pub struct Currency {
151    #[schemars(schema_with = "id_schema")]
152    pub id: CurrencyId,
153    #[schemars(title = "Название")]
154    pub name: i18n::I18nString,
155    #[schemars(title = "Описание")]
156    pub description: i18n::I18nString,
157    #[schemars(title = "URL картинки", schema_with = "webp_url_schema")]
158    pub icon_url: String,
159    #[schemars(title = "Иконка", schema_with = "asset_currency_icon_schema")]
160    pub icon_path: String,
161}
162
163#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema, Tsify, Default)]
164pub struct CurrencyUnit {
165    #[schemars(schema_with = "currency_link_id_schema")]
166    pub currency_id: CurrencyId,
167    #[schemars(title = "Количество")]
168    pub amount: i64,
169}
170
171impl PartialOrd for CurrencyUnit {
172    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
173        Some(self.cmp(other))
174    }
175}
176
177impl Ord for CurrencyUnit {
178    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
179        match self.currency_id.cmp(&other.currency_id) {
180            std::cmp::Ordering::Equal => self.amount.cmp(&other.amount),
181            ordering => ordering,
182        }
183    }
184}
185
186pub fn increase_currencies(
187    currencies_to_increase: &mut Vec<CurrencyUnit>,
188    currencies_to_add: &[CurrencyUnit],
189) {
190    for add_currency in currencies_to_add {
191        if let Some(unit) = currencies_to_increase
192            .iter_mut()
193            .find(|unit| unit.currency_id == add_currency.currency_id)
194        {
195            unit.amount += add_currency.amount;
196        } else {
197            currencies_to_increase.push(add_currency.clone());
198        }
199    }
200}
201
202pub fn decrease_currencies(
203    currencies_to_decrease: &mut [CurrencyUnit],
204    currencies_to_subtract: &[CurrencyUnit],
205) -> anyhow::Result<()> {
206    for subtract_currency in currencies_to_subtract {
207        if let Some(unit) = currencies_to_decrease
208            .iter_mut()
209            .find(|unit| unit.currency_id == subtract_currency.currency_id)
210        {
211            if unit.amount < subtract_currency.amount {
212                anyhow::bail!(
213                    "Required currency {subtract_currency:?} is bigger than available {unit:?}"
214                )
215            }
216
217            unit.amount -= subtract_currency.amount;
218        } else {
219            anyhow::bail!("Given currency {subtract_currency:?} isn't present in currencies")
220        }
221    }
222    Ok(())
223}
224
225pub fn force_decrease_currencies(
226    currencies_to_decrease: &mut Vec<CurrencyUnit>,
227    currencies_to_subtract: &[CurrencyUnit],
228) -> anyhow::Result<()> {
229    for subtract_currency in currencies_to_subtract {
230        if let Some(unit) = currencies_to_decrease
231            .iter_mut()
232            .find(|unit| unit.currency_id == subtract_currency.currency_id)
233        {
234            unit.amount -= subtract_currency.amount;
235        }
236    }
237
238    currencies_to_decrease.retain(|unit| unit.amount != 0);
239
240    Ok(())
241}
242
243pub fn check_can_decrease_currencies(
244    available_currencies: &[CurrencyUnit],
245    required_currencies: &[CurrencyUnit],
246) -> bool {
247    for subtract_currency in required_currencies {
248        if let Some(unit) = available_currencies
249            .iter()
250            .find(|unit| unit.currency_id == subtract_currency.currency_id)
251        {
252            if unit.amount < subtract_currency.amount {
253                return false;
254            }
255        } else {
256            return false;
257        }
258    }
259    true
260}
261
262pub fn from_es_currencies(es_currencies: &[ESCurrencyUnit]) -> Vec<CurrencyUnit> {
263    let mut result = vec![];
264    for es_currency in es_currencies.iter() {
265        result.push(CurrencyUnit {
266            currency_id: es_currency.currency_id,
267            amount: es_currency.amount,
268        });
269    }
270    result
271}