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