sf_api/
command.rs

1#![allow(deprecated)]
2use enum_map::Enum;
3use log::warn;
4use num_derive::FromPrimitive;
5use strum::EnumIter;
6
7use crate::{
8    gamestate::{
9        character::*,
10        dungeons::{CompanionClass, Dungeon},
11        fortress::*,
12        guild::{Emblem, GuildSkill},
13        idle::IdleBuildingType,
14        items::*,
15        social::Relationship,
16        underworld::*,
17        unlockables::{HabitatType, HellevatorTreatType, Unlockable},
18    },
19    PlayerId,
20};
21
22#[non_exhaustive]
23#[derive(Debug, Clone)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25/// A command, that can be send to the sf server
26pub enum Command {
27    /// If there is a command you somehow know/reverse engineered, or need to
28    /// extend the functionality of one of the existing commands, this is the
29    /// command for you
30    Custom {
31        /// The thing in the command, that comes before the ':'
32        cmd_name: String,
33        /// The values this command gets as arguments. These will be joines
34        /// with '/'
35        arguments: Vec<String>,
36    },
37    /// Manually sends a login request to the server.
38    /// **WARN:** The behaviour for a credentials mismatch, with the
39    /// credentials in the user is undefined. Use the login method instead
40    /// for a safer abstraction
41    #[deprecated = "Use the login method instead"]
42    Login {
43        /// The username of the player you are trying to login
44        username: String,
45        /// The sha1 hashed password of the player
46        pw_hash: String,
47        /// Honestly, I am not 100% sure what this is anymore, but it is
48        /// related to the maount of times you have logged in. Might be useful
49        /// for logging in again after error
50        login_count: u32,
51    },
52    #[cfg(feature = "sso")]
53    /// Manually sends a login request to the server.
54    /// **WARN:** The behaviour for a credentials mismatch, with the
55    /// credentials in the user is undefined. Use the login method instead for
56    /// a safer abstraction
57    #[deprecated = "Use a login method instead"]
58    SSOLogin {
59        /// The Identifies the s&f account, that has this character
60        uuid: String,
61        /// Identifies the specific character an account has
62        character_id: String,
63        /// The thing to authenticate with
64        bearer_token: String,
65    },
66    /// Registers a new normal character in the server. I am not sure about the
67    /// portrait, so currently this sets the same default portrait for every
68    /// char
69    #[deprecated = "Use the register method instead"]
70    Register {
71        /// The username of the new account
72        username: String,
73        /// The password of the new account
74        password: String,
75        /// The gender of the new character
76        gender: Gender,
77        /// The race of the new character
78        race: Race,
79        /// The class of the new character
80        class: Class,
81    },
82    /// Updates the current state of the entire gamestate. Also notifies the
83    /// guild, that the player is logged in. Should therefore be send
84    /// regularely
85    Update,
86    /// Queries 51 Hall of Fame entries starting from the top. Starts at 0
87    ///
88    /// **NOTE:** The server might return less then 51, if there is a "broken"
89    /// player encountered. This is NOT a library bug, this is a S&F bug and
90    /// will glitch out the UI, when trying to view the page in a browser.
91    // I assume this is because the player name contains some invalid
92    // character, because in the raw response string the last thing is a
93    // half written username "e(" in this case. I would guess that they
94    // were created before stricter input validation and never fixed. Might
95    // be insightful in the future to use the sequential id lookup in the
96    // playerlookat to see, if they can be viewed from there
97    HallOfFamePage {
98        /// The page of the Hall of Fame you want to query.
99        ///
100        /// 0 => rank(0..=50), 1 => rank(51..=101), ...
101        page: usize,
102    },
103    /// Queries 51 Hall of Fame entries for the fortress starting from the top.
104    /// Starts at 0
105    HallOfFameFortressPage {
106        /// The page of the Hall of Fame you want to query.
107        ///
108        /// 0 => rank(0..=50), 1 => rank(51..=101), ...
109        page: usize,
110    },
111    /// Looks at a specific player. Ident is either their name, or `player_id`.
112    /// The information about the player can then be found by using the
113    /// lookup_* methods on `HallOfFames`
114    ViewPlayer {
115        /// Either the name, or the `playerid.to_string()`
116        ident: String,
117    },
118    /// Buys a beer in the tavern
119    BuyBeer,
120    /// Starts one of the 3 tavern quests. **0,1,2**
121    StartQuest {
122        /// The position of the quest in the quest array
123        quest_pos: usize,
124        /// Has the player acknowledged, that their inventory is full and this
125        /// may lead to the loss of an item?
126        overwrite_inv: bool,
127    },
128    /// Cancels the currently running quest
129    CancelQuest,
130    /// Finishes the current quest, which starts the battle. This can be used
131    /// with a `QuestSkip` to skip the remaining time
132    FinishQuest {
133        /// If this is `Some()`, it will use the selected skip to skip the
134        /// remaining quest wait
135        skip: Option<TimeSkip>,
136    },
137    /// Goes working for the specified amount of hours (1-10)
138    StartWork {
139        /// The amount of hours you want to work
140        hours: u8,
141    },
142    /// Cancels the current guard job
143    CancelWork,
144    /// Collects the pay from the guard job
145    FinishWork,
146    /// Checks if the given name is still available to register
147    CheckNameAvailable {
148        /// The name to check
149        name: String,
150    },
151    /// Buys a mount, if the player has enough silver/mushrooms
152    BuyMount {
153        /// The mount you want to buy
154        mount: Mount,
155    },
156    /// Increases the given base attribute to the requested number. Should be
157    /// `current + 1`
158    IncreaseAttribute {
159        /// The attribute you want to increase
160        attribute: AttributeType,
161        /// The value you increase it to. This should be `current + 1`
162        increase_to: u32,
163    },
164    /// Removes the currently active potion 0,1,2
165    RemovePotion {
166        /// The position of the posion you want to remove
167        pos: usize,
168    },
169    /// Queries the currently available enemies in the arena
170    CheckArena,
171    /// Fights the selected enemy. This should be used for both arena fights
172    /// and normal fights. Note that this actually needs the name, not just the
173    /// id
174    Fight {
175        /// The name of the player you want to fight
176        name: String,
177        /// If the arena timer has not elapsed yet, this will spend a mushroom
178        /// and fight regardless. Currently the server ignores this and fights
179        /// always, but the client sends the correctly set command, so you
180        /// should too
181        use_mushroom: bool,
182    },
183    /// Collects the current reward from the calendar
184    CollectCalendar,
185    /// Queries information about another guild. The information can bet found
186    /// in `hall_of_fames.other_guilds`
187    ViewGuild {
188        /// Either the id, or name of the guild you want to look at
189        guild_ident: String,
190    },
191    /// Founds a new guild
192    GuildFound {
193        /// The name of the new guild you want to found
194        name: String,
195    },
196    /// Invites a player with the given name into the players guild
197    GuildInvitePlayer {
198        /// The name of the player you want to invite
199        name: String,
200    },
201    /// Kicks a player with the given name from the players guild
202    GuildKickPlayer {
203        /// The name of the guild member you want to kick
204        name: String,
205    },
206    /// Promote a player from the guild into the leader role
207    GuildSetLeader {
208        /// The name of the guild member you want to set as the guild leader
209        name: String,
210    },
211    /// Toggles a member between officer and normal member
212    GuildToggleOfficer {
213        /// The name of the player you want to toggle the officer status for
214        name: String,
215    },
216    /// Loads a mushroom into the catapult
217    GuildLoadMushrooms,
218    /// Increases one of the guild skills by 1. Needs to know the current, not
219    /// the new value for some reason
220    GuildIncreaseSkill {
221        /// The skill you want to increase
222        skill: GuildSkill,
223        /// The current value of the guild skill
224        current: u16,
225    },
226    /// Joins the current ongoing attack
227    GuildJoinAttack,
228    /// Joins the defense of the guild
229    GuildJoinDefense,
230    /// Starts an attack in another guild
231    GuildAttack {
232        /// The name of the guild you want to attack
233        guild: String,
234    },
235    /// Starts the next possible raid
236    GuildRaid,
237    /// Battles the enemy in the guildportal
238    GuildPortalBattle,
239    /// Flushes the toilet
240    ToiletFlush,
241    /// Opens the toilet door for the first time.
242    ToiletOpen,
243    /// Drops an item from one of the inventories into the toilet
244    ToiletDrop {
245        /// The inventory you want to take the item from
246        inventory: PlayerItemPlace,
247        /// The position of the item in the inventory. Starts at 0
248        pos: usize,
249    },
250    /// Buys an item from the shop and puts it in the inventoy slot specified
251    BuyShop {
252        /// The shop you want to buy from
253        shop_type: ShopType,
254        /// the position of the item you want to buy from the shop
255        shop_pos: usize,
256        /// The inventory you want to put the new item into
257        inventory: PlayerItemPlace,
258        /// The position in the chosen inventory you
259        inventory_pos: usize,
260    },
261    /// Sells an item from the players inventory. To make this more convenient,
262    /// this picks a shop&item position to sell to for you
263    SellShop {
264        /// The inventory you want to sell an item from
265        inventory: PlayerItemPlace,
266        /// The position of the item you want to sell
267        inventory_pos: usize,
268    },
269    /// Moves an item from one inventory position to another
270    InventoryMove {
271        /// The inventory you move the item from
272        inventory_from: PlayerItemPlace,
273        /// The position of the item you want to move
274        inventory_from_pos: usize,
275        /// The inventory you move the item to
276        inventory_to: PlayerItemPlace,
277        /// The inventory you move the item from
278        inventory_to_pos: usize,
279    },
280    /// Allows moving items from any position to any other position items can
281    /// be at. You should make sure, that the move makes sense (do not move
282    /// items from shop to shop)
283    ItemMove {
284        /// The place of thing you move the item from
285        from: ItemPlace,
286        /// The position of the item you want to move
287        from_pos: usize,
288        /// The place of thing you move the item to
289        to: ItemPlace,
290        /// The position of the item you want to move
291        to_pos: usize,
292    },
293    /// Allows using an potion from any position
294    UsePotion {
295        /// The place of the potion you use from
296        from: ItemPlace,
297        /// The position of the potion you want to use
298        from_pos: usize,
299    },
300    /// Opens the message at the specified index [0-100]
301    MessageOpen {
302        /// The index of the message in the inbox vec
303        pos: i32,
304    },
305    /// Deletes a single message, if you provide the index. -1 = all
306    MessageDelete {
307        /// The position of the message to delete in the inbox vec. If this is
308        /// -1, it deletes all
309        pos: i32,
310    },
311    /// Pulls up your scrapbook to reveal more info, than normal
312    ViewScrapbook,
313    /// Views a specific pet. This fetches its stats and places it into the
314    /// specified pet in the habitat
315    ViewPet {
316        /// The id of the pet, that you want to view
317        pet_id: u16,
318    },
319    /// Unlocks a feature. The these unlockables can be found in
320    /// `pending_unlocks` on `GameState`
321    UnlockFeature {
322        /// The thing to unlock
323        unlockable: Unlockable,
324    },
325    /// Starts a fight against the enemy in the players portal
326    FightPortal,
327    /// Enters a specific dungeon. This works for all dungeons, except the
328    /// Tower, which you must enter via the `FightTower` command
329    FightDungeon {
330        /// The dungeon you want to fight in (except the tower). If you only
331        /// have a `LightDungeon`, or `ShadowDungeon`, you need to call
332        /// `into()` to turn them into a generic dungeon
333        dungeon: Dungeon,
334        /// If this is true, you will spend a mushroom, if the timer has not
335        /// run out. Note, that this is currently ignored by the server for
336        /// some reason
337        use_mushroom: bool,
338    },
339    /// Attacks the requested level of the tower
340    FightTower {
341        /// The current level you are on the tower
342        current_level: u8,
343        /// If this is true, you will spend a mushroom, if the timer has not
344        /// run out. Note, that this is currently ignored by the server for
345        /// some reason
346        use_mush: bool,
347    },
348    /// Fights the player opponent with your pet
349    FightPetOpponent {
350        /// The habitat opponent you want to attack the opponent in
351        habitat: HabitatType,
352        /// The id of the player you want to fight
353        opponent_id: PlayerId,
354    },
355    /// Fights the pet in the specified habitat dungeon
356    FightPetDungeon {
357        /// If this is true, you will spend a mushroom, if the timer has not
358        /// run out. Note, that this is currently ignored by the server for
359        /// some reason
360        use_mush: bool,
361        /// The habitat, that you want to fight in
362        habitat: HabitatType,
363        /// This is `explored + 1` of the given habitat. Note that 20 explored
364        /// is the max, so providing 21 here will return an err
365        enemy_pos: u32,
366        /// This `pet_id` is the id of the pet you want to send into battle.
367        /// The pet has to be from the same habitat, as the dungeon you are
368        /// trying
369        player_pet_id: u32,
370    },
371    /// Sets the guild info. Note the info about length limit from
372    /// `SetDescription` for the description
373    GuildSetInfo {
374        /// The description you want to set
375        description: String,
376        /// The emblem you want to set
377        emblem: Emblem,
378    },
379    /// Gambles the desired amount of silver. Picking the right thing is not
380    /// actually required. That just masks the determined result. The result
381    /// will be in `gamble_result` on `Tavern`
382    GambleSilver {
383        /// The amount of silver to gamble
384        amount: u64,
385    },
386    /// Gambles the desired amount of mushrooms. Picking the right thing is not
387    /// actually required. That just masks the determined result. The result
388    /// will be in `gamble_result` on `Tavern`
389    GambleMushrooms {
390        /// The amount of mushrooms to gamble
391        amount: u64,
392    },
393    /// Sends a message to another player
394    SendMessage {
395        /// The name of the player to send a message to
396        to: String,
397        /// The message to send
398        msg: String,
399    },
400    /// The description may only be 240 chars long, when it reaches the
401    /// server. The problem is, that special chars like '/' have to get
402    /// escaped into two chars "$s" before getting send to the server.
403    /// That means this string can be 120-240 chars long depending on the
404    /// amount of escaped chars. We 'could' truncate the response, but
405    /// that could get weird with character boundaries in UTF8 and split the
406    /// escapes themself, so just make sure you provide a valid value here
407    /// to begin with and be prepared for a server error
408    SetDescription {
409        /// The description to set
410        description: String,
411    },
412    /// Drop the item from the specified position into the witches cauldron
413    WitchDropCauldron {
414        /// The inventory you want to move an item from
415        inventory_t: PlayerItemPlace,
416        /// The position of the item to move
417        position: usize,
418    },
419    /// Uses the blacksmith with the specified action on the specified item
420    Blacksmith {
421        /// The inventory the item you want to act upon is in
422        inventory_t: PlayerItemPlace,
423        /// The position of the item in the inventory
424        position: u8,
425        /// The action you want to use on the item
426        action: BlacksmithAction,
427    },
428    /// Sends the specified message in the guild chat
429    GuildSendChat {
430        /// The message to send
431        message: String,
432    },
433    /// Enchants the currently worn item, associated with this enchantment,
434    /// with the enchantment
435    WitchEnchant {
436        /// The enchantment to apply
437        enchantment: Enchantment,
438    },
439    /// Spins the wheel. All information about when you can spin, or what you
440    /// won are in `game_state.specials.wheel`
441    SpinWheelOfFortune {
442        /// The resource you want to spend to spin the wheel
443        payment: FortunePayment,
444    },
445    /// Collects the reward for event points
446    CollectEventTaskReward {
447        /// One of [0,1,2], depending on which reward has been unlocked
448        pos: usize,
449    },
450    /// Collects the reward for collecting points.
451    CollectDailyQuestReward {
452        /// One of [0,1,2], depending on which chest you want to collect
453        pos: usize,
454    },
455    /// Moves an item from a normal inventory, onto one of the companions
456    EquipCompanion {
457        /// The inventory of your character you take the item from
458        from_inventory: InventoryType,
459        /// The position in the inventory, that you
460        from_pos: u8,
461        /// The companion you want to equip
462        to_companion: CompanionClass,
463        /// The slot of the companion you want to equip
464        to_slot: EquipmentSlot,
465    },
466    /// Collects a specific resource from the fortress
467    FortressGather {
468        /// The type of resource you want to collect
469        resource: FortressResourceType,
470    },
471    /// Builds, or upgrades a building in the fortress
472    FortressBuild {
473        /// The building you want to upgrade, or build
474        f_type: FortressBuildingType,
475    },
476    /// Cancels the current build/upgrade, of the specified building in the
477    /// fortress
478    FortressBuildCancel {
479        /// The building you want to cancel the upgrade, or build of
480        f_type: FortressBuildingType,
481    },
482    FortressBuildFinish {
483        f_type: FortressBuildingType,
484        mushrooms: u32,
485    },
486    /// Builds new units of the selected type
487    FortressBuildUnit {
488        unit: FortressUnitType,
489        count: u32,
490    },
491    /// Starts the search for gems
492    FortressGemStoneSearch,
493    /// Cancels the search for gems
494    FortressGemStoneSearchCancel,
495    /// Finishes the gem stone search using the appropriate amount of
496    /// mushrooms. The price is one mushroom per 600 sec / 10 minutes of time
497    /// remaining
498    FortressGemStoneSearchFinish {
499        mushrooms: u32,
500    },
501    /// Attacks the current fortress attack target with the provided amount of
502    /// soldiers
503    FortressAttack {
504        soldiers: u32,
505    },
506    /// Rerolls the enemy in the fortress
507    FortressNewEnemy {
508        use_mushroom: bool,
509    },
510    /// Sets the fortress enemy to the counterattack target of the message
511    FortressSetCAEnemy {
512        msg_id: u32,
513    },
514    /// Upgrades the Hall of Knights to the next level
515    FortressUpgradeHallOfKnights,
516    /// Sends a wihsper message to another player
517    Whisper {
518        player_name: String,
519        message: String,
520    },
521    /// Collects the resources of the selected type in the underworld
522    UnderworldCollect {
523        resource: UnderWorldResourceType,
524    },
525    /// Upgrades the selected underworld unit by one level
526    UnderworldUnitUpgrade {
527        unit: UnderworldUnitType,
528    },
529    /// Starts the upgrade of a building in the underworld
530    UnderworldUpgradeStart {
531        building: UnderworldBuildingType,
532        mushrooms: u32,
533    },
534    /// Cancels the upgrade of a building in the underworld
535    UnderworldUpgradeCancel {
536        building: UnderworldUnitType,
537    },
538    /// Finishes an upgrade after the time has run out (or before using
539    /// mushrooms)
540    UnderworldUpgradeFinish {
541        building: UnderworldBuildingType,
542        mushrooms: u32,
543    },
544    /// Lures a player into the underworld
545    UnderworldAttack {
546        player_id: PlayerId,
547    },
548    /// Rolls the dice. The first round should be all rerolls, after that,
549    /// either reroll again, or take some of the dice on the table
550    RollDice {
551        payment: RollDicePrice,
552        dices: [DiceType; 5],
553    },
554    /// Feeds one of your pets
555    PetFeed {
556        pet_id: u32,
557        fruit_idx: u32,
558    },
559    /// Fights with the guild pet against the hydra
560    GuildPetBattle {
561        use_mushroom: bool,
562    },
563    /// Upgrades an idle building by the requested amount
564    IdleUpgrade {
565        typ: IdleBuildingType,
566        amount: u64,
567    },
568    /// Sacrifice all the money in the idle game for runes
569    IdleSacrifice,
570    /// Upgrades a skill to the requested attribute. Should probably be just
571    /// current + 1 to mimic a user clicking
572    UpgradeSkill {
573        attribute: AttributeType,
574        next_attribute: u32,
575    },
576    /// Spend 1 mushroom to update the inventory of a shop
577    RefreshShop {
578        shop: ShopType,
579    },
580    /// Fetches the Hall of Fame page for guilds
581    HallOfFameGroupPage {
582        page: u32,
583    },
584    /// Crawls the Hall of Fame page for the underworld
585    HallOfFameUnderworldPage {
586        page: u32,
587    },
588    HallOfFamePetsPage {
589        page: u32,
590    },
591    /// Switch equipment with the manequin, if it is unlocked
592    SwapManequin,
593    /// Updates your flag in the Hall of Fame
594    UpdateFlag {
595        flag: Option<Flag>,
596    },
597    /// Changes if you can receive invites or not
598    BlockGuildInvites {
599        block_invites: bool,
600    },
601    /// Changes if you want to gets tips in the gui. Does nothing for the API
602    ShowTips {
603        show_tips: bool,
604    },
605    /// Change your password. Note that I have not tested this and this might
606    /// invalidate your session
607    ChangePassword {
608        username: String,
609        old: String,
610        new: String,
611    },
612    /// Changes your mail to another address
613    ChangeMailAddress {
614        old_mail: String,
615        new_mail: String,
616        password: String,
617        username: String,
618    },
619    /// Sets the language of the character. This should be basically
620    /// irrelevant, but is still included for completeness sake. Expects a
621    /// valid county code. I have not tested all, but it should be one of:
622    /// `ru,fi,ar,tr,nl,ja,it,sk,fr,ko,pl,cs,el,da,en,hr,de,zh,sv,hu,pt,es,
623    /// pt-br, ro`
624    SetLanguage {
625        language: String,
626    },
627    /// Sets the relation to another player
628    SetPlayerRelation {
629        player_id: PlayerId,
630        relation: Relationship,
631    },
632    /// I have no character with anything but the default (0) to test this
633    /// with. If I had to guess, they continue sequentially
634    SetPortraitFrame {
635        portrait_id: i64,
636    },
637    /// Swaps the runes of two items
638    SwapRunes {
639        from: ItemPlace,
640        from_pos: usize,
641        to: ItemPlace,
642        to_pos: usize,
643    },
644    /// Changes the look of the item to the selected `raw_model_id` for 10
645    /// mushrooms. Note that this is NOT the normal model id. it is the
646    /// `model_id  + (class as usize) * 1000` if I remember correctly. Pretty
647    /// sure nobody  will ever uses this though, as it is only for looks.
648    ChangeItemLook {
649        inv: ItemPlace,
650        pos: usize,
651        raw_model_id: u16,
652    },
653    /// Continues the expedition by picking one of the <=3 encounters \[0,1,2\]
654    ExpeditionPickEncounter {
655        /// The position of the encounter you want to pick
656        pos: usize,
657    },
658    /// Continues the expedition, if you are currently in a situation, where
659    /// there is only one option. This can be starting a fighting, or starting
660    /// the wait after a fight (collecting the non item reward). Behind the
661    /// scenes this is just ExpeditionPickReward(0)
662    ExpeditionContinue,
663    /// If there are multiple items to choose from after fighting a boss, you
664    /// can choose which one to take here. \[0,1,2\]
665    ExpeditionPickReward {
666        /// The array position/index of the reward you want to take
667        pos: usize,
668    },
669    /// Starts one of the two expeditions \[0,1\]
670    ExpeditionStart {
671        /// The index of the expedition to start
672        pos: usize,
673    },
674    /// Skips the waiting period of the current expedition. Note that mushroom
675    /// may not always be possible
676    ExpeditionSkipWait {
677        /// The "currency" you want to skip the expedition
678        typ: TimeSkip,
679    },
680    /// This sets the "Questing instead of expeditions" value in the settings.
681    /// This will decide if you can go on expeditions, or do quests, when
682    /// expeditions are available. Going on the "wrong" one will return an
683    /// error. Similarly this setting can only be changed, when no Thirst for
684    /// Adventure has been used today, so make sure to check if that is full
685    /// and `beer_drunk == 0`
686    SetQuestsInsteadOfExpeditions {
687        /// The value you want to set
688        value: ExpeditionSetting,
689    },
690    HellevatorEnter,
691    HellevatorViewGuildRanking,
692    HellevatorFight {
693        use_mushroom: bool,
694    },
695    HellevatorBuy {
696        position: usize,
697        typ: HellevatorTreatType,
698        price: u32,
699        use_mushroom: bool,
700    },
701    HellevatorRefreshShop,
702    HellevatorJoinHellAttack {
703        use_mushroom: bool,
704        plain: usize,
705    },
706    HellevatorClaimDaily,
707    HellevatorClaimFinal,
708    HellevatorPreviewRewards,
709    HallOfFameHellevatorPage {
710        page: usize,
711    },
712    ClaimablePreview {
713        msg_id: i64,
714    },
715    ClaimableClaim {
716        msg_id: i64,
717    },
718    /// Spend 1000 mushrooms to buy a gold frame
719    BuyGoldFrame,
720}
721
722#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
723#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
724/// This is the "Questing instead of expeditions" value in the settings
725pub enum ExpeditionSetting {
726    /// When expeditions are available, this setting will enable expeditions to
727    /// be started. This will disable questing, until either this setting is
728    /// disabled, or expeditions have ended. Trying to start a quest with this
729    /// setting set will return an error
730    PreferExpeditions,
731    /// When expeditions are available, they will be ignored, until either this
732    /// setting is disabled, or expeditions have ended. Starting an
733    /// expedition with this setting set will error
734    #[default]
735    PreferQuests,
736}
737
738#[derive(Debug, Clone, Copy)]
739#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
740pub enum BlacksmithAction {
741    Dismantle = 201,
742    SocketUpgrade = 202,
743    SocketUpgradeWithMushrooms = 212,
744    GemExtract = 203,
745    GemExtractWithMushrooms = 213,
746    Upgrade = 204,
747}
748
749#[derive(Debug, Clone, Copy)]
750#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
751pub enum FortunePayment {
752    LuckyCoins = 0,
753    Mushrooms,
754    FreeTurn,
755}
756
757#[derive(Debug, Clone, Copy)]
758#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
759/// The price you have to pay to roll the dice
760pub enum RollDicePrice {
761    Free = 0,
762    Mushrooms,
763    Hourglass,
764}
765
766#[derive(Debug, Clone, Copy, FromPrimitive, PartialEq, Eq)]
767#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
768#[allow(missing_docs)]
769/// The type of dice you want to play with.
770pub enum DiceType {
771    /// This means you want to discard whatever dice was previously at this
772    /// position. This is also the type you want to fill the array with, if you
773    /// start a game
774    ReRoll,
775    Silver,
776    Stone,
777    Wood,
778    Souls,
779    Arcane,
780    Hourglass,
781}
782#[derive(Debug, Clone, Copy)]
783#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
784pub struct DiceReward {
785    /// The resource you have won
786    pub win_typ: DiceType,
787    /// The amounts of the resource you have won
788    pub amount: u32,
789}
790
791#[derive(
792    Debug, Copy, Clone, PartialEq, Eq, Enum, FromPrimitive, Hash, EnumIter,
793)]
794#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
795#[allow(missing_docs)]
796/// A type of attribute
797pub enum AttributeType {
798    Strength = 1,
799    Dexterity = 2,
800    Intelligence = 3,
801    Constitution = 4,
802    Luck = 5,
803}
804
805#[derive(Debug, Clone, Copy, PartialEq, Eq, Enum, EnumIter, Hash)]
806#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
807#[allow(missing_docs)]
808/// A type of shop. This is a subset of `ItemPlace`
809pub enum ShopType {
810    Weapon = 3,
811    Magic = 4,
812}
813
814#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
815#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
816#[allow(missing_docs)]
817/// The "currency" you want to use to skip a quest
818pub enum TimeSkip {
819    Mushroom = 1,
820    Glass = 2,
821}
822
823impl Command {
824    /// Returns the unencrypted string, that has to be send to the server to to
825    /// perform the request
826    #[allow(deprecated, clippy::useless_format)]
827    #[cfg(feature = "session")]
828    pub(crate) fn request_string(
829        &self,
830    ) -> Result<String, crate::error::SFError> {
831        const APP_VERSION: &str = "2100000000000";
832        use crate::{
833            error::SFError,
834            gamestate::dungeons::{LightDungeon, ShadowDungeon},
835            misc::{sha1_hash, to_sf_string, HASH_CONST},
836        };
837
838        Ok(match self {
839            Command::Custom {
840                cmd_name,
841                arguments: values,
842            } => {
843                format!("{cmd_name}:{}", values.join("/"))
844            }
845            Command::Login {
846                username,
847                pw_hash,
848                login_count,
849            } => {
850                let full_hash = sha1_hash(&format!("{pw_hash}{login_count}"));
851                format!(
852                    "AccountLogin:{username}/{full_hash}/{login_count}/\
853                     unity3d_webglplayer//{APP_VERSION}///0/"
854                )
855            }
856            #[cfg(feature = "sso")]
857            Command::SSOLogin {
858                uuid, character_id, ..
859            } => format!(
860                "SFAccountCharLogin:{uuid}/{character_id}/unity3d_webglplayer/\
861                 /{APP_VERSION}"
862            ),
863            Command::Register {
864                username,
865                password,
866                gender,
867                race,
868                class,
869            } => {
870                // TODO: Custom portrait
871                format!(
872                    "AccountCreate:{username}/{password}/{username}@playa.sso/\
873                     {}/{}/{}/8,203,201,6,199,3,1,2,1/0//en",
874                    *gender as usize + 1,
875                    *race as usize,
876                    *class as usize + 1
877                )
878            }
879            Command::Update => "Poll:".to_string(),
880            Command::HallOfFamePage { page } => {
881                let per_page = 51;
882                let pos = 26 + (per_page * page);
883                format!("PlayerGetHallOfFame:{pos}//25/25")
884            }
885            Command::HallOfFameFortressPage { page } => {
886                let per_page = 51;
887                let pos = 26 + (per_page * page);
888                format!("FortressGetHallOfFame:{pos}//25/25")
889            }
890            Command::HallOfFameGroupPage { page } => {
891                let per_page = 51;
892                let pos = 26 + (per_page * page);
893                format!("GroupGetHallOfFame:{pos}//25/25")
894            }
895            Command::HallOfFameUnderworldPage { page } => {
896                let per_page = 51;
897                let pos = 26 + (per_page * page);
898                format!("UnderworldGetHallOfFame:{pos}//25/25")
899            }
900            Command::HallOfFamePetsPage { page } => {
901                let per_page = 51;
902                let pos = 26 + (per_page * page);
903                format!("PetsGetHallOfFame:{pos}//25/25")
904            }
905            Command::ViewPlayer { ident } => format!("PlayerLookAt:{ident}"),
906            Command::BuyBeer => format!("PlayerBeerBuy:"),
907            Command::StartQuest {
908                quest_pos,
909                overwrite_inv,
910            } => {
911                format!(
912                    "PlayerAdventureStart:{}/{}",
913                    quest_pos + 1,
914                    u8::from(*overwrite_inv)
915                )
916            }
917            Command::CancelQuest => format!("PlayerAdventureStop:"),
918            Command::FinishQuest { skip } => {
919                format!(
920                    "PlayerAdventureFinished:{}",
921                    skip.map(|a| a as u8).unwrap_or(0)
922                )
923            }
924            Command::StartWork { hours } => format!("PlayerWorkStart:{hours}"),
925            Command::CancelWork => format!("PlayerWorkStop:"),
926            Command::FinishWork => format!("PlayerWorkFinished:"),
927            Command::CheckNameAvailable { name } => {
928                format!("AccountCheck:{name}")
929            }
930            Command::BuyMount { mount } => {
931                format!("PlayerMountBuy:{}", *mount as usize)
932            }
933            Command::IncreaseAttribute {
934                attribute,
935                increase_to,
936            } => format!(
937                "PlayerAttributIncrease:{}/{increase_to}",
938                *attribute as u8
939            ),
940            Command::RemovePotion { pos } => {
941                format!("PlayerPotionKill:{}", pos + 1)
942            }
943            Command::CheckArena => format!("PlayerArenaEnemy:"),
944            Command::Fight { name, use_mushroom } => {
945                format!("PlayerArenaFight:{name}/{}", u8::from(*use_mushroom))
946            }
947            Command::CollectCalendar => format!("PlayerOpenCalender:"),
948            Command::UpgradeSkill {
949                attribute,
950                next_attribute,
951            } => format!(
952                "PlayerAttributIncrease:{}/{next_attribute}",
953                *attribute as i64
954            ),
955            Command::RefreshShop { shop } => {
956                format!("PlayerNewWares:{}", *shop as usize - 2)
957            }
958            Command::ViewGuild { guild_ident } => {
959                format!("GroupLookAt:{guild_ident}")
960            }
961            Command::GuildFound { name } => format!("GroupFound:{name}"),
962            Command::GuildInvitePlayer { name } => {
963                format!("GroupInviteMember:{name}")
964            }
965            Command::GuildKickPlayer { name } => {
966                format!("GroupRemoveMember:{name}")
967            }
968            Command::GuildSetLeader { name } => {
969                format!("GroupSetLeader:{name}")
970            }
971            Command::GuildToggleOfficer { name } => {
972                format!("GroupSetOfficer:{name}")
973            }
974            Command::GuildLoadMushrooms => {
975                format!("GroupIncreaseBuilding:0")
976            }
977            Command::GuildIncreaseSkill { skill, current } => {
978                format!("GroupSkillIncrease:{}/{current}", *skill as usize)
979            }
980            Command::GuildJoinAttack => format!("GroupReadyAttack:"),
981            Command::GuildJoinDefense => format!("GroupReadyDefense:"),
982            Command::GuildAttack { guild } => {
983                format!("GroupAttackDeclare:{guild}")
984            }
985            Command::GuildRaid => format!("GroupRaidDeclare:"),
986            Command::ToiletFlush => format!("PlayerToilettFlush:"),
987            Command::ToiletOpen => format!("PlayerToilettOpenWithKey:"),
988            Command::FightTower {
989                current_level: progress,
990                use_mush,
991            } => {
992                format!("PlayerTowerBattle:{progress}/{}", u8::from(*use_mush))
993            }
994            Command::ToiletDrop { inventory, pos } => {
995                format!("PlayerToilettLoad:{}/{}", *inventory as usize, pos + 1)
996            }
997            Command::GuildPortalBattle => format!("GroupPortalBattle:"),
998            Command::FightPortal => format!("PlayerPortalBattle:"),
999            Command::MessageOpen { pos: index } => {
1000                format!("PlayerMessageView:{}", *index + 1)
1001            }
1002            Command::MessageDelete { pos: index } => format!(
1003                "PlayerMessageDelete:{}",
1004                match index {
1005                    -1 => -1,
1006                    x => *x + 1,
1007                }
1008            ),
1009            Command::ViewScrapbook => format!("PlayerPollScrapbook:"),
1010            Command::ViewPet { pet_id: pet_index } => {
1011                format!("PetsGetStats:{pet_index}")
1012            }
1013            Command::BuyShop {
1014                shop_type,
1015                shop_pos,
1016                inventory,
1017                inventory_pos,
1018            } => format!(
1019                "PlayerItemMove:{}/{}/{}/{}",
1020                *shop_type as usize,
1021                *shop_pos + 1,
1022                *inventory as usize,
1023                *inventory_pos + 1
1024            ),
1025            Command::SellShop {
1026                inventory,
1027                inventory_pos,
1028            } => {
1029                let mut rng = fastrand::Rng::new();
1030                let shop = if rng.bool() {
1031                    ShopType::Magic
1032                } else {
1033                    ShopType::Weapon
1034                };
1035                let shop_pos = rng.u32(0..6);
1036                format!(
1037                    "PlayerItemMove:{}/{}/{}/{}",
1038                    *inventory as usize,
1039                    *inventory_pos + 1,
1040                    shop as usize,
1041                    shop_pos + 1,
1042                )
1043            }
1044            Command::InventoryMove {
1045                inventory_from,
1046                inventory_from_pos,
1047                inventory_to,
1048                inventory_to_pos,
1049            } => format!(
1050                "PlayerItemMove:{}/{}/{}/{}",
1051                *inventory_from as usize,
1052                *inventory_from_pos + 1,
1053                *inventory_to as usize,
1054                *inventory_to_pos + 1
1055            ),
1056            Command::ItemMove {
1057                from,
1058                from_pos,
1059                to,
1060                to_pos,
1061            } => format!(
1062                "PlayerItemMove:{}/{}/{}/{}",
1063                *from as usize,
1064                *from_pos + 1,
1065                *to as usize,
1066                *to_pos + 1
1067            ),
1068            Command::UsePotion { from, from_pos } => {
1069                format!(
1070                    "PlayerItemMove:{}/{}/1/0/",
1071                    *from as usize,
1072                    *from_pos + 1
1073                )
1074            }
1075            Command::UnlockFeature { unlockable } => format!(
1076                "UnlockFeature:{}/{}",
1077                unlockable.main_ident, unlockable.sub_ident
1078            ),
1079            Command::GuildSetInfo {
1080                description,
1081                emblem,
1082            } => format!(
1083                "GroupSetDescription:{}ยง{}",
1084                emblem.server_encode(),
1085                to_sf_string(description)
1086            ),
1087            Command::SetDescription { description } => {
1088                format!("PlayerSetDescription:{}", &to_sf_string(description))
1089            }
1090            Command::GuildSendChat { message } => {
1091                format!("GroupChat:{}", &to_sf_string(message))
1092            }
1093            Command::GambleSilver { amount } => {
1094                format!("PlayerGambleGold:{amount}")
1095            }
1096            Command::GambleMushrooms { amount } => {
1097                format!("PlayerGambleCoins:{amount}")
1098            }
1099            Command::SendMessage { to, msg } => {
1100                format!("PlayerMessageSend:{to}/{}", to_sf_string(msg))
1101            }
1102            Command::WitchDropCauldron {
1103                inventory_t,
1104                position,
1105            } => format!(
1106                "PlayerWitchSpendItem:{}/{}",
1107                *inventory_t as usize,
1108                position + 1
1109            ),
1110            Command::Blacksmith {
1111                inventory_t,
1112                position,
1113                action,
1114            } => format!(
1115                "PlayerItemMove:{}/{}/{}/-1",
1116                *inventory_t as usize,
1117                position + 1,
1118                *action as usize
1119            ),
1120            Command::WitchEnchant { enchantment } => {
1121                format!("PlayerWitchEnchantItem:{}/1", enchantment.enchant_id())
1122            }
1123            Command::SpinWheelOfFortune {
1124                payment: fortune_payment,
1125            } => {
1126                format!("WheelOfFortune:{}", *fortune_payment as usize)
1127            }
1128            Command::FortressGather { resource } => {
1129                format!("FortressGather:{}", *resource as usize + 1)
1130            }
1131            Command::EquipCompanion {
1132                from_inventory,
1133                from_pos,
1134                to_slot,
1135                to_companion,
1136            } => format!(
1137                "PlayerItemMove:{}/{}/{}/{}",
1138                *from_inventory as usize,
1139                *from_pos,
1140                *to_companion as u8 + 101,
1141                *to_slot as usize
1142            ),
1143            Command::FortressBuild { f_type } => {
1144                format!("FortressBuildStart:{}/0", *f_type as usize + 1)
1145            }
1146            Command::FortressBuildCancel { f_type } => {
1147                format!("FortressBuildStop:{}", *f_type as usize + 1)
1148            }
1149            Command::FortressBuildFinish { f_type, mushrooms } => format!(
1150                "FortressBuildFinished:{}/{mushrooms}",
1151                *f_type as usize + 1
1152            ),
1153            Command::FortressBuildUnit { unit, count } => {
1154                format!("FortressBuildUnitStart:{}/{count}", *unit as usize + 1)
1155            }
1156            Command::FortressGemStoneSearch => {
1157                format!("FortressGemstoneStart:",)
1158            }
1159            Command::FortressGemStoneSearchCancel => {
1160                format!("FortressGemStoneStop:0")
1161            }
1162            Command::FortressGemStoneSearchFinish { mushrooms } => {
1163                format!("FortressGemstoneFinished:{mushrooms}",)
1164            }
1165            Command::FortressAttack { soldiers } => {
1166                format!("FortressAttack:{soldiers}")
1167            }
1168            Command::FortressNewEnemy { use_mushroom: pay } => {
1169                format!("FortressEnemy:{}", usize::from(*pay))
1170            }
1171            Command::FortressSetCAEnemy { msg_id } => {
1172                format!("FortressEnemy:0/{}", *msg_id)
1173            }
1174            Command::FortressUpgradeHallOfKnights => {
1175                format!("FortressGroupBonusUpgrade:")
1176            }
1177            Command::Whisper {
1178                player_name: player,
1179                message,
1180            } => format!(
1181                "PlayerMessageWhisper:{}/{}",
1182                player,
1183                to_sf_string(message)
1184            ),
1185            Command::UnderworldCollect {
1186                resource: resource_t,
1187            } => {
1188                format!("UnderworldGather:{}", *resource_t as usize + 1)
1189            }
1190            Command::UnderworldUnitUpgrade { unit: unit_t } => {
1191                format!("UnderworldUpgradeUnit:{}", *unit_t as usize + 1)
1192            }
1193            Command::UnderworldUpgradeStart {
1194                building,
1195                mushrooms,
1196            } => format!(
1197                "UnderworldBuildStart:{}/{mushrooms}",
1198                *building as usize + 1
1199            ),
1200            Command::UnderworldUpgradeCancel { building } => {
1201                format!("UnderworldBuildStop:{}", *building as usize + 1)
1202            }
1203            Command::UnderworldUpgradeFinish {
1204                building,
1205                mushrooms,
1206            } => format!(
1207                "UnderworldBuildFinished:{}/{mushrooms}",
1208                *building as usize + 1
1209            ),
1210            Command::UnderworldAttack { player_id } => {
1211                format!("UnderworldAttack:{player_id}")
1212            }
1213            Command::RollDice { payment, dices } => {
1214                let mut dices = dices.iter().fold(String::new(), |mut a, b| {
1215                    if !a.is_empty() {
1216                        a.push('/');
1217                    }
1218                    a.push((*b as u8 + b'0') as char);
1219                    a
1220                });
1221
1222                if dices.is_empty() {
1223                    // FIXME: This is dead code, right?
1224                    dices = "0/0/0/0/0".to_string();
1225                }
1226                format!("RollDice:{}/{}", *payment as usize, dices)
1227            }
1228            Command::PetFeed { pet_id, fruit_idx } => {
1229                format!("PlayerPetFeed:{pet_id}/{fruit_idx}")
1230            }
1231            Command::GuildPetBattle { use_mushroom } => {
1232                format!("GroupPetBattle:{}", usize::from(*use_mushroom))
1233            }
1234            Command::IdleUpgrade { typ: kind, amount } => {
1235                format!("IdleIncrease:{}/{}", *kind as usize, amount)
1236            }
1237            Command::IdleSacrifice => format!("IdlePrestige:0"),
1238            Command::SwapManequin => format!("PlayerDummySwap:301/1"),
1239            Command::UpdateFlag { flag } => format!(
1240                "PlayerSetFlag:{}",
1241                flag.map(Flag::code).unwrap_or_default()
1242            ),
1243            Command::BlockGuildInvites { block_invites } => {
1244                format!("PlayerSetNoGroupInvite:{}", u8::from(*block_invites))
1245            }
1246            Command::ShowTips { show_tips } => {
1247                #[allow(clippy::unreadable_literal)]
1248                {
1249                    format!(
1250                        "PlayerTutorialStatus:{}",
1251                        if *show_tips { 0 } else { 0xFFFFFFF }
1252                    )
1253                }
1254            }
1255            Command::ChangePassword { username, old, new } => {
1256                let old = sha1_hash(&format!("{old}{HASH_CONST}"));
1257                let new = sha1_hash(&format!("{new}{HASH_CONST}"));
1258                format!("AccountPasswordChange:{username}/{old}/106/{new}/")
1259            }
1260            Command::ChangeMailAddress {
1261                old_mail,
1262                new_mail,
1263                password,
1264                username,
1265            } => {
1266                let pass = sha1_hash(&format!("{password}{HASH_CONST}"));
1267                format!(
1268                    "AccountMailChange:{old_mail}/{new_mail}/{username}/\
1269                     {pass}/106"
1270                )
1271            }
1272            Command::SetLanguage { language } => {
1273                format!("AccountSetLanguage:{language}")
1274            }
1275            Command::SetPlayerRelation {
1276                player_id,
1277                relation,
1278            } => format!("PlayerFriendSet:{player_id}/{}", *relation as i32),
1279            Command::SetPortraitFrame { portrait_id } => {
1280                format!("PlayerSetActiveFrame:{portrait_id}")
1281            }
1282            Command::CollectDailyQuestReward { pos } => {
1283                format!("DailyTaskClaim:1/{}", pos + 1)
1284            }
1285            Command::CollectEventTaskReward { pos } => {
1286                format!("DailyTaskClaim:2/{}", pos + 1)
1287            }
1288            Command::SwapRunes {
1289                from,
1290                from_pos,
1291                to,
1292                to_pos,
1293            } => {
1294                format!(
1295                    "PlayerSmithSwapRunes:{}/{}/{}/{}",
1296                    *from as usize,
1297                    *from_pos + 1,
1298                    *to as usize,
1299                    *to_pos + 1
1300                )
1301            }
1302            Command::ChangeItemLook {
1303                inv,
1304                pos,
1305                raw_model_id: model_id,
1306            } => {
1307                format!(
1308                    "ItemChangePicture:{}/{}/{}",
1309                    *inv as usize,
1310                    pos + 1,
1311                    model_id
1312                )
1313            }
1314            Command::ExpeditionPickEncounter { pos } => {
1315                format!("ExpeditionProceed:{}", pos + 1)
1316            }
1317            Command::ExpeditionContinue => format!("ExpeditionProceed:1"),
1318            Command::ExpeditionPickReward { pos } => {
1319                format!("ExpeditionProceed:{}", pos + 1)
1320            }
1321            Command::ExpeditionStart { pos } => {
1322                format!("ExpeditionStart:{}", pos + 1)
1323            }
1324            Command::FightDungeon {
1325                dungeon,
1326                use_mushroom,
1327            } => match dungeon {
1328                Dungeon::Light(name) => {
1329                    if *name == LightDungeon::Tower {
1330                        return Err(SFError::InvalidRequest(
1331                            "The tower must be fought with the FightTower \
1332                             command",
1333                        ));
1334                    }
1335                    format!(
1336                        "PlayerDungeonBattle:{}/{}",
1337                        *name as usize + 1,
1338                        u8::from(*use_mushroom)
1339                    )
1340                }
1341                Dungeon::Shadow(name) => {
1342                    if *name == ShadowDungeon::Twister {
1343                        format!(
1344                            "PlayerDungeonBattle:{}/{}",
1345                            LightDungeon::Tower as u32 + 1,
1346                            u8::from(*use_mushroom)
1347                        )
1348                    } else {
1349                        format!(
1350                            "PlayerShadowBattle:{}/{}",
1351                            *name as u32 + 1,
1352                            u8::from(*use_mushroom)
1353                        )
1354                    }
1355                }
1356            },
1357            Command::FightPetOpponent {
1358                opponent_id,
1359                habitat: element,
1360            } => {
1361                format!("PetsPvPFight:0/{opponent_id}/{}", *element as u32 + 1)
1362            }
1363            Command::FightPetDungeon {
1364                use_mush,
1365                habitat: element,
1366                enemy_pos,
1367                player_pet_id,
1368            } => {
1369                format!(
1370                    "PetsDungeonFight:{}/{}/{enemy_pos}/{player_pet_id}",
1371                    u8::from(*use_mush),
1372                    *element as u8 + 1,
1373                )
1374            }
1375            Command::ExpeditionSkipWait { typ } => {
1376                format!("ExpeditionTimeSkip:{}", *typ as u8)
1377            }
1378            Command::SetQuestsInsteadOfExpeditions { value } => {
1379                let value = match value {
1380                    ExpeditionSetting::PreferExpeditions => 'a',
1381                    ExpeditionSetting::PreferQuests => 'b',
1382                };
1383                format!("UserSettingsUpdate:5/{value}")
1384            }
1385            Command::HellevatorEnter => format!("GroupTournamentJoin:"),
1386            Command::HellevatorViewGuildRanking => {
1387                format!("GroupTournamentRankingOwnGroup")
1388            }
1389            Command::HellevatorFight { use_mushroom } => {
1390                format!("GroupTournamentBattle:{}", u8::from(*use_mushroom))
1391            }
1392            Command::HellevatorBuy {
1393                position,
1394                typ,
1395                price,
1396                use_mushroom,
1397            } => format!(
1398                "GroupTournamentMerchantBuy:{position}/{}/{price}/{}",
1399                *typ as u32,
1400                if *use_mushroom { 2 } else { 1 }
1401            ),
1402            Command::HellevatorRefreshShop => {
1403                format!("GroupTournamentMerchantReroll:")
1404            }
1405            Command::HallOfFameHellevatorPage { page } => {
1406                let per_page = 51;
1407                let pos = 26 + (per_page * page);
1408                format!("GroupTournamentRankingAllGroups:{pos}//25/25")
1409            }
1410            Command::HellevatorJoinHellAttack {
1411                use_mushroom,
1412                plain: pos,
1413            } => format!(
1414                "GroupTournamentRaidParticipant:{}/{}",
1415                u8::from(*use_mushroom),
1416                *pos + 1
1417            ),
1418            Command::HellevatorClaimDaily => {
1419                format!("GroupTournamentClaimDaily:")
1420            }
1421            Command::HellevatorPreviewRewards => {
1422                format!("GroupTournamentPreview:")
1423            }
1424            Command::HellevatorClaimFinal => format!("GroupTournamentClaim:"),
1425            Command::ClaimablePreview { msg_id } => {
1426                format!("PendingRewardView:{msg_id}")
1427            }
1428            Command::ClaimableClaim { msg_id } => {
1429                format!("PendingRewardClaim:{msg_id}")
1430            }
1431            Command::BuyGoldFrame => {
1432                format!("PlayerGoldFrameBuy:")
1433            }
1434        })
1435    }
1436}
1437
1438macro_rules! generate_flag_enum {
1439    ($($variant:ident => $code:expr),*) => {
1440        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
1441        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1442        #[allow(missing_docs)]
1443        /// The flag of a country, that will be visible in the Hall of Fame
1444        pub enum Flag {
1445            $(
1446                $variant,
1447            )*
1448        }
1449
1450        impl Flag {
1451            pub(crate) fn code(self) -> &'static str {
1452                match self {
1453                    $(
1454                        Flag::$variant => $code,
1455                    )*
1456                }
1457            }
1458
1459            pub(crate) fn parse(value: &str) -> Option<Self> {
1460                if value.is_empty() {
1461                    return None;
1462                }
1463
1464                // Mapping from string codes to enum variants
1465                match value {
1466                    $(
1467                        $code => Some(Flag::$variant),
1468                    )*
1469
1470                    _ => {
1471                        warn!("Invalid flag value: {value}");
1472                        None
1473                    }
1474                }
1475            }
1476        }
1477    };
1478}
1479
1480// Use the macro to generate the Flag enum and its methods
1481// Source: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements
1482generate_flag_enum! {
1483    Argentina => "ar",
1484    Australia => "au",
1485    Austria => "at",
1486    Belgium => "be",
1487    Bolivia => "bo",
1488    Brazil => "br",
1489    Bulgaria => "bg",
1490    Canada => "ca",
1491    Chile => "cl",
1492    China => "cn",
1493    Colombia => "co",
1494    CostaRica => "cr",
1495    Czechia => "cz",
1496    Denmark => "dk",
1497    DominicanRepublic => "do",
1498    Ecuador => "ec",
1499    ElSalvador =>"sv",
1500    Finland => "fi",
1501    France => "fr",
1502    Germany => "de",
1503    GreatBritain => "gb",
1504    Greece => "gr",
1505    Honduras => "hn",
1506    Hungary => "hu",
1507    India => "in",
1508    Italy => "it",
1509    Japan => "jp",
1510    Lithuania => "lt",
1511    Mexico => "mx",
1512    Netherlands => "nl",
1513    Panama => "pa",
1514    Paraguay => "py",
1515    Peru => "pe",
1516    Philippines => "ph",
1517    Poland => "pl",
1518    Portugal => "pt",
1519    Romania => "ro",
1520    Russia => "ru",
1521    SaudiArabia => "sa",
1522    Slovakia => "sk",
1523    SouthKorea => "kr",
1524    Spain => "es",
1525    Sweden => "se",
1526    Switzerland => "ch",
1527    Thailand => "th",
1528    Turkey => "tr",
1529    Ukraine => "ua",
1530    UnitedArabEmirates => "ae",
1531    UnitedStates => "us",
1532    Uruguay => "uy",
1533    Venezuela => "ve",
1534    Vietnam => "vn"
1535}