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