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