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