Skip to main content

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