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