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