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    PlayerId,
9    gamestate::{
10        character::*,
11        dungeons::{CompanionClass, Dungeon},
12        fortress::*,
13        guild::{Emblem, GuildSkill},
14        idle::IdleBuildingType,
15        items::*,
16        social::Relationship,
17        underworld::*,
18        unlockables::{
19            EnchantmentIdent, HabitatType, HellevatorTreatType, Unlockable,
20        },
21    },
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, Default)]
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    #[default]
830    Weapon = 3,
831    Magic = 4,
832}
833
834#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
835#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
836#[allow(missing_docs)]
837/// The "currency" you want to use to skip a quest
838pub enum TimeSkip {
839    Mushroom = 1,
840    Glass = 2,
841}
842
843impl Command {
844    /// Returns the unencrypted string, that has to be send to the server to to
845    /// perform the request
846    #[allow(deprecated, clippy::useless_format)]
847    #[cfg(feature = "session")]
848    pub(crate) fn request_string(
849        &self,
850    ) -> Result<String, crate::error::SFError> {
851        const APP_VERSION: &str = "2700000000000";
852        use crate::{
853            error::SFError,
854            gamestate::dungeons::{LightDungeon, ShadowDungeon},
855            misc::{HASH_CONST, sha1_hash, to_sf_string},
856        };
857
858        Ok(match self {
859            Command::Custom {
860                cmd_name,
861                arguments: values,
862            } => {
863                format!("{cmd_name}:{}", values.join("/"))
864            }
865            Command::Login {
866                username,
867                pw_hash,
868                login_count,
869            } => {
870                let full_hash = sha1_hash(&format!("{pw_hash}{login_count}"));
871                format!(
872                    "AccountLogin:{username}/{full_hash}/{login_count}/\
873                     unity3d_webglplayer//{APP_VERSION}///0/"
874                )
875            }
876            #[cfg(feature = "sso")]
877            Command::SSOLogin {
878                uuid, character_id, ..
879            } => format!(
880                "SFAccountCharLogin:{uuid}/{character_id}/unity3d_webglplayer/\
881                 /{APP_VERSION}"
882            ),
883            Command::Register {
884                username,
885                password,
886                gender,
887                race,
888                class,
889            } => {
890                // TODO: Custom portrait
891                format!(
892                    "AccountCreate:{username}/{password}/{username}@playa.sso/\
893                     {}/{}/{}/8,203,201,6,199,3,1,2,1/0//en",
894                    *gender as usize + 1,
895                    *race as usize,
896                    *class as usize + 1
897                )
898            }
899            Command::Update => "Poll:".to_string(),
900            Command::HallOfFamePage { page } => {
901                let per_page = 51;
902                let pos = 26 + (per_page * page);
903                format!("PlayerGetHallOfFame:{pos}//25/25")
904            }
905            Command::HallOfFameFortressPage { page } => {
906                let per_page = 51;
907                let pos = 26 + (per_page * page);
908                format!("FortressGetHallOfFame:{pos}//25/25")
909            }
910            Command::HallOfFameGroupPage { page } => {
911                let per_page = 51;
912                let pos = 26 + (per_page * page);
913                format!("GroupGetHallOfFame:{pos}//25/25")
914            }
915            Command::HallOfFameUnderworldPage { page } => {
916                let per_page = 51;
917                let pos = 26 + (per_page * page);
918                format!("UnderworldGetHallOfFame:{pos}//25/25")
919            }
920            Command::HallOfFamePetsPage { page } => {
921                let per_page = 51;
922                let pos = 26 + (per_page * page);
923                format!("PetsGetHallOfFame:{pos}//25/25")
924            }
925            Command::ViewPlayer { ident } => format!("PlayerLookAt:{ident}"),
926            Command::BuyBeer => format!("PlayerBeerBuy:"),
927            Command::StartQuest {
928                quest_pos,
929                overwrite_inv,
930            } => {
931                format!(
932                    "PlayerAdventureStart:{}/{}",
933                    quest_pos + 1,
934                    u8::from(*overwrite_inv)
935                )
936            }
937            Command::CancelQuest => format!("PlayerAdventureStop:"),
938            Command::FinishQuest { skip } => {
939                format!(
940                    "PlayerAdventureFinished:{}",
941                    skip.map(|a| a as u8).unwrap_or(0)
942                )
943            }
944            Command::StartWork { hours } => format!("PlayerWorkStart:{hours}"),
945            Command::CancelWork => format!("PlayerWorkStop:"),
946            Command::FinishWork => format!("PlayerWorkFinished:"),
947            Command::CheckNameAvailable { name } => {
948                format!("AccountCheck:{name}")
949            }
950            Command::BuyMount { mount } => {
951                format!("PlayerMountBuy:{}", *mount as usize)
952            }
953            Command::IncreaseAttribute {
954                attribute,
955                increase_to,
956            } => format!(
957                "PlayerAttributIncrease:{}/{increase_to}",
958                *attribute as u8
959            ),
960            Command::RemovePotion { pos } => {
961                format!("PlayerPotionKill:{}", pos + 1)
962            }
963            Command::CheckArena => format!("PlayerArenaEnemy:"),
964            Command::Fight { name, use_mushroom } => {
965                format!("PlayerArenaFight:{name}/{}", u8::from(*use_mushroom))
966            }
967            Command::CollectCalendar => format!("PlayerOpenCalender:"),
968            Command::UpgradeSkill {
969                attribute,
970                next_attribute,
971            } => format!(
972                "PlayerAttributIncrease:{}/{next_attribute}",
973                *attribute as i64
974            ),
975            Command::RefreshShop { shop } => {
976                format!("PlayerNewWares:{}", *shop as usize - 2)
977            }
978            Command::ViewGuild { guild_ident } => {
979                format!("GroupLookAt:{guild_ident}")
980            }
981            Command::GuildFound { name } => format!("GroupFound:{name}"),
982            Command::GuildInvitePlayer { name } => {
983                format!("GroupInviteMember:{name}")
984            }
985            Command::GuildKickPlayer { name } => {
986                format!("GroupRemoveMember:{name}")
987            }
988            Command::GuildSetLeader { name } => {
989                format!("GroupSetLeader:{name}")
990            }
991            Command::GuildToggleOfficer { name } => {
992                format!("GroupSetOfficer:{name}")
993            }
994            Command::GuildLoadMushrooms => {
995                format!("GroupIncreaseBuilding:0")
996            }
997            Command::GuildIncreaseSkill { skill, current } => {
998                format!("GroupSkillIncrease:{}/{current}", *skill as usize)
999            }
1000            Command::GuildJoinAttack => format!("GroupReadyAttack:"),
1001            Command::GuildJoinDefense => format!("GroupReadyDefense:"),
1002            Command::GuildAttack { guild } => {
1003                format!("GroupAttackDeclare:{guild}")
1004            }
1005            Command::GuildRaid => format!("GroupRaidDeclare:"),
1006            Command::ToiletFlush => format!("PlayerToilettFlush:"),
1007            Command::ToiletOpen => format!("PlayerToilettOpenWithKey:"),
1008            Command::FightTower {
1009                current_level: progress,
1010                use_mush,
1011            } => {
1012                format!("PlayerTowerBattle:{progress}/{}", u8::from(*use_mush))
1013            }
1014            Command::ToiletDrop { inventory, pos } => {
1015                format!("PlayerToilettLoad:{}/{}", *inventory as usize, pos + 1)
1016            }
1017            Command::GuildPortalBattle => format!("GroupPortalBattle:"),
1018            Command::GuildGetFightableTargets => {
1019                format!("GroupFightableTargets:")
1020            }
1021            Command::FightPortal => format!("PlayerPortalBattle:"),
1022            Command::MessageOpen { pos: index } => {
1023                format!("PlayerMessageView:{}", *index + 1)
1024            }
1025            Command::MessageDelete { pos: index } => format!(
1026                "PlayerMessageDelete:{}",
1027                match index {
1028                    -1 => -1,
1029                    x => *x + 1,
1030                }
1031            ),
1032            Command::ViewScrapbook => format!("PlayerPollScrapbook:"),
1033            Command::ViewPet { pet_id: pet_index } => {
1034                format!("PetsGetStats:{pet_index}")
1035            }
1036            Command::BuyShop {
1037                shop_type,
1038                shop_pos,
1039                inventory,
1040                inventory_pos,
1041            } => format!(
1042                "PlayerItemMove:{}/{}/{}/{}",
1043                *shop_type as usize,
1044                *shop_pos + 1,
1045                *inventory as usize,
1046                *inventory_pos + 1
1047            ),
1048            Command::SellShop {
1049                inventory,
1050                inventory_pos,
1051            } => {
1052                let mut rng = fastrand::Rng::new();
1053                let shop = if rng.bool() {
1054                    ShopType::Magic
1055                } else {
1056                    ShopType::Weapon
1057                };
1058                let shop_pos = rng.u32(0..6);
1059                format!(
1060                    "PlayerItemMove:{}/{}/{}/{}",
1061                    *inventory as usize,
1062                    *inventory_pos + 1,
1063                    shop as usize,
1064                    shop_pos + 1,
1065                )
1066            }
1067            Command::InventoryMove {
1068                inventory_from,
1069                inventory_from_pos,
1070                inventory_to,
1071                inventory_to_pos,
1072            } => format!(
1073                "PlayerItemMove:{}/{}/{}/{}",
1074                *inventory_from as usize,
1075                *inventory_from_pos + 1,
1076                *inventory_to as usize,
1077                *inventory_to_pos + 1
1078            ),
1079            Command::ItemMove {
1080                from,
1081                from_pos,
1082                to,
1083                to_pos,
1084            } => format!(
1085                "PlayerItemMove:{}/{}/{}/{}",
1086                *from as usize,
1087                *from_pos + 1,
1088                *to as usize,
1089                *to_pos + 1
1090            ),
1091            Command::UsePotion { from, from_pos } => {
1092                format!(
1093                    "PlayerItemMove:{}/{}/1/0/",
1094                    *from as usize,
1095                    *from_pos + 1
1096                )
1097            }
1098            Command::UnlockFeature { unlockable } => format!(
1099                "UnlockFeature:{}/{}",
1100                unlockable.main_ident, unlockable.sub_ident
1101            ),
1102            Command::GuildSetInfo {
1103                description,
1104                emblem,
1105            } => format!(
1106                "GroupSetDescription:{}ยง{}",
1107                emblem.server_encode(),
1108                to_sf_string(description)
1109            ),
1110            Command::SetDescription { description } => {
1111                format!("PlayerSetDescription:{}", &to_sf_string(description))
1112            }
1113            Command::GuildSendChat { message } => {
1114                format!("GroupChat:{}", &to_sf_string(message))
1115            }
1116            Command::GambleSilver { amount } => {
1117                format!("PlayerGambleGold:{amount}")
1118            }
1119            Command::GambleMushrooms { amount } => {
1120                format!("PlayerGambleCoins:{amount}")
1121            }
1122            Command::SendMessage { to, msg } => {
1123                format!("PlayerMessageSend:{to}/{}", to_sf_string(msg))
1124            }
1125            Command::WitchDropCauldron {
1126                inventory_t,
1127                position,
1128            } => format!(
1129                "PlayerWitchSpendItem:{}/{}",
1130                *inventory_t as usize,
1131                position + 1
1132            ),
1133            Command::Blacksmith {
1134                inventory_t,
1135                position,
1136                action,
1137            } => format!(
1138                "PlayerItemMove:{}/{}/{}/-1",
1139                *inventory_t as usize,
1140                position + 1,
1141                *action as usize
1142            ),
1143            Command::WitchEnchant { enchantment } => {
1144                format!("PlayerWitchEnchantItem:{}/1", enchantment.0)
1145            }
1146            Command::SpinWheelOfFortune {
1147                payment: fortune_payment,
1148            } => {
1149                format!("WheelOfFortune:{}", *fortune_payment as usize)
1150            }
1151            Command::FortressGather { resource } => {
1152                format!("FortressGather:{}", *resource as usize + 1)
1153            }
1154            Command::FortressGatherSecretStorage { stone, wood } => {
1155                format!("FortressGatherTreasure:{wood}/{stone}")
1156            }
1157            Command::EquipCompanion {
1158                from_inventory,
1159                from_pos,
1160                to_slot,
1161                to_companion,
1162            } => format!(
1163                "PlayerItemMove:{}/{}/{}/{}",
1164                *from_inventory as usize,
1165                *from_pos,
1166                *to_companion as u8 + 101,
1167                *to_slot as usize
1168            ),
1169            Command::FortressBuild { f_type } => {
1170                format!("FortressBuildStart:{}/0", *f_type as usize + 1)
1171            }
1172            Command::FortressBuildCancel { f_type } => {
1173                format!("FortressBuildStop:{}", *f_type as usize + 1)
1174            }
1175            Command::FortressBuildFinish { f_type, mushrooms } => format!(
1176                "FortressBuildFinished:{}/{mushrooms}",
1177                *f_type as usize + 1
1178            ),
1179            Command::FortressBuildUnit { unit, count } => {
1180                format!("FortressBuildUnitStart:{}/{count}", *unit as usize + 1)
1181            }
1182            Command::FortressGemStoneSearch => {
1183                format!("FortressGemstoneStart:",)
1184            }
1185            Command::FortressGemStoneSearchCancel => {
1186                format!("FortressGemStoneStop:0")
1187            }
1188            Command::FortressGemStoneSearchFinish { mushrooms } => {
1189                format!("FortressGemstoneFinished:{mushrooms}",)
1190            }
1191            Command::FortressAttack { soldiers } => {
1192                format!("FortressAttack:{soldiers}")
1193            }
1194            Command::FortressNewEnemy { use_mushroom: pay } => {
1195                format!("FortressEnemy:{}", usize::from(*pay))
1196            }
1197            Command::FortressSetCAEnemy { msg_id } => {
1198                format!("FortressEnemy:0/{}", *msg_id)
1199            }
1200            Command::FortressUpgradeHallOfKnights => {
1201                format!("FortressGroupBonusUpgrade:")
1202            }
1203            Command::Whisper {
1204                player_name: player,
1205                message,
1206            } => format!(
1207                "PlayerMessageWhisper:{}/{}",
1208                player,
1209                to_sf_string(message)
1210            ),
1211            Command::UnderworldCollect {
1212                resource: resource_t,
1213            } => {
1214                format!("UnderworldGather:{}", *resource_t as usize + 1)
1215            }
1216            Command::UnderworldUnitUpgrade { unit: unit_t } => {
1217                format!("UnderworldUpgradeUnit:{}", *unit_t as usize + 1)
1218            }
1219            Command::UnderworldUpgradeStart {
1220                building,
1221                mushrooms,
1222            } => format!(
1223                "UnderworldBuildStart:{}/{mushrooms}",
1224                *building as usize + 1
1225            ),
1226            Command::UnderworldUpgradeCancel { building } => {
1227                format!("UnderworldBuildStop:{}", *building as usize + 1)
1228            }
1229            Command::UnderworldUpgradeFinish {
1230                building,
1231                mushrooms,
1232            } => {
1233                format!(
1234                    "UnderworldBuildFinished:{}/{mushrooms}",
1235                    *building as usize + 1
1236                )
1237            }
1238            Command::UnderworldAttack { player_id } => {
1239                format!("UnderworldAttack:{player_id}")
1240            }
1241            Command::RollDice { payment, dices } => {
1242                let mut dices = dices.iter().fold(String::new(), |mut a, b| {
1243                    if !a.is_empty() {
1244                        a.push('/');
1245                    }
1246                    a.push((*b as u8 + b'0') as char);
1247                    a
1248                });
1249
1250                if dices.is_empty() {
1251                    // FIXME: This is dead code, right?
1252                    dices = "0/0/0/0/0".to_string();
1253                }
1254                format!("RollDice:{}/{}", *payment as usize, dices)
1255            }
1256            Command::PetFeed { pet_id, fruit_idx } => {
1257                format!("PlayerPetFeed:{pet_id}/{fruit_idx}")
1258            }
1259            Command::GuildPetBattle { use_mushroom } => {
1260                format!("GroupPetBattle:{}", usize::from(*use_mushroom))
1261            }
1262            Command::IdleUpgrade { typ: kind, amount } => {
1263                format!("IdleIncrease:{}/{}", *kind as usize, amount)
1264            }
1265            Command::IdleSacrifice => format!("IdlePrestige:0"),
1266            Command::SwapManequin => format!("PlayerDummySwap:301/1"),
1267            Command::UpdateFlag { flag } => format!(
1268                "PlayerSetFlag:{}",
1269                flag.map(Flag::code).unwrap_or_default()
1270            ),
1271            Command::BlockGuildInvites { block_invites } => {
1272                format!("PlayerSetNoGroupInvite:{}", u8::from(*block_invites))
1273            }
1274            Command::ShowTips { show_tips } => {
1275                #[allow(clippy::unreadable_literal)]
1276                {
1277                    format!(
1278                        "PlayerTutorialStatus:{}",
1279                        if *show_tips { 0 } else { 0xFFFFFFF }
1280                    )
1281                }
1282            }
1283            Command::ChangePassword { username, old, new } => {
1284                let old = sha1_hash(&format!("{old}{HASH_CONST}"));
1285                let new = sha1_hash(&format!("{new}{HASH_CONST}"));
1286                format!("AccountPasswordChange:{username}/{old}/106/{new}/")
1287            }
1288            Command::ChangeMailAddress {
1289                old_mail,
1290                new_mail,
1291                password,
1292                username,
1293            } => {
1294                let pass = sha1_hash(&format!("{password}{HASH_CONST}"));
1295                format!(
1296                    "AccountMailChange:{old_mail}/{new_mail}/{username}/\
1297                     {pass}/106"
1298                )
1299            }
1300            Command::SetLanguage { language } => {
1301                format!("AccountSetLanguage:{language}")
1302            }
1303            Command::SetPlayerRelation {
1304                player_id,
1305                relation,
1306            } => {
1307                format!("PlayerFriendSet:{player_id}/{}", *relation as i32)
1308            }
1309            Command::SetPortraitFrame { portrait_id } => {
1310                format!("PlayerSetActiveFrame:{portrait_id}")
1311            }
1312            Command::CollectDailyQuestReward { pos } => {
1313                format!("DailyTaskClaim:1/{}", pos + 1)
1314            }
1315            Command::CollectEventTaskReward { pos } => {
1316                format!("DailyTaskClaim:2/{}", pos + 1)
1317            }
1318            Command::SwapRunes {
1319                from,
1320                from_pos,
1321                to,
1322                to_pos,
1323            } => {
1324                format!(
1325                    "PlayerSmithSwapRunes:{}/{}/{}/{}",
1326                    *from as usize,
1327                    *from_pos + 1,
1328                    *to as usize,
1329                    *to_pos + 1
1330                )
1331            }
1332            Command::ChangeItemLook {
1333                inv,
1334                pos,
1335                raw_model_id: model_id,
1336            } => {
1337                format!(
1338                    "ItemChangePicture:{}/{}/{}",
1339                    *inv as usize,
1340                    pos + 1,
1341                    model_id
1342                )
1343            }
1344            Command::ExpeditionPickEncounter { pos } => {
1345                format!("ExpeditionProceed:{}", pos + 1)
1346            }
1347            Command::ExpeditionContinue => format!("ExpeditionProceed:1"),
1348            Command::ExpeditionPickReward { pos } => {
1349                format!("ExpeditionProceed:{}", pos + 1)
1350            }
1351            Command::ExpeditionStart { pos } => {
1352                format!("ExpeditionStart:{}", pos + 1)
1353            }
1354            Command::FightDungeon {
1355                dungeon,
1356                use_mushroom,
1357            } => match dungeon {
1358                Dungeon::Light(name) => {
1359                    if *name == LightDungeon::Tower {
1360                        return Err(SFError::InvalidRequest(
1361                            "The tower must be fought with the FightTower \
1362                             command",
1363                        ));
1364                    }
1365                    format!(
1366                        "PlayerDungeonBattle:{}/{}",
1367                        *name as usize + 1,
1368                        u8::from(*use_mushroom)
1369                    )
1370                }
1371                Dungeon::Shadow(name) => {
1372                    if *name == ShadowDungeon::Twister {
1373                        format!(
1374                            "PlayerDungeonBattle:{}/{}",
1375                            LightDungeon::Tower as u32 + 1,
1376                            u8::from(*use_mushroom)
1377                        )
1378                    } else {
1379                        format!(
1380                            "PlayerShadowBattle:{}/{}",
1381                            *name as u32 + 1,
1382                            u8::from(*use_mushroom)
1383                        )
1384                    }
1385                }
1386            },
1387            Command::FightPetOpponent {
1388                opponent_id,
1389                habitat: element,
1390            } => {
1391                format!("PetsPvPFight:0/{opponent_id}/{}", *element as u32 + 1)
1392            }
1393            Command::FightPetDungeon {
1394                use_mush,
1395                habitat: element,
1396                enemy_pos,
1397                player_pet_id,
1398            } => {
1399                format!(
1400                    "PetsDungeonFight:{}/{}/{enemy_pos}/{player_pet_id}",
1401                    u8::from(*use_mush),
1402                    *element as u8 + 1,
1403                )
1404            }
1405            Command::ExpeditionSkipWait { typ } => {
1406                format!("ExpeditionTimeSkip:{}", *typ as u8)
1407            }
1408            Command::SetQuestsInsteadOfExpeditions { value } => {
1409                let value = match value {
1410                    ExpeditionSetting::PreferExpeditions => 'a',
1411                    ExpeditionSetting::PreferQuests => 'b',
1412                };
1413                format!("UserSettingsUpdate:5/{value}")
1414            }
1415            Command::HellevatorEnter => format!("GroupTournamentJoin:"),
1416            Command::HellevatorViewGuildRanking => {
1417                format!("GroupTournamentRankingOwnGroup")
1418            }
1419            Command::HellevatorFight { use_mushroom } => {
1420                format!("GroupTournamentBattle:{}", u8::from(*use_mushroom))
1421            }
1422            Command::HellevatorBuy {
1423                position,
1424                typ,
1425                price,
1426                use_mushroom,
1427            } => {
1428                format!(
1429                    "GroupTournamentMerchantBuy:{position}/{}/{price}/{}",
1430                    *typ as u32,
1431                    if *use_mushroom { 2 } else { 1 }
1432                )
1433            }
1434            Command::HellevatorRefreshShop => {
1435                format!("GroupTournamentMerchantReroll:")
1436            }
1437            Command::HallOfFameHellevatorPage { page } => {
1438                let per_page = 51;
1439                let pos = 26 + (per_page * page);
1440                format!("GroupTournamentRankingAllGroups:{pos}//25/25")
1441            }
1442            Command::HellevatorJoinHellAttack {
1443                use_mushroom,
1444                plain: pos,
1445            } => {
1446                format!(
1447                    "GroupTournamentRaidParticipant:{}/{}",
1448                    u8::from(*use_mushroom),
1449                    *pos + 1
1450                )
1451            }
1452            Command::HellevatorClaimDaily => {
1453                format!("GroupTournamentClaimDaily:")
1454            }
1455            Command::HellevatorClaimDailyYesterday => {
1456                format!("GroupTournamentClaimDailyYesterday:")
1457            }
1458            Command::HellevatorPreviewRewards => {
1459                format!("GroupTournamentPreview:")
1460            }
1461            Command::HellevatorClaimFinal => format!("GroupTournamentClaim:"),
1462            Command::ClaimablePreview { msg_id } => {
1463                format!("PendingRewardView:{msg_id}")
1464            }
1465            Command::ClaimableClaim { msg_id } => {
1466                format!("PendingRewardClaim:{msg_id}")
1467            }
1468            Command::BuyGoldFrame => {
1469                format!("PlayerGoldFrameBuy:")
1470            }
1471        })
1472    }
1473}
1474
1475macro_rules! generate_flag_enum {
1476    ($($variant:ident => $code:expr),*) => {
1477        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
1478        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
1479        #[allow(missing_docs)]
1480        /// The flag of a country, that will be visible in the Hall of Fame
1481        pub enum Flag {
1482            $(
1483                $variant,
1484            )*
1485        }
1486
1487        impl Flag {
1488            pub(crate) fn code(self) -> &'static str {
1489                match self {
1490                    $(
1491                        Flag::$variant => $code,
1492                    )*
1493                }
1494            }
1495
1496            pub(crate) fn parse(value: &str) -> Option<Self> {
1497                if value.is_empty() {
1498                    return None;
1499                }
1500
1501                // Mapping from string codes to enum variants
1502                match value {
1503                    $(
1504                        $code => Some(Flag::$variant),
1505                    )*
1506
1507                    _ => {
1508                        warn!("Invalid flag value: {value}");
1509                        None
1510                    }
1511                }
1512            }
1513        }
1514    };
1515}
1516
1517// Use the macro to generate the Flag enum and its methods
1518// Source: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements
1519generate_flag_enum! {
1520    Argentina => "ar",
1521    Australia => "au",
1522    Austria => "at",
1523    Belgium => "be",
1524    Bolivia => "bo",
1525    Brazil => "br",
1526    Bulgaria => "bg",
1527    Canada => "ca",
1528    Chile => "cl",
1529    China => "cn",
1530    Colombia => "co",
1531    CostaRica => "cr",
1532    Czechia => "cz",
1533    Denmark => "dk",
1534    DominicanRepublic => "do",
1535    Ecuador => "ec",
1536    ElSalvador =>"sv",
1537    Finland => "fi",
1538    France => "fr",
1539    Germany => "de",
1540    GreatBritain => "gb",
1541    Greece => "gr",
1542    Honduras => "hn",
1543    Hungary => "hu",
1544    India => "in",
1545    Italy => "it",
1546    Japan => "jp",
1547    Lithuania => "lt",
1548    Mexico => "mx",
1549    Netherlands => "nl",
1550    Panama => "pa",
1551    Paraguay => "py",
1552    Peru => "pe",
1553    Philippines => "ph",
1554    Poland => "pl",
1555    Portugal => "pt",
1556    Romania => "ro",
1557    Russia => "ru",
1558    SaudiArabia => "sa",
1559    Slovakia => "sk",
1560    SouthKorea => "kr",
1561    Spain => "es",
1562    Sweden => "se",
1563    Switzerland => "ch",
1564    Thailand => "th",
1565    Turkey => "tr",
1566    Ukraine => "ua",
1567    UnitedArabEmirates => "ae",
1568    UnitedStates => "us",
1569    Uruguay => "uy",
1570    Venezuela => "ve",
1571    Vietnam => "vn"
1572}