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