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